curio-team / narrowblast

Allows students to create (interactive) slides for narrowcasting. Students can (weekly) earn credits with good attendance and use these to enhance their slides. Teachers moderate the slides and can add their own slides to the rotation.
MIT License
0 stars 1 forks source link

Allow bidirectional communication within the invite system #12

Open Staninna opened 11 months ago

Staninna commented 11 months ago

The way invites work now doesn't let data move between the client and the server (slide). This makes it hard for interactive stuff like games or quizzes to work properly. Even though it's in early development, it's important to let the client and server talk using for example JSON.

luttje commented 11 months ago

You're absolutely right! So I dove into the prototype code for you to see what would have to be done, and I found something! In this post I'll describe a way to communicate and a problem I noticed.

⛓ Bidirectional communication

Looking at the system again, I feel it's currently technically possible, however the whole way of setting interaction data not very intuitive.

It should be possible for the interaction screen (on a user's phone) to postMessage just like the slide would. This is because the interaction screen is the same webpage as the slide on the monitor, except it has #interaction-{local_user_id} as the anchor. Sending any message from the slide, or any client will cause a message to be sent to the API and sent back to any listening slide/client:

// untested psuedo-code
window.parent.postMessage({
    type: 'requestSetInteractionData',
    data: {
        user_quiz_actions: {
            "tl10": [ "a", "c", "b" ],
            "staninna": [ "c", "b", "a" ],
        },
    },
}, '*');

The slide (or any other user interaction screen) could listen for it, similar to how the slide listens in the betting example, but then in the interaction screen:

// untested psuedo-code
function interactionScreen(localId) {
    window.addEventListener('message', function(event) {
        if (event.data.type === 'onInteractionData') {
            const interactionData = event.data.data;
            const localAnswers = interactionData.user_quiz_actions[localId];

            document.querySelector('#yourAnswers').innerText = localAnswers.join(', ');
        }
    });
}

Both the slide and all connected interaction screens (users) would get that data update.

If you have trouble implementing this let me know and I'll help out!

Here's a sketch showing how the same slide displays different content based on whether it's on the screen or on a user's phone. (I doubt it makes the situation much clearer, but I drew it already so just dumping it here) image

🗝 Security issue

Although the above should work, it's not at all safe. Seeing how a user could modify (and thus cheat) the data in any way. Since the server blindly accepts any requestSetInteractionData and sets it in the database as the data. A user could set the contents of user_quiz_actions to anything. They could change answers after they've been submitted, submit answers for the other user, etc.

If things work as I describe, then I'd like to turn this issue into a bug/security fix, there must be some way to set data from the slide authoritatively (so the slide can later know for sure it was the one to set data).

A low-effort fix that I'm thinking could resolve this is as follows (contains psuedo-code):

  1. The slide is hosted on the screen which sends along a X-Secret-Tick-Key with every request. The API could use this to authorize the slide and know that it was legit. If a slide then sets interaction data like requestSetInteractionData({ data.any_key: 10 }) the API would allow it, since the slide can write anywhere.
  2. When my user interaction screen sends data, it's limited to a sub-key with my ID, e.g: requestSetInteractionData({ data.users.tl10.answers: ['a', 'b'] }). This would work, but only the screen shown to me.
  3. If my user interaction screen writes to anywhere else (like the same key as step 1 data.any_key), the API would reject it, since the screen can only write to data.users.tl10.

Let me know if you manage to implement bidirectional communication. Especially let me know if you have suggestions on how to improve the system, so we get the right tools to make cool stuff.

Staninna commented 11 months ago

Thanks for the tips. I'll give this a shot soon and let you know how it goes. Appreciate the help!

Staninna commented 10 months ago

I played a bit around, but I can't get it to work, here is my current draft: gist

luttje commented 10 months ago

Your code looks like it should work based on what I explained, but now looking at it again I've made a mistake: https://github.com/curio-team/narrowblast/blob/1bde5753138fca76543b02ad70cfaf3c747497ea/resources/views/app/slides/iframe-scripts.blade.php#L373C21-L373C52

This code finds the iframe to send the interaction data to by checking the data.publicPath, which only points to the slide URL (which doesn't end in #interaction-etc). If you were to trim the hash off (data.publicPath.split('#')[0]) in the location I linked, I feel that your code might work.

luttje commented 10 months ago

I woke up thinking it might possibly be more complicated than I made it out to be. I'll try find some time this weekend to actually run, test and change the code. Instead of trying to read it and deduce it's workings. I'll let you know if I find something