SHOW:
|
|
- or go back to the newest paste.
1 | -- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe | |
2 | -- | |
3 | -- SPDX-License-Identifier: LicenseRef-CCPL | |
4 | ||
5 | local tArgs = { ... } | |
6 | ||
7 | local function printUsage() | |
8 | local programName = arg[0] or fs.getName(shell.getRunningProgram()) | |
9 | print("Usages:") | |
10 | print(programName .. " host <hostname>") | |
11 | print(programName .. " join <hostname> <nickname>") | |
12 | end | |
13 | ||
14 | local sOpenedModem = nil | |
15 | local function openModem() | |
16 | for _, sModem in ipairs(peripheral.getNames()) do | |
17 | if peripheral.getType(sModem) == "modem" then | |
18 | if not rednet.isOpen(sModem) then | |
19 | rednet.open(sModem) | |
20 | sOpenedModem = sModem | |
21 | end | |
22 | return true | |
23 | end | |
24 | end | |
25 | print("No modems found.") | |
26 | return false | |
27 | end | |
28 | ||
29 | local function closeModem() | |
30 | if sOpenedModem ~= nil then | |
31 | rednet.close(sOpenedModem) | |
32 | sOpenedModem = nil | |
33 | end | |
34 | end | |
35 | ||
36 | -- Colours | |
37 | local highlightColour, textColour | |
38 | if term.isColour() then | |
39 | textColour = colours.white | |
40 | highlightColour = colours.yellow | |
41 | else | |
42 | textColour = colours.white | |
43 | highlightColour = colours.white | |
44 | end | |
45 | ||
46 | local sCommand = tArgs[1] | |
47 | if sCommand == "host" then | |
48 | -- "chat host" | |
49 | -- Get hostname | |
50 | local sHostname = tArgs[2] | |
51 | if sHostname == nil then | |
52 | printUsage() | |
53 | return | |
54 | end | |
55 | ||
56 | -- Host server | |
57 | if not openModem() then | |
58 | return | |
59 | end | |
60 | rednet.host("chat", sHostname) | |
61 | print("0 users connected.") | |
62 | ||
63 | local tUsers = {} | |
64 | local nUsers = 0 | |
65 | local function send(sText, nUserID) | |
66 | if nUserID then | |
67 | local tUser = tUsers[nUserID] | |
68 | if tUser then | |
69 | rednet.send(tUser.nID, { | |
70 | sType = "text", | |
71 | nUserID = nUserID, | |
72 | sText = sText, | |
73 | }, "chat") | |
74 | end | |
75 | else | |
76 | for nUserID, tUser in pairs(tUsers) do | |
77 | rednet.send(tUser.nID, { | |
78 | sType = "text", | |
79 | nUserID = nUserID, | |
80 | sText = sText, | |
81 | }, "chat") | |
82 | end | |
83 | end | |
84 | end | |
85 | ||
86 | -- Setup ping pong | |
87 | local tPingPongTimer = {} | |
88 | local function ping(nUserID) | |
89 | local tUser = tUsers[nUserID] | |
90 | rednet.send(tUser.nID, { | |
91 | sType = "ping to client", | |
92 | nUserID = nUserID, | |
93 | }, "chat") | |
94 | ||
95 | local timer = os.startTimer(15) | |
96 | tUser.bPingPonged = false | |
97 | tPingPongTimer[timer] = nUserID | |
98 | end | |
99 | ||
100 | local function printUsers() | |
101 | local _, y = term.getCursorPos() | |
102 | term.setCursorPos(1, y - 1) | |
103 | term.clearLine() | |
104 | if nUsers == 1 then | |
105 | print(nUsers .. " user connected.") | |
106 | else | |
107 | print(nUsers .. " users connected.") | |
108 | end | |
109 | end | |
110 | ||
111 | -- Handle messages | |
112 | local ok, error = pcall(parallel.waitForAny, | |
113 | function() | |
114 | while true do | |
115 | local _, timer = os.pullEvent("timer") | |
116 | local nUserID = tPingPongTimer[timer] | |
117 | if nUserID and tUsers[nUserID] then | |
118 | local tUser = tUsers[nUserID] | |
119 | if tUser then | |
120 | if not tUser.bPingPonged then | |
121 | send("* " .. tUser.sUsername .. " has timed out") | |
122 | tUsers[nUserID] = nil | |
123 | nUsers = nUsers - 1 | |
124 | printUsers() | |
125 | else | |
126 | ping(nUserID) | |
127 | end | |
128 | end | |
129 | end | |
130 | end | |
131 | end, | |
132 | function() | |
133 | while true do | |
134 | local tCommands | |
135 | tCommands = { | |
136 | ["me"] = function(tUser, sContent) | |
137 | if #sContent > 0 then | |
138 | send("* " .. tUser.sUsername .. " " .. sContent) | |
139 | else | |
140 | send("* Usage: /me [words]", tUser.nUserID) | |
141 | end | |
142 | end, | |
143 | ["nick"] = function(tUser, sContent) | |
144 | if #sContent > 0 then | |
145 | local sOldName = tUser.sUsername | |
146 | tUser.sUsername = sContent | |
147 | send("* " .. sOldName .. " is now known as " .. tUser.sUsername) | |
148 | else | |
149 | send("* Usage: /nick [nickname]", tUser.nUserID) | |
150 | end | |
151 | end, | |
152 | ["users"] = function(tUser, sContent) | |
153 | send("* Connected Users:", tUser.nUserID) | |
154 | local sUsers = "*" | |
155 | for _, tUser in pairs(tUsers) do | |
156 | sUsers = sUsers .. " " .. tUser.sUsername | |
157 | end | |
158 | send(sUsers, tUser.nUserID) | |
159 | end, | |
160 | ["help"] = function(tUser, sContent) | |
161 | send("* Available commands:", tUser.nUserID) | |
162 | local sCommands = "*" | |
163 | for sCommand in pairs(tCommands) do | |
164 | sCommands = sCommands .. " /" .. sCommand | |
165 | end | |
166 | send(sCommands .. " /logout", tUser.nUserID) | |
167 | end, | |
168 | } | |
169 | ||
170 | local nSenderID, tMessage = rednet.receive("chat") | |
171 | if type(tMessage) == "table" then | |
172 | if tMessage.sType == "login" then | |
173 | -- Login from new client | |
174 | local nUserID = tMessage.nUserID | |
175 | local sUsername = tMessage.sUsername | |
176 | if nUserID and sUsername then | |
177 | tUsers[nUserID] = { | |
178 | nID = nSenderID, | |
179 | nUserID = nUserID, | |
180 | sUsername = sUsername, | |
181 | } | |
182 | nUsers = nUsers + 1 | |
183 | printUsers() | |
184 | send("* " .. sUsername .. " has joined the chat") | |
185 | ping(nUserID) | |
186 | end | |
187 | ||
188 | else | |
189 | -- Something else from existing client | |
190 | local nUserID = tMessage.nUserID | |
191 | local tUser = tUsers[nUserID] | |
192 | if tUser and tUser.nID == nSenderID then | |
193 | if tMessage.sType == "logout" then | |
194 | send("* " .. tUser.sUsername .. " has left the chat") | |
195 | tUsers[nUserID] = nil | |
196 | nUsers = nUsers - 1 | |
197 | printUsers() | |
198 | ||
199 | elseif tMessage.sType == "chat" then | |
200 | local sMessage = tMessage.sText | |
201 | if sMessage then | |
202 | local sCommand = string.match(sMessage, "^/([a-z]+)") | |
203 | if sCommand then | |
204 | local fnCommand = tCommands[sCommand] | |
205 | if fnCommand then | |
206 | local sContent = string.sub(sMessage, #sCommand + 3) | |
207 | fnCommand(tUser, sContent) | |
208 | else | |
209 | send("* Unrecognised command: /" .. sCommand, tUser.nUserID) | |
210 | end | |
211 | else | |
212 | send("<" .. tUser.sUsername .. "> " .. tMessage.sText) | |
213 | end | |
214 | end | |
215 | ||
216 | elseif tMessage.sType == "ping to server" then | |
217 | rednet.send(tUser.nID, { | |
218 | sType = "pong to client", | |
219 | nUserID = nUserID, | |
220 | }, "chat") | |
221 | ||
222 | elseif tMessage.sType == "pong to server" then | |
223 | tUser.bPingPonged = true | |
224 | ||
225 | end | |
226 | end | |
227 | end | |
228 | end | |
229 | end | |
230 | end | |
231 | ) | |
232 | if not ok then | |
233 | printError(error) | |
234 | end | |
235 | ||
236 | -- Unhost server | |
237 | for nUserID, tUser in pairs(tUsers) do | |
238 | rednet.send(tUser.nID, { | |
239 | sType = "kick", | |
240 | nUserID = nUserID, | |
241 | }, "chat") | |
242 | end | |
243 | rednet.unhost("chat") | |
244 | closeModem() | |
245 | ||
246 | elseif sCommand == "join" then | |
247 | -- "chat join" | |
248 | -- Get hostname and username | |
249 | local sHostname = tArgs[2] | |
250 | local sUsername = tArgs[3] | |
251 | if sHostname == nil or sUsername == nil then | |
252 | printUsage() | |
253 | return | |
254 | end | |
255 | ||
256 | -- Connect | |
257 | if not openModem() then | |
258 | return | |
259 | end | |
260 | write("Looking up " .. sHostname .. "... ") | |
261 | local nHostID = rednet.lookup("chat", sHostname) | |
262 | if nHostID == nil then | |
263 | print("Failed.") | |
264 | return | |
265 | else | |
266 | print("Success.") | |
267 | end | |
268 | ||
269 | -- Login | |
270 | local nUserID = math.random(1, 2147483647) | |
271 | rednet.send(nHostID, { | |
272 | sType = "login", | |
273 | nUserID = nUserID, | |
274 | sUsername = sUsername, | |
275 | }, "chat") | |
276 | ||
277 | -- Setup ping pong | |
278 | local bPingPonged = true | |
279 | local pingPongTimer = os.startTimer(0) | |
280 | ||
281 | local function ping() | |
282 | rednet.send(nHostID, { | |
283 | sType = "ping to server", | |
284 | nUserID = nUserID, | |
285 | }, "chat") | |
286 | bPingPonged = false | |
287 | pingPongTimer = os.startTimer(15) | |
288 | end | |
289 | ||
290 | -- Handle messages | |
291 | local w, h = term.getSize() | |
292 | local parentTerm = term.current() | |
293 | local titleWindow = window.create(parentTerm, 1, 1, w, 1, true) | |
294 | local historyWindow = window.create(parentTerm, 1, 2, w, h - 2, true) | |
295 | local promptWindow = window.create(parentTerm, 1, h, w, 1, true) | |
296 | historyWindow.setCursorPos(1, h - 2) | |
297 | ||
298 | term.clear() | |
299 | term.setTextColour(textColour) | |
300 | term.redirect(promptWindow) | |
301 | promptWindow.restoreCursor() | |
302 | ||
303 | local function drawTitle() | |
304 | local w = titleWindow.getSize() | |
305 | local sTitle = sUsername .. " on " .. sHostname | |
306 | titleWindow.setTextColour(highlightColour) | |
307 | titleWindow.setCursorPos(math.floor(w / 2 - #sTitle / 2), 1) | |
308 | titleWindow.clearLine() | |
309 | titleWindow.write(sTitle) | |
310 | promptWindow.restoreCursor() | |
311 | end | |
312 | ||
313 | local function printMessage(sMessage) | |
314 | term.redirect(historyWindow) | |
315 | print() | |
316 | if string.match(sMessage, "^%*") then | |
317 | -- Information | |
318 | term.setTextColour(highlightColour) | |
319 | write(sMessage) | |
320 | term.setTextColour(textColour) | |
321 | else | |
322 | -- Chat | |
323 | local sUsernameBit = string.match(sMessage, "^<[^>]*>") | |
324 | if sUsernameBit then | |
325 | term.setTextColour(highlightColour) | |
326 | write(sUsernameBit) | |
327 | term.setTextColour(textColour) | |
328 | write(string.sub(sMessage, #sUsernameBit + 1)) | |
329 | else | |
330 | write(sMessage) | |
331 | end | |
332 | end | |
333 | term.redirect(promptWindow) | |
334 | promptWindow.restoreCursor() | |
335 | end | |
336 | ||
337 | drawTitle() | |
338 | ||
339 | local ok, error = pcall(parallel.waitForAny, | |
340 | function() | |
341 | while true do | |
342 | local sEvent, timer = os.pullEvent() | |
343 | if sEvent == "timer" then | |
344 | if timer == pingPongTimer then | |
345 | if not bPingPonged then | |
346 | printMessage("Server timeout.") | |
347 | return | |
348 | else | |
349 | ping() | |
350 | end | |
351 | end | |
352 | ||
353 | elseif sEvent == "term_resize" then | |
354 | local w, h = parentTerm.getSize() | |
355 | titleWindow.reposition(1, 1, w, 1) | |
356 | historyWindow.reposition(1, 2, w, h - 2) | |
357 | promptWindow.reposition(1, h, w, 1) | |
358 | ||
359 | end | |
360 | end | |
361 | end, | |
362 | function() | |
363 | while true do | |
364 | local nSenderID, tMessage = rednet.receive("chat") | |
365 | if nSenderID == nHostID and type(tMessage) == "table" and tMessage.nUserID == nUserID then | |
366 | if tMessage.sType == "text" then | |
367 | local sText = tMessage.sText | |
368 | if sText then | |
369 | printMessage(sText) | |
370 | end | |
371 | ||
372 | elseif tMessage.sType == "ping to client" then | |
373 | rednet.send(nSenderID, { | |
374 | sType = "pong to server", | |
375 | nUserID = nUserID, | |
376 | }, "chat") | |
377 | ||
378 | elseif tMessage.sType == "pong to client" then | |
379 | bPingPonged = true | |
380 | ||
381 | elseif tMessage.sType == "kick" then | |
382 | return | |
383 | ||
384 | end | |
385 | end | |
386 | end | |
387 | end, | |
388 | function() | |
389 | local tSendHistory = {} | |
390 | while true do | |
391 | promptWindow.setCursorPos(1, 1) | |
392 | promptWindow.clearLine() | |
393 | promptWindow.setTextColor(highlightColour) | |
394 | promptWindow.write(": ") | |
395 | promptWindow.setTextColor(textColour) | |
396 | ||
397 | local sChat = read(nil, tSendHistory) | |
398 | if string.match(sChat, "^/logout") then | |
399 | break | |
400 | else | |
401 | rednet.send(nHostID, { | |
402 | sType = "chat", | |
403 | nUserID = nUserID, | |
404 | sText = sChat, | |
405 | }, "chat") | |
406 | table.insert(tSendHistory, sChat) | |
407 | end | |
408 | end | |
409 | end | |
410 | ) | |
411 | ||
412 | -- Close the windows | |
413 | term.redirect(parentTerm) | |
414 | ||
415 | -- Print error notice | |
416 | local _, h = term.getSize() | |
417 | term.setCursorPos(1, h) | |
418 | term.clearLine() | |
419 | term.setCursorBlink(false) | |
420 | if not ok then | |
421 | printError(error) | |
422 | end | |
423 | ||
424 | -- Logout | |
425 | rednet.send(nHostID, { | |
426 | sType = "logout", | |
427 | nUserID = nUserID, | |
428 | }, "chat") | |
429 | closeModem() | |
430 | ||
431 | -- Print disconnection notice | |
432 | print("Disconnected.") | |
433 | ||
434 | else | |
435 | -- "chat somethingelse" | |
436 | printUsage() | |
437 | ||
438 | end | |
439 |