/azure-mud

Primary LanguageTypeScriptMIT LicenseMIT

Yet Another Browser Mud

Linting and Validation Checks Deploy to Azure

This is a playful text-based online social chat space. You can think of it as a hybrid between communication apps like Slack and Discord and traditional text-based online game spaces such as MUDs and MOOs.

It was primarily built for Roguelike Celebration 2020, but can hopefully be repurposed for other events or communities.

On the backend, it's powered by a serverless system made up of Azure Functions, Azure SignalR Service, and a Redis instance (currently provided by Azure Cache for Redis).

On the frontend, it's a rich single-page webapp built in TypeScript and React, using the Flux architecture via the useContext React hook.

This article provides some insight into the design principles underlying this project.

Setting up a development environment

Frontend Dev

  1. Clone this repo

  2. Run npm install

  3. npm run dev will start a local development environment (at http://localhost:1234/index.html by default). It auto-watches changes to HTML, CSS, and JS/TS code, and attempts to live-reload any connected browser instances.

  4. npm run build will generate a bundled version of the webapp for distribution.

  5. Do note that your frontend installation will not work until you have properly set up Firebase authentication and pointed it at a valid server backend (all via a .env file locally, or via GitHub repository secrets if you're deploying via our GH Actions workflows). If you are on the Roguelike Celebration organizing team, get in touch with us and we can give you credentials for an existing server instance. Otherwise, you will need a valid backend as well as API keys for a valid Firebase authentication account (see next section for setup)

This repo is set up to automatically deploy to GitHub Pages when code is merged into master (see "Deploying new Changes via GitHub Actions" below for how to finish configuring that for your fork). If you want to host your frontend elsewhere, that's totally fine too! You can serve the asset bundle generated by npm run build anywhere, although it will need to be served over SSL/HTTPS.

Content Dev

There is a browser-based "level editor" / CMS located at [your domain]/admin/index.html. To access the CMS, you will need to be an admin in the space.

There's no public docs yet for this editor tool, but hopefully it should be relatively self-explanatory given an understanding of our room format (which I think also sadly isn't documented, but will look familiar to those who have used Twine).

In order to access this editor from any instance of azure-mud, you will need to be a moderator in the space. There are instructions at the end of this doc about how to make that happen.

Backend Dev

After cloning this repo, the server directory contains all of the backend code. You may want to run npm install within the server directory to pull in dependencies for IDE autocompletion and such.

However, you cannot actually run the backend locally. You'll need to deploy your own server instance of the backend to test changes.

Deploying This Project

Currently, this project only runs on Azure. This requires your own Azure subscription.

If you don't already have an Azure account/subscription, you'll get a few hundred bucks of credits to use your first month, but if that's not the case you will want to keep an eye on the fact that running this backend will cost you actual money.

Deploying via ARM Template

Deploy to Azure

The easiest way to deploy a backend is to use the template we have prepared. Going to this link will allow you to deploy a server backend to an Azure resource group you specify that will have everything configured, but won't actually contain code yet.

There are still a few things you need to manually configure before the app will function.

  1. You will need to set up Firebase. Strap in, this is non-trivial.

    1. Create a new Firebase project. To do this, follow steps 1, 3, and 5 in the setup guide here: https://firebase.google.com/docs/web/setup#create-project. Make sure the title is appropriate for display, since by default that's what will go in the confirmation emails.
    2. Register your app with the Firebase project you just created. To do this, follow steps 1-3 here: https://firebase.google.com/docs/web/setup#register-app
    3. Set up the client
      1. Go to the General tab in the Settings section for your new project
      2. In your GitHub repo, add repository Secrets (Settings -> Secrets -> New Repository Secret) for all of the values set in the firebaseConfig object of the JS config snippet. You should have Secrets titled FIREBASE_API_KEY, FIREBASE_AUTH_DOMAIN, FIREBASE_PROJECT_ID, FIREBASE_STORAGE_BUCKET, FIREBASE_MESSAGING_SENDER_ID, and FIREBASE_APP_ID.
      3. By default, these secrets will be injected into your webapp when GitHub Actions builds and deploys your front-end, but that means they won't work if you're running a local dev server. To enable Firebase login on your local dev server, copy the .env.sample file to one named .env, and then replace the fake sample values for each key with the appropriate actual values you also stored as GitHub Secrets.
    4. Set up the server
      1. Generate and download a private key file for your new project as instructed here: https://firebase.google.com/docs/admin/setup#initialize-sdk
      2. Create a new GitHub repository secret (Settings -> Secrets -> New Repository Secret) whose key is FIREBASE_SERVER_JSON and whose value is the entire text of the downloaded JSON file.
      3. You may also want to move the downloaded JSON file to server/firebase-admin.json. This file will by default not be tracked by git (and we don't recommend you commit it), but you will need this file to be present if you wish to deploy the backend via CLI or VS Code instead of GitHub Actions (see "Deploying new Changes" sections below)
    5. Go to the Firebase console -> Authentication -> Sign-in method and enable the providers as desired (we currently support email, Google, and Twitter) by following the instructions in each provider's section. For a dev setup, Email/Password should be sufficient.
    6. To enable email, you must:
      1. Check the Email link (passwordless sign-in) button.
      2. Add your server domain to the Authorized domains section beneath the providers list.
    7. Google is simple, just add the provider.
    8. For Twitter you have to get the API information from the Twitter dev portal (https://developer.twitter.com/en/portal/dashboard) and place it into the form.
  2. You'll need to modify the frontend to actually use your new backend! Add a new GitHub Repository Secret (Settings -> Secrets -> New Repository Secret) named SERVER_HOSTNAME that contains the URL to your own Function App instance (the Azure URL for your backend — typically https://your-project.azurewebsite.net, where your-project is the project name you entered when deploying the Azure ARM template). For local client development, you'll also need to add this to your local .env file. This should hopefully be set up from configuring Firebase, but if not you can rename .env.sample to .env and make that change.

  3. By default, GitHub Actions tooling will deploy the front-end to GitHub Pages. Because this repository is also the production repo we use for Roguelike Celebration, we have a CNAME file in the root of this repo that points to our production URL. If you want to use GitHub Pages, you'll want to either remove that CNAME file (if you just want to use default github.io hosting) or edit it to point to your own custom domain. If using your own custom domain, you will also need to configure that in the repo's GitHub Pages settings.

  4. If you want speech transcription to work, you'll need to create an Azure Cognitive Speech Services resource. Hopefully this will be automated as part of the ARM Template in the future.

    1. In the Azure Portal, search for "Speech Services", click through to the Speech Services page, and click "Create". Put it in the same region and resource group as your main backend, and choose the Standard (S0) paid tier. Name it whatever you want.
    2. After the resource has been created, click "Go to Resource", then "Manage keys". Write down Key 1 and the "region".
    3. Go to your backend's Function App. Under Configuration (in the "Settings" section of the left sidebar), add two App Secrets, COGNITIVE_SERVICES_KEY and COGNITIVE_SERVICES_REGION, containing the key and region from the previous step.
  5. For video chat, you'll need a Twilio Programmable Video account. As a warning, this is a work-in-progress, and these instructions may be underbaked.

    1. Sign up for a Twilio account. You'll probably need to fund it.
    2. On the Twilio Console, your Account SID will be immediately visible. Add that as an App Secret to the Function App with the key TWILIO_ACCOUNT_SID.
    3. On the Twilio Console, select Account -> API Keys on the top-right. Create a new API key. Store the SID and secret as App Secrets with the keys TWILIO_API_KEY and TWILIO_API_SECRET.
  6. Finally, you need to actually deploy the backend code before everything will work. You have three main options (below), but after doing this you should have a working app!

Deploying new Changes via GitHub Actions

By default, every time code is merged into the main branch in this repo, both the frontend and backend are deployed. It's very little work to configure this same behavior on your GitHub fork of this project:

  1. Add a GitHub Repository Secret (Settings -> Secrets -> Add Repository Secret) with the key AZURE_FUNCTION_APP_NAME whose value is your Azure app name. Follow these instructions to generate a publish profile and add that as a GH Secret as well.

  2. If you want to continue to use GH Pages, enable it for your project by going to Settings -> General and setting it to serve from the gh-pages branch and the root directory. Also, you should delete the CNAME file from the project root, or replace it with your own if you intend to use GitHub Actions with a custom domain. If you would like to use a different static site host, simply remove the "Deploy Frontend" step from the .github/workflows/deploy.yml file.

  3. Go to the "Actions" tab of your repo, and click the button to enable the preexisting forked Actions in your project.

  4. Optionally, the GitHub Action workflow will send a webhook to the server when a new deployment has completed, which allows us to notify connected clients that a new browser app has been deployed and they should refresh the page. To enable this, create a random string to use as a token (we recommend running uuidgen on a Mac or a Linux machine). Store it as a GitHub Repository Secret (Settings -> Secrets -> Add Repository Secret) under the key DEPLOY_WEBHOOK_KEY. Also store it as an ENV variable in the Azure Functions App (while viewing the Function App in the Portal, Configuration -> New Application Setting) under the key DEPLOY_WEBHOOK_KEY. Now, if you have your frontend open when you deploy via GitHub Actions, you should see a pop-up in the frontend instructing you to refresh.

The next time you commit code to the main branch, your workflow should run and code should deploy!

Deploying new Changes via VS Code

If you use VS Code as an IDE, the Azure Functions extension makes it extremely easy to deploy directly from there. Check out this tutorial for more info.

Deploying new Changes via CLI

If you have the Azure Functions Core Tools installed, you can run func azure functionapp publish [project name] to deploy directly from the CLI.

Costs and Scaling

For development purposes, you can use the free tier of both SignalR Service and Azure functions, you just need to pay for a small Redis instance (~$15/month). As mentioned, if you're a new Azure user, you'll get more than enough free credits to cover hosting this for your first month.

To get this ready for production use, all you need to do is scale up your SignalR Service usage tier. Running this project with a single SignalR unit (good for up to 1,000 concurrent users) will cost you roughly $2.50 per day, with each additional SignalR unit (another 1,000 concurrents) adding roughly an additional $1.50 per day. These numbers are all rough ballpark figures.

Other than managing SignalR units, you won't need to worry about adjusting capacity in order to scale. Azure Functions charges you based on usage, and it's extremely unlikely you'll need to scale up Redis unless you have tens of thousands of concurrent users. At Roguelike Celebration, with an average of around 300 concurrent users, we never hit more than a few hundred kilobytes of Redis data or used even 1% of our processing power.

Manual Deployment Instructions

If you would prefer to not use the ARM template above, here is how you can manually configure a set of Azure resources to run this project.

  1. Deploy the project to a new Azure Function App instance you control. I recommend using VS Code and the VS Code Azure Functions extension. See the "Publish the project to Azure" section of this tutorial for details. You can also use the Azure CLI or any other method.
  1. In the Azure Portal, sign up for a new Azure SignalR Service instance. For development purposes, you can probably start with the free tier.

  2. Grab the connection string from your Azure SignalR Service instance. Back in the settings for your Function App, go to the Configuration tab and add a Application Setting with the key AzureSignalRConnectionString

  3. Set up an Azure Cache for Redis instance. Again, the cheapest tier is likely acceptable for testing purposes. You could theoretically use an alternative Redis provider, as nothing about our use is Azure-specific, but I have not tried this.

  4. As above, you want to take your Redis access key, the hostname, and the port, and add them as Application settings to the Function App with the keys RedisKey and RedisHostname, RedisPort.

  5. Set up Twitter authentication. In the Azure Portal, pull up the Function App and go to "Authentication". In another window, go to https://developer.twitter.com/apps and register a new Twitter developer application. You will need to paste the consumer key and secret from Twitter into the Azure setup screen for Twitter. The callback URL to enter in the Twitter app is https://your-function-app.azurewebsites.net/.auth/login/twitter/callback, swapping in the URL hostname of your Function app. In the Azure Portal authentication screen, make sure that "Token Store" is on, and add "http://localhost:1234" (and any other URLs you want to be able to use) to the Allowed External Redirect URLs list.

  6. Set up CORS in the Azure Portal page for the Function app. There's a "CORS" menu item on the left. Allow http://localhost:1234 for local development, as well as whatever URLs you're using for a production version of the frontend.

  7. In src/config.ts in this repo, update the hostname to point to your own Function App instance (the Azure URL for your backend, NOT wherever you're hosting the app's frontend).

Deployment and CI/CD via GitHub Actions

By default, this project uses GitHub Actions for a few things.

The "lint" workflow runs on every open PR and on every commit to main, and checks (a) whether the code passes a TypeScript compilation/typecheck step, (b) whether all your room description links are valid (see below), and (c) whether the front-end code passes accessibility best standars (via axe linter)/.

The "deploy" workflow runs every time code is pushed to the main branch (or a PR is merged, etc). It builds the app, and deploys both the front-end (to GitHub Pages) and the back-end (to Azure Functions).

If you fork this project, you will get this behavior for free, although you will need to change a few things:

You can naturally disable either or both of these behaviors if you'd prefer.

Designing your own space

This project is open-source, and currently includes the scripting code for the space we used at Roguelike Celebration. However, we would ask that you design your own event space on this platform, rather than using Roguelike Celebration's space (room descriptions, map, etc). Fortunately, it's easy to customize the space!

Rooms are currently defined in server/src/rooms. Each room definition is a JSON object containing its description and other information; check the definitions in server/src/rooms/index.ts for an example there. The list of rooms in that file is definitive, but you can also see there how to define a room in an external file and link it in.

Within room descriptions, this project uses a custom Twine-like syntax for links.

  • A link to a [[room]] will link directly to a room whose id is room.
  • A link to [[another room->someOtherRoom]] will display the text "another room" but link to the room whose id is someOtherRoom
  • A link that looks like it links to the id "item" (e.g. [[a heavy book->item]]) will result in the player picking up that item, so long as that item string is an exact match in the list in server/src/allowedItems.ts.
  • Links can also trigger client-side code if the link id refers to a function in the client-side src/linkActions object. As an example, [[Get a random food item->generateFood]] will trigger the generateFood function that in turn assigns the player an inventory object that is a procedurally-generated piece of food.

There is an automated script to validate that none of your links are broken (i.e. all room links go to valid room IDs, all linked items are in the allowed item list, and all client-side functions actually exist). You can run this by typing npm run lint-rooms in the main project directory, and it also runs automatically on PRs if you have the default GitHub Actions running.

Links between rooms are purely visual. If an attendee is moving rooms using the map or the /move command, they can move to any room at any time. It's still best practice to include links to "nearby" rooms (matching your visual map) to help users navigate the space.

Right now, because room descriptions are part of the server codebase, changing room data requires redeploying the entire server backend. Changing that is a high development priority.

Editing the map

TODO: These instructions are slightly out-of-date. The map used for Roguelike Celebration 2021 was drawn using Playscii, but the thing that really needs to be documented is the supplemental data you need to add to inject active room counts and set up click/scroll targets. Check out src/components/MapView.tsx for more info until this is properly documented.

Outdated info: The ASCII map was created with MonoDraw, a Mac-only ASCII art tool. You'll want to open the map.monopic file in that, export your changes, paste the ASCII string into src/components/MapView.tsx, and then update any changes to the two datasets of persistence identifiers and clickable areas.

Adding a Mod

A moderator has the ability to make any other user a mod by clicking their username and choosing the appropriate option in the space. That said, you'll need to manually add the first mod.

  1. Find the user ID of the user you want to make a mod. In the space, if you open your browser's development tools and go to the network inspector, and then click their name (including your own name in the top-left corner if you want to mod your own user) and "view profile", you will see a HTTP request to /fetchProfile that contains the userId in JSON as the request payload.

  2. Go to your Redis instance in the Azure Portal. Click the "Console" button at the top-left of the main pane.

  3. In the Redis terminal, type sadd mods [userId] (swapping in the user ID you fetched earlier, without square brackets).

If that user reloads the space, they will now be a mod and have the ability to add other mods.

Contributions

If you're looking to get involved: awesome! There's a "Good First Issue" tag in this repo's GitHub Issues that may point you towards something. If you want to work on something, it might be nice to comment that you're looking into it in case others are already working on it or were thinking about it.

Fork this repo, make your changes, open a pull request! Once you've contributed, I'm fairly liberal with granting people contributor access, but the main branch is still locked.

Pull requests are run through a few automated checks. If the ESLint checks fail, first try running npm run eslint-fix to try to automatically fix as many of the errors as you can; anything that doesn't catch will need to be fixed manually.