View difference between Paste ID: garKajaK and EtJJq54E
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