TIMAS_Bro

Untitled

Oct 25th, 2025
69
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 29.43 KB | None | 0 0
  1. -- Full-featured WebSocket Client for ComputerCraft
  2. local url = "wss://socket.kiefe.ru/ws?token=kom43ijn345ubh234gvy23"
  3. local reconnect_delay = 3
  4. local proxy_mode = false
  5. local provided_url = nil
  6. if arg[1] == "proxy" then
  7. proxy_mode = true
  8. if arg[2] then
  9. provided_url = arg[2]
  10. end
  11. else
  12. provided_url = arg[1]
  13. end
  14. -- Base64 encoder
  15. local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
  16. local function encode_base64(data)
  17. local result = {}
  18. local pad = 2 - ((#data - 1) % 3)
  19. data = data .. string.rep('\0', pad)
  20. for i = 1, #data, 3 do
  21. local a, b, c = data:byte(i, i + 2)
  22. local n = a * 65536 + b * 256 + c
  23. local s1 = math.floor(n / 262144) % 64 + 1
  24. local s2 = math.floor(n / 4096) % 64 + 1
  25. local s3 = math.floor(n / 64) % 64 + 1
  26. local s4 = n % 64 + 1
  27. table.insert(result, b64chars:sub(s1, s1))
  28. table.insert(result, b64chars:sub(s2, s2))
  29. table.insert(result, b64chars:sub(s3, s3))
  30. table.insert(result, b64chars:sub(s4, s4))
  31. end
  32. for i = 1, pad do result[#result] = '=' end
  33. return table.concat(result)
  34. end
  35. -- UI state
  36. local history = {}
  37. local scroll = 0
  38. local hscroll = 0
  39. local input = ""
  40. local send_mode = "raw"
  41. local json_key = ""
  42. local json_value = ""
  43. local json_object = {}
  44. local json_input_focus = "key"
  45. local status = "Disconnected"
  46. local cursor_visible = true
  47. local paused = false
  48. local show_file_picker = false
  49. local show_help = false
  50. local file_list, file_filtered = {}, {}
  51. local file_selected = 1
  52. local spinner_state = 1
  53. local width, height = term.getSize()
  54. local history_win = window.create(term.current(), 1, 1, width, height - 4)
  55. local input_win = window.create(term.current(), 1, height - 3, width, 2)
  56. local tooltip_win = window.create(term.current(), 1, height, width, 1)
  57. local quit_app = false
  58. local clear_confirm = false
  59. local clear_timer = nil
  60. -- File picker helpers
  61. local file_search = ""
  62. local function list_files_recursive(path, results)
  63. results = results or {}
  64. local ok, list = pcall(fs.list, path)
  65. if not ok then return results end
  66. for _, f in ipairs(list) do
  67. local full = fs.combine(path, f)
  68. if fs.isDir(full) then
  69. list_files_recursive(full, results)
  70. else
  71. table.insert(results, full)
  72. end
  73. end
  74. return results
  75. end
  76. local function update_filtered_files()
  77. file_filtered = {}
  78. for _, f in ipairs(file_list) do
  79. if file_search == "" or f:lower():find(file_search:lower(), 1, true) then
  80. table.insert(file_filtered, f)
  81. end
  82. end
  83. if file_selected > #file_filtered then file_selected = #file_filtered end
  84. if file_selected < 1 then file_selected = 1 end
  85. end
  86. local function send_file(ws, path)
  87. if not fs.exists(path) or fs.isDir(path) then
  88. table.insert(history, { text = "Invalid file: " .. path, sender = "System" })
  89. return
  90. end
  91. local f = fs.open(path, "rb")
  92. local content = f.readAll()
  93. f.close()
  94. local encoded = encode_base64(content)
  95. local obj = { type = "file", name = path, content = encoded }
  96. ws.send(textutils.serializeJSON(obj))
  97. table.insert(history, { text = "Sent file: " .. path, sender = "Sent" })
  98. end
  99. local function get_pair_count()
  100. local count = 0
  101. for _ in pairs(json_object) do
  102. count = count + 1
  103. end
  104. return count
  105. end
  106. -- UI draw
  107. local function draw_status()
  108. input_win.setCursorPos(1, 1)
  109. local bg
  110. if status:find("Connecting") then
  111. bg = colors.orange
  112. elseif status:find("Failed") then
  113. bg = colors.red
  114. elseif paused then
  115. bg = colors.gray
  116. else
  117. bg = colors.blue
  118. end
  119. input_win.setBackgroundColor(bg)
  120. input_win.setTextColor(colors.white)
  121. input_win.clearLine()
  122. local spinner_chars = { '\x81', '\x82', '\x88', '\x9F', "\x90", "\x84" }
  123. local spinner = spinner_chars[spinner_state]
  124. local status_text = (" %s | Mode: %s | %s"):format(status, send_mode:upper(), paused and "Paused" or "")
  125. if proxy_mode then
  126. status_text = status_text .. "\x1B\x1A Proxy Active["..tostring(os.getComputerID()).."] "
  127. end
  128. input_win.write(status_text)
  129. if spinner == "\x9F" then
  130. input_win.setBackgroundColor(colors.white)
  131. input_win.setTextColor(bg)
  132. else
  133. input_win.setBackgroundColor(bg)
  134. input_win.setTextColor(colors.white)
  135. end
  136. input_win.write(spinner)
  137. input_win.setBackgroundColor(bg)
  138. input_win.setTextColor(colors.white)
  139. end
  140. local function draw_input()
  141. input_win.setCursorPos(1, 2)
  142. input_win.setBackgroundColor(colors.black)
  143. input_win.setTextColor(colors.white)
  144. input_win.clearLine()
  145. if send_mode == "raw" then
  146. input_win.write("Send: " .. input)
  147. else
  148. local key_color = json_input_focus == "key" and colors.yellow or colors.white
  149. local value_color = json_input_focus == "value" and colors.yellow or colors.white
  150. input_win.setTextColor(key_color)
  151. input_win.write("Key: " .. json_key)
  152. input_win.setTextColor(colors.gray)
  153. input_win.write(" | ")
  154. input_win.setTextColor(value_color)
  155. input_win.write("Value: " .. json_value)
  156. local pair_count = get_pair_count()
  157. if pair_count > 0 then
  158. input_win.setTextColor(colors.lime)
  159. input_win.write(" | Pairs: " .. pair_count)
  160. end
  161. end
  162. end
  163. local function draw_tooltip()
  164. tooltip_win.setCursorPos(1, 1)
  165. tooltip_win.setBackgroundColor(colors.gray)
  166. tooltip_win.setTextColor(colors.white)
  167. tooltip_win.clearLine()
  168. local tips = "[Ent] Send [Tab] Mode [F1] Pause [F4] Quit [F5] Help"
  169. tooltip_win.write(tips:sub(1, width))
  170. end
  171. function string.ljust(s, width, pad)
  172. pad = pad or " "
  173. return s .. string.rep(pad, math.max(0, width - #s))
  174. end
  175. local function draw_history()
  176. history_win.setBackgroundColor(colors.black)
  177. history_win.clear()
  178. local hwidth, hheight = history_win.getSize()
  179. local total_lines = #history
  180. if not paused then
  181. scroll = math.max(0, total_lines - hheight)
  182. end
  183. local max_scroll = math.max(0, total_lines - hheight)
  184. if scroll > max_scroll then scroll = max_scroll end
  185. if scroll < 0 then scroll = 0 end
  186. local start_line = 1 + scroll
  187. local end_line = math.min(total_lines, start_line + hheight - 1)
  188. for i = 1, hheight do
  189. history_win.setCursorPos(1, i)
  190. local idx = start_line + i - 1
  191. if idx <= total_lines then
  192. local msg = history[idx]
  193. local x = 1 + hscroll
  194. if type(msg.text) == "table" then
  195. -- Colored segments (for VM messages)
  196. for _, part in ipairs(msg.text) do
  197. history_win.setTextColor(part.color)
  198. history_win.setCursorPos(x, i)
  199. history_win.write(part.text)
  200. x = x + #part.text
  201. end
  202. else
  203. -- Default coloring
  204. if msg.sender == "Received" then
  205. history_win.setTextColor(colors.cyan)
  206. elseif msg.sender == "Sent" then
  207. history_win.setTextColor(colors.green)
  208. elseif msg.sender == "Proxy" then
  209. history_win.setTextColor(colors.yellow)
  210. elseif msg.sender == "Error" then
  211. history_win.setTextColor(colors.red)
  212. else
  213. history_win.setTextColor(colors.lightGray)
  214. end
  215. history_win.write((msg.text:sub(1 + hscroll, hwidth + hscroll)):ljust(hwidth, " "))
  216. end
  217. else
  218. history_win.setTextColor(colors.black)
  219. history_win.write(string.rep(" ", hwidth))
  220. end
  221. end
  222. if total_lines > hheight then
  223. local bar_height = math.max(1, math.floor((hheight / total_lines) * hheight))
  224. local bar_start = math.floor((scroll / max_scroll) * (hheight - bar_height)) + 1
  225. for i = 1, hheight do
  226. history_win.setCursorPos(hwidth, i)
  227. if i >= bar_start and i < bar_start + bar_height then
  228. history_win.setBackgroundColor(colors.white)
  229. else
  230. history_win.setBackgroundColor(colors.gray)
  231. end
  232. history_win.write(" ")
  233. end
  234. end
  235. end
  236.  
  237. local function draw_file_picker()
  238. local fw, fh = math.floor(width * 0.8), math.floor(height * 0.8)
  239. local fx, fy = math.floor((width - fw) / 2), math.floor((height - fh) / 2)
  240. local win = window.create(term.current(), fx, fy, fw, fh)
  241. win.setBackgroundColor(colors.gray)
  242. win.clear()
  243. win.setCursorPos(2, 1)
  244. win.setTextColor(colors.black)
  245. win.write("File Picker (F4=Quit, Esc=Cancel)")
  246. win.setCursorPos(2, 2)
  247. win.write("Search: " .. file_search)
  248. local start = math.max(1, file_selected - fh + 4)
  249. for i = 1, fh - 3 do
  250. local idx = start + i - 1
  251. if idx > #file_filtered then break end
  252. win.setCursorPos(2, i + 2)
  253. local f = file_filtered[idx]
  254. if idx == file_selected then
  255. win.setBackgroundColor(colors.white)
  256. win.setTextColor(colors.black)
  257. else
  258. win.setBackgroundColor(colors.gray)
  259. win.setTextColor(colors.black)
  260. end
  261. local text = f
  262. if #text > fw - 4 then
  263. text = text:sub(1, fw - 7) .. "..."
  264. end
  265. win.write(text)
  266. end
  267. end
  268. local function draw_help()
  269. local hw, hh = math.floor(width * 0.8), math.floor(height * 0.8)
  270. local hx, hy = math.floor((width - hw) / 2), math.floor((height - hh) / 2)
  271. local win = window.create(term.current(), hx, hy, hw, hh)
  272. win.setBackgroundColor(colors.blue)
  273. win.clear()
  274. win.setCursorPos(2, 1)
  275. win.setTextColor(colors.white)
  276. win.write("Hotkeys (Backspace to close)")
  277. local y = 3
  278. local shortcuts = {
  279. "Enter: Send message (raw) or add pair (JSON: key->value->add) or send object (if no key)",
  280. "Tab: Toggle raw/JSON; in JSON: key<->value",
  281. "F1: Toggle pause",
  282. "F3: Open file picker",
  283. "F4: Quit",
  284. "F5: Show help",
  285. "Up/Dn: Scroll history",
  286. "Right/Left: horizontal scroll history",
  287. "Up/Down: Navigate files (picker)",
  288. "Esc: Close picker/help",
  289. "Backspace: Delete char",
  290. "Chars: Input/search"
  291. }
  292. for _, line in ipairs(shortcuts) do
  293. win.setCursorPos(2, y)
  294. win.setTextColor(colors.white)
  295. if #line > hw - 3 then line = line:sub(1, hw - 3) .. "..." end
  296. win.write(line)
  297. y = y + 1
  298. if y > hh - 1 then break end
  299. end
  300. end
  301. local function show_splash()
  302. term.clear()
  303. local w, h = term.getSize()
  304. local lines = {
  305. "╔---------------------------╗",
  306. "| WSC Client |",
  307. "| WebSocket Chat Terminal |",
  308. "╚----------------------------╝",
  309. "",
  310. "Press any key to continue..."
  311. }
  312. for i, line in ipairs(lines) do
  313. term.setCursorPos(math.floor((w - #line)/2)+1, math.floor(h/2) - #lines/2 + i - 1)
  314. term.setTextColor(colors.yellow)
  315. term.write(line)
  316. end
  317. os.pullEvent("key")
  318. end
  319. local url_file = "wsc_url.cfg"
  320. local function load_url()
  321. if provided_url then
  322. return provided_url
  323. elseif fs.exists(url_file) then
  324. local f = fs.open(url_file, "r")
  325. local u = f.readAll()
  326. f.close()
  327. if u ~= "" then return u end
  328. end
  329. return nil
  330. end
  331. local function save_url(u)
  332. local f = fs.open(url_file, "w")
  333. f.write(u)
  334. f.close()
  335. end
  336. local function setup_url()
  337. term.clear()
  338. term.setCursorPos(1, 1)
  339. term.setTextColor(colors.white)
  340. term.setBackgroundColor(colors.black)
  341. print("=== WSC Client Setup ===")
  342. local default_url = load_url() or "wss://socket.kiefe.ru/ws?token=kom43ijn345ubh234gvy23"
  343. print("Enter WebSocket URL (leave blank to use previous):")
  344. print("Previous URL: " .. default_url)
  345. term.setCursorPos(1, 5)
  346. local input_url = read()
  347. if input_url ~= "" then
  348. url = input_url
  349. save_url(url)
  350. else
  351. url = default_url
  352. end
  353. end
  354. local function draw()
  355. draw_history()
  356. draw_status()
  357. draw_input()
  358. draw_tooltip()
  359. if show_file_picker then draw_file_picker() end
  360. if show_help then draw_help() end
  361. term.setCursorBlink(cursor_visible and not (show_file_picker or show_help))
  362. if not (show_file_picker or show_help) then
  363. if send_mode == "raw" then
  364. term.setCursorPos(7 + #input, height - 2)
  365. else
  366. local key_start = 6
  367. if json_input_focus == "key" then
  368. term.setCursorPos(key_start + #json_key, height - 2)
  369. else
  370. local sep_pos = key_start + #json_key + 3
  371. local value_start = sep_pos + 8
  372. term.setCursorPos(value_start + #json_value, height - 2)
  373. end
  374. end
  375. end
  376. end
  377. local function run_client()
  378. local modem = nil
  379. if proxy_mode then
  380. modem = peripheral.find("modem")
  381. if not modem then
  382. print("No modem found")
  383. return
  384. end
  385. local ok, err = pcall(function() rednet.open("top") end)
  386. if not ok then
  387. error("Failed to open rednet:"..err, 0)
  388. end
  389. end
  390. while not quit_app do
  391. status = "Connecting..."
  392. spinner_state = 1
  393. draw()
  394. local ws, err = http.websocket(url)
  395. if not ws then
  396. status = "Failed: " .. (err or "unknown") .. " | retry in " .. reconnect_delay .. "s"
  397. draw()
  398. sleep(reconnect_delay)
  399. else
  400. status = "Connected"
  401. table.insert(history, { text = "Connected to " .. url, sender = "System" })
  402. draw()
  403. local alive = true
  404. parallel.waitForAny(
  405. function()
  406. while alive do
  407. if not paused then
  408. local ok, msg = pcall(ws.receive, 0.05)
  409. if not ok then
  410. alive = false
  411. break
  412. end
  413. if msg then
  414. local ts = os.date("%H:%M:%S")
  415. local is_rednet = false
  416. if proxy_mode and msg:find("^{") then
  417. local ok_parse, data = pcall(textutils.unserializeJSON, msg)
  418. if ok_parse and data and data.type == "rednet_message" and data.receiver then
  419. local ok_send, err_send = pcall(rednet.send, data.receiver, data.message)
  420. if ok_send then
  421. table.insert(history, {text = "["..ts.."] WS -> rednet("..data.receiver.."): "..tostring(data.message), sender = "Proxy"})
  422. else
  423. table.insert(history, {text = "["..ts.."] Failed WS -> rednet("..data.receiver.."): "..tostring(err_send), sender = "Error"})
  424. end
  425. is_rednet = true
  426. end
  427. end
  428. if not is_rednet then
  429. local ok, data = pcall(textutils.unserializeJSON, msg)
  430. if ok and type(data) == "table" and data.vm_id then
  431. local ts_str = "["..ts.."] "
  432. table.insert(history, {
  433. text = {
  434. { text = ts_str, color = colors.gray },
  435. { text = "[ VirtualMachineId: "..tostring(data.vm_id).." ]", color = colors.blue },
  436. { text = " received.", color = colors.gray }
  437. },
  438. sender = "Received"
  439. })
  440. else
  441. table.insert(history, { text = "["..ts.."] "..msg, sender = "Received" })
  442. end
  443. end
  444. if scroll > 0 then scroll = 0 end
  445. draw()
  446. end
  447. else
  448. sleep(0.05)
  449. end
  450. end
  451. end,
  452. proxy_mode and function()
  453. while alive do
  454. local sender, message, protocol = rednet.receive(0.05)
  455. if sender then
  456. local forward_msg = textutils.serializeJSON({
  457. type = "rednet",
  458. sender = sender,
  459. message = tostring(message),
  460. protocol = protocol
  461. })
  462. local ok_send, err = pcall(ws.send, forward_msg)
  463. local ts = os.date("%H:%M:%S")
  464. if ok_send then
  465. table.insert(history, {text = "["..ts.."] rednet("..sender..") -> WS: "..tostring(message), sender = "Proxy"})
  466. else
  467. table.insert(history, {text = "["..ts.."] Failed rednet("..sender..") -> WS: "..tostring(err), sender = "Error"})
  468. end
  469. if scroll > 0 then scroll = 0 end
  470. draw()
  471. end
  472. end
  473. end or nil,
  474. function()
  475. while alive do
  476. local e, p1, p2, p3 = os.pullEvent()
  477. if show_help then
  478. if e == "key" and p1 == keys.backspace then
  479. show_help = false
  480. end
  481. elseif show_file_picker then
  482. if e == "char" then
  483. file_search = file_search .. p1; update_filtered_files(); draw()
  484. elseif e == "key" then
  485. if p1 == keys.backspace then
  486. file_search = file_search:sub(1, -2); update_filtered_files(); draw()
  487. elseif p1 == keys.up then
  488. file_selected = math.max(1, file_selected - 1); draw()
  489. elseif p1 == keys.down then
  490. file_selected = math.min(#file_filtered, file_selected + 1); draw()
  491. elseif p1 == keys.enter then
  492. if file_filtered[file_selected] then
  493. send_file(ws, file_filtered[file_selected]); show_file_picker = false
  494. end
  495. elseif p1 == keys.escape then
  496. show_file_picker = false
  497. elseif p1 == keys.f4 then
  498. alive = false; quit_app = true; if ws then ws.close() end; return
  499. end
  500. elseif e == "term_resize" then
  501. 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)
  502. end
  503. else
  504. if e == "char" then
  505. if send_mode == "raw" then
  506. input = input .. p1
  507. else
  508. if json_input_focus == "key" then
  509. json_key = json_key .. p1
  510. else
  511. json_value = json_value .. p1
  512. end
  513. end
  514. draw()
  515. elseif e == "key" then
  516. if p1 == keys.backspace then
  517. if send_mode == "raw" then
  518. input = input:sub(1, -2)
  519. else
  520. if json_input_focus == "key" then
  521. json_key = json_key:sub(1, -2)
  522. else
  523. json_value = json_value:sub(1, -2)
  524. end
  525. end
  526. draw()
  527. elseif p1 == keys.enter then
  528. if send_mode == "raw" then
  529. if #input > 0 then
  530. ws.send(input); local ts = os.date("%H:%M:%S");table.insert(history, { text = "["..ts.."] "..input, sender = "Sent" }); input = ""
  531. if scroll > 0 then scroll = 0 end
  532. end
  533. else
  534. if json_input_focus == "key" then
  535. if #json_key > 0 then
  536. json_input_focus = "value"
  537. elseif next(json_object) then
  538. local json_str = textutils.serializeJSON(json_object)
  539. ws.send(json_str)
  540. local ts = os.date("%H:%M:%S")
  541. table.insert(history, { text = "["..ts.."] "..json_str, sender = "Sent" })
  542. json_object = {}
  543. if scroll > 0 then scroll = 0 end
  544. end
  545. else
  546. if #json_key > 0 then
  547. local val = json_value
  548. local num = tonumber(json_value)
  549. if num and tostring(num) == json_value then
  550. val = num
  551. end
  552. json_object[json_key] = val
  553. table.insert(history,
  554. {
  555. text = "Added pair: " .. json_key .. " = " .. tostring(val),
  556. sender = "System"
  557. })
  558. json_key = ""
  559. json_value = ""
  560. json_input_focus = "key"
  561. end
  562. end
  563. end
  564. draw()
  565. elseif p1 == keys.tab then
  566. if send_mode == "raw" then
  567. send_mode = "json"; json_input_focus = "key"
  568. else
  569. if json_input_focus == "key" then
  570. json_input_focus = "value"
  571. else
  572. send_mode = "raw"; input = ""; json_object = {}
  573. end
  574. end
  575. draw()
  576. elseif p1 == keys.f1 then
  577. paused = not paused
  578. draw()
  579. elseif p1 == keys.f3 then
  580. file_list = list_files_recursive("/")
  581. file_search = ""; file_selected = 1
  582. update_filtered_files()
  583. show_file_picker = true
  584. draw()
  585. elseif p1 == keys.f4 then
  586. alive = false; quit_app = true; if ws then ws.close() end; return
  587. elseif p1 == keys.f5 then
  588. show_help = not show_help
  589. draw()
  590. elseif p1 == keys.delete then
  591. if clear_confirm then
  592. history = {}
  593. status = "History cleared"
  594. clear_confirm = false
  595. if clear_timer then os.cancelTimer(clear_timer) end
  596. draw()
  597. clear_timer = os.startTimer(1)
  598. else
  599. status = "Clear history (press Delete again)?"
  600. clear_confirm = true
  601. draw()
  602. clear_timer = os.startTimer(3)
  603. end
  604. elseif p1 == keys.down then
  605. scroll = scroll + 5
  606. draw()
  607. elseif p1 == keys.up then
  608. scroll = math.max(0, scroll - 5)
  609. draw()
  610. elseif p1 == keys.left then
  611. hscroll = math.max(0, hscroll - 5)
  612. draw()
  613. elseif p1 == keys.right then
  614. hscroll = hscroll + 5
  615. draw()
  616. end
  617. elseif e == "term_resize" then
  618. width, height = term.getSize()
  619. history_win.reposition(1, 1, width, height - 4)
  620. input_win.reposition(1, height - 3, width, 2)
  621. tooltip_win.reposition(1, height, width, 1)
  622. draw()
  623. elseif e == "timer" and p1 == clear_timer then
  624. if clear_confirm then
  625. clear_confirm = false
  626. status = paused and "Paused" or "Connected"
  627. draw()
  628. elseif status == "History cleared" then
  629. status = paused and "Paused" or "Connected"
  630. draw()
  631. end
  632. elseif e == "terminate" then
  633. if ws then
  634. ws.close()
  635. ws = nil
  636. status = "Reconnecting..."
  637. draw_status()
  638. end
  639. local ok, new_ws = pcall(http.websocket, url)
  640. if ok and new_ws then
  641. ws = new_ws
  642. status = "Reconnected"
  643. else
  644. status = "Failed: retrying..."
  645. sleep(3)
  646. end
  647. draw_status()
  648. end
  649. end
  650. end
  651. end,
  652. function()
  653. while alive do
  654. spinner_state = (spinner_state % 6) + 1
  655. if not show_file_picker and not show_help then draw() end
  656. sleep(0.1)
  657. end
  658. end
  659. )
  660. if ws then ws.close() end
  661. if proxy_mode and modem then
  662. rednet.close("top")
  663. end
  664. if quit_app then
  665. term.clear()
  666. print("finished: soft-close trigger")
  667. break
  668. end
  669. end
  670. end
  671. end
  672. setup_url()
  673. term.clear()
  674. term.setCursorBlink(true)
  675. draw()
  676. run_client()
  677.  
Add Comment
Please, Sign In to add comment