Files
slenpaste.koplugin/main.lua
Slendi 1ff93876c7 require net
Signed-off-by: Slendi <slendi@socopon.com>
2025-09-20 20:26:19 +03:00

269 lines
7.6 KiB
Lua

--[[--
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