mirror of
https://github.com/CCLeonOS/LeonOS.git
synced 2026-03-03 06:47:00 +00:00
添加 LeonOS 的基本实现,包括: - 核心 API 模块(colors, disk, gps, keys, multishell, parallel, rednet, redstone, settings, vector) - 命令行程序(about, alias, bg, clear, copy, delete, edit, fg, help, list, lua, mkdir, move, paint, peripherals, programs, reboot, set, shutdown, threads) - 系统启动脚本和包管理 - 文档(README.md, LICENSE) - 开发工具(devbin)和更新程序 实现功能: - 完整的线程管理系统 - 兼容 ComputerCraft 的 API 设计 - 改进的 shell 和命令补全系统 - 多标签终端支持 - 设置管理系统
294 lines
14 KiB
Lua
294 lines
14 KiB
Lua
--- gist.lua - Gist client for ComputerCraft
|
|
-- Made by JackMacWindows for LeonOS-PC and CC: Tweaked
|
|
--
|
|
-- @module cc.http.gist
|
|
|
|
local textutils = require("textutils")
|
|
local settings = require("settings")
|
|
local expect = require("cc.expect").expect
|
|
local colors = require("colors")
|
|
local write = require("rc").write
|
|
local term = require("term")
|
|
local http = require("http")
|
|
local read = term.read
|
|
|
|
local gist = {}
|
|
|
|
local function emptyfn() end -- to reduce memory/speed footprint when using empty functions
|
|
|
|
-- Internal functions
|
|
|
|
local function getGistFile(data)
|
|
if not data.truncated then return data.content else
|
|
local handle = http.get(data.raw_url)
|
|
if not handle then error("Could not connect to api.github.com.") end
|
|
if handle.getResponseCode() ~= 200 then
|
|
handle.close()
|
|
error("Failed to download file data.")
|
|
end
|
|
local d = handle.readAll()
|
|
handle.close()
|
|
return d
|
|
end
|
|
end
|
|
|
|
local function setTextColor(c) if term.isColor() then term.setTextColor(c) elseif c == colors.white or c == colors.yellow then term.setTextColor(colors.white) else term.setTextColor(colors.lightGray) end end
|
|
|
|
local function requestAuth(headers, interactive)
|
|
if settings.get("gist.id") ~= nil then
|
|
headers.Authorization = "token " .. settings.get("gist.id")
|
|
return true
|
|
elseif interactive then
|
|
setTextColor(colors.yellow)
|
|
write("You need to add a Personal Access Token (PAK) to upload Gists. Follow the instructions at ")
|
|
setTextColor(colors.blue)
|
|
write("https://tinyurl.com/GitHubPAK")
|
|
setTextColor(colors.yellow)
|
|
write(" to generate one. Make sure to check the '")
|
|
setTextColor(colors.blue)
|
|
write("gist")
|
|
setTextColor(colors.yellow)
|
|
print("' checkbox on step 7 (under 'Select scopes'). Once done, paste it here.")
|
|
setTextColor(colors.lime)
|
|
write("PAK: ")
|
|
setTextColor(colors.white)
|
|
local pak = read()
|
|
if pak == nil or pak == "" then error("Invalid PAK, please try again.") end
|
|
settings.set("gist.id", pak)
|
|
settings.save(".settings")
|
|
headers.Authorization = "token " .. pak
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- User API - this can be loaded with require "cc.http.gist"
|
|
|
|
-- ID can be either just the gist ID or a gist ID followed by a slash and a file name
|
|
-- (This also includes Gist URLs)
|
|
-- * If a file name is specified, retrieves that file
|
|
-- * Otherwise, if there's only one file, retrieves that file
|
|
-- * Otherwise, if there's a file named 'init.lua', retrieves 'init.lua'
|
|
-- * Otherwise, if there's more than one file but only one *.lua file, retrieves the Lua file
|
|
-- * Otherwise, retrieves the first Lua file alphabetically (with a warning)
|
|
-- * Otherwise, fails
|
|
|
|
--- Retrieves one file from a Gist using the specified ID.
|
|
-- @tparam string id The Gist ID to download from. See above comments for more details.
|
|
-- @tparam[opt] function progress A function to use to report status messages.
|
|
-- @treturn string|nil The contents of the specified Gist file, or nil on error.
|
|
-- @treturn string|nil The name of the file that was chosen to be downloaded, or a message on error.
|
|
function gist.get(id, progress)
|
|
expect(1, id, "string")
|
|
expect(2, progress, "function", "nil")
|
|
progress = progress or emptyfn
|
|
local file
|
|
if id:find("https?://") then id = id:gsub("https?://[^/]+/", ""):gsub("^[^/]*[^/%x]+[^/]*/", "") end
|
|
if id:find("/") ~= nil then id, file = id:match("^([0-9A-Fa-f:]+)/(.+)$") end
|
|
if id == nil or not id:match("^[0-9A-Fa-f][0-9A-Fa-f:]+[0-9A-Fa-f]$") then error("bad argument #1 to 'get' (invalid ID)", 2) end
|
|
if id:find(":") ~= nil then id = id:gsub(":", "/") end
|
|
progress("Connecting to api.github.com... ")
|
|
local handle = http.get("https://api.github.com/gists/" .. id)
|
|
if handle == nil then
|
|
progress("Failed.\n")
|
|
return nil, "Failed to connect"
|
|
end
|
|
local meta = textutils.unserializeJSON(handle.readAll())
|
|
local code = handle.getResponseCode()
|
|
handle.close()
|
|
if code ~= 200 then
|
|
progress("Failed.\n")
|
|
return nil, "Invalid response code (" .. code .. ")" .. (meta and ": " .. meta.message or "")
|
|
end
|
|
if meta == nil or meta.files == nil then
|
|
progress("Failed.\n")
|
|
return nil, meta and "GitHub API error: " .. meta.message or "Error parsing JSON"
|
|
end
|
|
progress("Success.\n")
|
|
if file then return getGistFile(meta.files[file]), file
|
|
elseif next(meta.files, next(meta.files)) == nil then return getGistFile(meta.files[next(meta.files)]), next(meta.files)
|
|
elseif meta.files["init.lua"] ~= nil then return getGistFile(meta.files["init.lua"]), "init.lua"
|
|
else
|
|
local luaFiles = {}
|
|
for k in pairs(meta.files) do if k:match("%.lua$") then table.insert(luaFiles, k) end end
|
|
table.sort(luaFiles)
|
|
if #luaFiles == 0 then
|
|
progress("Error: Could not find any Lua files to download!\n")
|
|
return nil, "Could not find any Lua files to download"
|
|
end
|
|
if #luaFiles > 1 then progress("Warning: More than one Lua file detected, downloading the first one alphabetically.\n") end
|
|
return getGistFile(meta.files[luaFiles[1]]), luaFiles[1]
|
|
end
|
|
end
|
|
|
|
--- Runs a specified Gist. This is a wrapper for convenience.
|
|
-- @tparam string id The Gist ID to download from. See above comments for more details.
|
|
-- @tparam[opt] function progress A function to use to report status messages. If
|
|
-- this is not a function, it will be used as an argument to the script.
|
|
-- @tparam[opt] any ... Any arguments to pass to the script.
|
|
-- @treturn any Any results returned from the script.
|
|
function gist.run(id, progress, ...)
|
|
expect(1, id, "string")
|
|
local args = table.pack(...)
|
|
if type(progress) ~= "function" and progress ~= nil then
|
|
table.insert(args, 1, progress)
|
|
progress = nil
|
|
end
|
|
local data, name = gist.get(id, progress)
|
|
if data == nil then return end
|
|
local fn, err = load(data, name, "t", _ENV)
|
|
if fn == nil then error(err) end
|
|
local retval = table.pack(pcall(fn, table.unpack(args)))
|
|
if not retval[1] then error(retval[2]) end
|
|
return table.unpack(retval, 2)
|
|
end
|
|
|
|
--- Retrieves a table of all files from a Gist.
|
|
-- @tparam string id The Gist ID to download.
|
|
-- @tparam[opt] function progress A function to use to report status messages.
|
|
-- @treturn table|nil A key-value list of all files in the Gist, or nil on error.
|
|
-- @treturn string|nil If an error occurred, a string describing the error.
|
|
function gist.getAll(id, progress)
|
|
expect(1, id, "string")
|
|
expect(2, progress, "function", "nil")
|
|
progress = progress or emptyfn
|
|
if id:find("https?://") then id = id:gsub("https?://[^/]+/", ""):gsub("^[^/]*[^/%x]+[^/]*/", "") end
|
|
if id:find("/") ~= nil then id = id:match("^([0-9A-Fa-f:]+)/.*$") end
|
|
if id == nil or not id:match("^[0-9A-Fa-f][0-9A-Fa-f:]+[0-9A-Fa-f]$") then error("bad argument #1 to 'getAll' (invalid ID)", 2) end
|
|
if id:find(":") ~= nil then id = id:gsub(":", "/") end
|
|
progress("Connecting to api.github.com... ")
|
|
local handle = http.get("https://api.github.com/gists/" .. id)
|
|
if handle == nil then progress("Failed.\n") return nil, "Failed to connect" end
|
|
local meta = textutils.unserializeJSON(handle.readAll())
|
|
local code = handle.getResponseCode()
|
|
handle.close()
|
|
if code ~= 200 then
|
|
progress("Failed.\n")
|
|
return nil, "Invalid response code (" .. code .. ")" .. (meta and ": " .. meta.message or "")
|
|
end
|
|
if meta == nil or meta.files == nil then
|
|
progress("Failed.\n")
|
|
return nil, meta and meta.message and "GitHub API error: " .. meta.message or "Error parsing JSON"
|
|
end
|
|
progress("Success.\n")
|
|
local retval = {}
|
|
for k, v in pairs(meta.files) do retval[k] = getGistFile(v) end
|
|
return retval
|
|
end
|
|
|
|
--- Returns some information about a Gist.
|
|
-- @tparam string id The Gist ID to get info about.
|
|
-- @tparam[opt] function progress A function to use to report status messages.
|
|
-- @treturn table|nil A table of information about the Gist. The table may
|
|
-- contain the following entries:
|
|
-- - description: The description for the Gist.
|
|
-- - author: The username of the author of the Gist.
|
|
-- - revisionCount: The number of revisions that have been made to the Gist.
|
|
-- - files: A list of all file names in the Gist, sorted alphabetically.
|
|
-- @treturn string|nil If an error occurred, a string describing the error.
|
|
function gist.info(id, progress)
|
|
expect(1, id, "string")
|
|
expect(2, progress, "function", "nil")
|
|
progress = progress or emptyfn
|
|
if id:find("https?://") then id = id:gsub("https?://[^/]+/", ""):gsub("^[^/]*[^/%x]+[^/]*/", "") end
|
|
if id:find("/") ~= nil then id = id:match("^([0-9A-Fa-f:]+)/.*$") end
|
|
if id == nil or not id:match("^[0-9A-Fa-f][0-9A-Fa-f:]+[0-9A-Fa-f]$") then error("bad argument #1 to 'info' (invalid ID)", 2) end
|
|
if id:find(":") ~= nil then id = id:gsub(":", "/") end
|
|
progress("Connecting to api.github.com... ")
|
|
local handle = http.get("https://api.github.com/gists/" .. id)
|
|
if handle == nil then progress("Failed.\n") return nil, "Failed to connect" end
|
|
local meta = textutils.unserializeJSON(handle.readAll())
|
|
local code = handle.getResponseCode()
|
|
handle.close()
|
|
if code ~= 200 then
|
|
progress("Failed.\n")
|
|
return nil, "Invalid response code (" .. code .. ")" .. (meta and ": " .. meta.message or "")
|
|
end
|
|
if meta == nil or meta.files == nil then
|
|
progress("Failed.\n")
|
|
return nil, meta and meta.message and "GitHub API error: " .. meta.message or "Error parsing JSON"
|
|
end
|
|
local f = {}
|
|
for k in pairs(meta.files) do table.insert(f, k) end
|
|
table.sort(f)
|
|
progress("Success.\n")
|
|
return { description = meta.description, author = meta.owner.login, revisionCount = #meta.history, files = f }
|
|
end
|
|
|
|
--- Uploads a list of files to Gist, updating a previous Gist if desired.
|
|
-- @tparam table files The files to upload to Gist. This table should be
|
|
-- structured with a key as file name and a string with the file contents. If
|
|
-- updating a Gist, files can be deleted by setting the data to textutils.json_null.
|
|
-- @tparam[opt] string description The description of the Gist. This is required
|
|
-- when updating a Gist, but is optional when uploading a Gist for the first
|
|
-- time. If you don't want to change the description when updating, you can get
|
|
-- the current description with gist.info() and pass in the description field.
|
|
-- @tparam[opt] string id The ID of the Gist to update. If nil, a new Gist will
|
|
-- be created.
|
|
-- @tparam[opt] boolean interactive Set this to true to allow asking for a PAK
|
|
-- if one is not available in the settings. If this is not specified, this
|
|
-- function will return nil if gist.id is not available in the settings.
|
|
-- @treturn string|nil The ID of the Gist, or nil on error.
|
|
-- @treturn string|nil The URL of the Gist, or a string on error.
|
|
function gist.put(files, description, id, interactive)
|
|
expect(1, files, "table")
|
|
expect(3, id, "string", "nil")
|
|
expect(2, description, "string", id == nil and "nil" or nil)
|
|
expect(4, interactive, "boolean", "nil")
|
|
if id then
|
|
if id:find("https?://") then id = id:gsub("https?://[^/]+/", ""):gsub("^[^/]*[^/%x]+[^/]*/", "") end
|
|
if id:find("/") ~= nil then id = id:match("^([0-9A-Fa-f:]+)/.*$") end
|
|
if id == nil or not id:match("^[0-9A-Fa-f][0-9A-Fa-f:]+[0-9A-Fa-f]$") then error("bad argument #3 to 'put' (invalid ID)", 2) end
|
|
if id:find(":") ~= nil then id = id:gsub(":", "/") end
|
|
end
|
|
local data = { files = {}, public = true, description = description }
|
|
for k, v in pairs(files) do if v == textutils.json_null then data.files[k] = v else data.files[k] = { content = v } end end
|
|
local headers = { ["Content-Type"] = "application/json" }
|
|
if not requestAuth(headers, interactive) then return nil, "Authentication required" end
|
|
if interactive then write("Connecting to api.github.com... ") end
|
|
local handle
|
|
if id then handle = http.post{ url = "https://api.github.com/gists/" .. id, body = textutils.serializeJSON(data):gsub("\n", "n"), headers = headers, method = "PATCH" }
|
|
else handle = http.post("https://api.github.com/gists", textutils.serializeJSON(data):gsub("\n", "n"), headers) end
|
|
if handle == nil then if interactive then print("Failed.") end return nil, "Could not connect" end
|
|
local resp = textutils.unserializeJSON(handle.readAll())
|
|
if handle.getResponseCode() ~= 201 and handle.getResponseCode() ~= 200 or resp == nil then
|
|
if interactive then print("Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")) end
|
|
handle.close()
|
|
return nil, "Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")
|
|
end
|
|
handle.close()
|
|
if interactive then print("Success.") end
|
|
return resp.id, resp.html_url
|
|
end
|
|
|
|
--- Deletes a Gist.
|
|
-- @tparam string id The Gist ID to delete.
|
|
-- @tparam[opt] boolean interactive Set this to true to allow asking for a PAK
|
|
-- if one is not available in the settings. If this is not specified, this
|
|
-- function will return false if gist.id is not available in the settings.
|
|
-- @treturn boolean Whether the request succeeded.
|
|
-- @treturn string|nil If an error occurred, a message describing the error.
|
|
function gist.delete(id, interactive)
|
|
expect(1, id, "string")
|
|
expect(2, interactive, "boolean", "nil")
|
|
if id:find("https?://") then id = id:gsub("https?://[^/]+/", ""):gsub("^[^/]*[^/%x]+[^/]*/", "") end
|
|
if id:find("/") ~= nil or id:find(":") ~= nil then id = id:match("^([0-9A-Fa-f]+)") end
|
|
if id == nil or not id:match("^[0-9A-Fa-f][0-9A-Fa-f:]+[0-9A-Fa-f]$") then error("bad argument #1 to 'delete' (invalid ID)", 2) end
|
|
local headers = {}
|
|
if not requestAuth(headers, interactive) then return false, "Authentication required" end
|
|
if interactive then write("Connecting to api.github.com... ") end
|
|
local handle = http.post{ url = "https://api.github.com/gists/" .. id, headers = headers, method = "DELETE" }
|
|
if handle == nil then if interactive then print("Failed.") end return false, "Could not connect" end
|
|
if handle.getResponseCode() ~= 204 then
|
|
local resp = textutils.unserializeJSON(handle.readAll())
|
|
if interactive then print("Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")) end
|
|
handle.close()
|
|
return false, "Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")
|
|
end
|
|
handle.close()
|
|
if interactive then print("Success.") end
|
|
return true
|
|
end
|
|
|
|
return gist
|