├── .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 | 32 | 36 | 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 | 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 | ? {`${emoji} 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 | 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 | 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 | 23 | {rows} 24 |
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 .

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 |
    25 | {list} 26 |
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 | 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 |
    6 |

    {props.text}

    7 |
    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 |
    10 |
    11 | 17 |
    18 | 25 |
    26 | 27 | 32 | -------------------------------------------------------------------------------- /style/badges.css: -------------------------------------------------------------------------------- 1 | #badges .badge { 2 | font-size: 25px; 3 | } 4 | 5 | #badges .selected-badge { 6 | background-color: rgba(255, 255, 255, 0.2); 7 | border: 2px solid var(--main-font); 8 | border-radius: 2px; 9 | display: inline-block; 10 | font-size: 25px; 11 | line-height: 32px; 12 | min-height: 32px; 13 | padding: 0 3px; 14 | margin: 5px; 15 | min-width: 1em; 16 | text-align: center; 17 | vertical-align: middle; 18 | } 19 | 20 | #badges .unlocked-badge, #badges .locked-badge { 21 | height: 28px; 22 | padding: 4px; 23 | padding-top: 8px; 24 | width: 30px; 25 | } 26 | 27 | #badges .locked-badge { 28 | opacity: 0.5; 29 | cursor: default; 30 | } 31 | 32 | #badges .unlocked-badge.selected { 33 | width: 26px; 34 | } 35 | 36 | #badges .selected { 37 | background: var(--alternate-background); 38 | border: 2px solid var(--active); 39 | border-radius: 2px; 40 | } 41 | 42 | 43 | #badges .badge-sections { 44 | display: inline-flex; 45 | width: 100%; 46 | } 47 | 48 | #badges .badge-sections .equipped { 49 | flex-shrink: 1; 50 | padding-right: 4rem; 51 | width: 33%; 52 | } 53 | 54 | #badges .toggle-badge-category { 55 | border: 0; 56 | color: var(--link); 57 | cursor: pointer; 58 | } 59 | -------------------------------------------------------------------------------- /style/chat.css: -------------------------------------------------------------------------------- 1 | /* TODO: refactor, this is all/only pertinent to the MessageView component now */ 2 | 3 | .message-wrapper .time { 4 | margin-right: 1.5em; 5 | color: var(--gray-text); 6 | float: left; 7 | font-size: 0.8em; 8 | text-transform: lowercase; 9 | } 10 | 11 | .message-wrapper { 12 | padding: 0.25rem; 13 | } 14 | 15 | .message-wrapper .time.show-on-hover { 16 | opacity: 0; 17 | } 18 | 19 | .message-wrapper:hover .time.show-on-hover { 20 | opacity: 1; 21 | } 22 | 23 | .message-wrapper.even-message { 24 | background-color: var(--alternate-background); 25 | } 26 | 27 | .message { 28 | overflow-wrap: anywhere; 29 | } 30 | 31 | .movement-message { 32 | opacity: 0.5; 33 | } 34 | 35 | .inline-custom-emoji { 36 | height: 1.3rem; 37 | position: relative; 38 | bottom: -0.2rem; 39 | } 40 | -------------------------------------------------------------------------------- /style/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "IBM Plex Mono"; 3 | src: url("/fonts/IBMPlexMono-Medium.ttf") format("truetype"); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: "IBM Plex Mono"; 10 | src: url("/fonts/IBMPlexMono-MediumItalic.ttf") format("truetype"); 11 | font-weight: normal; 12 | font-style: italic; 13 | } 14 | 15 | @font-face { 16 | font-family: "IBM Plex Mono"; 17 | src: url("/fonts/IBMPlexMono-SemiBold.ttf") format("truetype"); 18 | font-weight: bold; 19 | font-style: normal; 20 | } 21 | 22 | @font-face { 23 | font-family: "IBM Plex Mono"; 24 | src: url("/fonts/IBMPlexMono-SemiBoldItalic.ttf") format("truetype"); 25 | font-weight: bold; 26 | font-style: italic; 27 | } 28 | 29 | @font-face { 30 | font-family: "Oswald"; 31 | src: url("/fonts/Oswald-VariableFont_wght.ttf") format("truetype"); 32 | font-weight: bold; 33 | font-style: normal; 34 | } 35 | 36 | @font-face { 37 | font-family: "TerminalFont"; 38 | src: url("/fonts/Web437_IBM_VGA_8x16.woff") format("truetype"); 39 | font-weight: bold; 40 | font-style: normal; 41 | } 42 | -------------------------------------------------------------------------------- /style/input.css: -------------------------------------------------------------------------------- 1 | #chat-input { 2 | margin-right: 1em; 3 | width: 80%; 4 | } 5 | 6 | #input { 7 | bottom: 60px; 8 | margin-top: 2em; 9 | width: 100%; 10 | } 11 | 12 | #input .mentions { 13 | margin-right: 1rem; 14 | } 15 | 16 | .main-input { 17 | width: 100%; 18 | border-radius: 5px; 19 | font-family: "IBM Plex Mono", "Consolas", "Courier New", Courier, monospace; 20 | font-size: 14px; 21 | height: 2rem; 22 | background-color: white; 23 | white-space: nowrap; 24 | } 25 | 26 | .mentions { 27 | color:black; 28 | } 29 | 30 | .mentions--singleLine .mentions__control { 31 | display: inline-block; 32 | width: 0px; 33 | } 34 | .mentions--singleLine .mentions__highlighter { 35 | padding: 1px; 36 | } 37 | .mentions--singleLine .mentions__input { 38 | padding: 1px; 39 | border: 2px inset; 40 | } 41 | 42 | .mentions__suggestions__list { 43 | background-color: white; 44 | border: 1px solid rgba(0, 0, 0, 0.15); 45 | } 46 | 47 | .mentions__suggestions__item { 48 | padding: 5px 15px; 49 | border-bottom: 1px solid rgba(0, 0, 0, 0.15); 50 | } 51 | 52 | .mentions__suggestions__item--focused { 53 | background-color: #87ceeb; 54 | } 55 | 56 | .mentions__mention { 57 | position: relative; 58 | z-index: 1; 59 | color: blue; 60 | text-shadow: 1px 1px 1px white, 1px -1px 1px white, -1px 1px 1px white, 61 | -1px -1px 1px white; 62 | text-decoration: underline; 63 | pointer-events: none; 64 | } -------------------------------------------------------------------------------- /style/modal.css: -------------------------------------------------------------------------------- 1 | #modal-wrapper { 2 | background-color: var(--alternate-translucent); 3 | left: 0; 4 | right: 0; 5 | top: 0; 6 | bottom: 0; 7 | position: fixed; 8 | z-index: 100000; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | 14 | #modal { 15 | background-color:var(--main-background); 16 | border: 2px solid var(--main-font); 17 | border-radius: 4px; 18 | box-sizing: border-box; 19 | max-height: 90vh; 20 | margin: auto; 21 | overflow-y: auto; 22 | padding: 30px; 23 | 24 | width: 100%; 25 | max-width: 800px; 26 | 27 | } 28 | 29 | #modal.full-screen { 30 | /** This is a hard-coded value based on the current map size. 31 | A more automatic-updating system would be nice. */ 32 | max-width: 1250px; 33 | } 34 | 35 | #modal h1 { 36 | margin-top: 0; 37 | } 38 | 39 | td.time { 40 | white-space: nowrap; 41 | } 42 | 43 | th.break { 44 | padding: 0%; 45 | } 46 | 47 | #modal #close-button { 48 | float: right; 49 | position: relative; 50 | padding: 0px; 51 | background: transparent; 52 | } 53 | 54 | .riddle { 55 | white-space: pre; 56 | padding: 10px; 57 | border-style: solid; 58 | border-width: 2px; 59 | border-color: var(--main-font); 60 | } 61 | 62 | #bonus-modal .with-line-breaks { 63 | white-space: pre; 64 | } 65 | 66 | @media (max-width: 600px) { 67 | #modal { 68 | padding: 1rem; 69 | } 70 | } -------------------------------------------------------------------------------- /style/nav.css: -------------------------------------------------------------------------------- 1 | #side-menu { 2 | list-style: none; 3 | margin-top: 30px; 4 | position: fixed; 5 | width: 250px; 6 | z-index: 1; 7 | } 8 | 9 | @media only screen and (max-device-width: 500px) { 10 | #side-menu { 11 | background-color:var(--main-background); 12 | font-size: 30px; 13 | position: fixed; 14 | left: 0; 15 | height: 100vh; 16 | top: 0; 17 | width: 100vw; 18 | overflow: scroll; 19 | margin-top: 0; 20 | z-index: 10; 21 | } 22 | 23 | #side-menu #close-button { 24 | font-size: 64px; 25 | margin-right: 20px; 26 | } 27 | } 28 | 29 | #side-menu li { 30 | cursor: pointer; 31 | list-style: none; 32 | } 33 | 34 | button.nav-item { 35 | background-color: transparent; 36 | border: none; 37 | outline: none; 38 | color: var(--main-font); 39 | cursor: pointer; 40 | display: inline; 41 | font-family: "IBM Plex Mono", "Consolas", "Courier New", Courier, monospace; 42 | font-size: 1.0em; 43 | margin: 0; 44 | padding: 0; 45 | text-align: left; 46 | } 47 | 48 | button.nav-item:hover, button.nav-item:focus, button.nav-item:active { 49 | text-decoration: none; 50 | } 51 | 52 | #menu-button { 53 | cursor: pointer; 54 | -webkit-user-select: none; 55 | } 56 | 57 | #side-menu ul { 58 | padding: 15px; 59 | } 60 | 61 | #nav-map-button { 62 | margin-left: 15px; 63 | margin-top: 2em; 64 | } 65 | 66 | #mini-map { 67 | border: 1px solid var(--main-font); 68 | overflow: hidden; 69 | width: 100%; 70 | height: 300px; 71 | } 72 | 73 | #mini-map .map, #mini-map .map pre { 74 | width: 100%; 75 | height: 100%; 76 | } 77 | 78 | #mini-map .map { 79 | font-size: 10px; 80 | } 81 | -------------------------------------------------------------------------------- /style/noteWall.css: -------------------------------------------------------------------------------- 1 | .note-wall-description { 2 | margin-bottom: 2em; 3 | } 4 | 5 | .note { 6 | border: 1px dashed var(--highlight-line); 7 | border-radius: 0.25rem; 8 | display: inline-block; 9 | width: 40%; 10 | margin: 1.25rem; 11 | padding: 0.5rem; 12 | vertical-align: top; 13 | word-wrap: break-word; 14 | } 15 | 16 | button.note-delete { 17 | color: var(--pink); 18 | float: right; 19 | margin-left: 10px; 20 | } 21 | 22 | .like-button { 23 | color: var(--blue); 24 | float: right; 25 | padding-left: 20px; 26 | padding-right: 15px; 27 | } 28 | 29 | .like-button svg { 30 | vertical-align: top !important; 31 | margin-left: 4px; 32 | } 33 | 34 | @media (max-width: 600px) { 35 | .note { 36 | width: 90%; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /style/profileEditView.css: -------------------------------------------------------------------------------- 1 | input[type=text] { 2 | border-radius: 5px; 3 | font-family: "IBM Plex Mono", "Consolas", "Courier New", Courier, monospace; 4 | font-size: 14px; 5 | height: 2rem; 6 | } 7 | 8 | .close { 9 | align-self: start; 10 | background-color:var(--main-background); 11 | border: none; 12 | color: var(--main-font); 13 | cursor: pointer; 14 | font-size: 24px; 15 | justify-self: end; 16 | } 17 | 18 | .container { 19 | align-items: center; 20 | display: flex; 21 | justify-content: center; 22 | width: 100%; 23 | } 24 | 25 | #profile-edit .field { 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: space-between; 29 | margin-bottom: 3rem; 30 | } 31 | 32 | #profile-edit .form { 33 | width: 100%; 34 | width: 780px; 35 | } 36 | 37 | .ftue .form { 38 | margin-top: 8rem; 39 | } 40 | 41 | #profile-edit .form label { 42 | width: 600px; 43 | font-size: 1.4em; 44 | } 45 | 46 | #profile-edit .form label { 47 | font-size: 1.6em; 48 | } 49 | 50 | 51 | .grid { 52 | display: grid; 53 | grid-column-gap: 5%; 54 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 55 | grid-template-rows: repeat(3, 1fr) 56 | } 57 | 58 | #profile-edit .submit { 59 | float: right; 60 | } 61 | 62 | #profile-edit h1 { 63 | margin-top: 0; 64 | } 65 | 66 | 67 | @media only screen and (max-width: 600px) { 68 | #profile-edit .field { 69 | margin-bottom: 2rem; 70 | } 71 | 72 | .ftue .form { 73 | margin-top: 1rem; 74 | } 75 | 76 | #profile-edit em { 77 | line-height: 2em; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es2019", "DOM"], 5 | "strict": false, 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "jsx": "react", 11 | "downlevelIteration": true, 12 | }, 13 | "ts-node": { 14 | "esm": true 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["node_modules"] 18 | } 19 | --------------------------------------------------------------------------------