JackMacWindows

gist.lua

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