diff --git a/plugins/exporter.koplugin/main.lua b/plugins/exporter.koplugin/main.lua index 15bd9e282..5dea2a939 100644 --- a/plugins/exporter.koplugin/main.lua +++ b/plugins/exporter.koplugin/main.lua @@ -42,7 +42,8 @@ local _ = require("gettext") -- migrate settings from old "evernote.koplugin" or from previous (monolithic) "exporter.koplugin" local function migrateSettings() - local formats = { "flomo", "html", "joplin", "json", "memos", "my_clippings", "readwise", "text", "xmnote" } + -- these are for legacy formats. Don't add new targets here. + local formats = { "html", "joplin", "json", "readwise", "text" } local settings = G_reader_settings:readSetting("exporter") if not settings then @@ -108,6 +109,7 @@ local Exporter = WidgetContainer:extend{ markdown = require("target/markdown"), memos = require("target/memos"), my_clippings = require("target/my_clippings"), + nextcloud = require("target/nextcloud"), readwise = require("target/readwise"), text = require("target/text"), xmnote = require("target/xmnote"), diff --git a/plugins/exporter.koplugin/target/nextcloud.lua b/plugins/exporter.koplugin/target/nextcloud.lua new file mode 100644 index 000000000..52d1a84ac --- /dev/null +++ b/plugins/exporter.koplugin/target/nextcloud.lua @@ -0,0 +1,225 @@ +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