From a0e2f02c3234976dd431646fe27efab86ff13807 Mon Sep 17 00:00:00 2001 From: Mustafa Ali Mutlu Date: Mon, 30 Sep 2019 00:09:58 +0300 Subject: [PATCH] [Plugin] Joplin support (#5431) Adds joplin support, fixes https://github.com/koreader/koreader/issues/5086 Changes -adds a submenu to evernote menu - -Joplin |-Set IP and port |-Set authorization token |-Export to Joplin |-Help -adds EvernoteExporter:exportBooknotesToJoplin() -adds JoplinClient.lua -modifies html_export, txt_export and joplin_export flags to work with each other. (eg if user selects one others deactivated) --- plugins/evernote.koplugin/JoplinClient.lua | 136 ++++++++++++++ plugins/evernote.koplugin/main.lua | 202 ++++++++++++++++++++- 2 files changed, 332 insertions(+), 6 deletions(-) create mode 100644 plugins/evernote.koplugin/JoplinClient.lua diff --git a/plugins/evernote.koplugin/JoplinClient.lua b/plugins/evernote.koplugin/JoplinClient.lua new file mode 100644 index 000000000..3df4bf93b --- /dev/null +++ b/plugins/evernote.koplugin/JoplinClient.lua @@ -0,0 +1,136 @@ +local json = require("json") +local http = require("socket.http") +local ltn12 = require("ltn12") + +local JoplinClient = { + server_ip = "localhost", + server_port = 41184, + auth_token = "" +} + +function JoplinClient:new(o) + o = o or {} + self.__index = self + setmetatable(o, self) + return o +end + +function JoplinClient:_makeRequest(url, method, request_body) + local sink = {} + local request_body_json = json.encode(request_body) + local source = ltn12.source.string(request_body_json) + http.request{ + url = url, + method = method, + sink = ltn12.sink.table(sink), + source = source, + headers = { + ["Content-Length"] = #request_body_json, + ["Content-Type"] = "application/json" + } + } + + if not sink[1] then + error("No response from Joplin Server") + end + + local response = json.decode(sink[1]) + + if response.error then + error(response.error) + end + + return response +end + +function JoplinClient:ping() + local sink = {} + + http.request{ + url = "http://"..self.server_ip..":"..self.server_port.."/ping", + method = "GET", + sink = ltn12.sink.table(sink) + } + + if sink[1] == "JoplinClipperServer" then + return true + else + return false + end +end + +-- If successful returns id of found note. +function JoplinClient:findNoteByTitle(title, notebook_id) + local url = "http://"..self.server_ip..":"..self.server_port.."/notes?".."token="..self.auth_token.."&fields=id,title,parent_id" + + local notes = self:_makeRequest(url, "GET") + + for _, note in pairs(notes) do + if note.title == title then + if notebook_id == nil or note.parent_id == notebook_id then + return note.id + end + end + end + + return false + +end + +-- If successful returns id of found notebook (folder). +function JoplinClient:findNotebookByTitle(title) + local url = "http://"..self.server_ip..":"..self.server_port.."/folders?".."token="..self.auth_token.."&".."query="..title + + local folders = self:_makeRequest(url, "GET") + + for _, folder in pairs(folders) do + if folder.title== title then + return folder.id + end + end + + return false +end + +-- If successful returns id of created notebook (folder). +function JoplinClient:createNotebook(title, created_time) + local request_body = { + title = title, + created_time = created_time + } + + local url = "http://"..self.server_ip..":"..self.server_port.."/folders?".."token="..self.auth_token + local response = self:_makeRequest(url, "POST", request_body) + + return response.id +end + + +-- If successful returns id of created note. +function JoplinClient:createNote(title, note, parent_id, created_time) + local request_body = { + title = title, + body = note, + parent_id = parent_id, + created_time = created_time + } + local url = "http://"..self.server_ip..":"..self.server_port.."/notes?".."token="..self.auth_token + local response = self:_makeRequest(url, "POST", request_body) + + return response.id +end + +-- If successful returns id of updated note. +function JoplinClient:updateNote(note_id, note, title, parent_id) + local request_body = { + body = note, + title = title, + parent_id = parent_id + } + + local url = "http://"..self.server_ip..":"..self.server_port.."/notes/"..note_id.."?token="..self.auth_token + local response = self:_makeRequest(url, "PUT", request_body) + return response.id +end + +return JoplinClient diff --git a/plugins/evernote.koplugin/main.lua b/plugins/evernote.koplugin/main.lua index c1a66208d..d05f591c7 100644 --- a/plugins/evernote.koplugin/main.lua +++ b/plugins/evernote.koplugin/main.lua @@ -10,6 +10,7 @@ local Screen = require("device").screen local util = require("ffi/util") local Device = require("device") local DEBUG = require("dbg") +local JoplinClient = require("JoplinClient") local T = require("ffi/util").template local _ = require("gettext") local N_ = _.ngettext @@ -36,11 +37,21 @@ function EvernoteExporter:init() self.evernote_username = settings.username or "" self.evernote_token = settings.token self.notebook_guid = settings.notebook + self.joplin_IP = settings.joplin_IP or "localhost" + self.joplin_port = settings.joplin_port or 41185 + self.joplin_token = settings.joplin_token -- or your token + self.joplin_notebook_guid = settings.joplin_notebook_guid or nil self.html_export = settings.html_export or false + self.joplin_export = settings.joplin_export or false + self.txt_export = settings.txt_export or false + --- @todo Is this if block necessarry? Nowhere in the code they are assigned both true. + -- Do they check against external modifications to settings file? + if self.html_export then self.txt_export = false - else - self.txt_export = settings.txt_export or false + self.joplin_export = false + elseif self.txt_export then + self.joplin_export = false end self.parser = MyClipping:new{ @@ -59,7 +70,10 @@ function EvernoteExporter:isDocless() end function EvernoteExporter:readyToExport() - return self.evernote_token ~= nil or self.html_export ~= false or self.txt_export ~= false + return self.evernote_token ~= nil or + self.html_export ~= false or + self.txt_export ~= false or + self.joplin_export ~= false end function EvernoteExporter:migrateClippings() @@ -111,6 +125,129 @@ function EvernoteExporter:addToMainMenu(menu_items) } or nil end, }, + { + text = _("Joplin") , + checked_func = function() return self.joplin_export end, + sub_item_table ={ + { + text = _("Set Joplin IP and Port"), + keep_menu_open = true, + callback = function() + local MultiInputDialog = require("ui/widget/multiinputdialog") + local url_dialog + url_dialog = MultiInputDialog:new{ + title = _("Set Joplin IP and port number"), + fields = { + { + text = self.joplin_IP, + input_type = "string" + }, + { + text = self.joplin_port, + input_type = "number" + } + }, + buttons = { + { + { + text = _("Cancel"), + callback = function() + UIManager:close(url_dialog) + end + }, + { + text = _("OK"), + callback = function() + local fields = url_dialog:getFields() + local ip = fields[1] + local port = tonumber(fields[2]) + if ip ~= "" then + if port and port < 65355 then + self.joplin_IP = ip + self.joplin_port = port + end + self:saveSettings() + end + UIManager:close(url_dialog) + end + } + } + } + } + UIManager:show(url_dialog) + url_dialog:onShowKeyboard() + end + }, + { + text = _("Set authorization token"), + keep_menu_open = true, + callback = function() + local MultiInputDialog = require("ui/widget/multiinputdialog") + local auth_dialog + auth_dialog = MultiInputDialog:new{ + title = _("Set authorization token for Joplin"), + fields = { + { + text = self.joplin_token, + input_type = "string" + } + }, + buttons = { + { + { + text = _("Cancel"), + callback = function() + UIManager:close(auth_dialog) + end + }, + { + text = _("Set token"), + callback = function() + local auth_field = auth_dialog:getFields() + self.joplin_token = auth_field[1] + self:saveSettings() + UIManager:close(auth_dialog) + end + } + } + } + } + UIManager:show(auth_dialog) + auth_dialog:onShowKeyboard() + end + }, + { + text = _("Export to Joplin"), + checked_func = function() return self.joplin_export end, + callback = function() + self.joplin_export = not self.joplin_export + if self.joplin_export then + self.html_export = false + self.txt_export = false + end + self:saveSettings() + end + }, + { + text = _("Help"), + keep_menu_open = true, + callback = function() + UIManager:show(InfoMessage:new{ + text = T(_([[You can enter your auth token on your computer by saving an empty token. Then quit KOReader, edit the evernote.joplin_token field in %1/settings.reader.lua after creating a backup, and restart KOReader once you're done. + +To export to Joplin, you must forward the IP and port used by this plugin to the localhost:port on which Joplin is listening. This can be done with socat or a similar program. For example: + +For Windows: netsh interface portproxy add listeningaddress:0.0.0.0 listeningport:41185 connectaddress:localhost connectport:41184 + +For Linux: $socat tcp-listen:41185,reuseaddr,fork tcp:localhost:41184 + +For more information, please visit https://github.com/koreader/koreader/wiki/Evernote-export.]]) + ,DataStorage:getDataDir()) + }) + end + } + } + }, { text = _("Export all notes in this book"), enabled_func = function() @@ -148,7 +285,10 @@ function EvernoteExporter:addToMainMenu(menu_items) checked_func = function() return self.html_export end, callback = function() self.html_export = not self.html_export - if self.html_export then self.txt_export = false end + if self.html_export then + self.txt_export = false + self.joplin_export = false + end self:saveSettings() end }, @@ -157,7 +297,10 @@ function EvernoteExporter:addToMainMenu(menu_items) checked_func = function() return self.txt_export end, callback = function() self.txt_export = not self.txt_export - if self.txt_export then self.html_export = false end + if self.txt_export then + self.html_export = false + self.joplin_export = false + end self:saveSettings() end }, @@ -288,6 +431,11 @@ function EvernoteExporter:saveSettings() notebook = self.notebook_guid, html_export = self.html_export, txt_export = self.txt_export, + joplin_IP = self.joplin_IP, + joplin_port = self.joplin_port, + joplin_token = self.joplin_token, + joplin_notebook_guid = self.joplin_notebook_guid, + joplin_export = self.joplin_export } G_reader_settings:saveSetting("evernote", settings) end @@ -358,7 +506,8 @@ end function EvernoteExporter:exportClippings(clippings) local client = nil local exported_stamp - if not self.html_export and not self.txt_export then + local joplin_client + if not (self.html_export or self.txt_export or self.joplin_export) then client = require("EvernoteClient"):new{ domain = self.evernote_domain, authToken = self.evernote_token, @@ -368,6 +517,19 @@ function EvernoteExporter:exportClippings(clippings) exported_stamp= "html" elseif self.txt_export then exported_stamp = "txt" + elseif self.joplin_export then + exported_stamp = "joplin" + joplin_client = JoplinClient:new{ + server_ip = self.joplin_IP, + server_port = self.joplin_port, + auth_token = self.joplin_token + } + ---@todo Check if user deleted our notebook, in that case note + -- will end up in random folder in Joplin. + if not self.joplin_notebook_guid then + self.joplin_notebook_guid = joplin_client:createNotebook(self.notebook_name) + self:saveSettings() + end else assert("an exported_stamp is expected for a new export type") end @@ -386,6 +548,8 @@ function EvernoteExporter:exportClippings(clippings) ok, err = pcall(self.exportBooknotesToHTML, self, title, booknotes) elseif self.txt_export then ok, err = pcall(self.exportBooknotesToTXT, self, title, booknotes) + elseif self.joplin_export then + ok, err = pcall(self.exportBooknotesToJoplin, self, joplin_client, title, booknotes) else ok, err = pcall(self.exportBooknotesToEvernote, self, client, title, booknotes) end @@ -507,4 +671,30 @@ function EvernoteExporter:exportBooknotesToTXT(title, booknotes) end end +function EvernoteExporter:exportBooknotesToJoplin(client, title, booknotes) + if not client:ping() then + error("Cannot reach Joplin server") + end + + local note_guid = client:findNoteByTitle(title, self.joplin_notebook_guid) + local note = "" + for _, chapter in ipairs(booknotes) do + if chapter.title then + note = note .. "\n\t*" .. chapter.title .. "*\n\n * * *" + end + + for _, clipping in ipairs(chapter) do + note = note .. os.date("%Y-%m-%d %H:%M:%S \n", clipping.time) + note = note .. clipping.text .. "\n * * *\n" + end + end + + if note_guid then + client:updateNote(note_guid, note) + else + client:createNote(title, note, self.joplin_notebook_guid) + end + +end + return EvernoteExporter