Última actividad 1761117391

Code from https://mosfetarium.com/blog/gemini-ink-server/

travisshears's Avatar travisshears revisó este gist 1761117391. 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's Avatar travisshears revisó este gist 1761117347. 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 + }
Siguiente Anterior