Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- Full-featured WebSocket Client for ComputerCraft
- local url = "wss://socket.kiefe.ru/ws?token=kom43ijn345ubh234gvy23"
- local reconnect_delay = 3
- local proxy_mode = false
- local provided_url = nil
- if arg[1] == "proxy" then
- proxy_mode = true
- if arg[2] then
- provided_url = arg[2]
- end
- else
- provided_url = arg[1]
- end
- -- Base64 encoder
- local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
- local function encode_base64(data)
- local result = {}
- local pad = 2 - ((#data - 1) % 3)
- data = data .. string.rep('\0', pad)
- for i = 1, #data, 3 do
- local a, b, c = data:byte(i, i + 2)
- local n = a * 65536 + b * 256 + c
- local s1 = math.floor(n / 262144) % 64 + 1
- local s2 = math.floor(n / 4096) % 64 + 1
- local s3 = math.floor(n / 64) % 64 + 1
- local s4 = n % 64 + 1
- table.insert(result, b64chars:sub(s1, s1))
- table.insert(result, b64chars:sub(s2, s2))
- table.insert(result, b64chars:sub(s3, s3))
- table.insert(result, b64chars:sub(s4, s4))
- end
- for i = 1, pad do result[#result] = '=' end
- return table.concat(result)
- end
- -- UI state
- local history = {}
- local scroll = 0
- local hscroll = 0
- local input = ""
- local send_mode = "raw"
- local json_key = ""
- local json_value = ""
- local json_object = {}
- local json_input_focus = "key"
- local status = "Disconnected"
- local cursor_visible = true
- local paused = false
- local show_file_picker = false
- local show_help = false
- local file_list, file_filtered = {}, {}
- local file_selected = 1
- local spinner_state = 1
- local width, height = term.getSize()
- local history_win = window.create(term.current(), 1, 1, width, height - 4)
- local input_win = window.create(term.current(), 1, height - 3, width, 2)
- local tooltip_win = window.create(term.current(), 1, height, width, 1)
- local quit_app = false
- local clear_confirm = false
- local clear_timer = nil
- -- File picker helpers
- local file_search = ""
- local function list_files_recursive(path, results)
- results = results or {}
- local ok, list = pcall(fs.list, path)
- if not ok then return results end
- for _, f in ipairs(list) do
- local full = fs.combine(path, f)
- if fs.isDir(full) then
- list_files_recursive(full, results)
- else
- table.insert(results, full)
- end
- end
- return results
- end
- local function update_filtered_files()
- file_filtered = {}
- for _, f in ipairs(file_list) do
- if file_search == "" or f:lower():find(file_search:lower(), 1, true) then
- table.insert(file_filtered, f)
- end
- end
- if file_selected > #file_filtered then file_selected = #file_filtered end
- if file_selected < 1 then file_selected = 1 end
- end
- local function send_file(ws, path)
- if not fs.exists(path) or fs.isDir(path) then
- table.insert(history, { text = "Invalid file: " .. path, sender = "System" })
- return
- end
- local f = fs.open(path, "rb")
- local content = f.readAll()
- f.close()
- local encoded = encode_base64(content)
- local obj = { type = "file", name = path, content = encoded }
- ws.send(textutils.serializeJSON(obj))
- table.insert(history, { text = "Sent file: " .. path, sender = "Sent" })
- end
- local function get_pair_count()
- local count = 0
- for _ in pairs(json_object) do
- count = count + 1
- end
- return count
- end
- -- UI draw
- local function draw_status()
- input_win.setCursorPos(1, 1)
- local bg
- if status:find("Connecting") then
- bg = colors.orange
- elseif status:find("Failed") then
- bg = colors.red
- elseif paused then
- bg = colors.gray
- else
- bg = colors.blue
- end
- input_win.setBackgroundColor(bg)
- input_win.setTextColor(colors.white)
- input_win.clearLine()
- local spinner_chars = { '\x81', '\x82', '\x88', '\x9F', "\x90", "\x84" }
- local spinner = spinner_chars[spinner_state]
- local status_text = (" %s | Mode: %s | %s"):format(status, send_mode:upper(), paused and "Paused" or "")
- if proxy_mode then
- status_text = status_text .. "\x1B\x1A Proxy Active["..tostring(os.getComputerID()).."] "
- end
- input_win.write(status_text)
- if spinner == "\x9F" then
- input_win.setBackgroundColor(colors.white)
- input_win.setTextColor(bg)
- else
- input_win.setBackgroundColor(bg)
- input_win.setTextColor(colors.white)
- end
- input_win.write(spinner)
- input_win.setBackgroundColor(bg)
- input_win.setTextColor(colors.white)
- end
- local function draw_input()
- input_win.setCursorPos(1, 2)
- input_win.setBackgroundColor(colors.black)
- input_win.setTextColor(colors.white)
- input_win.clearLine()
- if send_mode == "raw" then
- input_win.write("Send: " .. input)
- else
- local key_color = json_input_focus == "key" and colors.yellow or colors.white
- local value_color = json_input_focus == "value" and colors.yellow or colors.white
- input_win.setTextColor(key_color)
- input_win.write("Key: " .. json_key)
- input_win.setTextColor(colors.gray)
- input_win.write(" | ")
- input_win.setTextColor(value_color)
- input_win.write("Value: " .. json_value)
- local pair_count = get_pair_count()
- if pair_count > 0 then
- input_win.setTextColor(colors.lime)
- input_win.write(" | Pairs: " .. pair_count)
- end
- end
- end
- local function draw_tooltip()
- tooltip_win.setCursorPos(1, 1)
- tooltip_win.setBackgroundColor(colors.gray)
- tooltip_win.setTextColor(colors.white)
- tooltip_win.clearLine()
- local tips = "[Ent] Send [Tab] Mode [F1] Pause [F4] Quit [F5] Help"
- tooltip_win.write(tips:sub(1, width))
- end
- function string.ljust(s, width, pad)
- pad = pad or " "
- return s .. string.rep(pad, math.max(0, width - #s))
- end
- local function draw_history()
- history_win.setBackgroundColor(colors.black)
- history_win.clear()
- local hwidth, hheight = history_win.getSize()
- local total_lines = #history
- if not paused then
- scroll = math.max(0, total_lines - hheight)
- end
- local max_scroll = math.max(0, total_lines - hheight)
- if scroll > max_scroll then scroll = max_scroll end
- if scroll < 0 then scroll = 0 end
- local start_line = 1 + scroll
- local end_line = math.min(total_lines, start_line + hheight - 1)
- for i = 1, hheight do
- history_win.setCursorPos(1, i)
- local idx = start_line + i - 1
- if idx <= total_lines then
- local msg = history[idx]
- local x = 1 + hscroll
- if type(msg.text) == "table" then
- -- Colored segments (for VM messages)
- for _, part in ipairs(msg.text) do
- history_win.setTextColor(part.color)
- history_win.setCursorPos(x, i)
- history_win.write(part.text)
- x = x + #part.text
- end
- else
- -- Default coloring
- if msg.sender == "Received" then
- history_win.setTextColor(colors.cyan)
- elseif msg.sender == "Sent" then
- history_win.setTextColor(colors.green)
- elseif msg.sender == "Proxy" then
- history_win.setTextColor(colors.yellow)
- elseif msg.sender == "Error" then
- history_win.setTextColor(colors.red)
- else
- history_win.setTextColor(colors.lightGray)
- end
- history_win.write((msg.text:sub(1 + hscroll, hwidth + hscroll)):ljust(hwidth, " "))
- end
- else
- history_win.setTextColor(colors.black)
- history_win.write(string.rep(" ", hwidth))
- end
- end
- if total_lines > hheight then
- local bar_height = math.max(1, math.floor((hheight / total_lines) * hheight))
- local bar_start = math.floor((scroll / max_scroll) * (hheight - bar_height)) + 1
- for i = 1, hheight do
- history_win.setCursorPos(hwidth, i)
- if i >= bar_start and i < bar_start + bar_height then
- history_win.setBackgroundColor(colors.white)
- else
- history_win.setBackgroundColor(colors.gray)
- end
- history_win.write(" ")
- end
- end
- end
- local function draw_file_picker()
- local fw, fh = math.floor(width * 0.8), math.floor(height * 0.8)
- local fx, fy = math.floor((width - fw) / 2), math.floor((height - fh) / 2)
- local win = window.create(term.current(), fx, fy, fw, fh)
- win.setBackgroundColor(colors.gray)
- win.clear()
- win.setCursorPos(2, 1)
- win.setTextColor(colors.black)
- win.write("File Picker (F4=Quit, Esc=Cancel)")
- win.setCursorPos(2, 2)
- win.write("Search: " .. file_search)
- local start = math.max(1, file_selected - fh + 4)
- for i = 1, fh - 3 do
- local idx = start + i - 1
- if idx > #file_filtered then break end
- win.setCursorPos(2, i + 2)
- local f = file_filtered[idx]
- if idx == file_selected then
- win.setBackgroundColor(colors.white)
- win.setTextColor(colors.black)
- else
- win.setBackgroundColor(colors.gray)
- win.setTextColor(colors.black)
- end
- local text = f
- if #text > fw - 4 then
- text = text:sub(1, fw - 7) .. "..."
- end
- win.write(text)
- end
- end
- local function draw_help()
- local hw, hh = math.floor(width * 0.8), math.floor(height * 0.8)
- local hx, hy = math.floor((width - hw) / 2), math.floor((height - hh) / 2)
- local win = window.create(term.current(), hx, hy, hw, hh)
- win.setBackgroundColor(colors.blue)
- win.clear()
- win.setCursorPos(2, 1)
- win.setTextColor(colors.white)
- win.write("Hotkeys (Backspace to close)")
- local y = 3
- local shortcuts = {
- "Enter: Send message (raw) or add pair (JSON: key->value->add) or send object (if no key)",
- "Tab: Toggle raw/JSON; in JSON: key<->value",
- "F1: Toggle pause",
- "F3: Open file picker",
- "F4: Quit",
- "F5: Show help",
- "Up/Dn: Scroll history",
- "Right/Left: horizontal scroll history",
- "Up/Down: Navigate files (picker)",
- "Esc: Close picker/help",
- "Backspace: Delete char",
- "Chars: Input/search"
- }
- for _, line in ipairs(shortcuts) do
- win.setCursorPos(2, y)
- win.setTextColor(colors.white)
- if #line > hw - 3 then line = line:sub(1, hw - 3) .. "..." end
- win.write(line)
- y = y + 1
- if y > hh - 1 then break end
- end
- end
- local function show_splash()
- term.clear()
- local w, h = term.getSize()
- local lines = {
- "╔---------------------------╗",
- "| WSC Client |",
- "| WebSocket Chat Terminal |",
- "╚----------------------------╝",
- "",
- "Press any key to continue..."
- }
- for i, line in ipairs(lines) do
- term.setCursorPos(math.floor((w - #line)/2)+1, math.floor(h/2) - #lines/2 + i - 1)
- term.setTextColor(colors.yellow)
- term.write(line)
- end
- os.pullEvent("key")
- end
- local url_file = "wsc_url.cfg"
- local function load_url()
- if provided_url then
- return provided_url
- elseif fs.exists(url_file) then
- local f = fs.open(url_file, "r")
- local u = f.readAll()
- f.close()
- if u ~= "" then return u end
- end
- return nil
- end
- local function save_url(u)
- local f = fs.open(url_file, "w")
- f.write(u)
- f.close()
- end
- local function setup_url()
- term.clear()
- term.setCursorPos(1, 1)
- term.setTextColor(colors.white)
- term.setBackgroundColor(colors.black)
- print("=== WSC Client Setup ===")
- local default_url = load_url() or "wss://socket.kiefe.ru/ws?token=kom43ijn345ubh234gvy23"
- print("Enter WebSocket URL (leave blank to use previous):")
- print("Previous URL: " .. default_url)
- term.setCursorPos(1, 5)
- local input_url = read()
- if input_url ~= "" then
- url = input_url
- save_url(url)
- else
- url = default_url
- end
- end
- local function draw()
- draw_history()
- draw_status()
- draw_input()
- draw_tooltip()
- if show_file_picker then draw_file_picker() end
- if show_help then draw_help() end
- term.setCursorBlink(cursor_visible and not (show_file_picker or show_help))
- if not (show_file_picker or show_help) then
- if send_mode == "raw" then
- term.setCursorPos(7 + #input, height - 2)
- else
- local key_start = 6
- if json_input_focus == "key" then
- term.setCursorPos(key_start + #json_key, height - 2)
- else
- local sep_pos = key_start + #json_key + 3
- local value_start = sep_pos + 8
- term.setCursorPos(value_start + #json_value, height - 2)
- end
- end
- end
- end
- local function run_client()
- local modem = nil
- if proxy_mode then
- modem = peripheral.find("modem")
- if not modem then
- print("No modem found")
- return
- end
- local ok, err = pcall(function() rednet.open("top") end)
- if not ok then
- error("Failed to open rednet:"..err, 0)
- end
- end
- while not quit_app do
- status = "Connecting..."
- spinner_state = 1
- draw()
- local ws, err = http.websocket(url)
- if not ws then
- status = "Failed: " .. (err or "unknown") .. " | retry in " .. reconnect_delay .. "s"
- draw()
- sleep(reconnect_delay)
- else
- status = "Connected"
- table.insert(history, { text = "Connected to " .. url, sender = "System" })
- draw()
- local alive = true
- parallel.waitForAny(
- function()
- while alive do
- if not paused then
- local ok, msg = pcall(ws.receive, 0.05)
- if not ok then
- alive = false
- break
- end
- if msg then
- local ts = os.date("%H:%M:%S")
- local is_rednet = false
- if proxy_mode and msg:find("^{") then
- local ok_parse, data = pcall(textutils.unserializeJSON, msg)
- if ok_parse and data and data.type == "rednet_message" and data.receiver then
- local ok_send, err_send = pcall(rednet.send, data.receiver, data.message)
- if ok_send then
- table.insert(history, {text = "["..ts.."] WS -> rednet("..data.receiver.."): "..tostring(data.message), sender = "Proxy"})
- else
- table.insert(history, {text = "["..ts.."] Failed WS -> rednet("..data.receiver.."): "..tostring(err_send), sender = "Error"})
- end
- is_rednet = true
- end
- end
- if not is_rednet then
- local ok, data = pcall(textutils.unserializeJSON, msg)
- if ok and type(data) == "table" and data.vm_id then
- local ts_str = "["..ts.."] "
- table.insert(history, {
- text = {
- { text = ts_str, color = colors.gray },
- { text = "[ VirtualMachineId: "..tostring(data.vm_id).." ]", color = colors.blue },
- { text = " received.", color = colors.gray }
- },
- sender = "Received"
- })
- else
- table.insert(history, { text = "["..ts.."] "..msg, sender = "Received" })
- end
- end
- if scroll > 0 then scroll = 0 end
- draw()
- end
- else
- sleep(0.05)
- end
- end
- end,
- proxy_mode and function()
- while alive do
- local sender, message, protocol = rednet.receive(0.05)
- if sender then
- local forward_msg = textutils.serializeJSON({
- type = "rednet",
- sender = sender,
- message = tostring(message),
- protocol = protocol
- })
- local ok_send, err = pcall(ws.send, forward_msg)
- local ts = os.date("%H:%M:%S")
- if ok_send then
- table.insert(history, {text = "["..ts.."] rednet("..sender..") -> WS: "..tostring(message), sender = "Proxy"})
- else
- table.insert(history, {text = "["..ts.."] Failed rednet("..sender..") -> WS: "..tostring(err), sender = "Error"})
- end
- if scroll > 0 then scroll = 0 end
- draw()
- end
- end
- end or nil,
- function()
- while alive do
- local e, p1, p2, p3 = os.pullEvent()
- if show_help then
- if e == "key" and p1 == keys.backspace then
- show_help = false
- end
- elseif show_file_picker then
- if e == "char" then
- file_search = file_search .. p1; update_filtered_files(); draw()
- elseif e == "key" then
- if p1 == keys.backspace then
- file_search = file_search:sub(1, -2); update_filtered_files(); draw()
- elseif p1 == keys.up then
- file_selected = math.max(1, file_selected - 1); draw()
- elseif p1 == keys.down then
- file_selected = math.min(#file_filtered, file_selected + 1); draw()
- elseif p1 == keys.enter then
- if file_filtered[file_selected] then
- send_file(ws, file_filtered[file_selected]); show_file_picker = false
- end
- elseif p1 == keys.escape then
- show_file_picker = false
- elseif p1 == keys.f4 then
- alive = false; quit_app = true; if ws then ws.close() end; return
- end
- elseif e == "term_resize" then
- width, height = term.getSize(); history_win.reposition(1, 1, width, height - 4); input_win.reposition(1, height - 3, width, 2); tooltip_win.reposition(1, height, width, 1)
- end
- else
- if e == "char" then
- if send_mode == "raw" then
- input = input .. p1
- else
- if json_input_focus == "key" then
- json_key = json_key .. p1
- else
- json_value = json_value .. p1
- end
- end
- draw()
- elseif e == "key" then
- if p1 == keys.backspace then
- if send_mode == "raw" then
- input = input:sub(1, -2)
- else
- if json_input_focus == "key" then
- json_key = json_key:sub(1, -2)
- else
- json_value = json_value:sub(1, -2)
- end
- end
- draw()
- elseif p1 == keys.enter then
- if send_mode == "raw" then
- if #input > 0 then
- ws.send(input); local ts = os.date("%H:%M:%S");table.insert(history, { text = "["..ts.."] "..input, sender = "Sent" }); input = ""
- if scroll > 0 then scroll = 0 end
- end
- else
- if json_input_focus == "key" then
- if #json_key > 0 then
- json_input_focus = "value"
- elseif next(json_object) then
- local json_str = textutils.serializeJSON(json_object)
- ws.send(json_str)
- local ts = os.date("%H:%M:%S")
- table.insert(history, { text = "["..ts.."] "..json_str, sender = "Sent" })
- json_object = {}
- if scroll > 0 then scroll = 0 end
- end
- else
- if #json_key > 0 then
- local val = json_value
- local num = tonumber(json_value)
- if num and tostring(num) == json_value then
- val = num
- end
- json_object[json_key] = val
- table.insert(history,
- {
- text = "Added pair: " .. json_key .. " = " .. tostring(val),
- sender = "System"
- })
- json_key = ""
- json_value = ""
- json_input_focus = "key"
- end
- end
- end
- draw()
- elseif p1 == keys.tab then
- if send_mode == "raw" then
- send_mode = "json"; json_input_focus = "key"
- else
- if json_input_focus == "key" then
- json_input_focus = "value"
- else
- send_mode = "raw"; input = ""; json_object = {}
- end
- end
- draw()
- elseif p1 == keys.f1 then
- paused = not paused
- draw()
- elseif p1 == keys.f3 then
- file_list = list_files_recursive("/")
- file_search = ""; file_selected = 1
- update_filtered_files()
- show_file_picker = true
- draw()
- elseif p1 == keys.f4 then
- alive = false; quit_app = true; if ws then ws.close() end; return
- elseif p1 == keys.f5 then
- show_help = not show_help
- draw()
- elseif p1 == keys.delete then
- if clear_confirm then
- history = {}
- status = "History cleared"
- clear_confirm = false
- if clear_timer then os.cancelTimer(clear_timer) end
- draw()
- clear_timer = os.startTimer(1)
- else
- status = "Clear history (press Delete again)?"
- clear_confirm = true
- draw()
- clear_timer = os.startTimer(3)
- end
- elseif p1 == keys.down then
- scroll = scroll + 5
- draw()
- elseif p1 == keys.up then
- scroll = math.max(0, scroll - 5)
- draw()
- elseif p1 == keys.left then
- hscroll = math.max(0, hscroll - 5)
- draw()
- elseif p1 == keys.right then
- hscroll = hscroll + 5
- draw()
- end
- elseif e == "term_resize" then
- width, height = term.getSize()
- history_win.reposition(1, 1, width, height - 4)
- input_win.reposition(1, height - 3, width, 2)
- tooltip_win.reposition(1, height, width, 1)
- draw()
- elseif e == "timer" and p1 == clear_timer then
- if clear_confirm then
- clear_confirm = false
- status = paused and "Paused" or "Connected"
- draw()
- elseif status == "History cleared" then
- status = paused and "Paused" or "Connected"
- draw()
- end
- elseif e == "terminate" then
- if ws then
- ws.close()
- ws = nil
- status = "Reconnecting..."
- draw_status()
- end
- local ok, new_ws = pcall(http.websocket, url)
- if ok and new_ws then
- ws = new_ws
- status = "Reconnected"
- else
- status = "Failed: retrying..."
- sleep(3)
- end
- draw_status()
- end
- end
- end
- end,
- function()
- while alive do
- spinner_state = (spinner_state % 6) + 1
- if not show_file_picker and not show_help then draw() end
- sleep(0.1)
- end
- end
- )
- if ws then ws.close() end
- if proxy_mode and modem then
- rednet.close("top")
- end
- if quit_app then
- term.clear()
- print("finished: soft-close trigger")
- break
- end
- end
- end
- end
- setup_url()
- term.clear()
- term.setCursorBlink(true)
- draw()
- run_client()
Add Comment
Please, Sign In to add comment