travisshears revisó este gist . Ir a la revisión
1 file changed, 1 insertion, 1 deletion
README.md
| @@ -1,4 +1,4 @@ | |||
| 1 | - | GEMINI INK DEMO SERVER | |
| 1 | + | # GEMINI INK DEMO SERVER | |
| 2 | 2 | ||
| 3 | 3 | This little node js server is a demonstration of playing text-only narrative games created with Ink | |
| 4 | 4 | (https://github.com/inkle/ink) over the gemini protocol (https://gemini.circumlunar.space/). It uses | |
travisshears revisó este gist . Ir a la revisión
4 files changed, 241 insertions
README.md(archivo creado)
| @@ -0,0 +1,43 @@ | |||
| 1 | + | GEMINI INK DEMO SERVER | |
| 2 | + | ||
| 3 | + | This little node js server is a demonstration of playing text-only narrative games created with Ink | |
| 4 | + | (https://github.com/inkle/ink) over the gemini protocol (https://gemini.circumlunar.space/). It uses | |
| 5 | + | basically a bunch of standard nodejs functionality and inkjs (https://github.com/y-lohse/inkjs). The | |
| 6 | + | demo game is The Intercept, also made by Inkle, not me! So don't give me credit for the game | |
| 7 | + | (https://github.com/inkle/the-intercept). | |
| 8 | + | ||
| 9 | + | To run this. You will need to install node and npm. You will also need a gemini server cert! | |
| 10 | + | ||
| 11 | + | Here's how to generate a self-signed cert using openssl: | |
| 12 | + | ||
| 13 | + | > openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -x509 -days 3650 -out server-cert.pem | |
| 14 | + | ||
| 15 | + | This nodejs server expects the key and cert to be in those specific file names. The most important | |
| 16 | + | detail when generating is that you set the common name to the name of your host. If you're just | |
| 17 | + | testing locally, that's localhost. Otherwise it's your server domain. | |
| 18 | + | ||
| 19 | + | Anyway, to actually run the server, first install dependencies: | |
| 20 | + | ||
| 21 | + | > npm install | |
| 22 | + | ||
| 23 | + | Currently that's only inkjs. We're version locked at the moment to 1.11.0. This version locks us to | |
| 24 | + | ink games compiled with inklecate 0.9.0. | |
| 25 | + | ||
| 26 | + | To get it running at all, it's a simple: | |
| 27 | + | ||
| 28 | + | > nodejs index.js | |
| 29 | + | ||
| 30 | + | That'll boot it in local mode though (meaning your cert will need to be for localhost). To boot it in | |
| 31 | + | public server mode, you'll need to atleast set the host address: | |
| 32 | + | ||
| 33 | + | > GEMINI_INK_ADDR=your-gemini-server.com nodejs index.js | |
| 34 | + | ||
| 35 | + | You can all set an ink json file of your choice with GEMINI_INK_FILE, and the port with | |
| 36 | + | GEMINI_INK_PORT. | |
| 37 | + | ||
| 38 | + | Anyway. That should be everything. index.js is fairly well commented so if you want to repurpose this | |
| 39 | + | it should be quite easy. Warning that I'm not very experienced with javascript at this time. So I'm | |
| 40 | + | probably doing something disgusting. | |
| 41 | + | ||
| 42 | + | If you have any questions, comments, or anything, please reach out to me (mosfetarium.com/about). I'd | |
| 43 | + | love to hear from you :) | |
index.js(archivo creado)
| @@ -0,0 +1,183 @@ | |||
| 1 | + | /* | |
| 2 | + | GEMINI INK DEMO SERVER | |
| 3 | + | ||
| 4 | + | See the README for more details | |
| 5 | + | ||
| 6 | + | This code was made by DistractedMOSFET of mosfetarium.com. But consider it as public domain as physically | |
| 7 | + | possible :) | |
| 8 | + | */ | |
| 9 | + | ||
| 10 | + | var Story = require('inkjs').Story; | |
| 11 | + | var fs = require('fs'); | |
| 12 | + | var tls = require('tls'); | |
| 13 | + | ||
| 14 | + | var hostAddr = process.env.GEMINI_INK_ADDR || 'localhost'; | |
| 15 | + | var port = process.env.GEMINI_INK_POST || 1965; | |
| 16 | + | ||
| 17 | + | // load the ink file, I took this from the inkjs github so I don't know what the story of the replace is. | |
| 18 | + | var inkFile = fs.readFileSync(process.env.GEMINI_INK_FILE || 'intercept.ink.json', 'UTF-8').replace(/^\uFEFF/, ''); | |
| 19 | + | ||
| 20 | + | // Options for the TLS listen server | |
| 21 | + | var tlsOptions = { | |
| 22 | + | // most notable thing, these paths define where we load the key and cert from. | |
| 23 | + | key: fs.readFileSync('server-key.pem'), | |
| 24 | + | cert: fs.readFileSync('server-cert.pem'), | |
| 25 | + | // we need to request a cert from the connecting client because we use client TLS certs for game sessions. | |
| 26 | + | requestCert: true, | |
| 27 | + | // we accept certless connections anyway so we can send the normal gemini reject message | |
| 28 | + | rejectUnauthorized: false | |
| 29 | + | }; | |
| 30 | + | ||
| 31 | + | // we remember everyone's games as a map of client tls SHA256 fingerprint -> (lastActionUnixTime, inkStory) | |
| 32 | + | // we evict stale games every so often | |
| 33 | + | // also, i'm not entirely sure at this point in time of the differences between js Maps vs Objects | |
| 34 | + | // and if I should be using this or just an object, but oh well. | |
| 35 | + | var activeGames = new Map(); | |
| 36 | + | ||
| 37 | + | // listen... | |
| 38 | + | tls.createServer(tlsOptions, function(sock) { | |
| 39 | + | // this function handles incoming data | |
| 40 | + | // so this is where we need to do all the gemini stuff! | |
| 41 | + | sock.on('data', function(data) { | |
| 42 | + | // a gemini request is just: <URI><CRLF> | |
| 43 | + | ||
| 44 | + | // slice in here to remove the CRLF | |
| 45 | + | var schemeSplit = data.toString().slice(0, -2).split('://'); | |
| 46 | + | ||
| 47 | + | // they don't need to say gemini:// at the start, so 1 is valid length | |
| 48 | + | if (schemeSplit.length == 2) { | |
| 49 | + | if (schemeSplit[0] != 'gemini') { | |
| 50 | + | sock.write('50 Unsupported Protocol\r\n'); | |
| 51 | + | sock.pipe(sock); | |
| 52 | + | return; | |
| 53 | + | } | |
| 54 | + | } else if (schemeSplit.length != 1) { | |
| 55 | + | sock.write('50 Bad URI?\r\n'); | |
| 56 | + | sock.pipe(sock); | |
| 57 | + | return; | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | var path = schemeSplit.pop(); | |
| 61 | + | ||
| 62 | + | // if the URI isn't for us, uhhhh? | |
| 63 | + | if (!path.startsWith(hostAddr)) { | |
| 64 | + | sock.write('50 Wrong audience?\r\n'); | |
| 65 | + | sock.pipe(sock); | |
| 66 | + | return; | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | path = path.slice(hostAddr.length); | |
| 70 | + | ||
| 71 | + | if (path == '' || path == '/') { | |
| 72 | + | // show the landing page | |
| 73 | + | // i mean, I could like try to load a markdown file or something? but I don't care. | |
| 74 | + | sock.write('20 text/gemini\r\n'); | |
| 75 | + | sock.write('# Gemini Ink Demo!\r\n'); | |
| 76 | + | sock.write('Welcome to the Gemini-ink demo! This demo shows playing a game using inkle\'s ink engine. The demo game is called the Intercept. You will need a client certificate active in your gemini client to be able to play, if your inactive for more than 15 minutes, your game session will be lost. The game is short though!\r\n'); | |
| 77 | + | sock.write('=> gemini://' + hostAddr + '/play Start!'); | |
| 78 | + | sock.pipe(sock); | |
| 79 | + | } else if (path.startsWith('/play')) { | |
| 80 | + | // GAME PLAY!!!! | |
| 81 | + | ||
| 82 | + | // So we need them to have a certifcate so that we can track their session | |
| 83 | + | // Seemingly, you can't just do like, == {} in js (ugh) and so to check for an empty cert | |
| 84 | + | // (ie, no cert) I have to do this stupid check. | |
| 85 | + | var clientCert = sock.getPeerCertificate(); | |
| 86 | + | if (Object.keys(clientCert).length == 0) { | |
| 87 | + | sock.write('60 You need an active gemini certificate in order to play.\r\n'); | |
| 88 | + | sock.pipe(sock); | |
| 89 | + | return; | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | var sessionId = clientCert.fingerprint256; | |
| 93 | + | // make a game, if needed. but we do have a cap just to make the server not cry | |
| 94 | + | if (!activeGames.has(sessionId)) { | |
| 95 | + | if (activeGames.size > 49) { | |
| 96 | + | sock.write('40 Too many concurrent games at the moment! Try again later!\r\n'); | |
| 97 | + | sock.pipe(sock); | |
| 98 | + | return; | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | console.log('Creating new game for: ' + sock.remoteAddress + ':' + sock.remotePort); | |
| 102 | + | activeGames.set(sessionId, [Date.now(), new Story(inkFile)]); | |
| 103 | + | } else { | |
| 104 | + | // update the active time so that I don't evict the game. | |
| 105 | + | activeGames.get(sessionId)[0] = Date.now(); | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | var game = activeGames.get(sessionId)[1]; | |
| 109 | + | ||
| 110 | + | // check if this is a URI for picking a game option | |
| 111 | + | ||
| 112 | + | var option = path.slice('/play/'.length); | |
| 113 | + | ||
| 114 | + | if (option != '') { | |
| 115 | + | if (isNaN(option)) { | |
| 116 | + | sock.write('50 Bad choice option?\r\n'); | |
| 117 | + | sock.pipe(sock); | |
| 118 | + | return; | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | option = parseInt(option); | |
| 122 | + | ||
| 123 | + | if (option < 0 || option >= game.currentChoices.length) { | |
| 124 | + | sock.write('50 Bad choice option?\r\n'); | |
| 125 | + | sock.pipe(sock); | |
| 126 | + | return; | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | game.ChooseChoiceIndex(option); | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // get as much text as possible | |
| 133 | + | var text = game.ContinueMaximally(); | |
| 134 | + | ||
| 135 | + | // make links for choices | |
| 136 | + | for (var choiceIndex = 0; choiceIndex < game.currentChoices.length; choiceIndex += 1) { | |
| 137 | + | text += '\r\n=> gemini://' + hostAddr + '/play/' + choiceIndex + ' ' + game.currentChoices[choiceIndex].text; | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | // game has ended. send an end message and clean up. | |
| 141 | + | if (!game.canContinue && game.currentChoices.length === 0) { | |
| 142 | + | text += '\r\n\r\nThank you trying out Gemini Ink! That\'s the end.\r\n'; | |
| 143 | + | text += '=> https://mosfetarium.com/ Please check out my blog, follow and give feedback (not on gemini yet, sorry!)\r\n'; | |
| 144 | + | text += '=> gemini://' + hostAddr +'/play/ Restart'; | |
| 145 | + | ||
| 146 | + | console.log('Someone finished! ' + sock.remoteAddress + ':' + sock.remotePort); | |
| 147 | + | ||
| 148 | + | activeGames.delete(sessionId); | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | //Send the text! | |
| 152 | + | ||
| 153 | + | sock.write('20 text/gemini\r\n'); | |
| 154 | + | sock.write(text); | |
| 155 | + | sock.pipe(sock); | |
| 156 | + | } else { | |
| 157 | + | sock.write('50 There\'s nothing here.\r\n'); | |
| 158 | + | sock.pipe(sock); | |
| 159 | + | return; | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | sock.end(); | |
| 163 | + | }); | |
| 164 | + | }).listen(port, hostAddr); | |
| 165 | + | ||
| 166 | + | function cleanUpOldGames() { | |
| 167 | + | var now = Date.now(); | |
| 168 | + | for (var [key, entry] of activeGames.entries()) { | |
| 169 | + | if ((now-entry[0]) > 1000*60*15) { // 15 minute life | |
| 170 | + | console.log('Cleaning up game for... :' + key); | |
| 171 | + | activeGames.delete(key); | |
| 172 | + | } | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | // queue check for ten minutes from now | |
| 176 | + | setTimeout(cleanUpOldGames, 1000*60*10); | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | ||
| 180 | + | cleanUpOldGames(); | |
| 181 | + | ||
| 182 | + | ||
| 183 | + | ||
intercept.ink.json(archivo creado)
Diferencia truncada porque es demasiado grande para mostrarse
package.json(archivo creado)
| @@ -0,0 +1,14 @@ | |||
| 1 | + | { | |
| 2 | + | "name": "gemini-ink", | |
| 3 | + | "version": "1.0.0", | |
| 4 | + | "description": "A gemini server for playing ink games", | |
| 5 | + | "main": "y", | |
| 6 | + | "scripts": { | |
| 7 | + | "test": "echo \"Error: no test specified\" && exit 1" | |
| 8 | + | }, | |
| 9 | + | "author": "DistractedMOSFET", | |
| 10 | + | "license": "ISC", | |
| 11 | + | "dependencies": { | |
| 12 | + | "inkjs": "^1.11.0" | |
| 13 | + | } | |
| 14 | + | } | |