├── .deployment
├── .env.sample
├── .eslintrc.cjs
├── .github
├── .axe-linter.yml
└── workflows
│ ├── deploy.yml
│ ├── lint.yml
│ ├── prod.yml
│ └── staging.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── CNAME
├── LICENSE
├── README.md
├── admin.html
├── deployWebhook.cjs
├── favicon.png
├── fonts
├── IBMPlexMono-Medium.ttf
├── IBMPlexMono-MediumItalic.ttf
├── IBMPlexMono-SemiBold.ttf
├── IBMPlexMono-SemiBoldItalic.ttf
├── Oswald-VariableFont_wght.ttf
└── Web437_IBM_VGA_8x16.woff
├── images
├── auto-fire
│ ├── AutoFireDesertTarget1280.jpg
│ ├── AutoFireGarage1280.jpg
│ ├── AutoFireOverworld1280.jpg
│ └── AutoFireRuinsCars1280.jpg
├── badges
│ ├── OBELISK_1.png
│ ├── device_of_luthien.png
│ ├── eel_0.png
│ ├── eel_1.png
│ ├── goblin_appreciation.png
│ ├── goblin_barbie.png
│ ├── golden_thesis.png
│ ├── leg-of-yendor.png
│ ├── modbadge.png
│ ├── nega_ticket.png
│ ├── phylactery.png
│ ├── procgen_artificer.png
│ ├── procgen_bard.png
│ ├── procgen_cleric.png
│ ├── procgen_druid.png
│ ├── procgen_paladin.png
│ ├── procgen_ranger.png
│ ├── procgen_sorceror.png
│ ├── procgen_warlock.png
│ ├── procgen_wizard.png
│ ├── speakerbadge.png
│ └── undermuffin.png
├── cantrip
│ ├── 0.png
│ └── 1.png
├── cdszzt
│ ├── cdszzt-montage.png
│ ├── cdszzt-title.png
│ └── cdszzt-town.png
├── fuzz-force
│ ├── DiceSwap_Web.jpg
│ ├── Dotty_Halls_Web.jpg
│ ├── Event_Web.jpg
│ ├── FerretyShop_Dot_Web.jpg
│ ├── Finn_Dice_Web.jpg
│ └── ForestLevel_Web.jpg
├── gesuido
│ ├── gesuido_ss01.png
│ └── gesuido_ss02.png
├── happy-grumps
│ ├── 0.png
│ ├── 1.png
│ └── logo.png
├── lords-of-cyberspace
│ ├── loc1.png
│ ├── loc2.png
│ ├── loc3.png
│ └── loc4.png
├── mech@mor-showdown
│ ├── 0.png
│ ├── 1.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ └── 5.png
├── nongunz-de
│ ├── NongunzDE_011_Scaled.png
│ ├── NongunzDE_012_Scaled.png
│ └── NongunzDE_09_Scaled.png
├── peglin
│ ├── peglin1.png
│ ├── peglin2.png
│ ├── peglin3.png
│ ├── peglin4.png
│ ├── peglin5.png
│ └── peglin_video.mp4
├── puppy.jpg
├── rift-wizard
│ ├── ss_16.png
│ ├── ss_17.png
│ ├── ss_6.png
│ └── ss_9.png
├── roundguard
│ ├── Roundguard_1.png
│ ├── Roundguard_2.png
│ └── Roundguard_3.png
├── ultimate-adom
│ ├── left.png
│ └── right.png
└── wizard-wars-io
│ ├── 0.png
│ ├── 1.png
│ ├── 2.png
│ └── 3.png
├── index.html
├── lintLinks.ts
├── map.monopic
├── map2021.psci
├── package-lock.json
├── package.json
├── server
├── .funcignore
├── .gitignore
├── .vscode
│ ├── extensions.json
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── ARM Template Notes.md
├── ARM-TODO.md
├── addObeliskNote
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── addRoomNote
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── banUser
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── clientDeployedWebhook
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── cognitiveServicesKey
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── connect
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── deleteMessage
│ ├── function.json
│ └── index.ts
├── deleteObeliskNote
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── deleteRoom
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── deleteRoomNote
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── disconnect
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── displayMessage
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── equipBadge
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── fetchProfile
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── getAllRooms
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── getRoom
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── getRoomIds
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── heartbeat
│ ├── function.json
│ ├── index.ts
│ └── readme.md
├── host.json
├── isRegistered
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── leaveVideoChat
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── likeObeliskNote
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── likeRoomNote
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── moveAllUsersToEntryway
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── moveRoom
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── negotiate
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── negotiatePubSub
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── openOrCloseSpace
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── orderNewDrink
│ ├── function.json
│ └── index.ts
├── package-lock.json
├── package.json
├── pickUpItem
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── pong
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── proxies.json
├── resetBadgeData
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── resetRoomData
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── sendCaption
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── sendChatMessage
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── serverSettings
│ ├── function.json
│ └── index.ts
├── src
│ ├── allowedItems.ts
│ ├── authenticate.ts
│ ├── azureWrap.ts
│ ├── badges.ts
│ ├── config.ts
│ ├── cookie.ts
│ ├── cosmosdb.ts
│ ├── dance.ts
│ ├── database.ts
│ ├── endpoint.ts
│ ├── endpoints
│ │ ├── banUser.ts
│ │ ├── clientDeployedWebhook.ts
│ │ ├── connect.ts
│ │ ├── deleteMessage.ts
│ │ ├── deleteRoom.ts
│ │ ├── disconnect.ts
│ │ ├── displayMessage.ts
│ │ ├── equipBadge.ts
│ │ ├── fetchProfile.ts
│ │ ├── getAllRooms.ts
│ │ ├── getRoom.ts
│ │ ├── getRoomIds.ts
│ │ ├── heartbeat.ts
│ │ ├── isRegistered.ts
│ │ ├── leaveVideoChat.ts
│ │ ├── moveAllUsersToEntryway.ts
│ │ ├── moveRoom.ts
│ │ ├── obelisk
│ │ │ ├── addObeliskNote.ts
│ │ │ ├── deleteObeliskNote.ts
│ │ │ ├── likeObeliskNote.ts
│ │ │ ├── readme.md
│ │ │ ├── startObservingObelisk.ts
│ │ │ └── stopObservingObelisk.ts
│ │ ├── openOrCloseSpace.ts
│ │ ├── orderNewDrink.ts
│ │ ├── pickUpItem.ts
│ │ ├── pong.ts
│ │ ├── resetBadgeData.ts
│ │ ├── resetRoomData.ts
│ │ ├── roomNote
│ │ │ ├── addRoomNote.ts
│ │ │ ├── deleteRoomNote.ts
│ │ │ └── likeRoomNote.ts
│ │ ├── sendCaption.ts
│ │ ├── sendChatMessage.ts
│ │ ├── serverSettings.ts
│ │ ├── toggleModStatus.ts
│ │ ├── toggleSpeakerStatus.ts
│ │ ├── updateFontReward.ts
│ │ ├── updateProfile.ts
│ │ ├── updateProfileColor.ts
│ │ └── updateRoom.ts
│ ├── generators
│ │ ├── boba.ts
│ │ ├── bodyWorksCharacter.ts
│ │ ├── chadSilverbow.ts
│ │ ├── chasm.ts
│ │ ├── closedSigns.ts
│ │ ├── craneGame.ts
│ │ ├── doctorHope.ts
│ │ ├── drinkContents.ts
│ │ ├── drinkNames.ts
│ │ ├── drinkSkeletons.ts
│ │ ├── drinkVessels.ts
│ │ ├── flower.ts
│ │ ├── fortuneCookies.ts
│ │ ├── gameRecs.ts
│ │ ├── hotDogGuy.ts
│ │ ├── index.ts
│ │ ├── kebabs.ts
│ │ ├── loudRobert.ts
│ │ ├── motivationPosters.ts
│ │ ├── pickupLines.ts
│ │ ├── polymorph.ts
│ │ ├── randorTheTwisted.ts
│ │ ├── ray.ts
│ │ ├── robots.ts
│ │ ├── seersCatalog.ts
│ │ ├── tacos.ts
│ │ ├── terribleJokes.ts
│ │ ├── ubizaraTheBartender.ts
│ │ ├── veganFood.ts
│ │ ├── vendingMachineFood.ts
│ │ ├── zara.ts
│ │ └── zeroCrash.ts
│ ├── globalPresenceMessage.ts
│ ├── heartbeat.ts
│ ├── interact.ts
│ ├── logSignalR.ts
│ ├── look.ts
│ ├── moveToRoom.ts
│ ├── polymorph.ts
│ ├── redis.ts
│ ├── roomNote.ts
│ ├── rooms
│ │ ├── data
│ │ │ └── roomData.json
│ │ └── index.ts
│ ├── sendToDiscord.ts
│ ├── setUpRoomsForUser.ts
│ ├── shout.ts
│ ├── types.ts
│ ├── user.ts
│ └── whisper.ts
├── startObservingObelisk
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── startPonderingOrbs
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── stopObservingObelisk
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── template.json
├── toggleModStatus
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── toggleSpeakerStatus
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── tsconfig.json
├── twilioToken
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── updateFontReward
│ ├── function.json
│ └── index.ts
├── updateProfile
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── updateProfileColor
│ ├── function.json
│ └── index.ts
└── updateRoom
│ ├── function.json
│ ├── index.ts
│ └── sample.dat
├── src
├── Actions.ts
├── App.tsx
├── Deferred.ts
├── SlashCommands.ts
├── admin
│ ├── actions.ts
│ ├── components
│ │ ├── App.tsx
│ │ ├── LoggedOutView.tsx
│ │ ├── RoomList.tsx
│ │ └── RoomOptionsView.tsx
│ ├── index.tsx
│ ├── reducer.ts
│ └── style.css
├── audioAnalysis.ts
├── authentication.ts
├── components
│ ├── BadgeUnlockModal.tsx
│ ├── BadgeView.tsx
│ ├── BadgesModalView.tsx
│ ├── ClientDeployedModal.tsx
│ ├── CodeOfConductView.tsx
│ ├── DisconnectModalView.tsx
│ ├── EmailVerifiedView.tsx
│ ├── GoHomeView.tsx
│ ├── HappeningNowView.tsx
│ ├── HeldItemView.tsx
│ ├── HelpView.tsx
│ ├── InputView.tsx
│ ├── LocalMediaView.tsx
│ ├── LoggedOutView.tsx
│ ├── MapModalView.tsx
│ ├── MapView.tsx
│ ├── MediaChatButtonView.tsx
│ ├── MediaChatView.tsx
│ ├── MediaSelectorView.tsx
│ ├── MenuButtonView.tsx
│ ├── MessageItem
│ │ ├── MessageItem.css
│ │ ├── MessageItem.tsx
│ │ └── index.ts
│ ├── MessageList
│ │ ├── MessageList.css
│ │ ├── MessageList.tsx
│ │ ├── hooks
│ │ │ ├── index.ts
│ │ │ ├── useAutoscroll.ts
│ │ │ └── useShouldHideTimestamp.ts
│ │ └── index.ts
│ ├── MessageView.tsx
│ ├── MiniMapView.tsx
│ ├── ModalView.tsx
│ ├── NameView.tsx
│ ├── NoteView.tsx
│ ├── NoteWallView.tsx
│ ├── ObeliskView.tsx
│ ├── ParticipantChatView.tsx
│ ├── PresenceView.tsx
│ ├── ProfileEditView.tsx
│ ├── ProfileView.tsx
│ ├── RateTalkView.tsx
│ ├── RiddleModal.tsx
│ ├── RoomListView.tsx
│ ├── RoomView.tsx
│ ├── ScheduleView.tsx
│ ├── ServerSettingsView.tsx
│ ├── SettingsView.tsx
│ ├── SideNavView.tsx
│ ├── SpecialTextModalView.tsx
│ ├── VerifyEmailView.tsx
│ ├── VideoAudioSettingsView.tsx
│ ├── VirtualizationProvider
│ │ ├── VirtualizationProvider.tsx
│ │ └── index.ts
│ ├── WelcomeModalView.tsx
│ ├── YouAreBannedView.tsx
│ └── feature
│ │ ├── ConfettiRoomView.tsx
│ │ ├── DullDoorViews.tsx
│ │ ├── FullRoomIndexViews.tsx
│ │ └── RainbowGateViews.tsx
├── config.ts
├── emoji
│ ├── customEmojiMap.json
│ ├── index.tsx
│ └── reservedEmojiMap.json
├── index.tsx
├── linkActions.ts
├── message
│ ├── enums.ts
│ ├── index.ts
│ ├── types.ts
│ └── utils.ts
├── modals.ts
├── networking.ts
├── reducer.ts
├── room.ts
├── speechRecognizer.ts
├── storage.ts
├── types.ts
├── useReducerWithThunk.ts
├── utils.ts
└── videochat
│ ├── mediaChatContext.tsx
│ ├── twilio
│ ├── AudioTrack.tsx
│ ├── ParticipantTracks.tsx
│ ├── Publication.tsx
│ ├── VideoTrack.tsx
│ ├── useMediaStreamTrack.ts
│ ├── usePublications.ts
│ ├── useTrack.tsx
│ └── useVideoTrackDimensions.ts
│ └── twilioChatContext.tsx
├── stream.html
├── style
├── badges.css
├── chat.css
├── fonts.css
├── input.css
├── modal.css
├── nameView.css
├── nav.css
├── noteWall.css
├── profileEditView.css
├── profileView.css
├── room.css
├── style.css
└── videoChat.css
└── tsconfig.json
/.deployment:
--------------------------------------------------------------------------------
1 | [config]
2 | project = server
--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------
1 | SERVER_HOSTNAME=https://localhost
2 | FIREBASE_API_KEY=firebase_api_key
3 | FIREBASE_AUTH_DOMAIN=firebase_auth_domain
4 | FIREBASE_PROJECT_ID=firebase_project_id
5 | FIREBASE_STORAGE_BUCKET=firebase_storage_bucket
6 | FIREBASE_MESSAGING_SENDER_ID=firebase_messaging_sender_id
7 | FIREBASE_APP_ID=firebase_app_id
8 |
9 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2020: true
5 | },
6 | extends: [
7 | 'plugin:react/recommended',
8 | 'standard',
9 | 'plugin:jsx-a11y/recommended'
10 | ],
11 | parser: '@typescript-eslint/parser',
12 | parserOptions: {
13 | ecmaFeatures: {
14 | jsx: true
15 | },
16 | ecmaVersion: 11,
17 | sourceType: 'module'
18 | },
19 | plugins: [
20 | 'react',
21 | '@typescript-eslint',
22 | 'jsx-a11y'
23 | ],
24 | rules: {
25 | 'no-unused-vars': 0,
26 | 'react/prop-types': 0
27 | },
28 | settings: {
29 | react: {
30 | version: 'detect'
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.github/.axe-linter.yml:
--------------------------------------------------------------------------------
1 | exclude:
2 | - ./src/components/ModalView.tsx # I don't want to exclude this! The per-file ignore syntax to ignore the spurious click-events-have-key-events error isn't working
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Linting and Validation Checks
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-node@v2.1.0
16 | - run: npm install
17 |
18 | # This produces compiled.json, which needs to exist for lint-rooms to work
19 | - name: server build
20 | run: |
21 | cd server
22 | npm install
23 | npm run build
24 |
25 | - name: build
26 | run: npm run build
27 |
28 | - name: lint
29 | run: npm run lint
30 |
31 | # - name: lintRooms
32 | # run: npm run lint-rooms
33 |
34 | - name: typecheck
35 | run: cd server && npm install && cd ..; npm run typecheck
36 |
--------------------------------------------------------------------------------
/.github/workflows/prod.yml:
--------------------------------------------------------------------------------
1 | name: Production Build and Deploy
2 |
3 | on: workflow_dispatch
4 |
5 | jobs:
6 | build-and-deploy:
7 | uses: ./.github/workflows/deploy.yml
8 | secrets:
9 | SERVER_HOSTNAME: ${{ secrets.SERVER_HOSTNAME }}
10 | FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
11 | FIREBASE_AUTH_DOMAIN: ${{ secrets.FIREBASE_AUTH_DOMAIN }}
12 | FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }}
13 | FIREBASE_STORAGE_BUCKET: ${{ secrets.FIREBASE_STORAGE_BUCKET }}
14 | FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.FIREBASE_MESSAGING_SENDER_ID }}
15 | FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}
16 | AZURE_FUNCTION_APP_NAME: ${{ secrets.AZURE_FUNCTION_APP_NAME }}
17 | AZURE_FUNCTIONAPP_PUBLISH_PROFILE: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
18 | FIREBASE_SERVER_JSON: ${{ secrets.FIREBASE_SERVER_JSON }}
19 | DEPLOY_WEBHOOK_KEY: ${{ secrets.DEPLOY_WEBHOOK_KEY }}
20 | ASWA_API_TOKEN: ${{secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
21 |
--------------------------------------------------------------------------------
/.github/workflows/staging.yml:
--------------------------------------------------------------------------------
1 | name: Staging Build and Deploy
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | build-and-deploy:
9 | uses: ./.github/workflows/deploy.yml
10 | secrets:
11 | SERVER_HOSTNAME: ${{ secrets.STAGING_SERVER_HOSTNAME }}
12 | FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
13 | FIREBASE_AUTH_DOMAIN: ${{ secrets.FIREBASE_AUTH_DOMAIN }}
14 | FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }}
15 | FIREBASE_STORAGE_BUCKET: ${{ secrets.FIREBASE_STORAGE_BUCKET }}
16 | FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.FIREBASE_MESSAGING_SENDER_ID }}
17 | FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}
18 | AZURE_FUNCTION_APP_NAME: ${{ secrets.STAGING_AZURE_FUNCTION_APP_NAME }}
19 | AZURE_FUNCTIONAPP_PUBLISH_PROFILE: ${{ secrets.STAGING_AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}
20 | FIREBASE_SERVER_JSON: ${{ secrets.FIREBASE_SERVER_JSON }}
21 | DEPLOY_WEBHOOK_KEY: ${{ secrets.DEPLOY_WEBHOOK_KEY }}
22 | ASWA_API_TOKEN: ${{secrets.STAGING_ASWA_API_TOKEN}}
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | server/firebase-admin.*.json
2 | node_modules
3 | dist
4 | .env
5 | .env.*
6 | !.env.sample
7 | .parcel-cache
8 | .cache
9 | .DS_Store
10 | .idea
11 | **/.DS_Store
12 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "ms-azuretools.vscode-azurefunctions"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Attach to Node Functions",
6 | "type": "node",
7 | "request": "attach",
8 | "port": 9229,
9 | "preLaunchTask": "func: host start"
10 | }
11 | ]
12 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "azureFunctions.deploySubpath": "server",
3 | "azureFunctions.postDeployTask": "npm install",
4 | "azureFunctions.projectLanguage": "TypeScript",
5 | "azureFunctions.projectRuntime": "~4",
6 | "debug.internalConsoleOptions": "neverOpen",
7 | "azureFunctions.preDeployTask": "npm prune",
8 | "editor.codeActionsOnSave": {
9 | "source.fixAll": "explicit"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "func",
6 | "command": "host start",
7 | "problemMatcher": "$func-watch",
8 | "isBackground": true,
9 | "dependsOn": "npm build",
10 | "options": {
11 | "cwd": "${workspaceFolder}/server"
12 | }
13 | },
14 | {
15 | "type": "shell",
16 | "label": "npm build",
17 | "command": "npm run build",
18 | "dependsOn": "npm install",
19 | "problemMatcher": "$tsc",
20 | "options": {
21 | "cwd": "${workspaceFolder}/server"
22 | }
23 | },
24 | {
25 | "type": "shell",
26 | "label": "npm install",
27 | "command": "npm install",
28 | "options": {
29 | "cwd": "${workspaceFolder}/server"
30 | }
31 | },
32 | {
33 | "type": "shell",
34 | "label": "npm prune",
35 | "command": "npm prune --production",
36 | "dependsOn": "npm build",
37 | "problemMatcher": [],
38 | "options": {
39 | "cwd": "${workspaceFolder}/server"
40 | }
41 | }
42 | ]
43 | }
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | chat.roguelike.club
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020, 2021, 2022, 2023, 2024, 2025 Roguelike Celebration Contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/admin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | azure-mud admin panel
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/deployWebhook.cjs:
--------------------------------------------------------------------------------
1 | const request = require('request')
2 | request.post({
3 | url: `https://${process.env.APP_NAME}.azurewebsites.net/api/clientDeployedWebhook`,
4 | headers: { 'content-type': 'application/json' },
5 | body: JSON.stringify({ key: process.env.TOKEN })
6 | })
7 |
--------------------------------------------------------------------------------
/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/favicon.png
--------------------------------------------------------------------------------
/fonts/IBMPlexMono-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/fonts/IBMPlexMono-Medium.ttf
--------------------------------------------------------------------------------
/fonts/IBMPlexMono-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/fonts/IBMPlexMono-MediumItalic.ttf
--------------------------------------------------------------------------------
/fonts/IBMPlexMono-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/fonts/IBMPlexMono-SemiBold.ttf
--------------------------------------------------------------------------------
/fonts/IBMPlexMono-SemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/fonts/IBMPlexMono-SemiBoldItalic.ttf
--------------------------------------------------------------------------------
/fonts/Oswald-VariableFont_wght.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/fonts/Oswald-VariableFont_wght.ttf
--------------------------------------------------------------------------------
/fonts/Web437_IBM_VGA_8x16.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/fonts/Web437_IBM_VGA_8x16.woff
--------------------------------------------------------------------------------
/images/auto-fire/AutoFireDesertTarget1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/auto-fire/AutoFireDesertTarget1280.jpg
--------------------------------------------------------------------------------
/images/auto-fire/AutoFireGarage1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/auto-fire/AutoFireGarage1280.jpg
--------------------------------------------------------------------------------
/images/auto-fire/AutoFireOverworld1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/auto-fire/AutoFireOverworld1280.jpg
--------------------------------------------------------------------------------
/images/auto-fire/AutoFireRuinsCars1280.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/auto-fire/AutoFireRuinsCars1280.jpg
--------------------------------------------------------------------------------
/images/badges/OBELISK_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/OBELISK_1.png
--------------------------------------------------------------------------------
/images/badges/device_of_luthien.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/device_of_luthien.png
--------------------------------------------------------------------------------
/images/badges/eel_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/eel_0.png
--------------------------------------------------------------------------------
/images/badges/eel_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/eel_1.png
--------------------------------------------------------------------------------
/images/badges/goblin_appreciation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/goblin_appreciation.png
--------------------------------------------------------------------------------
/images/badges/goblin_barbie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/goblin_barbie.png
--------------------------------------------------------------------------------
/images/badges/golden_thesis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/golden_thesis.png
--------------------------------------------------------------------------------
/images/badges/leg-of-yendor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/leg-of-yendor.png
--------------------------------------------------------------------------------
/images/badges/modbadge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/modbadge.png
--------------------------------------------------------------------------------
/images/badges/nega_ticket.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/nega_ticket.png
--------------------------------------------------------------------------------
/images/badges/phylactery.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/phylactery.png
--------------------------------------------------------------------------------
/images/badges/procgen_artificer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/procgen_artificer.png
--------------------------------------------------------------------------------
/images/badges/procgen_bard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/procgen_bard.png
--------------------------------------------------------------------------------
/images/badges/procgen_cleric.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/procgen_cleric.png
--------------------------------------------------------------------------------
/images/badges/procgen_druid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/procgen_druid.png
--------------------------------------------------------------------------------
/images/badges/procgen_paladin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/procgen_paladin.png
--------------------------------------------------------------------------------
/images/badges/procgen_ranger.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/procgen_ranger.png
--------------------------------------------------------------------------------
/images/badges/procgen_sorceror.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/procgen_sorceror.png
--------------------------------------------------------------------------------
/images/badges/procgen_warlock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/procgen_warlock.png
--------------------------------------------------------------------------------
/images/badges/procgen_wizard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/procgen_wizard.png
--------------------------------------------------------------------------------
/images/badges/speakerbadge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/speakerbadge.png
--------------------------------------------------------------------------------
/images/badges/undermuffin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/badges/undermuffin.png
--------------------------------------------------------------------------------
/images/cantrip/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/cantrip/0.png
--------------------------------------------------------------------------------
/images/cantrip/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/cantrip/1.png
--------------------------------------------------------------------------------
/images/cdszzt/cdszzt-montage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/cdszzt/cdszzt-montage.png
--------------------------------------------------------------------------------
/images/cdszzt/cdszzt-title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/cdszzt/cdszzt-title.png
--------------------------------------------------------------------------------
/images/cdszzt/cdszzt-town.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/cdszzt/cdszzt-town.png
--------------------------------------------------------------------------------
/images/fuzz-force/DiceSwap_Web.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/fuzz-force/DiceSwap_Web.jpg
--------------------------------------------------------------------------------
/images/fuzz-force/Dotty_Halls_Web.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/fuzz-force/Dotty_Halls_Web.jpg
--------------------------------------------------------------------------------
/images/fuzz-force/Event_Web.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/fuzz-force/Event_Web.jpg
--------------------------------------------------------------------------------
/images/fuzz-force/FerretyShop_Dot_Web.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/fuzz-force/FerretyShop_Dot_Web.jpg
--------------------------------------------------------------------------------
/images/fuzz-force/Finn_Dice_Web.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/fuzz-force/Finn_Dice_Web.jpg
--------------------------------------------------------------------------------
/images/fuzz-force/ForestLevel_Web.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/fuzz-force/ForestLevel_Web.jpg
--------------------------------------------------------------------------------
/images/gesuido/gesuido_ss01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/gesuido/gesuido_ss01.png
--------------------------------------------------------------------------------
/images/gesuido/gesuido_ss02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/gesuido/gesuido_ss02.png
--------------------------------------------------------------------------------
/images/happy-grumps/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/happy-grumps/0.png
--------------------------------------------------------------------------------
/images/happy-grumps/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/happy-grumps/1.png
--------------------------------------------------------------------------------
/images/happy-grumps/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/happy-grumps/logo.png
--------------------------------------------------------------------------------
/images/lords-of-cyberspace/loc1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/lords-of-cyberspace/loc1.png
--------------------------------------------------------------------------------
/images/lords-of-cyberspace/loc2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/lords-of-cyberspace/loc2.png
--------------------------------------------------------------------------------
/images/lords-of-cyberspace/loc3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/lords-of-cyberspace/loc3.png
--------------------------------------------------------------------------------
/images/lords-of-cyberspace/loc4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/lords-of-cyberspace/loc4.png
--------------------------------------------------------------------------------
/images/mech@mor-showdown/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/mech@mor-showdown/0.png
--------------------------------------------------------------------------------
/images/mech@mor-showdown/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/mech@mor-showdown/1.png
--------------------------------------------------------------------------------
/images/mech@mor-showdown/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/mech@mor-showdown/2.png
--------------------------------------------------------------------------------
/images/mech@mor-showdown/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/mech@mor-showdown/3.png
--------------------------------------------------------------------------------
/images/mech@mor-showdown/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/mech@mor-showdown/4.png
--------------------------------------------------------------------------------
/images/mech@mor-showdown/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/mech@mor-showdown/5.png
--------------------------------------------------------------------------------
/images/nongunz-de/NongunzDE_011_Scaled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/nongunz-de/NongunzDE_011_Scaled.png
--------------------------------------------------------------------------------
/images/nongunz-de/NongunzDE_012_Scaled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/nongunz-de/NongunzDE_012_Scaled.png
--------------------------------------------------------------------------------
/images/nongunz-de/NongunzDE_09_Scaled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/nongunz-de/NongunzDE_09_Scaled.png
--------------------------------------------------------------------------------
/images/peglin/peglin1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/peglin/peglin1.png
--------------------------------------------------------------------------------
/images/peglin/peglin2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/peglin/peglin2.png
--------------------------------------------------------------------------------
/images/peglin/peglin3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/peglin/peglin3.png
--------------------------------------------------------------------------------
/images/peglin/peglin4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/peglin/peglin4.png
--------------------------------------------------------------------------------
/images/peglin/peglin5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/peglin/peglin5.png
--------------------------------------------------------------------------------
/images/peglin/peglin_video.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/peglin/peglin_video.mp4
--------------------------------------------------------------------------------
/images/puppy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/puppy.jpg
--------------------------------------------------------------------------------
/images/rift-wizard/ss_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/rift-wizard/ss_16.png
--------------------------------------------------------------------------------
/images/rift-wizard/ss_17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/rift-wizard/ss_17.png
--------------------------------------------------------------------------------
/images/rift-wizard/ss_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/rift-wizard/ss_6.png
--------------------------------------------------------------------------------
/images/rift-wizard/ss_9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/rift-wizard/ss_9.png
--------------------------------------------------------------------------------
/images/roundguard/Roundguard_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/roundguard/Roundguard_1.png
--------------------------------------------------------------------------------
/images/roundguard/Roundguard_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/roundguard/Roundguard_2.png
--------------------------------------------------------------------------------
/images/roundguard/Roundguard_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/roundguard/Roundguard_3.png
--------------------------------------------------------------------------------
/images/ultimate-adom/left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/ultimate-adom/left.png
--------------------------------------------------------------------------------
/images/ultimate-adom/right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/ultimate-adom/right.png
--------------------------------------------------------------------------------
/images/wizard-wars-io/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/wizard-wars-io/0.png
--------------------------------------------------------------------------------
/images/wizard-wars-io/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/wizard-wars-io/1.png
--------------------------------------------------------------------------------
/images/wizard-wars-io/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/wizard-wars-io/2.png
--------------------------------------------------------------------------------
/images/wizard-wars-io/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/images/wizard-wars-io/3.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Roguelike Celebration 2025
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/map.monopic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/map.monopic
--------------------------------------------------------------------------------
/server/.funcignore:
--------------------------------------------------------------------------------
1 | *.js.map
2 | *.ts
3 | .git*
4 | .vscode
5 | local.settings.json
6 | test
7 | tsconfig.json
--------------------------------------------------------------------------------
/server/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "ms-azuretools.vscode-azurefunctions"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/server/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Attach to Node Functions",
6 | "type": "node",
7 | "request": "attach",
8 | "port": 9229,
9 | "preLaunchTask": "func: host start"
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/server/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "azureFunctions.deploySubpath": ".",
3 | "azureFunctions.postDeployTask": "npm install",
4 | "azureFunctions.projectLanguage": "TypeScript",
5 | "azureFunctions.projectRuntime": "~4",
6 | "debug.internalConsoleOptions": "neverOpen",
7 | "azureFunctions.preDeployTask": "npm prune"
8 | }
9 |
--------------------------------------------------------------------------------
/server/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "func",
6 | "command": "host start",
7 | "problemMatcher": "$func-watch",
8 | "isBackground": true,
9 | "dependsOn": "npm build"
10 | },
11 | {
12 | "type": "shell",
13 | "label": "npm build",
14 | "command": "npm run build",
15 | "dependsOn": "npm install",
16 | "problemMatcher": "$tsc"
17 | },
18 | {
19 | "type": "shell",
20 | "label": "npm install",
21 | "command": "npm install"
22 | },
23 | {
24 | "type": "shell",
25 | "label": "npm prune",
26 | "command": "npm prune --production",
27 | "dependsOn": "npm build",
28 | "problemMatcher": []
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/server/ARM Template Notes.md:
--------------------------------------------------------------------------------
1 | Some scrambled notes on deployment, for future-Emilia or anyone else who touches our devops (I'm sorry!)
2 |
3 | ## ASWA and Azure Functions
4 | I tried to unify our ASWA and Functions set up.
5 | - We can't use the built-in magical functions because they only support HTTP triggers (and we need SignalR triggers, timer triggers, and likely soon PubSub triggeres)
6 | - The "byo functions" option where you link a function app would work, but requires all dev accounts to use a paid plan, which is annoying
7 | - For now, we just manually add CORS entries based on the completed URL of the ASWA. This means getting the dependency chain right
8 |
9 | ## GitHub Environments
10 | I'm deeply unhappy with how we handle deploy environments. The main repo has a separate set of variables for e.g. `STAGING_[some secret name]`. The actual underlying problem is the confusion whereby any forks from volunteer devs are stuck in the same boat of "manual prod deploys, automatic staging deploys", which they probably don't want, and they can't change without making upstream PRs annoying.
11 |
12 | We could refactor this to use GitHub environments. I don't think it saves us anything other than moving around some variable names, as it then un-DRYs my shared logic for prod vs staging deploys.
--------------------------------------------------------------------------------
/server/ARM-TODO.md:
--------------------------------------------------------------------------------
1 | * Enable CORS
2 | * Is Application Insights on?
3 | * Actual deployment
4 | * "Storage is not configured properly, scaling will be limited"
--------------------------------------------------------------------------------
/server/addObeliskNote/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | },
18 | {
19 | "type": "webPubSub",
20 | "name": "actions",
21 | "hub": "chat",
22 | "direction": "out"
23 | },
24 | {
25 | "tableName": "auditLog",
26 | "name": "tableBinding",
27 | "type": "table",
28 | "direction": "out"
29 | }
30 | ],
31 | "scriptFile": "../dist/addObeliskNote/index.js"
32 | }
--------------------------------------------------------------------------------
/server/addObeliskNote/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import addObeliskNote from '../src/endpoints/obelisk/addObeliskNote'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, addObeliskNote, { audit: true })
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/addObeliskNote/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/addRoomNote/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | },
18 | {
19 | "type": "webPubSub",
20 | "name": "actions",
21 | "hub": "chat",
22 | "direction": "out"
23 | },
24 | {
25 | "tableName": "auditLog",
26 | "name": "tableBinding",
27 | "type": "table",
28 | "direction": "out"
29 | }
30 | ],
31 | "scriptFile": "../dist/addRoomNote/index.js"
32 | }
33 |
--------------------------------------------------------------------------------
/server/addRoomNote/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import addRoomNote from '../src/endpoints/roomNote/addRoomNote'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, addRoomNote, { audit: true })
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/addRoomNote/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/banUser/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "http",
12 | "direction": "out",
13 | "name": "res"
14 | },
15 | {
16 | "type": "webPubSub",
17 | "name": "actions",
18 | "hub": "chat",
19 | "direction": "out"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/banUser/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/banUser/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import banUser from '../src/endpoints/banUser'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await authenticatedAzureWrap(context, req, banUser, { audit: true, mod: true })
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/banUser/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/clientDeployedWebhook/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | }
21 | ],
22 | "scriptFile": "../dist/clientDeployedWebhook/index.js"
23 | }
24 |
--------------------------------------------------------------------------------
/server/clientDeployedWebhook/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { azureWrap } from '../src/azureWrap'
3 | import clientDeployedWebhook from '../src/endpoints/clientDeployedWebhook'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await azureWrap(context, req, clientDeployedWebhook)
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/clientDeployedWebhook/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/cognitiveServicesKey/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | }
18 | ],
19 | "scriptFile": "../dist/cognitiveServicesKey/index.js"
20 | }
21 |
--------------------------------------------------------------------------------
/server/cognitiveServicesKey/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { getUserIdFromHeaders } from '../src/authenticate'
3 |
4 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
5 | const userId = await getUserIdFromHeaders(context, req)
6 |
7 | if (!userId) {
8 | context.res = {
9 | status: 401,
10 | body: 'The user name is required to ensure their access token'
11 | }
12 | return
13 | }
14 |
15 | context.res = {
16 | body: {
17 | cognitiveServicesKey: process.env.COGNITIVE_SERVICES_KEY,
18 | cognitiveServicesRegion: process.env.COGNITIVE_SERVICES_REGION
19 | }
20 | }
21 | }
22 |
23 | export default httpTrigger
24 |
--------------------------------------------------------------------------------
/server/cognitiveServicesKey/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/connect/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/connect/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/connect/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import connect from '../src/endpoints/connect'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | context.log('In connect')
10 | await authenticatedAzureWrap(context, req, connect, { audit: true })
11 | }
12 |
13 | export default httpTrigger
14 |
--------------------------------------------------------------------------------
/server/connect/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/deleteMessage/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["post"]
9 | },
10 | {
11 | "type": "http",
12 | "direction": "out",
13 | "name": "res"
14 | },
15 | {
16 | "type": "webPubSub",
17 | "name": "actions",
18 | "hub": "chat",
19 | "direction": "out"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/deleteMessage/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/deleteMessage/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import deleteMessage from '../src/endpoints/deleteMessage'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await authenticatedAzureWrap(context, req, deleteMessage, { audit: true, mod: true })
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/deleteObeliskNote/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | },
18 | {
19 | "type": "webPubSub",
20 | "name": "actions",
21 | "hub": "chat",
22 | "direction": "out"
23 | },
24 | {
25 | "tableName": "auditLog",
26 | "name": "tableBinding",
27 | "type": "table",
28 | "direction": "out"
29 | }
30 | ],
31 | "scriptFile": "../dist/deleteObeliskNote/index.js"
32 | }
--------------------------------------------------------------------------------
/server/deleteObeliskNote/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import deleteRoomNote from '../src/endpoints/roomNote/deleteRoomNote'
4 | import deleteObeliskNote from '../src/endpoints/obelisk/deleteObeliskNote'
5 |
6 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
7 | await authenticatedAzureWrap(context, req, deleteObeliskNote, { audit: true })
8 | }
9 |
10 | export default httpTrigger
11 |
--------------------------------------------------------------------------------
/server/deleteObeliskNote/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/deleteRoom/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | }
18 | ],
19 | "scriptFile": "../dist/deleteRoom/index.js"
20 | }
21 |
--------------------------------------------------------------------------------
/server/deleteRoom/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import deleteRoom from '../src/endpoints/deleteRoom'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | // This doesn't *need* to be mod-only,
10 | // but as long as we're only calling it in the editor, why not
11 | await authenticatedAzureWrap(context, req, deleteRoom, { mod: true })
12 | }
13 |
14 | export default httpTrigger
15 |
--------------------------------------------------------------------------------
/server/deleteRoom/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/deleteRoomNote/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | },
18 | {
19 | "type": "webPubSub",
20 | "name": "actions",
21 | "hub": "chat",
22 | "direction": "out"
23 | },
24 | {
25 | "tableName": "auditLog",
26 | "name": "tableBinding",
27 | "type": "table",
28 | "direction": "out"
29 | }
30 | ],
31 | "scriptFile": "../dist/deleteRoomNote/index.js"
32 | }
33 |
--------------------------------------------------------------------------------
/server/deleteRoomNote/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import deleteRoomNote from '../src/endpoints/roomNote/deleteRoomNote'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, deleteRoomNote, { audit: true })
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/deleteRoomNote/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/disconnect/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/disconnect/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/disconnect/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import disconnect from '../src/endpoints/disconnect'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await authenticatedAzureWrap(context, req, disconnect, { audit: true })
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/disconnect/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/displayMessage/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | }
21 | ],
22 | "scriptFile": "../dist/displayMessage/index.js"
23 | }
24 |
--------------------------------------------------------------------------------
/server/displayMessage/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import displayMessage from '../src/endpoints/displayMessage'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, displayMessage)
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/displayMessage/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/equipBadge/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "http",
12 | "direction": "out",
13 | "name": "res"
14 | },
15 | {
16 | "type": "webPubSub",
17 | "name": "actions",
18 | "hub": "chat",
19 | "direction": "out"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/equipBadge/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/equipBadge/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import equipBadgeFunction from '../src/endpoints/equipBadge'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | console.log('In equipBadge??')
10 | await authenticatedAzureWrap(context, req, equipBadgeFunction, { audit: true })
11 | }
12 |
13 | export default httpTrigger
14 |
--------------------------------------------------------------------------------
/server/equipBadge/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/fetchProfile/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | }
18 | ],
19 | "scriptFile": "../dist/fetchProfile/index.js"
20 | }
21 |
--------------------------------------------------------------------------------
/server/fetchProfile/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { azureWrap } from '../src/azureWrap'
3 | import fetchProfile from '../src/endpoints/fetchProfile'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await azureWrap(context, req, fetchProfile)
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/fetchProfile/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/getAllRooms/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | }
18 | ],
19 | "scriptFile": "../dist/getAllRooms/index.js"
20 | }
21 |
--------------------------------------------------------------------------------
/server/getAllRooms/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import getAllRooms from '../src/endpoints/getAllRooms'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | // This doesn't *need* to be mod-only,
10 | // but as long as we're only calling it in the editor, why not
11 | await authenticatedAzureWrap(context, req, getAllRooms, { mod: true })
12 | }
13 |
14 | export default httpTrigger
15 |
--------------------------------------------------------------------------------
/server/getAllRooms/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/getRoom/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | }
18 | ],
19 | "scriptFile": "../dist/getRoom/index.js"
20 | }
21 |
--------------------------------------------------------------------------------
/server/getRoom/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import getRoom from '../src/endpoints/getRoom'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | // This doesn't *need* to be mod-only,
10 | // but as long as we're only calling it in the editor, why not
11 | await authenticatedAzureWrap(context, req, getRoom, { mod: true })
12 | }
13 |
14 | export default httpTrigger
15 |
--------------------------------------------------------------------------------
/server/getRoom/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/getRoomIds/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | }
18 | ],
19 | "scriptFile": "../dist/getRoomIds/index.js"
20 | }
21 |
--------------------------------------------------------------------------------
/server/getRoomIds/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import getRoomIds from '../src/endpoints/getRoomIds'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | // This doesn't *need* to be mod-only,
10 | // but as long as we're only calling it in the editor, why not
11 | await authenticatedAzureWrap(context, req, getRoomIds, { mod: true })
12 | }
13 |
14 | export default httpTrigger
15 |
--------------------------------------------------------------------------------
/server/getRoomIds/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/heartbeat/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "name": "myTimer",
5 | "type": "timerTrigger",
6 | "direction": "in",
7 | "schedule": "0 * * * * *"
8 | },
9 | {
10 | "type": "webPubSub",
11 | "name": "actions",
12 | "hub": "chat",
13 | "direction": "out"
14 | }
15 | ],
16 | "scriptFile": "../dist/heartbeat/index.js"
17 | }
18 |
--------------------------------------------------------------------------------
/server/heartbeat/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context } from '@azure/functions'
2 | import { azureWrap } from '../src/azureWrap'
3 | import heartbeat from '../src/endpoints/heartbeat'
4 |
5 | const timerTrigger: AzureFunction = async function (
6 | context: Context,
7 | myTimer: any
8 | ): Promise {
9 | await azureWrap(context, undefined, heartbeat)
10 | }
11 |
12 | export default timerTrigger
13 |
--------------------------------------------------------------------------------
/server/heartbeat/readme.md:
--------------------------------------------------------------------------------
1 | # TimerTrigger - TypeScript
2 |
3 | The `TimerTrigger` makes it incredibly easy to have your functions executed on a schedule. This sample demonstrates a simple use case of calling your function every 5 minutes.
4 |
5 | ## How it works
6 |
7 | For a `TimerTrigger` to work, you provide a schedule in the form of a [cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)(See the link for full details). A cron expression is a string with 6 separate expressions which represent a given schedule via patterns. The pattern we use to represent every 5 minutes is `0 */5 * * * *`. This, in plain text, means: "When seconds is equal to 0, minutes is divisible by 5, for any hour, day of the month, month, day of the week, or year".
8 |
9 | ## Learn more
10 |
11 | Documentation
--------------------------------------------------------------------------------
/server/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0",
3 | "logging": {
4 | "applicationInsights": {
5 | "samplingSettings": {
6 | "isEnabled": true,
7 | "excludedTypes": "Request"
8 | }
9 | }
10 | },
11 | "extensionBundle": {
12 | "id": "Microsoft.Azure.Functions.ExtensionBundle",
13 | "version": "[3.3.*, 4.0.0)"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/isRegistered/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | }
18 | ],
19 | "scriptFile": "../dist/isRegistered/index.js"
20 | }
21 |
--------------------------------------------------------------------------------
/server/isRegistered/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { azureWrap } from '../src/azureWrap'
3 | import isRegistered from '../src/endpoints/isRegistered'
4 | import { getUserIdFromHeaders } from '../src/authenticate'
5 |
6 | const httpTrigger: AzureFunction = async function (
7 | context: Context,
8 | req: HttpRequest
9 | ): Promise {
10 | const userId = await getUserIdFromHeaders(context, req)
11 | await azureWrap(context, req, isRegistered, { userId: userId })
12 | }
13 |
14 | export default httpTrigger
15 |
--------------------------------------------------------------------------------
/server/isRegistered/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/leaveVideoChat/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | }
21 | ],
22 | "scriptFile": "../dist/leaveVideoChat/index.js"
23 | }
24 |
--------------------------------------------------------------------------------
/server/leaveVideoChat/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 |
3 | import { authenticatedAzureWrap } from '../src/azureWrap'
4 | import leaveVideoChat from '../src/endpoints/leaveVideoChat'
5 |
6 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
7 | await authenticatedAzureWrap(context, req, leaveVideoChat)
8 | }
9 |
10 | export default httpTrigger
11 |
--------------------------------------------------------------------------------
/server/leaveVideoChat/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/likeObeliskNote/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | },
18 | {
19 | "type": "webPubSub",
20 | "name": "actions",
21 | "hub": "chat",
22 | "direction": "out"
23 | }
24 | ],
25 | "scriptFile": "../dist/likeObeliskNote/index.js"
26 | }
--------------------------------------------------------------------------------
/server/likeObeliskNote/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import likeObeliskNote from '../src/endpoints/obelisk/likeObeliskNote'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, likeObeliskNote)
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/likeObeliskNote/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/likeRoomNote/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | },
18 | {
19 | "type": "webPubSub",
20 | "name": "actions",
21 | "hub": "chat",
22 | "direction": "out"
23 | }
24 | ],
25 | "scriptFile": "../dist/likeRoomNote/index.js"
26 | }
27 |
--------------------------------------------------------------------------------
/server/likeRoomNote/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import likeRoomNote from '../src/endpoints/roomNote/likeRoomNote'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, likeRoomNote)
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/likeRoomNote/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/moveAllUsersToEntryway/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/moveAllUsersToEntryway/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/moveAllUsersToEntryway/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import moveAllUsersToEntryway from '../src/endpoints/moveAllUsersToEntryway'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await authenticatedAzureWrap(context, req, moveAllUsersToEntryway)
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/moveAllUsersToEntryway/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/moveRoom/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/moveRoom/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/moveRoom/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import moveRoom from '../src/endpoints/moveRoom'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await authenticatedAzureWrap(context, req, moveRoom)
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/moveRoom/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/negotiate/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [{
3 | "authLevel": "anonymous",
4 | "type": "httpTrigger",
5 | "direction": "in",
6 | "name": "req",
7 | "methods": [
8 | "post"
9 | ]
10 | },
11 | {
12 | "type": "http",
13 | "direction": "out",
14 | "name": "res"
15 | },
16 | {
17 | "type": "signalRConnectionInfo",
18 | "name": "connectionInfo",
19 | "hubName": "chat",
20 | "userId": "{headers.userid}",
21 | "connectionStringSetting": "AzureSignalRConnectionString",
22 | "direction": "in"
23 | }
24 | ],
25 | "scriptFile": "../dist/negotiate/index.js"
26 | }
--------------------------------------------------------------------------------
/server/negotiate/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 |
3 | const httpTrigger: AzureFunction = async function (
4 | context: Context,
5 | req: HttpRequest,
6 | connectionInfo
7 | ): Promise {
8 | context.res.json(connectionInfo)
9 | }
10 |
11 | export default httpTrigger
12 |
--------------------------------------------------------------------------------
/server/negotiate/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/negotiatePubSub/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "http",
12 | "direction": "out",
13 | "name": "res"
14 | },
15 | {
16 | "type": "webPubSubConnection",
17 | "name": "connection",
18 | "hub": "chat",
19 | "userId": "{headers.userid}",
20 | "direction": "in"
21 | }
22 | ],
23 | "scriptFile": "../dist/negotiatePubSub/index.js"
24 | }
--------------------------------------------------------------------------------
/server/negotiatePubSub/index.ts:
--------------------------------------------------------------------------------
1 | module.exports = function (context, req, connection) {
2 | context.res = { body: connection }
3 | context.done()
4 | }
5 |
--------------------------------------------------------------------------------
/server/negotiatePubSub/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/openOrCloseSpace/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/openOrCloseSpace/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/openOrCloseSpace/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import openOrCloseSpace from '../src/endpoints/openOrCloseSpace'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, openOrCloseSpace, { audit: true, mod: true })
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/openOrCloseSpace/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/orderNewDrink/function.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "bindings": [
4 | {
5 | "authLevel": "anonymous",
6 | "type": "httpTrigger",
7 | "direction": "in",
8 | "name": "req",
9 | "methods": [ "get", "post" ]
10 | },
11 | {
12 | "type": "http",
13 | "direction": "out",
14 | "name": "res"
15 | },
16 | {
17 | "type": "webPubSub",
18 | "name": "actions",
19 | "hub": "chat",
20 | "direction": "out"
21 | },
22 | {
23 | "tableName": "auditLog",
24 | "name": "tableBinding",
25 | "type": "table",
26 | "direction": "out"
27 | }
28 | ],
29 | "scriptFile": "../dist/orderNewDrink/index.js"
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/server/orderNewDrink/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import orderNewDrink from '../src/endpoints/orderNewDrink'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await authenticatedAzureWrap(context, req, orderNewDrink, { audit: true })
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "scripts": {
6 | "build": "tsc",
7 | "watch": "tsc -w",
8 | "deploy": "func azure functionapp publish roguelike-celebration-mud",
9 | "prestart": "npm run build",
10 | "start": "func start",
11 | "test": "echo \"No tests yet...\""
12 | },
13 | "dependencies": {
14 | "@azure/communication-administration": "^1.0.0-beta.3",
15 | "@azure/cosmos": "^3.9.5",
16 | "firebase-admin": "^9.11.1",
17 | "node-fetch": "^2.6.7",
18 | "redis": "^3.0.2",
19 | "storyboard-engine": "0.0.5",
20 | "tracery-grammar": "^2.7.4",
21 | "twilio": "^3.55.0",
22 | "uuid": "^8.3.0"
23 | },
24 | "devDependencies": {
25 | "@azure/functions": "^1.0.2-beta2",
26 | "@types/redis": "^2.8.24",
27 | "recursive-readdir": "^2.2.2",
28 | "typescript": "^3.3.3"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/server/pickUpItem/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | }
21 | ],
22 | "scriptFile": "../dist/pickUpItem/index.js"
23 | }
24 |
--------------------------------------------------------------------------------
/server/pickUpItem/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import pickUpItem from '../src/endpoints/pickUpItem'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, pickUpItem)
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/pickUpItem/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/pong/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | }
18 | ],
19 | "scriptFile": "../dist/pong/index.js"
20 | }
21 |
--------------------------------------------------------------------------------
/server/pong/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap, azureWrap } from '../src/azureWrap'
3 | import pong from '../src/endpoints/pong'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await authenticatedAzureWrap(context, req, pong)
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/pong/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/proxies.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/proxies",
3 | "proxies": {}
4 | }
5 |
--------------------------------------------------------------------------------
/server/resetBadgeData/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "http",
12 | "direction": "out",
13 | "name": "res"
14 | },
15 | {
16 | "type": "webPubSub",
17 | "name": "actions",
18 | "hub": "chat",
19 | "direction": "out"
20 | }
21 | ],
22 | "scriptFile": "../dist/resetBadgeData/index.js"
23 | }
24 |
--------------------------------------------------------------------------------
/server/resetBadgeData/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import resetBadgeData from '../src/endpoints/resetBadgeData'
3 | import { authenticatedAzureWrap } from '../src/azureWrap'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await authenticatedAzureWrap(context, req, resetBadgeData, { mod: true })
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/resetBadgeData/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/resetRoomData/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "http",
12 | "direction": "out",
13 | "name": "res"
14 | }
15 | ],
16 | "scriptFile": "../dist/resetRoomData/index.js"
17 | }
18 |
--------------------------------------------------------------------------------
/server/resetRoomData/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import resetRoomData from '../src/endpoints/resetRoomData'
3 | import { authenticatedAzureWrap } from '../src/azureWrap'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await authenticatedAzureWrap(context, req, resetRoomData, { mod: true })
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/resetRoomData/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/sendCaption/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/sendCaption/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/sendCaption/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import sendCaption from '../src/endpoints/sendCaption'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await authenticatedAzureWrap(context, req, sendCaption, { audit: true })
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/sendCaption/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/sendChatMessage/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/sendChatMessage/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/sendChatMessage/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import sendChatMessage from '../src/endpoints/sendChatMessage'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | await authenticatedAzureWrap(context, req, sendChatMessage, { audit: true })
10 | }
11 |
12 | export default httpTrigger
13 |
--------------------------------------------------------------------------------
/server/sendChatMessage/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/serverSettings/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "http",
12 | "direction": "out",
13 | "name": "res"
14 | },
15 | {
16 | "type": "webPubSub",
17 | "name": "actions",
18 | "hub": "chat",
19 | "direction": "out"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/serverSettings/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/serverSettings/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { getServerSettings, postServerSettings } from '../src/endpoints/serverSettings'
3 | import { azureWrap, authenticatedAzureWrap } from '../src/azureWrap'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | if (req.method === 'GET') {
10 | await azureWrap(context, req, getServerSettings)
11 | } else if (req.method === 'POST') {
12 | await authenticatedAzureWrap(context, req, postServerSettings, { mod: true })
13 | }
14 | }
15 |
16 | export default httpTrigger
17 |
--------------------------------------------------------------------------------
/server/src/allowedItems.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | 'Roguelike Celebration socks',
3 | 'a +1 longbow',
4 | 'an unidentified scroll',
5 | 'a tiny puppy',
6 | 'Platinum Yendorian Express Card',
7 | 'a wand of digging',
8 | 'a Proof of Stremf',
9 | 'a shotgun',
10 | 'a pair of seven league boots',
11 | 'Planepacked',
12 | 'Divine Nectar',
13 | 'the Fizzbuzz',
14 | 'Yet Another Silly Drink',
15 | 'Orb of Zot',
16 | 'Amulet of Yendor',
17 | 'box of crayons',
18 | 'lemonade',
19 | 'slightly stale cookies',
20 | 'negasocks',
21 | 'negalongbow',
22 | 'healing scroll',
23 | 'negapuppy',
24 | 'Her-yell, Goblin Knife-Princess',
25 | 'Mr Stabbums, Battle Kitty',
26 | 'Ooze, Faithful Sidekick',
27 | 'Roguelike Celebration Shower Curtain',
28 | 'a mystic dagger',
29 | 'a pet eel',
30 | 'a turkey leg of Yendor'
31 | ]
32 |
--------------------------------------------------------------------------------
/server/src/config.ts:
--------------------------------------------------------------------------------
1 | export const MESSAGE_MAX_LENGTH = 631
2 |
--------------------------------------------------------------------------------
/server/src/cookie.ts:
--------------------------------------------------------------------------------
1 | import { User } from './user'
2 | import generators from '../src/generators'
3 | import { Result } from './endpoint'
4 |
5 | export function cookie (user: User, messageId: string): Result {
6 | const generator = generators.fortuneCookies
7 |
8 | if (!generator) {
9 | return {
10 | httpResponse: {
11 | status: 400,
12 | body: { error: 'You included an invalid list: fortuneCookies' }
13 | }
14 | }
15 | }
16 |
17 | const fortune = generator.generate()
18 | const privateActionString = generator.actionString(fortune)
19 |
20 | return {
21 | messages: [
22 | {
23 | groupId: user.roomId,
24 | target: 'emote',
25 | arguments: [messageId, user.id, 'cracks open a fortune cookie.']
26 | },
27 | {
28 | userId: user.id,
29 | target: 'privateCommand',
30 | arguments: [privateActionString]
31 | }
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/src/endpoints/clientDeployedWebhook.ts:
--------------------------------------------------------------------------------
1 | import { EndpointFunction, LogFn } from '../endpoint'
2 |
3 | const clientDeployedWebhook: EndpointFunction = async (inputs: any, log: LogFn) => {
4 | const inputtedKey = inputs.key
5 | const actualKey = process.env.DEPLOY_WEBHOOK_KEY
6 | if (!inputtedKey || inputtedKey !== actualKey) {
7 | return {
8 | httpResponse: {
9 | status: 403
10 | }
11 | }
12 | }
13 |
14 | return {
15 | messages: [
16 | {
17 | target: 'clientDeployed',
18 | arguments: []
19 | }
20 | ]
21 | }
22 | }
23 |
24 | export default clientDeployedWebhook
25 |
--------------------------------------------------------------------------------
/server/src/endpoints/deleteMessage.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import { User } from '../user'
3 |
4 | const deleteMessage: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
5 | const messageId = inputs.messageId
6 | if (!messageId) {
7 | return {
8 | httpResponse: {
9 | status: 400,
10 | body: { error: 'You did not include a message to delete.' }
11 | }
12 | }
13 | }
14 |
15 | return {
16 | messages: [
17 | {
18 | target: 'deleteMessage',
19 | arguments: [user.id, messageId]
20 | }
21 | ],
22 | httpResponse: {
23 | status: 200,
24 | body: {}
25 | }
26 | }
27 | }
28 |
29 | export default deleteMessage
30 |
--------------------------------------------------------------------------------
/server/src/endpoints/deleteRoom.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import DB from '../redis'
3 | import { User } from '../user'
4 |
5 | const getRoomIds: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const roomId = inputs.roomId
7 | if (!roomId) {
8 | return {
9 | httpResponse: {
10 | status: 200,
11 | body: { error: 'You did not include a roomId to delete' }
12 | }
13 | }
14 | }
15 |
16 | const room = await DB.deleteRoomData(roomId)
17 |
18 | return {
19 | httpResponse: {
20 | status: 200,
21 | body: { room }
22 | }
23 | }
24 | }
25 |
26 | export default getRoomIds
27 |
--------------------------------------------------------------------------------
/server/src/endpoints/disconnect.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn, Message } from '../endpoint'
2 | import { User } from '../user'
3 | import { DB } from '../database'
4 | import { globalPresenceMessage } from '../globalPresenceMessage'
5 |
6 | const disconnect: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
7 | await DB.setUserAsActive(user, false)
8 |
9 | return {
10 | groupManagementTasks: [{
11 | userId: user.id,
12 | groupId: user.roomId,
13 | action: 'remove'
14 | }],
15 | messages: [
16 | {
17 | groupId: user.roomId,
18 | target: 'playerDisconnected',
19 | arguments: [user.id]
20 | },
21 | await globalPresenceMessage([user.roomId])
22 | ],
23 | httpResponse: {
24 | status: 200
25 | }
26 | }
27 | }
28 |
29 | export default disconnect
30 |
--------------------------------------------------------------------------------
/server/src/endpoints/displayMessage.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn, Message } from '../endpoint'
2 | import { User } from '../user'
3 | import generators from '../generators'
4 |
5 | const displayMessage: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | let displayMessage: string
7 | let actionMessage: string
8 |
9 | if (inputs.list) {
10 | const generator = generators[inputs.list]
11 |
12 | if (!generator) {
13 | return {
14 | httpResponse: {
15 | status: 400,
16 | body: { error: `You included an invalid list: ${inputs.list}` }
17 | }
18 | }
19 | }
20 |
21 | displayMessage = generator.generate()
22 | actionMessage = generator.actionString(displayMessage)
23 | log(displayMessage)
24 | log(actionMessage)
25 | } else if (inputs.message) {
26 | actionMessage = inputs.message
27 | } else {
28 | return {
29 | httpResponse: {
30 | status: 400,
31 | body: { error: 'Include an item or list!' }
32 | }
33 | }
34 | }
35 |
36 | const messages: Message[] = [
37 | {
38 | userId: user.id,
39 | target: 'privateCommand',
40 | arguments: [actionMessage]
41 | }
42 | ]
43 |
44 | return {
45 | messages,
46 | httpResponse: { status: 200 }
47 | }
48 | }
49 |
50 | export default displayMessage
51 |
--------------------------------------------------------------------------------
/server/src/endpoints/equipBadge.ts:
--------------------------------------------------------------------------------
1 | import { isNumber } from 'lodash'
2 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
3 | import { equipBadge, minimizeUser, User } from '../user'
4 |
5 | const equipBadgeFunction: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const { badge, index } = inputs
7 |
8 | if (!isNumber(index)) {
9 | return {
10 | httpResponse: {
11 | status: 400,
12 | body: { error: "You didn't pass in an index!" }
13 | }
14 | }
15 | } else if (index < 0 || index > 1) {
16 | return {
17 | httpResponse: {
18 | status: 400,
19 | body: { error: 'Index must be 0 or 1!' }
20 | }
21 | }
22 | }
23 |
24 | const newUser = await equipBadge(user.id, badge, index)
25 | return {
26 | messages: [{
27 | target: 'usernameMap',
28 | arguments: [{ [user.id]: minimizeUser(newUser) }]
29 | }],
30 | httpResponse: {
31 | status: 200,
32 | body: { badges: newUser.equippedBadges }
33 | }
34 | }
35 | }
36 |
37 | export default equipBadgeFunction
38 |
--------------------------------------------------------------------------------
/server/src/endpoints/fetchProfile.ts:
--------------------------------------------------------------------------------
1 | import { EndpointFunction, LogFn } from '../endpoint'
2 | import { DB } from '../database'
3 |
4 | const fetchProfile: EndpointFunction = async (inputs: any, log: LogFn) => {
5 | const userId = inputs.userId
6 | if (!inputs.userId) {
7 | return {
8 | httpResponse: {
9 | status: 200,
10 | body: { error: 'You did not include a userId to fetch' }
11 | }
12 | }
13 | }
14 |
15 | const user = await DB.getUser(inputs.userId)
16 | return {
17 | httpResponse: {
18 | status: 200,
19 | body: { user }
20 | }
21 | }
22 | }
23 |
24 | export default fetchProfile
25 |
--------------------------------------------------------------------------------
/server/src/endpoints/getAllRooms.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import DB from '../redis'
3 | import { Room } from '../rooms'
4 | import { User } from '../user'
5 |
6 | const getRoomIds: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
7 | const roomIds = await DB.getRoomIds()
8 | const roomData: {[roomId: string]: Room} = {}
9 |
10 | await Promise.all(roomIds.map(async (roomId) => {
11 | const room = await DB.getRoomData(roomId)
12 | roomData[roomId] = room
13 | }))
14 | return {
15 | httpResponse: {
16 | status: 200,
17 | body: { roomData }
18 | }
19 | }
20 | }
21 |
22 | export default getRoomIds
23 |
--------------------------------------------------------------------------------
/server/src/endpoints/getRoom.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import DB from '../redis'
3 | import { User } from '../user'
4 |
5 | const getRoomIds: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const roomId = inputs.roomId
7 | if (!roomId) {
8 | return {
9 | httpResponse: {
10 | status: 200,
11 | body: { error: 'You did not include a roomId to fetch' }
12 | }
13 | }
14 | }
15 | const room = await DB.getRoomData(inputs.roomId)
16 | return {
17 | httpResponse: {
18 | status: 200,
19 | body: { room }
20 | }
21 | }
22 | }
23 |
24 | export default getRoomIds
25 |
--------------------------------------------------------------------------------
/server/src/endpoints/getRoomIds.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import DB from '../redis'
3 | import { User } from '../user'
4 |
5 | const getRoomIds: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const roomIds = await DB.getRoomIds()
7 | return {
8 | httpResponse: {
9 | status: 200,
10 | body: { roomIds }
11 | }
12 | }
13 | }
14 |
15 | export default getRoomIds
16 |
--------------------------------------------------------------------------------
/server/src/endpoints/isRegistered.ts:
--------------------------------------------------------------------------------
1 | import { EndpointFunction, LogFn } from '../endpoint'
2 | import DB from '../redis'
3 |
4 | const isRegistered: EndpointFunction = async (inputs: any, log: LogFn) => {
5 | if (!inputs.userId) {
6 | return {
7 | httpResponse: {
8 | status: 401,
9 | body: { registered: false, error: 'You are not logged in!' }
10 | }
11 | }
12 | }
13 |
14 | log('Checking if user is registered', inputs.userId)
15 |
16 | const user = await DB.getUser(inputs.userId)
17 | const spaceIsClosed = (await DB.getServerSettings()).spaceIsClosed
18 |
19 | log('Got user?', user)
20 |
21 | return {
22 | httpResponse: {
23 | status: 200,
24 | body: { registered: user && user.username, spaceIsClosed, isMod: user && user.isMod, isBanned: user && user.isBanned }
25 | }
26 | }
27 | }
28 |
29 | export default isRegistered
30 |
--------------------------------------------------------------------------------
/server/src/endpoints/leaveVideoChat.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import { User } from '../user'
3 | import { DB } from '../database'
4 |
5 | // TODO: Fully remove this, as video chat is no longer supported and this should never be invoked on account of there
6 | // being no way to enter video chat!
7 | const leaveVideoChat: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
8 | const videoChatters = await DB.updateVideoPresenceForUser(user, false)
9 |
10 | return {
11 | messages: []
12 | }
13 | }
14 |
15 | export default leaveVideoChat
16 |
--------------------------------------------------------------------------------
/server/src/endpoints/moveRoom.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import { moveToRoom } from '../moveToRoom'
3 | import { User } from '../user'
4 |
5 | const moveRoom: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const toId = inputs.to
7 | if (!toId) {
8 | return {
9 | httpResponse: {
10 | status: 400,
11 | body: 'Include a room ID!'
12 | }
13 | }
14 | }
15 |
16 | return await moveToRoom(user, toId)
17 | }
18 |
19 | export default moveRoom
20 |
--------------------------------------------------------------------------------
/server/src/endpoints/obelisk/addObeliskNote.ts:
--------------------------------------------------------------------------------
1 | import DB from '../../redis'
2 | import { User } from '../../user'
3 | import { AuthenticatedEndpointFunction, LogFn } from '../../endpoint'
4 |
5 | const addObeliskNote: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const message = inputs.message
7 | const id = inputs.id
8 | if (!message || !id) {
9 | return {
10 | httpResponse: {
11 | status: 500,
12 | body: 'Include a post-it message and an ID!'
13 | }
14 | }
15 | }
16 |
17 | await DB.addRoomNote('obelisk', { id, message, authorId: user.id })
18 |
19 | return {
20 | messages: [
21 | {
22 | groupId: 'sidebar-obelisk',
23 | target: 'obeliskNoteAdded',
24 | arguments: [id, message, user.id]
25 | },
26 | {
27 | groupId: 'obelisk',
28 | target: 'noteAdded',
29 | arguments: ['obelisk', id, message, user.id]
30 | }
31 | ],
32 | httpResponse: {
33 | status: 200
34 | }
35 | }
36 | }
37 |
38 | export default addObeliskNote
39 |
--------------------------------------------------------------------------------
/server/src/endpoints/obelisk/deleteObeliskNote.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn, Message } from '../../endpoint'
2 | import { isMod, User } from '../../user'
3 | import DB from '../../redis'
4 | import { v4 as uuid } from 'uuid'
5 |
6 | const deleteObeliskNote: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
7 | const noteId = inputs.noteId
8 | if (!noteId) {
9 | return {
10 | httpResponse: {
11 | status: 500,
12 | body: 'Include a note ID!'
13 | }
14 | }
15 | }
16 |
17 | const notes = await DB.getRoomNotes('obelisk')
18 | const note = notes.find(n => n.id === noteId)
19 |
20 | if (note.authorId !== user.id && !(await isMod(user.id))) {
21 | return {
22 | httpResponse: {
23 | status: 403,
24 | body: 'You cannot delete this note!'
25 | }
26 | }
27 | }
28 |
29 | await DB.deleteRoomNote('obelisk', noteId)
30 |
31 | const messages: Message[] = [
32 | {
33 | groupId: 'obelisk',
34 | target: 'noteRemoved',
35 | arguments: ['obelisk', noteId]
36 | },
37 | {
38 | groupId: 'sidebar-obelisk',
39 | target: 'obeliskNoteRemoved',
40 | arguments: [noteId]
41 | }
42 | ]
43 |
44 | if (note.authorId !== user.id) {
45 | messages.push({
46 | userId: note.authorId,
47 | target: 'emote',
48 | arguments: [uuid(), user.id, 'has removed a note of yours from the obelisk']
49 | })
50 | }
51 |
52 | return {
53 | messages,
54 | httpResponse: { status: 200 }
55 | }
56 | }
57 |
58 | export default deleteObeliskNote
59 |
--------------------------------------------------------------------------------
/server/src/endpoints/obelisk/likeObeliskNote.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../../endpoint'
2 | import { User } from '../../user'
3 | import DB from '../../redis'
4 |
5 | const likeObeliskNote: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const noteId = inputs.noteId
7 | if (!noteId) {
8 | return {
9 | httpResponse: {
10 | status: 500,
11 | body: 'Include a note ID!'
12 | }
13 | }
14 | }
15 |
16 | const doLike = inputs.like
17 | let likes: string[] = []
18 | if (doLike) {
19 | likes = await DB.likeRoomNote('obelisk', noteId, user.id)
20 | } else {
21 | likes = await DB.unlikeRoomNote('obelisk', noteId, user.id)
22 | }
23 |
24 | return {
25 | messages: [
26 | {
27 | groupId: 'sidebar-obelisk',
28 | target: 'obeliskNoteLikesUpdated',
29 | arguments: [noteId, likes]
30 | },
31 | {
32 | groupId: 'obelisk',
33 | target: 'noteLikesUpdated',
34 | arguments: ['obelisk', noteId, likes]
35 | }
36 | ],
37 | httpResponse: { status: 200 }
38 | }
39 | }
40 |
41 | export default likeObeliskNote
42 |
--------------------------------------------------------------------------------
/server/src/endpoints/obelisk/startObservingObelisk.ts:
--------------------------------------------------------------------------------
1 | import DB from '../../redis'
2 | import { User } from '../../user'
3 | import { AuthenticatedEndpointFunction, LogFn } from '../../endpoint'
4 |
5 | const startObservingObelisk: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | // The DB just knows there's a single obelisk, and doesn't distinguish between sidebar and the room
7 | const notes = await DB.getRoomNotes('obelisk')
8 |
9 | return {
10 | groupManagementTasks: [
11 | {
12 | userId: user.id,
13 | groupId: 'sidebar-obelisk',
14 | action: 'add'
15 | }
16 | ],
17 | httpResponse: {
18 | status: 200,
19 | body: { notes }
20 | }
21 | }
22 | }
23 |
24 | export default startObservingObelisk
25 |
--------------------------------------------------------------------------------
/server/src/endpoints/obelisk/stopObservingObelisk.ts:
--------------------------------------------------------------------------------
1 | import { User } from '../../user'
2 | import { AuthenticatedEndpointFunction, LogFn } from '../../endpoint'
3 |
4 | const stopObservingObelisk: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
5 | return {
6 | groupManagementTasks: [
7 | {
8 | userId: user.id,
9 | groupId: 'sidebar-obelisk',
10 | action: 'remove'
11 | }
12 | ],
13 | httpResponse: {
14 | status: 200
15 | }
16 | }
17 | }
18 |
19 | export default stopObservingObelisk
20 |
--------------------------------------------------------------------------------
/server/src/endpoints/openOrCloseSpace.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import { User } from '../user'
3 | import { DB } from '../database'
4 |
5 | const openOrCloseSpace: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const spaceIsClosed = inputs.spaceIsClosed
7 | if (typeof spaceIsClosed === 'undefined') {
8 | return {
9 | httpResponse: {
10 | status: 400,
11 | body: { error: 'Explicitly specify whether the space should be open or not' }
12 | }
13 | }
14 | }
15 |
16 | // Coercing this to a bool makes sure nothing bad happens if clients pass in something unexpected
17 | await DB.setServerSettings({ spaceIsClosed: !!spaceIsClosed })
18 |
19 | return {
20 | messages: [
21 | {
22 | target: 'spaceOpenedOrClosed',
23 | arguments: [spaceIsClosed]
24 | }
25 | ],
26 | httpResponse: { status: 200 }
27 | }
28 | }
29 |
30 | export default openOrCloseSpace
31 |
--------------------------------------------------------------------------------
/server/src/endpoints/pong.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import { DB } from '../database'
3 | import { User } from '../user'
4 |
5 | const pong: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | await DB.setUserHeartbeat(user)
7 |
8 | return { httpResponse: { status: 200 } }
9 | }
10 |
11 | export default pong
12 |
--------------------------------------------------------------------------------
/server/src/endpoints/resetBadgeData.ts:
--------------------------------------------------------------------------------
1 | import { FreeBadges } from '../badges'
2 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
3 | import DB from '../redis'
4 | import { minimizeUser, User } from '../user'
5 |
6 | const resetBadgeData: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
7 | user.unlockedBadges = []
8 | user.equippedBadges = []
9 | await DB.setUserProfile(user.id, user)
10 |
11 | return {
12 | httpResponse: {
13 | status: 200,
14 | body: { unlockedBadges: FreeBadges, equippedBadges: [] }
15 | },
16 | messages: [{
17 | target: 'usernameMap',
18 | arguments: [{ [user.id]: minimizeUser(user) }]
19 | }]
20 | }
21 | }
22 |
23 | export default resetBadgeData
24 |
--------------------------------------------------------------------------------
/server/src/endpoints/resetRoomData.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import Redis from '../redis'
3 | import { Room } from '../rooms'
4 | import roomData from '../rooms/data/roomData.json'
5 | import { User } from '../user'
6 |
7 | const resetRoomData: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
8 | // TODO: Allow this to just wipe a specific room
9 |
10 | // First, delete all current rooms
11 | const roomIds = await Redis.getRoomIds()
12 | await Promise.all(roomIds.map(Redis.deleteRoomData))
13 |
14 | // Then, add new data
15 | await Promise.all(Object.values(roomData).map(room => {
16 | return Redis.setRoomData(room as Room)
17 | }))
18 |
19 | return {
20 | httpResponse: {
21 | status: 200,
22 | body: { roomData }
23 | }
24 | }
25 | }
26 |
27 | export default resetRoomData
28 |
--------------------------------------------------------------------------------
/server/src/endpoints/roomNote/addRoomNote.ts:
--------------------------------------------------------------------------------
1 | import DB from '../../redis'
2 | import { User } from '../../user'
3 | import { AuthenticatedEndpointFunction, LogFn } from '../../endpoint'
4 |
5 | const addRoomNote: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const message = inputs.message
7 | const id = inputs.id
8 | if (!message || !id) {
9 | return {
10 | httpResponse: {
11 | status: 500,
12 | body: 'Include a post-it message and an ID!'
13 | }
14 | }
15 | }
16 |
17 | await DB.addRoomNote(user.roomId, { id, message, authorId: user.id })
18 |
19 | const result = {
20 | messages: [
21 | {
22 | groupId: user.roomId,
23 | target: 'noteAdded',
24 | arguments: [user.roomId, id, message, user.id]
25 | }
26 | ],
27 | httpResponse: {
28 | status: 200
29 | }
30 | }
31 |
32 | // The sidebar obelisk and obelisk room share a noteWall, but have different pubsub groups to notify
33 | // Adding an obelisk note from the sidebar is a different function, this is just the obelisk room case
34 | if (user.roomId === 'obelisk') {
35 | result.messages.push({
36 | groupId: 'sidebar-obelisk',
37 | target: 'obeliskNoteAdded',
38 | arguments: [id, message, user.id]
39 | })
40 | }
41 |
42 | return result
43 | }
44 |
45 | export default addRoomNote
46 |
--------------------------------------------------------------------------------
/server/src/endpoints/roomNote/likeRoomNote.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../../endpoint'
2 | import { User } from '../../user'
3 | import DB from '../../redis'
4 |
5 | const likeRoomNote: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const noteId = inputs.noteId
7 | if (!noteId) {
8 | return {
9 | httpResponse: {
10 | status: 500,
11 | body: 'Include a note ID!'
12 | }
13 | }
14 | }
15 |
16 | const doLike = inputs.like
17 | let likes: string[] = []
18 | if (doLike) {
19 | likes = await DB.likeRoomNote(user.roomId, noteId, user.id)
20 | } else {
21 | likes = await DB.unlikeRoomNote(user.roomId, noteId, user.id)
22 | }
23 |
24 | const messages = [
25 | {
26 | groupId: user.roomId,
27 | target: 'noteLikesUpdated',
28 | arguments: [user.roomId, noteId, likes]
29 | }
30 | ]
31 |
32 | // The sidebar obelisk and obelisk room share a noteWall, but have different pubsub groups to notify
33 | // Liking an obelisk note from the sidebar is a different function, this is just the obelisk room case
34 | if (user.roomId === 'obelisk') {
35 | messages.push({
36 | groupId: 'sidebar-obelisk',
37 | target: 'obeliskNoteLikesUpdated',
38 | arguments: [noteId, likes]
39 | })
40 | }
41 |
42 | return {
43 | messages,
44 | httpResponse: { status: 200 }
45 | }
46 | }
47 |
48 | export default likeRoomNote
49 |
--------------------------------------------------------------------------------
/server/src/endpoints/sendCaption.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 |
3 | import { MESSAGE_MAX_LENGTH } from '../config'
4 | import { User } from '../user'
5 |
6 | const sendCaption: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
7 | const message = inputs.text
8 | if (!message) {
9 | return {
10 | httpResponse: {
11 | status: 500,
12 | body: 'Include a user ID and a message!'
13 | }
14 | }
15 | } else if (message.length > MESSAGE_MAX_LENGTH) {
16 | // TODO: Not sure if this makes sense for captions.
17 | return {
18 | httpResponse: {
19 | status: 400,
20 | body: 'Message length too long!'
21 | }
22 | }
23 | }
24 |
25 | log(`Sending caption to ${user.roomId}: ${message} from ${user.id}`)
26 |
27 | return {
28 | messages: [
29 | {
30 | groupId: user.roomId,
31 | target: 'caption',
32 | arguments: [inputs.id, user.id, message]
33 | }
34 | ],
35 | httpResponse: { status: 200 }
36 | }
37 | }
38 |
39 | export default sendCaption
40 |
--------------------------------------------------------------------------------
/server/src/endpoints/serverSettings.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, EndpointFunction, LogFn } from '../endpoint'
2 | import { User } from '../user'
3 | import { DB } from '../database'
4 | import { toServerSettings } from '../types'
5 |
6 | const getServerSettings: EndpointFunction = async (inputs: any, log: LogFn) => {
7 | const settings = await DB.getServerSettings()
8 | return {
9 | httpResponse: {
10 | status: 200,
11 | body: settings
12 | }
13 | }
14 | }
15 |
16 | const postServerSettings: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
17 | if (!inputs) {
18 | return {
19 | httpResponse: {
20 | status: 400,
21 | body: { error: 'Must include a body!' }
22 | }
23 | }
24 | }
25 | const newSettings = toServerSettings(inputs)
26 | if (!newSettings) {
27 | return {
28 | httpResponse: {
29 | status: 400,
30 | body: { error: 'Could not parse server settings!' }
31 | }
32 | }
33 | }
34 |
35 | await DB.setServerSettings(newSettings)
36 |
37 | return {
38 | messages: [
39 | {
40 | target: 'serverSettings',
41 | arguments: [newSettings]
42 | }
43 | ],
44 | httpResponse: { status: 200 }
45 | }
46 | }
47 |
48 | export { getServerSettings, postServerSettings }
49 |
--------------------------------------------------------------------------------
/server/src/endpoints/toggleModStatus.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import { isMod, minimizeUser, updateModStatus, User } from '../user'
3 | import { DB } from '../database'
4 |
5 | const toggleModStatus: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const userIdToToggle: string = inputs.userId
7 | if (!userIdToToggle) {
8 | return {
9 | httpResponse: {
10 | status: 400,
11 | body: { error: 'You did not include a user to mod/unmod' }
12 | }
13 | }
14 | }
15 |
16 | if (await isMod(userIdToToggle)) {
17 | log(`[MOD] Setting user ${userIdToToggle} to mod=false`)
18 | await DB.setModStatus(userIdToToggle, false)
19 | } else {
20 | log(`[MOD] Setting user ${userIdToToggle} to mod=true`)
21 | await DB.setModStatus(userIdToToggle, true)
22 | }
23 |
24 | // Update mod status for everyone else
25 | const toggledUser: User = await DB.getUser(userIdToToggle)
26 | return {
27 | messages: [{
28 | target: 'usernameMap',
29 | arguments: [{ [userIdToToggle]: minimizeUser(toggledUser) }]
30 | }]
31 | }
32 | }
33 |
34 | export default toggleModStatus
35 |
--------------------------------------------------------------------------------
/server/src/endpoints/updateFontReward.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import { updateUserFontReward, updateUserProfileColor, User } from '../user'
3 | import { ValidFontRewards } from '../types'
4 |
5 | const updateFontReward: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const font = inputs.font
7 | if (font && !(font in ValidFontRewards)) {
8 | return {
9 | httpResponse: {
10 | status: 400,
11 | body: 'The font reward included is not a valid font reward from our list!'
12 | }
13 | }
14 | }
15 |
16 | const profile = await updateUserFontReward(user.id, font)
17 |
18 | return {
19 | messages: [{
20 | target: 'usernameMap',
21 | arguments: [{ [user.id]: profile }]
22 | }]
23 | }
24 | }
25 |
26 | export default updateFontReward
27 |
--------------------------------------------------------------------------------
/server/src/endpoints/updateProfile.ts:
--------------------------------------------------------------------------------
1 | import { EndpointFunction, LogFn } from '../endpoint'
2 | import { minimizeUser, updateUserProfile, User } from '../user'
3 |
4 | const updateProfile: EndpointFunction = async (inputs: any, log: LogFn) => {
5 | if (!inputs.userId) {
6 | return {
7 | httpResponse: {
8 | status: 401,
9 | body: { registered: false, error: 'You are not logged in!' }
10 | }
11 | }
12 | }
13 |
14 | const data: Partial = inputs.user
15 | if (!data) {
16 | return {
17 | httpResponse: {
18 | status: 400,
19 | body: 'Include profile data!'
20 | }
21 | }
22 | }
23 |
24 | try {
25 | const profile = await updateUserProfile(inputs.userId, data, inputs.isNew)
26 | const minimalUser = minimizeUser(profile)
27 |
28 | return {
29 | messages: [{
30 | target: 'usernameMap',
31 | arguments: [{ [inputs.userId]: minimalUser }]
32 | }],
33 | httpResponse: {
34 | status: 200,
35 | body: { valid: true, user: profile }
36 | }
37 | }
38 | } catch (e) {
39 | // Should be status 409
40 | // the client doesnt currently have a good way to handle non-200 status codes :(
41 | return {
42 | httpResponse: {
43 | status: 200,
44 | body: { valid: false, error: e.message }
45 | }
46 | }
47 | }
48 | }
49 |
50 | export default updateProfile
51 |
--------------------------------------------------------------------------------
/server/src/endpoints/updateProfileColor.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import { updateUserProfileColor, User } from '../user'
3 | import { ValidColors } from '../types'
4 |
5 | const updateProfileColor: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const color = inputs.color
7 | if (color && !(color in ValidColors)) {
8 | return {
9 | httpResponse: {
10 | status: 400,
11 | body: 'The color included is not a valid color!'
12 | }
13 | }
14 | }
15 |
16 | const profile = await updateUserProfileColor(user.id, color)
17 |
18 | return {
19 | messages: [{
20 | target: 'usernameMap',
21 | arguments: [{ [user.id]: profile }]
22 | }]
23 | }
24 | }
25 |
26 | export default updateProfileColor
27 |
--------------------------------------------------------------------------------
/server/src/endpoints/updateRoom.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticatedEndpointFunction, LogFn } from '../endpoint'
2 | import DB from '../redis'
3 | import { User } from '../user'
4 |
5 | const updateRoom: AuthenticatedEndpointFunction = async (user: User, inputs: any, log: LogFn) => {
6 | const roomId = inputs.roomId
7 | const data = inputs.roomData
8 | if (!roomId) {
9 | return {
10 | httpResponse: {
11 | status: 200,
12 | body: { error: 'You did not include a roomId to update' }
13 | }
14 | }
15 | }
16 |
17 | if (!data) {
18 | return {
19 | httpResponse: {
20 | status: 200,
21 | body: { error: 'You did not include data' }
22 | }
23 | }
24 | }
25 |
26 | data.roomId = roomId
27 | const room = await DB.setRoomData(data)
28 |
29 | return {
30 | httpResponse: {
31 | status: 200,
32 | body: { room }
33 | }
34 | }
35 | }
36 |
37 | export default updateRoom
38 |
--------------------------------------------------------------------------------
/server/src/generators/bodyWorksCharacter.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 |
3 | import tracery from 'tracery-grammar'
4 |
5 | export const actionString = (bodyWorksCharacter: string) => {
6 | return `${bodyWorksCharacter}`
7 | }
8 |
9 | export const generate = () => {
10 | const grammar = tracery.createGrammar({
11 | origin: [
12 | 'They look at you with a mischievous grin as they mix two smoking fluids together.',
13 | 'The sudden smell of perfume slams into your senses, temporarily blinding you.',
14 | 'A flash of light comes from the cauldron in front of them as they add some mysterious powder into a bubbling concoction of some kind.',
15 | 'They stir a cauldron and you hear muttered words in some language you don\'t understand.',
16 | 'You notice a new growth of mushrooms pop up from the ground in the wake of their footsteps.',
17 | 'Their hand flashes out and snatches a bottle off a shelf, adding its contents to the cauldron.'
18 | ]
19 | })
20 |
21 | grammar.addModifiers(tracery.baseEngModifiers)
22 |
23 | return grammar.flatten('#origin#')
24 | }
25 | /* eslint-enable quotes */
26 |
--------------------------------------------------------------------------------
/server/src/generators/chadSilverbow.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 |
3 | import tracery from 'tracery-grammar'
4 |
5 | export const actionString = (chadSilverbow: string) => {
6 | return `${chadSilverbow}`
7 | }
8 |
9 | export const generate = () => {
10 | var grammar = tracery.createGrammar({
11 | origin: [
12 | 'You can see fresh oil glistening across his muscular chest, which he put prominently on display.',
13 | 'He takes some bags out of his pockets and starts to juggle, but immediately drops one and hits himself with the other two.',
14 | 'You take a moment to admite all the golden medallions and other jewelry dangling from his jaunty bard outfit',
15 | 'He\'s holding #instrument.a# and threatening to play it. You may want to cover your ears.',
16 | 'Coming from his mouth is a sound that most closely resembles a cat being rhythmically stepped on.',
17 | 'He appears to still be setting up for a joke he began 5 minutes ago, and the audience looks ready to attack.',
18 | 'His brightly-colored bard garb looks to be custom tailored by the finest tailor a king\'s ransom could buy.',
19 | 'He exudes an aura of just earnestly wanting everyone around him to be happy.',
20 | 'He tells another horrible joke and then gives you a knowing look.'
21 | ],
22 | instrument: [
23 | 'lute',
24 | 'ukelele',
25 | 'acoustic guitar',
26 | 'pan flute',
27 | 'accordion',
28 | 'otamatone',
29 | 'trombone'
30 | ]
31 | })
32 |
33 | grammar.addModifiers(tracery.baseEngModifiers)
34 | return grammar.flatten('#origin#')
35 | }
36 | /* eslint-enable quotes */
37 |
--------------------------------------------------------------------------------
/server/src/generators/chasm.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 |
3 | import tracery from 'tracery-grammar'
4 |
5 | export const actionString = (tossARock: string) => {
6 | return `You drop a rock down into the chasm ${tossARock}`
7 | }
8 |
9 | export const generate = () => {
10 | var grammar = tracery.createGrammar({
11 | origin: [
12 | "and you never hear it land.",
13 | "and the walls briefly shake, dust falling from above.",
14 | "and it flies back up into your hand.",
15 | "and it comes back to you with a +1 enchantment."
16 | ]
17 | })
18 |
19 | grammar.addModifiers(tracery.baseEngModifiers)
20 | return grammar.flatten('#origin#')
21 | }
22 | /* eslint-enable quotes */
23 |
--------------------------------------------------------------------------------
/server/src/generators/craneGame.ts:
--------------------------------------------------------------------------------
1 | import tracery from 'tracery-grammar'
2 |
3 | export const actionString = (item: string) => {
4 | return `The crane grabs hold of something with the weakest possible grip, releasing ${item} into the prize chute.`
5 | }
6 |
7 | export const generate = () => {
8 | var grammar = tracery.createGrammar({
9 | origin: [
10 | '#material.a# #item#',
11 | '#material.a# #item#',
12 | '#size.a# #material# #item#',
13 | '#color.a# #material# #item#',
14 | '#size.a# #color# #material# #item#'
15 | ],
16 | size: [
17 | 'huge',
18 | 'miniature',
19 | 'average',
20 | 'microscopic',
21 | 'life-sized',
22 | 'room-sized',
23 | 'pocket-sized',
24 | 'extra-large',
25 | 'tiny',
26 | 'gigantic',
27 | 'eel-sized'
28 | ],
29 | color: [
30 | 'pale',
31 | 'golden',
32 | 'silver',
33 | 'red',
34 | 'pink',
35 | 'yellow',
36 | 'green',
37 | 'blue',
38 | 'cyan',
39 | 'violet'
40 | ],
41 | material: [
42 | 'stuffed',
43 | 'plush',
44 | 'plastic',
45 | 'mithril',
46 | 'leather',
47 | 'dragonglass',
48 | 'faux-leather',
49 | 'stainless steel',
50 | 'jello',
51 | 'wooden',
52 | 'glass',
53 | 'ceramic',
54 | 'cardboard',
55 | 'ceramic',
56 | 'cloth',
57 | 'golden',
58 | 'titanium',
59 | 'eelskin'
60 | ],
61 | item: [
62 | 'eel'
63 | ]
64 | })
65 |
66 | grammar.addModifiers(tracery.baseEngModifiers)
67 | return grammar.flatten('#origin#')
68 | }
69 |
--------------------------------------------------------------------------------
/server/src/generators/drinkSkeletons.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 |
3 | import tracery from 'tracery-grammar'
4 |
5 | export const actionString = (drinkSkeleton: string) => {
6 | return `${drinkSkeleton}`
7 | }
8 |
9 | /* -drinkContent-, -drinkVessel-, -drinkName-, and -userName- are all replaced when orderNewDrink or orderExistingDrink are called. */
10 | export const generate = () => {
11 | var grammar = tracery.createGrammar({
12 | origin: [
13 | "#vessel_sentence# -drinkContent- #name_sentence#"
14 | ],
15 | vessel_sentence: [
16 | "The bartender hands -userName- -drinkVessel-.",
17 | "-userName-'s drink comes in -drinkVessel-.",
18 | "Ubizara places -drinkVessel- in front of -userName."
19 | ],
20 | name_sentence: [
21 | "\"-drinkName-, as you ordered,\" Ubizara smiles.",
22 | "\"It's called -drinkName-,\" the bartender grins.",
23 | "Ubizara shouts that the name of the drink is -drinkName-."
24 | ]
25 | })
26 |
27 | grammar.addModifiers(tracery.baseEngModifiers)
28 | return grammar.flatten('#origin#')
29 | }
30 | /* eslint-enable quotes */
31 |
--------------------------------------------------------------------------------
/server/src/generators/flower.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 |
3 | import tracery from 'tracery-grammar'
4 |
5 | export const actionString = (flower: string) => {
6 | return `${flower}`
7 | }
8 |
9 | export const generate = () => {
10 | var grammar = tracery.createGrammar({
11 | origin: [
12 | 'GO...',
13 | 'UNDERWORLD...',
14 | 'GO... UNDER... WORLD...'
15 | ]
16 | })
17 |
18 | grammar.addModifiers(tracery.baseEngModifiers)
19 | return grammar.flatten('#origin#')
20 | }
21 | /* eslint-enable quotes */
22 |
--------------------------------------------------------------------------------
/server/src/generators/hotDogGuy.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 |
3 | import tracery from 'tracery-grammar'
4 |
5 | export const actionString = (hotDogGuy: string) => {
6 | return `${hotDogGuy}`
7 | }
8 |
9 | export const generate = () => {
10 | const grammar = tracery.createGrammar({
11 | origin: [
12 | 'A young goblin stares at you, wearing a floppy, multicolored hat, his eyes pleading for your mercy.',
13 | 'He glances at you as he dips a battered hotdog into hot oil.',
14 | 'His outfit\'s bright colors seem to leap out at you, no matter where you turn.',
15 | 'He looks out into the food court at all the trays left haphazardly about, and sighs.',
16 | 'A beeping sound rings out from behind him, but he does not move.',
17 | 'You catch him maliciously slapping the side of an ice cream machine, cursing loudly.',
18 | 'You notice a small notepad on the side of the cash register, filled with sketches.'
19 | ]
20 | })
21 |
22 | grammar.addModifiers(tracery.baseEngModifiers)
23 |
24 | return grammar.flatten('#origin#')
25 | }
26 | /* eslint-enable quotes */
27 |
--------------------------------------------------------------------------------
/server/src/generators/loudRobert.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 |
3 | import tracery from 'tracery-grammar'
4 |
5 | export const actionString = (loudRobert: string) => {
6 | return `${loudRobert}`
7 | }
8 |
9 | export const generate = () => {
10 | const grammar = tracery.createGrammar({
11 | origin: [
12 | 'His short, bulky frame is supporting an enormous green coat that hangs down to his feet.',
13 | 'On his head rests a backwards baseball cap which no longer seems to serve any purpose.',
14 | 'The two men stand facing each other and all you hear are expletives, but somehow they seem to understand each other.',
15 | 'He stares at you intensely, but doesn’t say anything.'
16 | ]
17 | })
18 |
19 | grammar.addModifiers(tracery.baseEngModifiers)
20 |
21 | return grammar.flatten('#origin#')
22 | }
23 | /* eslint-enable quotes */
24 |
--------------------------------------------------------------------------------
/server/src/generators/ray.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 |
3 | import tracery from 'tracery-grammar'
4 |
5 | export const actionString = (ray: string) => {
6 | return `${ray}`
7 | }
8 |
9 | export const generate = () => {
10 | const grammar = tracery.createGrammar({
11 | origin: [
12 | 'His tall, lanky frame is wrapped in a bright yellow jacket 2 sizes too large.',
13 | 'A grey beanie sits atop his long mane of dirty blond hair, and all of it is haphazardly put together with little concern for fashion.',
14 | 'You get a whiff of a strange plant smell any time you so much as glance at him.',
15 | 'The two men stand facing each other and all you hear are expletives, but somehow they seem to understand each other.',
16 | 'As you look at him, he breaks into an impromptu fit of rap.'
17 | ]
18 | })
19 |
20 | grammar.addModifiers(tracery.baseEngModifiers)
21 |
22 | return grammar.flatten('#origin#')
23 | }
24 | /* eslint-enable quotes */
25 |
--------------------------------------------------------------------------------
/server/src/generators/zara.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 |
3 | import tracery from 'tracery-grammar'
4 |
5 | export const actionString = (zara: string) => {
6 | return `${zara}`
7 | }
8 |
9 | export const generate = () => {
10 | const grammar = tracery.createGrammar({
11 | origin: [
12 | 'She flashes you a charming smile around her two enormous fangs and braided muttonchops.',
13 | 'She stands only three and a half feet tall, but it\'s the most imposing, muscular three feet you\'ve ever seen.',
14 | 'Zara looks at you with a mischievous grin. The twinkle in her eye says she would bench press you, if given a chance.',
15 | 'You catch her eye for an instant before she runs off to break up a pair of feral teens arguing about obscure funk bands.',
16 | 'She stands looking over the food court with absolute authority, a twisted orc axe’s handle extending far over her left shoulder.',
17 | 'You notice a slight gleam in her immaculate, braided muttonchops. A feature she clearly devotes much of her time towards.'
18 | ]
19 | })
20 |
21 | grammar.addModifiers(tracery.baseEngModifiers)
22 |
23 | return grammar.flatten('#origin#')
24 | }
25 | /* eslint-enable quotes */
26 |
--------------------------------------------------------------------------------
/server/src/generators/zeroCrash.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes */
2 |
3 | import tracery from 'tracery-grammar'
4 |
5 | export const actionString = (zeroCrash: string) => {
6 | return `${zeroCrash}`
7 | }
8 |
9 | export const generate = () => {
10 | var grammar = tracery.createGrammar({
11 | origin: [
12 | 'Their black leather jacket drapes all the way down to the floor, sweeping the dust as they pace back and forth.',
13 | 'Their eyes dart back and forth, frantically searching every inch of the room.',
14 | 'Their clothes seem to be entirely made of black leather, highlighted with spikes and strips of bright pink cloth.',
15 | 'The visor on their forehead glows a bright green and you can make out nonsensical symbols marching across its screen.',
16 | 'You glance down as they move and could swear you caught a glimpse of a romance novel sticking out of an interior pocket.',
17 | 'You take a moment to appreciate their freshly-shaved head, and the effort they clearly put into their appearance.',
18 | 'You notice their boots appear to include a 3 inch rise, and look incredibly uncomfortable to walk in.',
19 | 'Your eyes are dazzled by the light reflecting off numerous metal spikes adorning their coat.',
20 | 'Their eyes plead with the world, begging the universe itself to send some small relief, just this once.'
21 | ]
22 | })
23 |
24 | grammar.addModifiers(tracery.baseEngModifiers)
25 | return grammar.flatten('#origin#')
26 | }
27 | /* eslint-enable quotes */
28 |
--------------------------------------------------------------------------------
/server/src/globalPresenceMessage.ts:
--------------------------------------------------------------------------------
1 | import { DB } from './database'
2 | import { Message } from './endpoint'
3 |
4 | /** Fetches presence data for a set of rooms and returns a SignalR message
5 | * to broadcast current presence data to all users */
6 | export async function globalPresenceMessage (roomIds: string[]): Promise {
7 | // TODO: This could be written as a single DB query for all rooms, instead of n queries for n rooms
8 | // However, we currently call this primarily with 1 room, and only once with 2
9 | // so this optimization is relatively low-priority for now.
10 |
11 | const data: { [roomId: string]: string[] } = {}
12 |
13 | console.log(roomIds)
14 | await Promise.all(
15 | roomIds.map(async (id) => {
16 | const occupants = await DB.roomOccupants(id)
17 | data[id] = occupants
18 | })
19 | )
20 |
21 | return {
22 | target: 'presenceData',
23 | arguments: [data]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/server/src/heartbeat.ts:
--------------------------------------------------------------------------------
1 | import { DB } from './database'
2 | import { User } from './user'
3 |
4 | /** All of these store heartbeats as Unix timestamps as numbers.
5 | * When I stored either a timestamp number or a ISO8601 string,
6 | * constructing a new Date() object was returning an invalid object.
7 | *
8 | * Since I'm just compring Unix timestamps anyway, this is a lazy solution.
9 | */
10 | export async function getHeartbeatData (): Promise<{
11 | [userId: string]: number;
12 | }> {
13 | const activeUserIds: string[] = await DB.getActiveUsers()
14 | const data: { [userId: string]: number } = {}
15 |
16 | for (let i = 0; i < activeUserIds.length; i++) {
17 | const userId = activeUserIds[i]
18 | const date = await DB.getUserHeartbeat(userId)
19 | data[userId] = date
20 | }
21 |
22 | return data
23 | }
24 |
25 | export async function userHeartbeatReceived (user: User) {
26 | console.log('userHeartbeatReceived')
27 | await DB.setUserHeartbeat(user)
28 | console.log('Did setUserHeartbeat')
29 | await DB.setUserAsActive(user, true)
30 | console.log('Did setAsActive')
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/interact.ts:
--------------------------------------------------------------------------------
1 | import { User } from './user'
2 | import { cookie } from '../src/cookie'
3 | import { polymorph, cancellation } from '../src/polymorph'
4 | import { Result } from './endpoint'
5 |
6 | export async function interact (user: User, messageId: string, inspectedObject: string): Promise {
7 | if (user.roomId === 'oracle' && (inspectedObject.includes('cookie') || inspectedObject.includes('fortune'))) {
8 | return cookie(user, messageId)
9 | }
10 | if (inspectedObject.includes('potion')) {
11 | // Inspecting a potion
12 | if (inspectedObject.includes('colourful') || inspectedObject.includes('colorful') || inspectedObject.includes('coloured') || inspectedObject.includes('colored')) {
13 | return await polymorph(user, messageId)
14 | } else if (inspectedObject.includes('clear') || inspectedObject.includes('plain')) {
15 | return await cancellation(user, messageId)
16 | } else {
17 | return {
18 | httpResponse: {
19 | status: 200,
20 | body: { error: 'Sorry, I don\'t know a potion of that description.' }
21 | }
22 | }
23 | }
24 | }
25 | return {
26 | httpResponse: {
27 | status: 200,
28 | body: { error: 'Sorry, that isn\'t an interactive object.' }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/server/src/logSignalR.ts:
--------------------------------------------------------------------------------
1 | import { Context } from '@azure/functions'
2 |
3 | export default function logSignalR (context: Context) {
4 | context.log('HTTP response', context.res)
5 | context.log('Group actions', context.bindings.signalRGroupActions)
6 | context.log('Messages', context.bindings.signalRMessages)
7 | }
8 |
--------------------------------------------------------------------------------
/server/src/look.ts:
--------------------------------------------------------------------------------
1 | import { Result } from './endpoint'
2 | import { DB } from './database'
3 |
4 | export async function look (target: string): Promise {
5 | const profile = await DB.getUser(target)
6 |
7 | return {
8 | httpResponse: {
9 | status: 200,
10 | body: { user: profile }
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/server/src/roomNote.ts:
--------------------------------------------------------------------------------
1 | export interface RoomNote {
2 | // UUID generated by the client
3 | id: string
4 |
5 | // The plaintext message of the note
6 | message: string
7 |
8 | // user ID
9 | authorId: string
10 |
11 | // Array of user IDs
12 | likes?: string[]
13 | }
14 |
--------------------------------------------------------------------------------
/server/src/sendToDiscord.ts:
--------------------------------------------------------------------------------
1 | import fetch from 'node-fetch'
2 |
3 | export async function sendToDiscord ({ message, username }) {
4 | const msg = {
5 | content: `${username}: ${message}`
6 | }
7 |
8 | await fetch(process.env.DISCORD_URL, {
9 | method: 'post',
10 | headers: { 'content-type': 'application/json' },
11 | body: JSON.stringify(msg)
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/server/src/setUpRoomsForUser.ts:
--------------------------------------------------------------------------------
1 | import { GroupManagementTask } from './endpoint'
2 | import Redis from './redis'
3 |
4 | /** This returns an array of SignalRGroupActions that remove the given user
5 | * from all room-specific SignalR groups other than the specified one. */
6 | export default async (userId: string, exclude?: string): Promise => {
7 | let allRooms = await Redis.getRoomIds()
8 | if (exclude) {
9 | allRooms = allRooms.filter((k) => k !== exclude)
10 | }
11 | return allRooms.map((r): GroupManagementTask => {
12 | return {
13 | userId,
14 | groupId: r,
15 | action: 'remove'
16 | }
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/server/src/shout.ts:
--------------------------------------------------------------------------------
1 | import { User } from './user'
2 | import { DB } from './database'
3 | import { Result } from './endpoint'
4 |
5 | export async function shout (user: User, messageId: string, message: string): Promise {
6 | // Currently hardcode a 2-minute shout cooldown
7 | const date = await DB.lastShoutedForUser(user.id)
8 | if (date) {
9 | const cooldownMinutes = 2
10 | const diff = new Date().valueOf() - date.valueOf()
11 | if (!user.isMod && Math.floor(diff / 1000 / 60) < cooldownMinutes) {
12 | return {
13 | httpResponse: {
14 | status: 200,
15 | body: {
16 | error:
17 | 'Your voice is still hoarse from your last shout. Try again in a minute or two.'
18 | }
19 | }
20 | }
21 | }
22 | }
23 |
24 | await DB.userJustShouted(user)
25 | return {
26 | messages: [
27 | {
28 | target: 'shout',
29 | arguments: [messageId, user.id, message]
30 | }
31 | ],
32 | httpResponse: { status: 200 }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/src/whisper.ts:
--------------------------------------------------------------------------------
1 | import { Result } from './endpoint'
2 | import { User, getUserIdForOnlineUsername, getUserIdForUsername } from './user'
3 |
4 | export async function whisper (
5 | from: User,
6 | toUsername: string,
7 | message: string
8 | ): Promise {
9 | console.log('Attempting to whisper to username', toUsername)
10 | const toUser = await getUserIdForUsername(toUsername)
11 | console.log('toUser', toUser)
12 | const toUserIsOnline = await getUserIdForOnlineUsername(toUsername)
13 |
14 | // TODO: Return this as metadata so the client can NameView the username
15 | // Also: I think maybe a 404 is a better error code? but we can worry about that later if at all.
16 | if (!toUser) {
17 | return {
18 | httpResponse: {
19 | status: 200,
20 | body: {
21 | error: `${toUsername} does not match any usernames! You may also get this if the username was previously correct, but the user has changed their name since.`
22 | }
23 | }
24 | }
25 | } else if (!toUserIsOnline) {
26 | return {
27 | httpResponse: {
28 | status: 200,
29 | body: {
30 | error: `${toUsername} is not online and will not receive your message.`
31 | }
32 | }
33 | }
34 | }
35 |
36 | return {
37 | messages: [
38 | {
39 | userId: toUser,
40 | target: 'whisper',
41 | arguments: [from.id, message]
42 | }
43 | ],
44 | httpResponse: { status: 200 }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/server/startObservingObelisk/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/startObservingObelisk/index.js"
29 | }
--------------------------------------------------------------------------------
/server/startObservingObelisk/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import startObservingObelisk from '../src/endpoints/obelisk/startObservingObelisk'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, startObservingObelisk)
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/startObservingObelisk/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/startPonderingOrbs/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get"
10 | ]
11 | },
12 | {
13 | "type": "http",
14 | "direction": "out",
15 | "name": "res"
16 | },
17 | {
18 | "type": "webPubSubConnection",
19 | "name": "connection",
20 | "hub": "chat",
21 | "userId": "{headers.userid}",
22 | "direction": "in"
23 | },
24 | {
25 | "type": "webPubSub",
26 | "name": "actions",
27 | "hub": "chat",
28 | "direction": "out"
29 | }
30 | ],
31 | "scriptFile": "../dist/startPonderingOrbs/index.js"
32 | }
--------------------------------------------------------------------------------
/server/startPonderingOrbs/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 |
3 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest, connection): Promise {
4 | context.log('pondering orbs')
5 | if (req.headers.userid !== process.env.ORB_PONDER_USER) {
6 | context.res = {
7 | status: 401,
8 | body: 'Unauthorized'
9 | }
10 | return
11 | }
12 |
13 | context.log('valid orb ponderer')
14 |
15 | context.res = { body: connection }
16 | }
17 |
18 | export default httpTrigger
19 |
--------------------------------------------------------------------------------
/server/startPonderingOrbs/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/stopObservingObelisk/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "webPubSub",
12 | "name": "actions",
13 | "hub": "chat",
14 | "direction": "out"
15 | },
16 | {
17 | "type": "http",
18 | "direction": "out",
19 | "name": "res"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/stopObservingObelisk/index.js"
29 | }
--------------------------------------------------------------------------------
/server/stopObservingObelisk/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import stopObservingObelisk from '../src/endpoints/obelisk/stopObservingObelisk'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, stopObservingObelisk)
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/stopObservingObelisk/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/toggleModStatus/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | },
18 | {
19 | "type": "webPubSub",
20 | "name": "actions",
21 | "hub": "chat",
22 | "direction": "out"
23 | },
24 | {
25 | "tableName": "auditLog",
26 | "name": "tableBinding",
27 | "type": "table",
28 | "direction": "out"
29 | }
30 | ],
31 | "scriptFile": "../dist/toggleModStatus/index.js"
32 | }
33 |
--------------------------------------------------------------------------------
/server/toggleModStatus/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import toggleModStatus from '../src/endpoints/toggleModStatus'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, toggleModStatus, { audit: true, mod: true })
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/toggleModStatus/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/toggleSpeakerStatus/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "http",
12 | "direction": "out",
13 | "name": "res"
14 | },
15 | {
16 | "type": "webPubSub",
17 | "name": "actions",
18 | "hub": "chat",
19 | "direction": "out"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/toggleSpeakerStatus/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/toggleSpeakerStatus/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import toggleSpeakerStatus from '../src/endpoints/toggleSpeakerStatus'
4 |
5 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
6 | await authenticatedAzureWrap(context, req, toggleSpeakerStatus, { audit: true, mod: true })
7 | }
8 |
9 | export default httpTrigger
10 |
--------------------------------------------------------------------------------
/server/toggleSpeakerStatus/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "outDir": "dist",
6 | "rootDir": ".",
7 | "resolveJsonModule": true,
8 | "esModuleInterop": true,
9 | "sourceMap": true,
10 | "strict": false,
11 | "skipLibCheck": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/server/twilioToken/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | }
18 | ],
19 | "scriptFile": "../dist/twilioToken/index.js"
20 | }
21 |
--------------------------------------------------------------------------------
/server/twilioToken/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { getUserIdFromHeaders } from '../src/authenticate'
3 | import Twilio from 'twilio'
4 |
5 | const VideoGrant = Twilio.jwt.AccessToken.VideoGrant
6 | const MAX_ALLOWED_SESSION_DURATION = 14400
7 |
8 | const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise {
9 | const userId = await getUserIdFromHeaders(context, req)
10 |
11 | if (!userId) {
12 | context.res = {
13 | status: 401,
14 | body: 'The user name is required to ensure their access token'
15 | }
16 | }
17 |
18 | const token = new Twilio.jwt.AccessToken(
19 | process.env.TWILIO_ACCOUNT_SID,
20 | process.env.TWILIO_API_KEY,
21 | process.env.TWILIO_API_SECRET,
22 | { ttl: MAX_ALLOWED_SESSION_DURATION }
23 | );
24 |
25 | // Assign the generated identity to the token.
26 | // This code used to work, then we switched from require to import
27 | // This production code seems to work (famous last words)
28 | // so tentatively Just A TypeScript Thing?
29 | // (Em, 8/14/22)
30 | (token as any).identity = userId
31 |
32 | // Grant the access token Twilio Video capabilities.
33 | const grant = new VideoGrant()
34 | token.addGrant(grant)
35 |
36 | // Serialize the token to a JWT string.
37 | context.res = {
38 | body: token.toJwt()
39 | }
40 | }
41 |
42 | export default httpTrigger
43 |
--------------------------------------------------------------------------------
/server/twilioToken/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/updateFontReward/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["post"]
9 | },
10 | {
11 | "type": "http",
12 | "direction": "out",
13 | "name": "res"
14 | },
15 | {
16 | "type": "webPubSub",
17 | "name": "actions",
18 | "hub": "chat",
19 | "direction": "out"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/updateFontReward/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/updateFontReward/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 |
3 | import { authenticatedAzureWrap } from '../src/azureWrap'
4 | import updateFontReward from '../src/endpoints/updateFontReward'
5 |
6 | const httpTrigger: AzureFunction = async function (
7 | context: Context,
8 | req: HttpRequest
9 | ): Promise {
10 | await authenticatedAzureWrap(context, req, updateFontReward, { audit: true })
11 | }
12 |
13 | export default httpTrigger
14 |
--------------------------------------------------------------------------------
/server/updateProfile/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["get", "post"]
9 | },
10 | {
11 | "type": "http",
12 | "direction": "out",
13 | "name": "res"
14 | },
15 | {
16 | "type": "webPubSub",
17 | "name": "actions",
18 | "hub": "chat",
19 | "direction": "out"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/updateProfile/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/updateProfile/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { azureWrap } from '../src/azureWrap'
3 | import updateProfile from '../src/endpoints/updateProfile'
4 | import { getUserIdFromHeaders } from '../src/authenticate'
5 | import { DB } from '../src/database'
6 |
7 | const httpTrigger: AzureFunction = async function (
8 | context: Context,
9 | req: HttpRequest
10 | ): Promise {
11 | const userId = await getUserIdFromHeaders(context, req)
12 |
13 | await azureWrap(context, req, updateProfile, { userId: userId })
14 |
15 | // We don't yet have an abstraction to do custom audit logs within our wrapped Azure functions
16 | // Adding a lil bit of business logic here is a quick fix for now.
17 | if (userId) {
18 | const username = (await DB.getUser(userId) || {}).username
19 |
20 | // Special case audit log entry - see authenticate(...) for general case audit
21 | context.bindings.tableBinding = [{
22 | PartitionKey: userId,
23 | RowKey: Date.now().toString(),
24 | userId: userId,
25 | username: username,
26 | endpoint: req.url.replace('https://' + process.env.WEBSITE_HOSTNAME, ''),
27 | request: req.body
28 | }]
29 | }
30 | }
31 |
32 | export default httpTrigger
33 |
--------------------------------------------------------------------------------
/server/updateProfile/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/server/updateProfileColor/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": ["post"]
9 | },
10 | {
11 | "type": "http",
12 | "direction": "out",
13 | "name": "res"
14 | },
15 | {
16 | "type": "webPubSub",
17 | "name": "actions",
18 | "hub": "chat",
19 | "direction": "out"
20 | },
21 | {
22 | "tableName": "auditLog",
23 | "name": "tableBinding",
24 | "type": "table",
25 | "direction": "out"
26 | }
27 | ],
28 | "scriptFile": "../dist/updateProfileColor/index.js"
29 | }
30 |
--------------------------------------------------------------------------------
/server/updateProfileColor/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 |
3 | import { authenticatedAzureWrap } from '../src/azureWrap'
4 | import updateProfileColor from '../src/endpoints/updateProfileColor'
5 |
6 | const httpTrigger: AzureFunction = async function (
7 | context: Context,
8 | req: HttpRequest
9 | ): Promise {
10 | await authenticatedAzureWrap(context, req, updateProfileColor, { audit: true })
11 | }
12 |
13 | export default httpTrigger
14 |
--------------------------------------------------------------------------------
/server/updateRoom/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "bindings": [
3 | {
4 | "authLevel": "anonymous",
5 | "type": "httpTrigger",
6 | "direction": "in",
7 | "name": "req",
8 | "methods": [
9 | "get",
10 | "post"
11 | ]
12 | },
13 | {
14 | "type": "http",
15 | "direction": "out",
16 | "name": "res"
17 | }
18 | ],
19 | "scriptFile": "../dist/updateRoom/index.js"
20 | }
21 |
--------------------------------------------------------------------------------
/server/updateRoom/index.ts:
--------------------------------------------------------------------------------
1 | import { AzureFunction, Context, HttpRequest } from '@azure/functions'
2 | import { authenticatedAzureWrap } from '../src/azureWrap'
3 | import updateRoom from '../src/endpoints/updateRoom'
4 |
5 | const httpTrigger: AzureFunction = async function (
6 | context: Context,
7 | req: HttpRequest
8 | ): Promise {
9 | // This doesn't *need* to be mod-only,
10 | // but as long as we're only calling it in the editor, why not
11 | await authenticatedAzureWrap(context, req, updateRoom, { mod: true })
12 | }
13 |
14 | export default httpTrigger
15 |
--------------------------------------------------------------------------------
/server/updateRoom/sample.dat:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Azure"
3 | }
--------------------------------------------------------------------------------
/src/Deferred.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * based loosely on the following:
3 | *
4 | * @see https://stackoverflow.com/questions/44728779/deferred-that-extends-promise
5 | * @see https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/core/src/Misc/deferred.ts
6 | */
7 | export class Deferred {
8 | private _promise: Promise;
9 | private _resolve: (value: T | PromiseLike) => void;
10 | private _reject: (reason?: any) => void;
11 |
12 | constructor () {
13 | this._promise = new Promise((resolve, reject) => {
14 | this._resolve = resolve
15 | this._reject = reject
16 | })
17 | }
18 |
19 | public get promise () {
20 | return this._promise
21 | }
22 |
23 | public get resolve () {
24 | return this._resolve
25 | }
26 |
27 | public get reject () {
28 | return this._reject
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/admin/actions.ts:
--------------------------------------------------------------------------------
1 | import { Room } from '../room'
2 |
3 | export type Action =
4 | | LoggedInAction
5 | | UpdateRoomIdsAction
6 | | UpdateAndShowRoomAction
7 |
8 | export enum ActionType {
9 | LoggedIn = 'LOGGED_IN',
10 | UpdateRoomIds = 'UPDATE_ROOM_IDS',
11 | UpdateAndShowRoom = 'UPDATE_AND_SHOW_ROOM'
12 | }
13 |
14 | interface LoggedInAction {
15 | type: ActionType.LoggedIn;
16 | value: undefined;
17 | }
18 |
19 | export const LoggedInAction = (): LoggedInAction => {
20 | return {
21 | type: ActionType.LoggedIn,
22 | value: undefined
23 | }
24 | }
25 |
26 | interface UpdateRoomIdsAction {
27 | type: ActionType.UpdateRoomIds;
28 | value: string[];
29 | }
30 |
31 | export const UpdateRoomIds = (roomIds: string[]): UpdateRoomIdsAction => {
32 | return {
33 | type: ActionType.UpdateRoomIds,
34 | value: roomIds
35 | }
36 | }
37 |
38 | interface UpdateAndShowRoomAction {
39 | type: ActionType.UpdateAndShowRoom;
40 | value: {
41 | roomId: string
42 | roomData: Room
43 | };
44 | }
45 |
46 | export const UpdateAndShowRoomAction = (roomId: string, roomData: Room): UpdateAndShowRoomAction => {
47 | return {
48 | type: ActionType.UpdateAndShowRoom,
49 | value: { roomId, roomData }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/admin/components/LoggedOutView.tsx:
--------------------------------------------------------------------------------
1 | // Note - we're doing firebase 8 because the firebaseui stuff doesn't work with 9, big F
2 | import { currentUser, sendSignInLinkToEmail } from '../../authentication'
3 | import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth'
4 | import React from 'react'
5 | import firebase from 'firebase/app'
6 | import 'firebase/auth'
7 |
8 | const uiConfig = {
9 | callbacks: {
10 | // The documentation on the firebaseui README appears somewhat borked at time of writing; the structure of
11 | // AuthResult doesn't line up with itself! If you go back to that README treat it with caution.
12 | signInSuccessWithAuthResult: function (authResult, redirectUrl) {
13 | const user = currentUser()
14 | if (user.shouldVerifyEmail) {
15 | sendSignInLinkToEmail(user.email)
16 | }
17 | window.location.reload()
18 | return false
19 | }
20 | },
21 | signInOptions: [
22 | firebase.auth.EmailAuthProvider.PROVIDER_ID,
23 | firebase.auth.GoogleAuthProvider.PROVIDER_ID
24 | ]
25 | }
26 |
27 | export default function LoggedOutView () {
28 | return (
29 |
30 |
31 | Welcome to the secret Roguelike Celebration backstage area!
32 |
33 |
34 |
35 | If you're not a Roguelike Celebration admin, this isn't for you. If you should be an admin, but can't log in, ask in our other chat.
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/admin/components/RoomOptionsView.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/display-name */
2 | import _ from 'lodash'
3 | import React from 'react'
4 |
5 | import { deleteRoom } from '../../networking'
6 |
7 | export default function (props: {roomId: string, updateRoom: () => void, createRoom: (name: string) => void}) {
8 | const clickedNew = async () => {
9 | const name = prompt('What would you like the room to be called?')
10 | if (!name) return
11 | await props.createRoom(name)
12 | }
13 |
14 | const clickedDelete = async () => {
15 | if (!confirm('Are you SURE you want to delete this? This will delete it from the server memory, and you will only be able to restore it if it is saved on disk. (Will also cause a page refresh)')) return
16 |
17 | await deleteRoom(props.roomId)
18 | // TODO: Remove roomId from local list in a less silly way
19 | window.location.reload()
20 | }
21 |
22 | const clickedSave = () => {
23 | props.updateRoom()
24 | }
25 |
26 | return (
27 |
28 |
30 | Create
31 |
32 |
34 | Delete
35 |
36 |
38 | Save
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/admin/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ReactDOM from 'react-dom'
3 |
4 | import App from './components/App'
5 |
6 | window.addEventListener('DOMContentLoaded', async () => {
7 | ReactDOM.render( , document.getElementById('root') as HTMLElement)
8 | })
9 |
--------------------------------------------------------------------------------
/src/admin/reducer.ts:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app'
2 | import Config from '../config'
3 | import { Room } from '../room'
4 | import { Action, ActionType } from './actions'
5 |
6 | export interface State {
7 | firebaseApp: firebase.app.App
8 | isLoggedIn: boolean
9 |
10 | roomIds?: string[]
11 | roomData: {[roomId: string]: Room}
12 |
13 | displayedRoomId?: string
14 | }
15 |
16 | export const defaultState: State = {
17 | firebaseApp: firebase.initializeApp(Config.FIREBASE_CONFIG),
18 | isLoggedIn: false,
19 | roomData: {}
20 | }
21 |
22 | // TODO: Split this out into separate reducers based on worldstate actions vs UI actions?
23 | export default (oldState: State, action: Action): State => {
24 | const state: State = JSON.parse(JSON.stringify(oldState))
25 |
26 | if (action.type === ActionType.LoggedIn) {
27 | state.isLoggedIn = true
28 | }
29 |
30 | if (action.type === ActionType.UpdateRoomIds) {
31 | state.roomIds = action.value
32 | }
33 |
34 | if (action.type === ActionType.UpdateAndShowRoom) {
35 | state.roomData[action.value.roomId] = action.value.roomData
36 | state.displayedRoomId = action.value.roomId
37 | }
38 |
39 | return state
40 | }
41 |
--------------------------------------------------------------------------------
/src/admin/style.css:
--------------------------------------------------------------------------------
1 | #room-list {
2 | overflow: scroll;
3 | height: 95vh;
4 | }
5 |
6 | li {
7 | list-style: none;
8 | }
9 |
10 | #editor {
11 | position: absolute;
12 | top: 80px;
13 | left: 400px;
14 | height: 100vh;
15 | }
16 |
17 | #room-options-view {
18 | position: absolute;
19 | top: 10px;
20 | right: 20px;
21 | }
22 |
23 | #room-options-view button {
24 | margin-left: 20px;
25 | }
--------------------------------------------------------------------------------
/src/components/BadgeUnlockModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { Badge } from '../../server/src/badges'
3 | import { ShowModalAction } from '../Actions'
4 | import { DispatchContext } from '../App'
5 | import { Modal } from '../modals'
6 | import BadgeView from './BadgeView'
7 |
8 | export default function BadgeUnlockModal (props: { badge: Badge }) {
9 | const { badge } = props
10 | const dispatch = useContext(DispatchContext)
11 | const showBadges = () => {
12 | dispatch(ShowModalAction(Modal.Badges))
13 | }
14 |
15 | return (
16 |
17 |
You earned a badge!
18 |
19 |
You can now equip the badge!
20 |
21 |
Show Badge Inventory
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/BadgeView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import customEmojiMap from '../emoji/customEmojiMap.json'
3 | import reservedEmojiMap from '../emoji/reservedEmojiMap.json'
4 |
5 | function BadgeView (props: {emoji: string, description: string, isCustom?: boolean}) {
6 | const { emoji, description, isCustom } = props
7 |
8 | if (!(description && emoji)) {
9 | return
10 | }
11 |
12 | const content = isCustom
13 | ?
14 | : {emoji}
15 |
16 | // The weird nested badge-text-[emoji] span is for drag preview shenanigans
17 | // See the dragstart event handler in BadgesModalView.tsx for context
18 | return (
19 | <>
20 |
21 | {content}
22 |
23 | >
24 | )
25 | }
26 |
27 | export default React.memo(BadgeView)
28 |
--------------------------------------------------------------------------------
/src/components/ClientDeployedModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function ClientDeployedModal () {
4 | return (
5 |
6 |
New Version Available!
7 |
We just updated the social space with new features and bugfixes!
8 |
Please reload this page whenever is convenient so you can be running the latest code.
9 |
window.location.reload()}>Reload
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/CodeOfConductView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function CodeOfConductView () {
4 | return (
5 |
6 |
Our goal is to have a fun and welcoming celebration of roguelike games, so we have a formalized code of conduct that sets these expectations.
7 |
Read our Code of Conduct.
8 |
If you're being harassed, notice someone being harassed, or have any other concerns related to the code of conduct, you have a few options:
9 |
10 | Type /mod
into the chat box, followed by a message, to contact all moderators.
11 | Email contact@roguelike.club .
12 | If you have a report or concern related to an organizer, please contact a different member of the organizing team outside of this tool (via email, Twitter DM, Discord, or some other service).
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/EmailVerifiedView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import firebase from 'firebase/app'
3 | import 'firebase/auth'
4 |
5 | export default function EmailVerifiedView () {
6 | const user = firebase.auth().currentUser
7 |
8 | if (!user.emailVerified) {
9 | firebase.auth().signInWithEmailLink(user.email, window.location.href).then(() => {
10 | // A crude way of forcing a rerender, because I don't want to go through the trouble of making an action.
11 | window.location.reload()
12 | })
13 | }
14 |
15 | if (!user.emailVerified) {
16 | return (
17 |
18 |
19 | Attempting to verify email.
20 |
21 |
22 |
23 | Please wait while your email is verified.
24 |
25 |
26 |
27 | )
28 | } else {
29 | return (
30 |
31 |
32 | Your email has been successfully verified!
33 |
34 |
35 |
36 | You may now close this window and go back to the main app.
37 |
38 |
39 |
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/GoHomeView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function GoHomeView () {
4 | return (
5 |
6 |
A magical force repels you from the entrance
7 |
Roguelike Celebration is currently closed! Check the schedule to see when our doors will be open again.
8 |
We look forward to celebrating again with you soon :)
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/HeldItemView.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 |
3 | import { UserMapContext } from '../App'
4 | import { dropItem } from '../networking'
5 |
6 | const HeldItemView = () => {
7 | const { userMap, myId } = useContext(UserMapContext)
8 | const user = userMap[myId]
9 |
10 | const dropHeldItem = () => {
11 | dropItem()
12 | }
13 |
14 | if (user.item) {
15 | return (
16 |
17 | You are holding {user.item}.{' '}
18 |
19 | Drop it
20 |
21 | .
22 |
23 | )
24 | } else {
25 | return null
26 | }
27 | }
28 |
29 | export default HeldItemView
30 |
--------------------------------------------------------------------------------
/src/components/HelpView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { SlashCommands } from '../SlashCommands'
3 |
4 | export default function HelpView () {
5 | const rows = SlashCommands.map(command => {
6 | return (
7 |
8 | {command.invocations.join(', ')}
9 | {command.description}
10 |
11 | )
12 | })
13 |
14 | return (
15 |
16 |
Welcome!
17 |
Welcome to the Roguelike Celebration social space! This is an open-source chat app / virtual world built specifically for Roguelike Celebration.
18 |
You can access this reference at any time by clicking the Help button in the left sidebar or by typing /help into the chat box.
19 |
If you have questions, feedback, or need to report a code of conduct violation, you can contact us via the /mod command.
20 |
Command Reference
21 |
You can enter any of these commands by typing directly into the chat box.
22 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/MapModalView.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { ShowModalAction } from '../Actions'
3 | import { DispatchContext } from '../App'
4 | import { Modal } from '../modals'
5 | import { Room } from '../room'
6 | import MapView from './MapView'
7 |
8 | interface Props {
9 | presenceData: { [roomId: string]: number };
10 | currentRoomId: string
11 | }
12 |
13 | export default function MapModalView (props: Props) {
14 | const dispatch = useContext(DispatchContext)
15 |
16 | const showList = () => {
17 | dispatch(ShowModalAction(Modal.RoomList))
18 | }
19 | return (
20 |
21 |
Map
22 |
Click on a room to move there!
23 |
If you find the map difficult to use, you can also view a list of all rooms .
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/MessageItem/MessageItem.css:
--------------------------------------------------------------------------------
1 | .message-item {
2 | width: 100%;
3 |
4 | margin: 0;
5 | padding: 0;
6 |
7 | position: absolute;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/MessageItem/index.ts:
--------------------------------------------------------------------------------
1 | export { MessageItem } from './MessageItem'
2 |
--------------------------------------------------------------------------------
/src/components/MessageList/MessageList.css:
--------------------------------------------------------------------------------
1 | .message-list {
2 | border: 1px solid var(--fbc-white);
3 | border-radius: 4px;
4 | height: 40vh;
5 | list-style-type: none;
6 |
7 | margin: 0;
8 | padding: 0;
9 |
10 | overflow-y: scroll;
11 | overflow-x: hidden;
12 |
13 | position: relative;
14 | }
15 |
16 | .messages-load-progress {
17 | background: var(--main-background);
18 |
19 | width: 100%;
20 | height: 40vh;
21 |
22 | display: flex;
23 | align-items: center;
24 | justify-content: center;
25 |
26 | position: absolute;
27 | }
28 |
29 | .sentinel {
30 | width: 100%;
31 | height: 1px;
32 | position: absolute;
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/src/components/MessageList/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { useAutoscroll } from './useAutoscroll'
2 | export { useShouldHideTimestamp } from './useShouldHideTimestamp'
3 |
--------------------------------------------------------------------------------
/src/components/MessageList/hooks/useShouldHideTimestamp.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import { Message } from '../../../message'
3 |
4 | const THREE_MINUTES = 1_000 * 60 * 3
5 |
6 | type ShouldHideTimestamp = (
7 | message: Message,
8 | previousMessage: Message | undefined
9 | ) => boolean;
10 |
11 | export const useShouldHideTimestamp = () =>
12 | useCallback(
13 | (message, previousMessage) =>
14 | previousMessage &&
15 | 'userId' in previousMessage &&
16 | 'userId' in message &&
17 | previousMessage.userId === message.userId &&
18 | new Date(message.timestamp).getTime() -
19 | new Date(previousMessage.timestamp).getTime() <
20 | THREE_MINUTES,
21 | []
22 | )
23 |
--------------------------------------------------------------------------------
/src/components/MessageList/index.ts:
--------------------------------------------------------------------------------
1 | export { MessageList } from './MessageList'
2 |
--------------------------------------------------------------------------------
/src/components/MiniMapView.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { ShowModalAction } from '../Actions'
3 | import { DispatchContext } from '../App'
4 | import { Modal } from '../modals'
5 | import { Room } from '../room'
6 |
7 | import MapView from './MapView'
8 |
9 | interface Props {
10 | presenceData: { [roomId: string]: number };
11 | currentRoomId: string
12 | }
13 |
14 | export default function MiniMapView (props: Props) {
15 | const dispatch = useContext(DispatchContext)
16 |
17 | const handleClickCapture = (e) => {
18 | dispatch(ShowModalAction(Modal.Map))
19 | e.stopPropagation()
20 | }
21 |
22 | return (
23 |
24 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/RateTalkView.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Roguelike-Celebration/azure-mud/6d3029d741b82ae625ad9277f4fb9eb4ecc57099/src/components/RateTalkView.tsx
--------------------------------------------------------------------------------
/src/components/RiddleModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function RiddleModalView (props: {riddles: string[]}) {
4 | const riddleViews = props.riddles.map(function (val, index) { return {val}
})
5 | const riddleTitle = (props.riddles.length > 1 ? 'The engraved riddles read...' : 'The engraved riddle reads...')
6 |
7 | return (
8 |
9 |
{riddleTitle}
10 | {riddleViews}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/RoomListView.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react'
2 | import { FaVideo } from 'react-icons/fa'
3 | import { UserMapContext } from '../App'
4 | import { moveToRoom } from '../networking'
5 |
6 | import { Room } from '../room'
7 |
8 | interface Props {
9 | rooms: Room[]
10 | }
11 |
12 | export default function RoomListView (props: Props) {
13 | const { userMap, myId } = useContext(UserMapContext)
14 |
15 | const list = props.rooms
16 | .sort((a, b) => a.displayName.toLowerCase() > b.displayName.toLowerCase() ? 1 : -1)
17 | .map((r) => {
18 | return (r.hidden && !userMap[myId].isMod) ? '' :
19 | })
20 |
21 | return (
22 |
23 |
List of Rooms
24 |
27 |
28 | )
29 | }
30 |
31 | const RoomListItem = (props: { room: Room }) => {
32 | const { room } = props
33 |
34 | const onClick = () => {
35 | moveToRoom(room.id)
36 | }
37 | const userCount = room.users ? `(${room.users.length})` : ''
38 | const videoIcon = room.videoUsers && room.videoUsers.length > 0 ? : ''
39 |
40 | return (
41 |
42 |
43 | {room.hidden ? '(hidden) ' : ''}{room.displayName} {userCount} {videoIcon}
44 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/SpecialTextModalView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function SpecialTextModalView (props: {text: string}) {
4 | return (
5 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/VirtualizationProvider/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | VirtualizationContext,
3 | VirtualizationProvider
4 | } from './VirtualizationProvider'
5 |
--------------------------------------------------------------------------------
/src/components/WelcomeModalView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function WelcomeModalView (props: {}) {
4 | return (
5 |
6 |
This isn't a normal chat app!
7 |
8 | In the Roguelike Celebration conference space, you can only send/receive
9 | messages in the chat room you are currently in .
10 |
11 |
12 | You can move from virtual room to virtual room to talk to different
13 | people.
14 |
15 |
16 | You may see or hear others on videochat, but they can't see or hear you
17 | unless you explicitly turn on your audio or video.
18 |
19 |
20 | Our hope is to facilitate smaller group conversations, to capture the feel of an in-person conference, and to give
21 | you a fun and playful space to explore with your fellow attendees.
22 |
23 |
24 | Happy wandering, and be wary of reading scrolls without identifying them
25 | first ;)
26 |
27 |
-The Roguelike Celebration team
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/YouAreBannedView.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export default function YouAreBannedView () {
4 | return (
5 |
6 |
You are banned
7 |
You have been banned from the social space. If you believe you have been banned in error, please email contact at roguelike.club .
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/feature/ConfettiRoomView.tsx:
--------------------------------------------------------------------------------
1 | import ConfettiGenerator from 'confetti-js'
2 | import React, { useEffect } from 'react'
3 |
4 | export const ConfettiRoomView = () => {
5 | useEffect(() => {
6 | const confettiSettings = { target: 'confetti-canvas' }
7 | const confetti = new ConfettiGenerator(confettiSettings)
8 | confetti.render()
9 |
10 | return () => confetti.clear()
11 | }, [])
12 | return (
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | SERVER_HOSTNAME: process.env.SERVER_HOSTNAME,
3 | FIREBASE_CONFIG: {
4 | apiKey: process.env.FIREBASE_API_KEY,
5 | authDomain: process.env.FIREBASE_AUTH_DOMAIN,
6 | projectId: process.env.FIREBASE_PROJECT_ID,
7 | storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
8 | messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
9 | appId: process.env.FIREBASE_APP_ID
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/emoji/customEmojiMap.json:
--------------------------------------------------------------------------------
1 | {
2 | "artificer": "images/badges/procgen_artificer.png",
3 | "bard": "images/badges/procgen_bard.png",
4 | "cleric": "images/badges/procgen_cleric.png",
5 | "device_of_luthien": "images/badges/device_of_luthien.png",
6 | "druid": "images/badges/procgen_druid.png",
7 | "golden_thesis": "images/badges/golden_thesis.png",
8 | "nega_ticket": "images/badges/nega_ticket.png",
9 | "paladin": "images/badges/procgen_paladin.png",
10 | "phylactery": "images/badges/phylactery.png",
11 | "ranger": "images/badges/procgen_ranger.png",
12 | "sorceror": "images/badges/procgen_sorceror.png",
13 | "spelunky_pug": "https://static.wikia.nocookie.net/spelunky/images/a/a4/Monty_Link_S2.png",
14 | "warlock": "images/badges/procgen_warlock.png",
15 | "wizard": "images/badges/procgen_wizard.png",
16 | "undermuffin": "images/badges/undermuffin.png",
17 | "eelhead": "images/badges/eel_0.png",
18 | "eeltail": "images/badges/eel_1.png",
19 | "obelisk": "images/badges/OBELISK_1.png",
20 | "turkey_leg": "images/badges/leg-of-yendor.png",
21 | "goblin_barbie": "images/badges/goblin_barbie.png",
22 | "goblin_appreciation": "images/badges/goblin_appreciation.png"
23 | }
24 |
--------------------------------------------------------------------------------
/src/emoji/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import customEmojiMap from './customEmojiMap.json'
4 |
5 | export function renderCustomEmojiString (string: string) {
6 | const stringParts = string.split(/(:.*?:)/)
7 | const emojiComponent = stringParts.reduce((newComponent, stringPart) => {
8 | if (stringPart.startsWith(':') && stringPart.endsWith(':')) {
9 | const emojiIdRegex = /:(.*):/
10 | const [_, emojiId] = emojiIdRegex.exec(stringPart)
11 |
12 | if (customEmojiMap[emojiId]) {
13 | return <>{newComponent} { }>
14 | }
15 |
16 | return <>{newComponent} {':' + emojiId + ':'}>
17 | }
18 |
19 | return <>{newComponent} {stringPart}>
20 | }, <>>)
21 |
22 | return emojiComponent
23 | }
24 |
25 | export const emojiMentionsData = Object.keys(customEmojiMap).map(key => {
26 | return {
27 | id: key
28 | }
29 | })
30 |
--------------------------------------------------------------------------------
/src/emoji/reservedEmojiMap.json:
--------------------------------------------------------------------------------
1 | {
2 | "mod": "images/badges/modbadge.png",
3 | "speaker": "images/badges/speakerbadge.png"
4 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ReactDOM from 'react-dom'
3 |
4 | import App from './App'
5 | import { currentTheme } from './storage'
6 |
7 | window.addEventListener('DOMContentLoaded', async () => {
8 | ReactDOM.render( , document.getElementById('root') as HTMLElement)
9 | document.body.classList.add(await currentTheme())
10 | })
11 |
--------------------------------------------------------------------------------
/src/message/enums.ts:
--------------------------------------------------------------------------------
1 | export enum MessageType {
2 | Connected = 'CONNECTED',
3 | Disconnected = 'DISCONNECTED',
4 | Entered = 'ENTERED',
5 | Left = 'LEFT',
6 | MovedRoom = 'MOVED',
7 | SameRoom = 'SAME',
8 | Chat = 'CHAT',
9 | Whisper = 'WHISPER',
10 | Shout = 'SHOUT',
11 | Emote = 'EMOTE',
12 | Dance = 'DANCE',
13 | Mod = 'MOD',
14 | Error = 'ERROR',
15 | Command = 'COMMAND',
16 | Caption = 'CAPTION'
17 | }
18 |
--------------------------------------------------------------------------------
/src/message/index.ts:
--------------------------------------------------------------------------------
1 | export { MessageType } from './enums'
2 | export {
3 | CaptionMessage,
4 | ChatMessage,
5 | CommandMessage,
6 | ConnectedMessage,
7 | DanceMessage,
8 | DisconnectedMessage,
9 | EmoteMessage,
10 | EnteredMessage,
11 | ErrorMessage,
12 | LeftMessage,
13 | Message,
14 | ModMessage,
15 | MovedRoomMessage,
16 | SameRoomMessage,
17 | ShoutMessage,
18 | WhisperMessage
19 | } from './types'
20 | export {
21 | createCaptionMessage,
22 | createChatMessage,
23 | createCommandMessage,
24 | createConnectedMessage,
25 | createDanceMessage,
26 | createDisconnectedMessage,
27 | createEmoteMessage,
28 | createEnteredMessage,
29 | createErrorMessage,
30 | createLeftMessage,
31 | createModMessage,
32 | createMovedRoomMessage,
33 | createSameRoomMessage,
34 | createShoutMessage,
35 | createWhisperMessage,
36 | isCaptionMessage,
37 | isDeletableMessage,
38 | isMovementMessage
39 | } from './utils'
40 |
--------------------------------------------------------------------------------
/src/modals.ts:
--------------------------------------------------------------------------------
1 | // Enumerates what modal screens we might be showing on top of the chat view
2 | export enum Modal {
3 | None = 0,
4 | ProfileEdit,
5 | NoteWall,
6 | Settings,
7 | MediaSelector,
8 | CodeOfConduct,
9 | Schedule,
10 | Help,
11 | Map,
12 | Welcome,
13 | RoomList,
14 | FeatureRainbowGate,
15 | FeatureDullDoor,
16 | ServerSettings,
17 | ClientDeployed,
18 | Disconnected,
19 | FeatureFullRoomIndex,
20 | HappeningNow,
21 | Riddles,
22 | Badges,
23 | BadgeUnlock,
24 | SpecialFeatureText,
25 | Obelisk
26 | }
27 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface EntityState {
2 | entities: Record;
3 | ids: string[];
4 | }
5 |
6 | export interface PayloadAction {
7 | type: T;
8 | payload: P;
9 | }
10 |
--------------------------------------------------------------------------------
/src/useReducerWithThunk.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Loosely adapted to TS from Solute Labs, slightly updated to use the "Thunk
3 | * API" from redux-thunk
4 | *
5 | * @see https://www.solutelabs.com/blog/configuring-thunk-action-creators-and-redux-dev-tools-with-reacts-use-reducer-hook
6 | * @see https://github.com/reduxjs/redux-thunk
7 | */
8 | import { Reducer, useReducer } from 'react'
9 |
10 | export type ThunkDispatch = (action: A | Thunk ) => void;
11 | type GetState = () => S;
12 | export type Thunk = (
13 | dispatch: ThunkDispatch ,
14 | getState: GetState
15 | ) => void;
16 |
17 | const isFunction = (a: A | Thunk ): a is Thunk =>
18 | typeof a === 'function'
19 |
20 | export function useReducerWithThunk (
21 | reducer: Reducer,
22 | initialState: S
23 | ): [S, ThunkDispatch] {
24 | const [state, dispatch] = useReducer(reducer, initialState)
25 |
26 | const thunkDispatch = (action: A | Thunk ) => {
27 | if (isFunction(action)) {
28 | action(thunkDispatch, () => state)
29 | } else {
30 | dispatch(action)
31 | }
32 | }
33 | return [state, thunkDispatch]
34 | }
35 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const nie = (_: T): void => {
2 | throw Error('Not Implemented')
3 | }
4 |
5 | /**
6 | * converts a frame count per second to the number of milliseconds per frame,
7 | * returns 0 if you pass in a frame count less than or equal to 0 (negative or
8 | * zero fps doesn't make any sense)
9 | *
10 | * n.b. `1e3` is just kind of a "neat" way to write "one thousand" ... if it
11 | * feels "too clever" feel free to rewrite it as `1_000` or just `1000` (though
12 | * I do like the numeric separators if we're going for absolute clarity)
13 | *
14 | * @param frames the number of frames per second
15 | * @returns the number of milliseconds per frame
16 | */
17 | export const fps = (frames: number) => frames <= 0 ? 0 : 1e3 / frames
18 |
--------------------------------------------------------------------------------
/src/videochat/twilio/AudioTrack.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 | import { AudioTrack as IAudioTrack } from 'twilio-video'
3 |
4 | interface AudioTrackProps {
5 | track: IAudioTrack;
6 | }
7 |
8 | // TODO: Figure out what's up with activeSinkId
9 |
10 | export default function AudioTrack ({ track }: AudioTrackProps) {
11 | // const { activeSinkId } = useAppState()
12 | const audioEl = useRef()
13 |
14 | useEffect(() => {
15 | audioEl.current = track.attach()
16 | audioEl.current.setAttribute('data-cy-audio-track-name', track.name)
17 | document.body.appendChild(audioEl.current)
18 | return () => track.detach().forEach(el => el.remove())
19 | }, [track])
20 |
21 | // useEffect(() => {
22 | // audioEl.current?.setSinkId?.(activeSinkId)
23 | // }, [activeSinkId])
24 |
25 | return null
26 | }
27 |
--------------------------------------------------------------------------------
/src/videochat/twilio/Publication.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import useTrack from './useTrack'
3 | import AudioTrack from './AudioTrack'
4 | import VideoTrack from './VideoTrack'
5 |
6 | import {
7 | AudioTrack as IAudioTrack,
8 | LocalTrackPublication,
9 | LocalVideoTrack,
10 | RemoteTrackPublication,
11 | RemoteVideoTrack,
12 | Track
13 | } from 'twilio-video'
14 |
15 | interface PublicationProps {
16 | publication: LocalTrackPublication | RemoteTrackPublication;
17 | isLocalParticipant?: boolean;
18 | videoOnly?: boolean;
19 | videoPriority?: Track.Priority | null;
20 | }
21 |
22 | export default function Publication ({ publication, isLocalParticipant, videoOnly, videoPriority }: PublicationProps) {
23 | const track = useTrack(publication)
24 |
25 | if (!track) return null
26 |
27 | switch (track.kind) {
28 | case 'video':
29 | return (
30 |
35 | )
36 | case 'audio':
37 | return videoOnly ? null :
38 | default:
39 | return null
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/videochat/twilio/useMediaStreamTrack.ts:
--------------------------------------------------------------------------------
1 |
2 | import { useState, useEffect } from 'react'
3 | import { AudioTrack, VideoTrack } from 'twilio-video'
4 |
5 | /*
6 | * This hook allows components to reliably use the 'mediaStreamTrack' property of
7 | * an AudioTrack or a VideoTrack. Whenever 'localTrack.restart(...)' is called, it
8 | * will replace the mediaStreamTrack property of the localTrack, but the localTrack
9 | * object will stay the same. Therefore this hook is needed in order for components
10 | * to rerender in response to the mediaStreamTrack changing.
11 | */
12 | export default function useMediaStreamTrack (track?: AudioTrack | VideoTrack) {
13 | const [mediaStreamTrack, setMediaStreamTrack] = useState(track?.mediaStreamTrack)
14 |
15 | useEffect(() => {
16 | setMediaStreamTrack(track?.mediaStreamTrack)
17 |
18 | if (track) {
19 | const handleStarted = () => { console.log('Set media stream track!'); setMediaStreamTrack(track.mediaStreamTrack) }
20 | track.on('started', handleStarted)
21 | return () => {
22 | track.off('started', handleStarted)
23 | }
24 | }
25 | }, [track])
26 |
27 | return mediaStreamTrack
28 | }
29 |
--------------------------------------------------------------------------------
/src/videochat/twilio/usePublications.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { LocalTrackPublication, Participant, RemoteTrackPublication } from 'twilio-video'
3 |
4 | export type TrackPublication = LocalTrackPublication | RemoteTrackPublication;
5 |
6 | export default function usePublications (participant: Participant) {
7 | const [publications, setPublications] = useState([])
8 |
9 | useEffect(() => {
10 | // Reset the publications when the 'participant' variable changes.
11 | setPublications(Array.from(participant.tracks.values()) as TrackPublication[])
12 |
13 | const publicationAdded = (publication: TrackPublication) =>
14 | setPublications(prevPublications => [...prevPublications, publication])
15 | const publicationRemoved = (publication: TrackPublication) =>
16 | setPublications(prevPublications => prevPublications.filter(p => p !== publication))
17 |
18 | participant.on('trackPublished', publicationAdded)
19 | participant.on('trackUnpublished', publicationRemoved)
20 | return () => {
21 | participant.off('trackPublished', publicationAdded)
22 | participant.off('trackUnpublished', publicationRemoved)
23 | }
24 | }, [participant])
25 |
26 | return publications
27 | }
28 |
--------------------------------------------------------------------------------
/src/videochat/twilio/useTrack.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { useEffect, useState } from 'react'
3 | import { LocalTrackPublication, RemoteTrackPublication } from 'twilio-video'
4 |
5 | export default function useTrack (publication: LocalTrackPublication | RemoteTrackPublication | undefined) {
6 | const [track, setTrack] = useState(publication && publication.track)
7 |
8 | useEffect(() => {
9 | // Reset the track when the 'publication' variable changes.
10 | setTrack(publication && publication.track)
11 |
12 | if (publication) {
13 | const removeTrack = () => setTrack(null)
14 |
15 | publication.on('subscribed', setTrack)
16 | publication.on('unsubscribed', removeTrack)
17 | return () => {
18 | publication.off('subscribed', setTrack)
19 | publication.off('unsubscribed', removeTrack)
20 | }
21 | }
22 | }, [publication])
23 |
24 | return track
25 | }
26 |
--------------------------------------------------------------------------------
/src/videochat/twilio/useVideoTrackDimensions.ts:
--------------------------------------------------------------------------------
1 |
2 | import { useState, useEffect } from 'react'
3 | import { LocalVideoTrack, RemoteVideoTrack } from 'twilio-video'
4 |
5 | type TrackType = LocalVideoTrack | RemoteVideoTrack;
6 |
7 | export default function useVideoTrackDimensions (track?: TrackType) {
8 | const [dimensions, setDimensions] = useState(track?.dimensions)
9 |
10 | useEffect(() => {
11 | setDimensions(track?.dimensions)
12 |
13 | if (track) {
14 | const handleDimensionsChanged = (track: TrackType) => setDimensions({
15 | width: track.dimensions.width,
16 | height: track.dimensions.height
17 | })
18 | track.on('dimensionsChanged', handleDimensionsChanged)
19 | return () => {
20 | track.off('dimensionsChanged', handleDimensionsChanged)
21 | }
22 | }
23 | }, [track])
24 |
25 | return dimensions
26 | }
27 |
--------------------------------------------------------------------------------
/stream.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Roguelike Celebration Stream
4 |
5 |
6 |
7 |
8 |
9 |
26 |
27 |
32 |