local InfoMessage = require("ui/widget/infomessage") local MultiInputDialog = require("ui/widget/multiinputdialog") local UIManager = require("ui/uimanager") local mime = require("mime") local md = require("template/md") local json = require("json") local http = require("socket.http") local ltn12 = require("ltn12") local socket = require("socket") local socketutil = require("socketutil") local logger = require("logger") local T = require("ffi/util").template local _ = require("gettext") -- nextcloud notes exporter local NextcloudExporter = require("base"):new { name = "nextcloud_notes", category = _("KOReader"), is_remote = true, } -- fetching all notes from Nextcloud is costly, so we keep a copy here -- while we determine wether to update existing or create a new note local notes_cache local function makeRequest(url, auth, method, request_body) local sink = {} local request_body_json = json.encode(request_body) local source = ltn12.source.string(request_body_json) local request = { url = url, method = method, sink = ltn12.sink.table(sink), source = source, headers = { ["Content-Length"] = #request_body_json, ["Content-Type"] = "application/json", ["Authorization"] = "Basic " .. auth, ["OCS-APIRequest"] = "true", }, } socketutil:set_timeout(socketutil.LARGE_BLOCK_TIMEOUT, socketutil.LARGE_TOTAL_TIMEOUT) local code, headers, status = socket.skip(1, http.request(request)) socketutil:reset_timeout() if code ~= 200 then logger.warn("Nextcloud: HTTP response code <> 200. Response status:", status or code or "network unreachable") logger.dbg("Response headers:", headers) return nil, status end if not sink[1] then return nil, "No response from Nextcloud" end local response = json.decode(sink[1]) return response end function NextcloudExporter:isReadyToExport() return self.settings.host and self.settings.username and self.settings.password end function NextcloudExporter:getMenuTable() local dialog_title = _("Setup Nextcloud Notes plugin") return { text = _("Nextcloud Notes"), checked_func = function() return self:isEnabled() end, sub_item_table = { { text = dialog_title, keep_menu_open = true, callback = function() local url_dialog url_dialog = MultiInputDialog:new { title = dialog_title, fields = { { description = _("Nextcloud URL"), hint = "https://yournextcloud.com", text = self.settings.host, input_type = "string" }, { description = _("Username"), hint = _("Username"), text = self.settings.username, input_type = "string" }, { description = _("App password"), hint = _("Security -> Devices & sessions"), text = self.settings.password, input_type = "string" } }, buttons = { { { text = _("Cancel"), callback = function() UIManager:close(url_dialog) end }, { text = _("OK"), callback = function() local fields = url_dialog:getFields() local host = fields[1] local username = fields[2] local password = fields[3] if host ~= "" then self.settings.host = host self:saveSettings() end if username ~= "" then self.settings.username = username self:saveSettings() end if password ~= "" then self.settings.password = password self:saveSettings() end UIManager:close(url_dialog) end } } } } UIManager:show(url_dialog) url_dialog:onShowKeyboard() end }, { text = _("Export to Nextcloud Notes"), checked_func = function() return self:isEnabled() end, callback = function() self:toggleEnabled() end, }, { text = _("Help"), keep_menu_open = true, callback = function() UIManager:show(InfoMessage:new { text = T(_([[For Nextcloud Notes setup instructions, see %1 Markdown formatting can be configured in: Export highlights > Choose formats and services > Markdown.]]), "https://github.com/koreader/koreader/wiki/Nextcloud-notes") }) end } } } end function NextcloudExporter:export(t) if not self:isReadyToExport() then return false end -- determine if markdown export is set local plugin_settings = G_reader_settings:readSetting("exporter") or {} local markdown_settings = plugin_settings.markdown -- setup Nextcloud variables local url_base = string.format("%s/index.php/apps/notes/api/v1/", self.settings.host) local auth = mime.b64(self.settings.username .. ":" .. self.settings.password) local note_id local verb local request_body local response local err -- fetch existing notes from Nextcloud local url = url_base .. "notes?category=" .. self.category notes_cache, err = makeRequest(url, auth, "GET") if not notes_cache then logger.warn("Error fetching existing notes from Nextcloud", err) return false end -- export each note for _, booknotes in pairs(t) do local note = md.prepareBookContent(booknotes, markdown_settings.formatting_options, markdown_settings.highlight_formatting) local note_title = string.format("%s - %s", booknotes.author, booknotes.title) -- search for existing note, and in that case use its ID for update note_id = nil if notes_cache then for i, note_cached in ipairs(notes_cache) do if note_cached.title == note_title then note_id = note_cached.id break end end end -- note body is the same for create and update request_body = { title = note_title, content = table.concat(note, "\n"), category = self.category, } -- set up create or update specific parameters if note_id then verb = "PUT" url = string.format("%snotes/%s", url_base, note_id) else verb = "POST" url = url_base.."notes" end -- save note in Nextcloud response, err = makeRequest(url, auth, verb, request_body) if not response then logger.warn("Error saving note in Nextcloud", err) return false end end return true end return NextcloudExporter