Advertisement
JackMacWindows

gist.lua (full)

Jul 19th, 2021
2,193
1
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 19.83 KB | None | 1 0
  1. -- gist.lua - Gist client for ComputerCraft
  2. -- Made by JackMacWindows for CraftOS-PC
  3.  
  4. if not http then
  5.     printError("Gist requires http API")
  6.     if _G.config ~= nil then printError("Set http_enable to true in the CraftOS-PC configuration")
  7.     else printError("Set http_enable to true in ComputerCraft's configuration") end
  8.     return 2
  9. end
  10.  
  11. local gist
  12. do
  13. --- gist.lua - Gist client for ComputerCraft
  14. -- Made by JackMacWindows for CraftOS-PC
  15. --
  16. -- MIT License
  17. --
  18. -- Copyright (c) 2019-2020 JackMacWindows
  19. --
  20. -- Permission is hereby granted, free of charge, to any person obtaining a copy
  21. -- of this software and associated documentation files (the "Software"), to deal
  22. -- in the Software without restriction, including without limitation the rights
  23. -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  24. -- copies of the Software, and to permit persons to whom the Software is
  25. -- furnished to do so, subject to the following conditions:
  26. --
  27. -- The above copyright notice and this permission notice shall be included in all
  28. -- copies or substantial portions of the Software.
  29. --
  30. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  31. -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  32. -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  33. -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  34. -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  35. -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  36. -- SOFTWARE.
  37.  
  38. local expect = require and require("cc.expect").expect or dofile("/rom/modules/main/cc/expect.lua").expect
  39.  
  40. if not http then
  41.     if _G.config ~= nil then error("Gist requires http API\nSet http_enable to true in the CraftOS-PC configuration")
  42.     else error("Gist requires http API\nSet http_enable to true in ComputerCraft's configuration") end
  43. end
  44.  
  45. gist = {}
  46.  
  47. local function emptyfn() end -- to reduce memory/speed footprint when using empty functions
  48.  
  49. -- Internal functions
  50.  
  51. local function getGistFile(data)
  52.     if not data.truncated then return data.content else
  53.         local handle = http.get(data.raw_url)
  54.         if not handle then error("Could not connect to api.github.com.") end
  55.         if handle.getResponseCode() ~= 200 then
  56.             handle.close()
  57.             error("Failed to download file data.")
  58.         end
  59.         local d = handle.readAll()
  60.         handle.close()
  61.         return d
  62.     end
  63. end
  64.  
  65. 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
  66.  
  67. local function requestAuth(headers, interactive)
  68.     if settings.get("gist.id") ~= nil then
  69.         headers.Authorization = "token " .. settings.get("gist.id")
  70.         return true
  71.     elseif interactive then
  72.         setTextColor(colors.yellow)
  73.         write("You need to add a Personal Access Token (PAK) to upload Gists. Follow the instructions at ")
  74.         setTextColor(colors.blue)
  75.         write("https://tinyurl.com/GitHubPAK")
  76.         setTextColor(colors.yellow)
  77.         write(" to generate one. Make sure to check the '")
  78.         setTextColor(colors.blue)
  79.         write("gist")
  80.         setTextColor(colors.yellow)
  81.         print("' checkbox on step 7 (under 'Select scopes'). Once done, paste it here.")
  82.         setTextColor(colors.lime)
  83.         write("PAK: ")
  84.         setTextColor(colors.white)
  85.         local pak = read()
  86.         if pak == nil or pak == "" then error("Invalid PAK, please try again.") end
  87.         settings.set("gist.id", pak)
  88.         settings.save(".settings")
  89.         headers.Authorization = "token " .. pak
  90.         return true
  91.     end
  92.     return false
  93. end
  94.  
  95. -- User API - this can be loaded with require "cc.http.gist"
  96.  
  97. -- ID can be either just the gist ID or a gist ID followed by a slash and a file name
  98. -- * If a file name is specified, retrieves that file
  99. -- * Otherwise, if there's only one file, retrieves that file
  100. -- * Otherwise, if there's a file named 'init.lua', retrieves 'init.lua'
  101. -- * Otherwise, if there's more than one file but only one *.lua file, retrieves the Lua file
  102. -- * Otherwise, retrieves the first Lua file alphabetically (with a warning)
  103. -- * Otherwise, fails
  104.  
  105. --- Retrieves one file from a Gist using the specified ID.
  106. -- @tparam string id The Gist ID to download from. See above comments for more details.
  107. -- @tparam[opt] function progress A function to use to report status messages.
  108. -- @treturn string|nil The contents of the specified Gist file, or nil on error.
  109. -- @treturn string|nil The name of the file that was chosen to be downloaded, or a message on error.
  110. function gist.get(id, progress)
  111.     expect(1, id, "string")
  112.     expect(2, progress, "function", "nil")
  113.     progress = progress or emptyfn
  114.     local file
  115.     if id:find("/") ~= nil then id, file = id:match("^([0-9A-Fa-f:]+)/(.+)$") end
  116.     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
  117.     if id:find(":") ~= nil then id = id:gsub(":", "/") end
  118.     progress("Connecting to api.github.com... ")
  119.     local handle = http.get("https://api.github.com/gists/" .. id)
  120.     if handle == nil then
  121.         progress("Failed.\n")
  122.         return nil, "Failed to connect"
  123.     end
  124.     local meta = textutils.unserializeJSON(handle.readAll())
  125.     local code = handle.getResponseCode()
  126.     handle.close()
  127.     if code ~= 200 then
  128.         progress("Failed.\n")
  129.         return nil, "Invalid response code (" .. code .. ")" .. (meta and ": " .. meta.message or "")
  130.     end
  131.     if meta == nil or meta.files == nil then
  132.         progress("Failed.\n")
  133.         return nil, meta and "GitHub API error: " .. meta.message or "Error parsing JSON"
  134.     end
  135.     progress("Success.\n")
  136.     if file then return getGistFile(meta.files[file]), file
  137.     elseif next(meta.files, next(meta.files)) == nil then return getGistFile(meta.files[next(meta.files)]), next(meta.files)
  138.     elseif meta.files["init.lua"] ~= nil then return getGistFile(meta.files["init.lua"]), "init.lua"
  139.     else
  140.         local luaFiles = {}
  141.         for k in pairs(meta.files) do if k:match("%.lua$") then table.insert(luaFiles, k) end end
  142.         table.sort(luaFiles)
  143.         if #luaFiles == 0 then
  144.             progress("Error: Could not find any Lua files to download!\n")
  145.             return nil, "Could not find any Lua files to download"
  146.         end
  147.         if #luaFiles > 1 then progress("Warning: More than one Lua file detected, downloading the first one alphabetically.\n") end
  148.         return getGistFile(meta.files[luaFiles[1]]), luaFiles[1]
  149.     end
  150. end
  151.  
  152. --- Runs a specified Gist. This is a wrapper for convenience.
  153. -- @tparam string id The Gist ID to download from. See above comments for more details.
  154. -- @tparam[opt] function progress A function to use to report status messages. If
  155. -- this is not a function, it will be used as an argument to the script.
  156. -- @tparam[opt] any ... Any arguments to pass to the script.
  157. -- @treturn any Any results returned from the script.
  158. function gist.run(id, progress, ...)
  159.     expect(1, id, "string")
  160.     local args = table.pack(...)
  161.     if type(progress) ~= "function" and progress ~= nil then
  162.         table.insert(args, 1, progress)
  163.         progress = nil
  164.     end
  165.     local data, name = gist.get(id, progress)
  166.     if data == nil then return end
  167.     local fn, err = load(data, name, "t", _ENV)
  168.     if fn == nil then error(err) end
  169.     local retval = table.pack(pcall(fn, table.unpack(args)))
  170.     if not retval[1] then error(retval[2]) end
  171.     return table.unpack(retval, 2)
  172. end
  173.  
  174. --- Retrieves a table of all files from a Gist.
  175. -- @tparam string id The Gist ID to download.
  176. -- @tparam[opt] function progress A function to use to report status messages.
  177. -- @treturn table|nil A key-value list of all files in the Gist, or nil on error.
  178. -- @treturn string|nil If an error occurred, a string describing the error.
  179. function gist.getAll(id, progress)
  180.     expect(1, id, "string")
  181.     expect(2, progress, "function", "nil")
  182.     progress = progress or emptyfn
  183.     if id:find("/") ~= nil then id = id:match("^([0-9A-Fa-f:]+)/.*$") end
  184.     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
  185.     if id:find(":") ~= nil then id = id:gsub(":", "/") end
  186.     progress("Connecting to api.github.com... ")
  187.     local handle = http.get("https://api.github.com/gists/" .. id)
  188.     if handle == nil then progress("Failed.\n") return nil, "Failed to connect" end
  189.     local meta = textutils.unserializeJSON(handle.readAll())
  190.     local code = handle.getResponseCode()
  191.     handle.close()
  192.     if code ~= 200 then
  193.         progress("Failed.\n")
  194.         return nil, "Invalid response code (" .. code .. ")" .. (meta and ": " .. meta.message or "")
  195.     end
  196.     if meta == nil or meta.files == nil then
  197.         progress("Failed.\n")
  198.         return nil, meta and meta.message and "GitHub API error: " .. meta.message or "Error parsing JSON"
  199.     end
  200.     progress("Success.\n")
  201.     local retval = {}
  202.     for k, v in pairs(meta.files) do retval[k] = getGistFile(v) end
  203.     return retval
  204. end
  205.  
  206. --- Returns some information about a Gist.
  207. -- @tparam string id The Gist ID to get info about.
  208. -- @tparam[opt] function progress A function to use to report status messages.
  209. -- @treturn table|nil A table of information about the Gist. The table may
  210. -- contain the following entries:
  211. --  - description: The description for the Gist.
  212. --  - author: The username of the author of the Gist.
  213. --  - revisionCount: The number of revisions that have been made to the Gist.
  214. --  - files: A list of all file names in the Gist, sorted alphabetically.
  215. -- @treturn string|nil If an error occurred, a string describing the error.
  216. function gist.info(id, progress)
  217.     expect(1, id, "string")
  218.     expect(2, progress, "function", "nil")
  219.     progress = progress or emptyfn
  220.     if id:find("/") ~= nil then id = id:match("^([0-9A-Fa-f:]+)/.*$") end
  221.     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
  222.     if id:find(":") ~= nil then id = id:gsub(":", "/") end
  223.     progress("Connecting to api.github.com... ")
  224.     local handle = http.get("https://api.github.com/gists/" .. id)
  225.     if handle == nil then progress("Failed.\n") return nil, "Failed to connect" end
  226.     local meta = textutils.unserializeJSON(handle.readAll())
  227.     local code = handle.getResponseCode()
  228.     handle.close()
  229.     if code ~= 200 then
  230.         progress("Failed.\n")
  231.         return nil, "Invalid response code (" .. code .. ")" .. (meta and ": " .. meta.message or "")
  232.     end
  233.     if meta == nil or meta.files == nil then
  234.         progress("Failed.\n")
  235.         return nil, meta and meta.message and "GitHub API error: " .. meta.message or "Error parsing JSON"
  236.     end
  237.     local f = {}
  238.     for k in pairs(meta.files) do table.insert(f, k) end
  239.     table.sort(f)
  240.     progress("Success.\n")
  241.     return { description = meta.description, author = meta.owner.login, revisionCount = #meta.history, files = f }
  242. end
  243.  
  244. --- Uploads a list of files to Gist, updating a previous Gist if desired.
  245. -- @tparam table files The files to upload to Gist. This table should be
  246. -- structured with a key as file name and a string with the file contents. If
  247. -- updating a Gist, files can be deleted by setting the data to textutils.json_null.
  248. -- @tparam[opt] string description The description of the Gist. This is required
  249. -- when updating a Gist, but is optional when uploading a Gist for the first
  250. -- time. If you don't want to change the description when updating, you can get
  251. -- the current description with gist.info() and pass in the description field.
  252. -- @tparam[opt] string id The ID of the Gist to update. If nil, a new Gist will
  253. -- be created.
  254. -- @tparam[opt] boolean interactive Set this to true to allow asking for a PAK
  255. -- if one is not available in the settings. If this is not specified, this
  256. -- function will return nil if gist.id is not available in the settings.
  257. -- @treturn string|nil The ID of the Gist, or nil on error.
  258. -- @treturn string|nil The URL of the Gist, or a string on error.
  259. function gist.put(files, description, id, interactive)
  260.     expect(1, files, "table")
  261.     expect(3, id, "string", "nil")
  262.     expect(2, description, "string", id == nil and "nil" or nil)
  263.     expect(4, interactive, "boolean", "nil")
  264.     if id then
  265.         if id:find("/") ~= nil then id = id:match("^([0-9A-Fa-f:]+)/.*$") end
  266.         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
  267.         if id:find(":") ~= nil then id = id:gsub(":", "/") end
  268.     end
  269.     local data = { files = {}, public = true, description = description }
  270.     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
  271.     local headers = { ["Content-Type"] = "application/json" }
  272.     if not requestAuth(headers, interactive) then return nil, "Authentication required" end
  273.     if interactive then write("Connecting to api.github.com... ") end
  274.     local handle
  275.     if id then handle = http.post{ url = "https://api.github.com/gists/" .. id, body = textutils.serializeJSON(data):gsub("\n", "n"), headers = headers, method = "PATCH" }
  276.     else handle = http.post("https://api.github.com/gists", textutils.serializeJSON(data):gsub("\n", "n"), headers) end
  277.     if handle == nil then if interactive then print("Failed.") end return nil, "Could not connect" end
  278.     local resp = textutils.unserializeJSON(handle.readAll())
  279.     if handle.getResponseCode() ~= 201 and handle.getResponseCode() ~= 200 or resp == nil then
  280.         if interactive then print("Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")) end
  281.         handle.close()
  282.         return nil, "Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")
  283.     end
  284.     handle.close()
  285.     if interactive then print("Success.") end
  286.     return resp.id, resp.html_url
  287. end
  288.  
  289. --- Deletes a Gist.
  290. -- @tparam string id The Gist ID to delete.
  291. -- @tparam[opt] boolean interactive Set this to true to allow asking for a PAK
  292. -- if one is not available in the settings. If this is not specified, this
  293. -- function will return false if gist.id is not available in the settings.
  294. -- @treturn boolean Whether the request succeeded.
  295. -- @treturn string|nil If an error occurred, a message describing the error.
  296. function gist.delete(id, interactive)
  297.     expect(1, id, "string")
  298.     expect(2, interactive, "boolean", "nil")
  299.     if id:find("/") ~= nil or id:find(":") ~= nil then id = id:match("^([0-9A-Fa-f]+)") end
  300.     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
  301.     local headers = {}
  302.     if not requestAuth(headers, interactive) then return false, "Authentication required" end
  303.     if interactive then write("Connecting to api.github.com... ") end
  304.     local handle = http.post{ url = "https://api.github.com/gists/" .. id, headers = headers, method = "DELETE" }
  305.     if handle == nil then if interactive then print("Failed.") end return false, "Could not connect" end
  306.     if handle.getResponseCode() ~= 204 then
  307.         local resp = textutils.unserializeJSON(handle.readAll())
  308.         if interactive then print("Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")) end
  309.         handle.close()
  310.         return false, "Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")
  311.     end
  312.     handle.close()
  313.     if interactive then print("Success.") end
  314.     return true
  315. end
  316.  
  317. end
  318.  
  319. local args = { ... }
  320.  
  321. local function readFile(filename, files, isEditing)
  322.     if fs.isDir(shell.resolve(filename)) then
  323.         for _, v in ipairs(fs.list(shell.resolve(filename))) do if readFile(fs.combine(filename, v), files, isEditing) then return true end end
  324.     else
  325.         if files[fs.getName(filename)] then print("Cannot upload files with duplicate names.") return true end
  326.         local file = fs.open(shell.resolve(filename), "rb")
  327.         if file == nil then
  328.             if not isEditing then print("Could not read " .. filename .. ".") return true
  329.             else files[fs.getName(filename)] = textutils.json_null end
  330.         else
  331.             files[fs.getName(filename)] = file.readAll()
  332.             file.close()
  333.         end
  334.     end
  335. end
  336.  
  337. local function getFiles(isEditing)
  338.     local files = {}
  339.     local i = isEditing and 3 or 2
  340.     while args[i] ~= nil and args[i] ~= "--" do
  341.         if readFile(args[i], files, isEditing) then return nil end
  342.         i = i + 1
  343.     end
  344.     if args[i] == "--" then return files, table.concat({ table.unpack(args, i + 1) }, " ") end
  345.     return files
  346. end
  347.  
  348. 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
  349.  
  350. local helpstr = "Usages:\ngist put <files...> [-- description...]\ngist edit <id> <files...> [-- description]\ngist delete <id>\ngist get <id> <filename>\ngist run <id> [arguments...]\ngist info <id>"
  351.  
  352. if #args < 2 then
  353.     print(helpstr)
  354.     return 1
  355. end
  356.  
  357. if args[1] == "get" then
  358.     if #args < 3 then print(helpstr) return 1 end
  359.     if args[3]:sub(#args[3]) == "/" or fs.isDir(shell.resolve(args[3])) then
  360.         fs.makeDir(shell.resolve(args[3]))
  361.         local files, err = gist.getAll(args[2], write)
  362.         if files == nil then printError(err) return 3 end
  363.         for k, v in pairs(files) do
  364.             local file = fs.open(shell.resolve(fs.combine(args[3], k)), "wb")
  365.             file.write(v)
  366.             file.close()
  367.         end
  368.         print("Downloaded all files to " .. shell.resolve(args[3]))
  369.     else
  370.         local data, err = gist.get(args[2], write)
  371.         if data == nil then printError(err) return 3 end
  372.         local file = fs.open(shell.resolve(args[3]), "wb")
  373.         file.write(data)
  374.         file.close()
  375.         print("Downloaded as " .. shell.resolve(args[3]))
  376.     end
  377. elseif args[1] == "run" then
  378.     return gist.run(args[2], write, table.unpack(args, 3))
  379. elseif args[1] == "put" then
  380.     local files, description = getFiles(false)
  381.     if files == nil then return end
  382.     local id, html_url = gist.put(files, description, nil, true)
  383.     if id ~= nil then print("Uploaded as " .. html_url .. "\nRun 'gist get " .. id .. "' to download anywhere")
  384.     else printError(html_url) return 3 end
  385. elseif args[1] == "info" then
  386.     local tab, err = gist.info(args[2], write)
  387.     if tab == nil then printError(err) return 3 end
  388.     setTextColor(colors.yellow)
  389.     write("Description: ")
  390.     setTextColor(colors.white)
  391.     print(tab.description)
  392.     setTextColor(colors.yellow)
  393.     write("Author: ")
  394.     setTextColor(colors.white)
  395.     print(tab.author)
  396.     setTextColor(colors.yellow)
  397.     write("Revisions: ")
  398.     setTextColor(colors.white)
  399.     print(tab.revisionCount)
  400.     setTextColor(colors.yellow)
  401.     print("Files in this Gist:")
  402.     setTextColor(colors.white)
  403.     textutils.tabulate(tab.files)
  404. elseif args[1] == "edit" then
  405.     if #args < 3 then print(helpstr) return 1 end
  406.     local files, description = getFiles(true)
  407.     if files == nil then return 2 end
  408.     if not description then description = gist.info(args[2], write).description end
  409.     local id, html_url = gist.put(files, description, args[2], true)
  410.     if id then print("Uploaded as " .. html_url .. "\nRun 'gist get " .. args[2] .. "' to download anywhere")
  411.     else printError(html_url) return 3 end
  412. elseif args[1] == "delete" then
  413.     local ok, err = gist.delete(args[2], true)
  414.     if ok then print("The requested Gist has been deleted.") else printError(err) return 3 end
  415. else print(helpstr) return 1 end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement