--[[-- KOReader plugin adding a "Upload to slenpaste" action to the File Manager long-press menu. @module koplugin.slenpaste --]]-- local http = require("socket.http") local ltn12 = require("ltn12") local Device = require("device") local InfoMessage = require("ui/widget/infomessage") local QRMessage = require("ui/widget/qrmessage") local RadioButtonWidget = require("ui/widget/radiobuttonwidget") local ButtonDialog = require("ui/widget/buttondialog") local Font = require("ui/font") local TextBoxWidget = require("ui/widget/textboxwidget") local UIManager = require("ui/uimanager") local NetworkMgr = require("ui/network/manager") local WidgetContainer = require("ui/widget/container/widgetcontainer") local util = require("util") local _ = require("gettext") local T = require("ffi/util").template local SlenPaste = WidgetContainer:extend{ name = "slenpaste", is_doc_only = false, upload_url = "https://paste.slendi.dev/", settings_key = "slenpaste", } SlenPaste.expiry_options = { { label = _("5 minutes"), value = "5m" }, { label = _("1 hour"), value = "1h" }, { label = _("1 day"), value = "1d" }, { label = _("7 days"), value = "7d" }, { label = _("30 days"), value = "30d" }, { label = _("Expires on first view"), value = "view" }, { label = _("Never"), value = "0" }, } function SlenPaste:init() self.settings = G_reader_settings:readSetting(self.settings_key, {}) if not self.ui.document then self:registerFileManagerActions() end end function SlenPaste:registerFileManagerActions() local file_manager = self.ui if not file_manager or not file_manager.addFileDialogButtons then return end local plugin = self file_manager:addFileDialogButtons("slenpaste_upload", function(file, is_file) if not is_file then return nil end return { { text = _("Upload to Slenpaste"), callback = function() if file_manager.file_dialog then UIManager:close(file_manager.file_dialog) end plugin:promptExpiryAndUpload(file) end, }, } end) end function SlenPaste:getDefaultExpiry() local remembered = self.settings and self.settings.last_expiry if remembered then return remembered end return "1d" end function SlenPaste:promptExpiryAndUpload(file_path) local radio_buttons = {} local default_expiry = self:getDefaultExpiry() for index, option in ipairs(self.expiry_options) do radio_buttons[index] = { { text = option.label, provider = option.value, checked = option.value == default_expiry, }, } end UIManager:show(RadioButtonWidget:new{ title_text = _("Slenpaste expiry"), ok_text = _("Upload"), cancel_text = _("Cancel"), radio_buttons = radio_buttons, callback = function(widget) local expiry = widget.provider self.settings.last_expiry = expiry G_reader_settings:saveSetting(self.settings_key, self.settings) NetworkMgr:runWhenConnected(function() self:startUpload(file_path, expiry) end) end, }) end function SlenPaste:startUpload(file_path, expiry) local uploading_message = InfoMessage:new{ text = _("Uploading to slenpaste…"), } UIManager:show(uploading_message) local ok, result = self:performUpload(file_path, expiry) UIManager:close(uploading_message) if ok then self:showSuccessDialog(result) else self:showError(result) end end function SlenPaste:performUpload(file_path, expiry) local file, err = io.open(file_path, "rb") if not file then return false, err or _("Unable to open file") end local filename = file_path:match("([^/]+)$") or "upload" filename = filename:gsub('["\r\n]', "_") local boundary = "----KOReaderSlenpaste" .. tostring(os.time()) .. tostring(math.random(1000, 9999)) local CRLF = "\r\n" local prefix = table.concat({ "--", boundary, CRLF, "Content-Disposition: form-data; name=\"expiry\"", CRLF, CRLF, tostring(expiry), CRLF, "--", boundary, CRLF, string.format("Content-Disposition: form-data; name=\"file\"; filename=\"%s\"", filename), CRLF, "Content-Type: application/octet-stream", CRLF, CRLF, }) local suffix = CRLF .. "--" .. boundary .. "--" .. CRLF local file_size = file:seek("end") file:seek("set", 0) local response_body = {} local file_closed = false local function close_file() if not file_closed then file_closed = true file:close() end end local function file_source() if file_closed then return nil end local chunk = file:read(8192) if chunk then return chunk end close_file() return nil end local request_body = ltn12.source.cat( ltn12.source.string(prefix), file_source, ltn12.source.string(suffix) ) local ok, status_code, headers, status_line = http.request{ url = self.upload_url, method = "POST", headers = { ["Content-Type"] = "multipart/form-data; boundary=" .. boundary, ["Content-Length"] = tostring(#prefix + file_size + #suffix), }, source = request_body, sink = ltn12.sink.table(response_body), } close_file() if not ok then return false, status_code or _("HTTP request failed") end local code = tonumber(status_code) or 0 if code < 200 or code >= 300 then local body_text = util.trim(table.concat(response_body)) if body_text ~= "" then return false, string.format("%s (%s)", status_line or "HTTP error", body_text) end return false, status_line or string.format("HTTP status %d", code) end local url = util.trim(table.concat(response_body)) if url == "" then return false, _("Empty response from server") end return true, url end function SlenPaste:showSuccessDialog(url) local buttons local dialog buttons = { { { text = _("Copy link"), enabled = Device:hasClipboard(), callback = function() Device.input.setClipboardText(url) UIManager:show(InfoMessage:new{ text = _("Link copied to clipboard"), }) end, }, { text = _("Show QR code"), callback = function() UIManager:show(QRMessage:new{ text = url, width = Device.screen:getWidth(), height = Device.screen:getHeight(), }) end, }, }, { { text = _("Close"), callback = function() UIManager:close(dialog) end, }, }, } dialog = ButtonDialog:new{ title = _("Slenpaste upload complete"), buttons = buttons, } dialog:addWidget(TextBoxWidget:new{ text = url, width = dialog:getAddedWidgetAvailableWidth(), face = Font:getFace("infofont"), }) UIManager:show(dialog) end function SlenPaste:showError(message) UIManager:show(InfoMessage:new{ text = T(_("Upload failed:\n%s"), message), }) end return SlenPaste