cynicaloptimist / improved-initiative

Combat tracker for Dungeons and Dragons (D&D) 5th Edition
https://www.patreon.com/improvedinitiative
MIT License
313 stars 109 forks source link

Encounter command API #231

Open cynicaloptimist opened 6 years ago

cynicaloptimist commented 6 years ago

There is some precedent for commands being passed through the socket, see https://github.com/cynicaloptimist/improved-initiative/blob/master/server/sockets.ts#L31 for an example. That command comes from a PlayerView that's connected to the socket, so its corresponding client call is at https://github.com/cynicaloptimist/improved-initiative/blob/master/client/Player/PlayerViewClient.ts#L12 .

Sockets may be accessible cross-domain. I haven't researched or experimented with this yet, but I'm guessing this requires some configuration to achieve. It might possibly be better to build a POST endpoint that accepts encounter commands, then forwards them to the appropriate socket channel.

zhudotexe commented 6 years ago

Experimenting with the DM view, it looks like the server and client don't sync the actual encounter state (i.e. two DM views controlling the same encounter ID can have two different encounters running, and a PlayerView will display the last one that changed). Would it be possible to have a DM view reflect changes to an encounter made by a different client?

My thought was that both third-party applications would be able to manage adding and modifying combatants, tags, etc. to an encounter programmatically, or an end user over the web GUI.

cynicaloptimist commented 6 years ago

It is true that the Tracker view doesn't sync encounter state with the server. This is because the encounter state is mastered in the Tracker view client; changes in encounter state never flow back to the DM view. #149 contains some notes about to this architecture.

I don't have plans to change this architecture for the foreseeable future. This means that any API calls to update the encounter would need to flow through the Tracker view client the same way that Damage Suggest does.

To implement this without requiring an 'Accept' dialog on the tracker view for any given action, it may require introducing something like a 'secret encounter key' at mentioned in #149 that allows anyone with the key to freely push actions.

zhudotexe commented 6 years ago

Got it. To address the concerns brought up in #149, maybe a POST endpoint to start an encounter that would return such a secret could be used, although that wouldn't allow for third-party apps to issue encounter commands to a web-initiated encounter. This key could then be supplied in either websocket payloads or HTTP requests to update the encounter.

As for third-party clients receiving updates about encounter state from II, does the encounter updated socket event expose the full SavedEncounter object (edit: and associated SavedCombatant objects)? If so, it may possibly be enough for clients to listen to that stream (assuming II allows cross-origin socket subscriptions). edit 2: Unless I'm wrong, it looks like it returns StaticCombatantViewModels, as seen in https://github.com/cynicaloptimist/improved-initiative/blob/master/client/PlayerViewModel.ts#L53. In this case, would it be possible to either get the SavedCombatant or Combatant objects? (My thought was a webhook, defined with the encounter creation, that II could POST to, but there may be better implementations.)

cynicaloptimist commented 6 years ago

You can GET the current SavedEncounter through the app.get("/playerviews/:id") endpoint without having to wait on a socket update. The PlayerView makes a request to this endpoint when it first connects in order to get the current encounter state. Since it's accessible to anyone with the encounter ID, this is not a good place to expose a secret key.

One approach might be to add a Connect to Avrae button somewhere in the II menus that posts a secret key to your app. Then you could use that secret key to send commands to a POST endpoint.

zhudotexe commented 6 years ago

It appears as though the /playerviews/:id endpoint returns SavedEncounter<StaticCombatantViewModel>; is there any way to get SavedEncounter<SavedCombatant> or SavedEncounter<Combatant>?

A "Connect to Avrae" button would probably be unnecessary (my comment was in case you wanted this API to be for a more general use case), since the main flow would be:

  1. User begins combat in Avrae
  2. Avrae begins combat in II (maybe through the earlier mentioned POST endpoint)
  3. II returns encounter ID and secret to Avrae, which Avrae can use to send updates to II - this is the only time the secret is transmitted
  4. Avrae directs users (DM and players) to II interfaces (but users can still manage combat through Avrae, as well)

From there, the two apps would continue to send the other updates, either via socket, webhooks, or some other implementation. Does that sound realistic?

cynicaloptimist commented 6 years ago

This could work. Have you taken a look at the /importencounter/ endpoint that's used by Kobold Fight Club? This is the ideal way to set up an initial encounter state, and the endpoint could be updated to return a key that can be used to issue other encounter commands.

cynicaloptimist commented 6 years ago

That endpoint is designed for the user's browser to post the encounter from another domain, which is why it responds with a redirect, so maybe there ought to be a new endpoint that you'd hit if you wanted to initialize a remotely-run encounter.

zhudotexe commented 6 years ago

I'm thinking the latter would probably be the better option, since Avrae would initialize the encounter without combatants, then a user could either add them on Avrae (which would send the update to II), or on II (which would send the update to Avrae).

Maybe something like this?

POST /integratedencounter/ HTTP/1.1
Content-Type: application/json
{
    "webhook_url": "https://avrae.io/api/combat/...",
    "origin": "avrae"
}

Which would return something like this (plus any other metadata, if needed):

{
    "encounter_secret": "...",
    "encounter_id": "..."
}
cynicaloptimist commented 6 years ago

This proposal looks great. Did you find out if it's possible to make a cross-origin socket connection? I imagine that using socket.io is a bit more efficient than hitting a webhook on every encounter update. Maybe that's too much of a pre-optimization.

If the cross-origin socket is not trivial, having II hit the supplied webhook on every encounter update seems reasonable. I can see how the webhooks approach makes it overall easier for other apps like yours to integrate.

zhudotexe commented 6 years ago

While it looks like it's possible to make cross-origin socket connections, there isn't really a robust socket.io client library for Python (which Avrae is programmed in), meaning that if I were to use that method, I'd likely have to build one from the lower-level socket connection. Since Avrae's load isn't too large (relatively speaking), using webhooks seems like the best approach for now. In the future, that can be optimized if it needs to be.