diff --git a/shard.lock b/shard.lock index 4802305..22a0428 100644 --- a/shard.lock +++ b/shard.lock @@ -20,6 +20,10 @@ shards: github: crystal-lang/crystal-db version: 0.9.0 + duktape: + github: jessedoyle/duktape.cr + version: 0.20.0 + exception_page: github: crystal-loot/exception_page version: 0.1.4 @@ -36,6 +40,10 @@ shards: github: jeromegn/kilt version: 0.4.0 + myhtml: + github: kostya/myhtml + version: 1.5.1 + radix: github: luislavena/radix version: 0.3.9 diff --git a/shard.yml b/shard.yml index 22ed26d..1fb1326 100644 --- a/shard.yml +++ b/shard.yml @@ -27,3 +27,8 @@ dependencies: github: crystal-ameba/ameba clim: github: at-grandpa/clim + duktape: + github: jessedoyle/duktape.cr + version: ~> 0.20.0 + myhtml: + github: kostya/myhtml diff --git a/src/config.cr b/src/config.cr index c96bf6f..27ec3f2 100644 --- a/src/config.cr +++ b/src/config.cr @@ -16,6 +16,7 @@ class Config property log_level : String = "info" property upload_path : String = File.expand_path "~/mango/uploads", home: true + property plugin_path : String = File.expand_path "~/mango/plugins" property mangadex = Hash(String, String | Int32).new @[YAML::Field(ignore: true)] diff --git a/src/plugin/plugin.cr b/src/plugin/plugin.cr new file mode 100644 index 0000000..b607dc6 --- /dev/null +++ b/src/plugin/plugin.cr @@ -0,0 +1,151 @@ +require "duktape/runtime" +require "myhtml" +require "http" + +class Plugin + class Error < ::Exception + end + + class MetadataError < Error + end + + class PluginException < Error + end + + class SyntaxError < Error + end + + {% for name in ["id", "title", "author", "version", "placeholder"] %} + getter {{name.id}} = "" + {% end %} + getter wait_seconds : UInt64 = 0 + + def self.list + dir = Config.current.plugin_path + Dir.children(dir) + .select do |f| + fp = File.join dir, f + File.file?(fp) && File.extname(fp) == ".js" + end + .map do |f| + File.basename f, ".js" + end + end + + def initialize(@path : String) + @rt = Duktape::Runtime.new do |sbx| + sbx.push_global_object + + sbx.del_prop_string -1, "print" + sbx.del_prop_string -1, "alert" + sbx.del_prop_string -1, "console" + + def_helper_functions sbx + end + + js = File.open @path, &.gets_to_end + eval js + + begin + data = eval_json "metadata" + {% for name in ["id", "title", "author", "version", "placeholder"] %} + @{{name.id}} = data[{{name}}].as_s + {% end %} + @wait_seconds = data["wait_seconds"].as_i.to_u64 + rescue e + raise MetadataError.new "Failed to retrieve metadata from plugin " \ + "at #{@path}. Error: #{e.message}" + end + end + + def search(query : String) + eval_json "search('#{query}')" + end + + def select_chapter(id : String) + eval_json "selectChapter('#{id}')" + end + + def next_page + eval_json "nextPage()" + end + + private def eval(str) + @rt.eval str + rescue e : Duktape::SyntaxError + raise SyntaxError.new e.message + rescue e : Duktape::Error + raise Error.new e.message + end + + private def eval_json(str) + JSON.parse eval(str).as String + end + + private def def_helper_functions(sbx) + sbx.push_object + + sbx.push_proc LibDUK::VARARGS do |ptr| + env = Duktape::Sandbox.new ptr + url = env.require_string 0 + + headers = HTTP::Headers.new + + if env.get_top == 2 + env.enum 1, LibDUK::Enum::OwnPropertiesOnly + while env.next -1, true + k = env.require_string -2 + v = env.require_string -1 + headers.add k, v + env.pop_2 + end + end + + res = HTTP::Client.get url, headers + body = res.body + + env.push_string body + env.call_success + end + sbx.put_prop_string -2, "get" + + sbx.push_proc 2 do |ptr| + env = Duktape::Sandbox.new ptr + html = env.require_string 0 + selector = env.require_string 1 + + myhtml = Myhtml::Parser.new html + json = myhtml.css(selector).map(&.to_html).to_a.to_json + + env.push_string json + env.call_success + end + sbx.put_prop_string -2, "css" + + sbx.push_proc 1 do |ptr| + env = Duktape::Sandbox.new ptr + html = env.require_string 0 + + myhtml = Myhtml::Parser.new html + root = myhtml.root + + str = "" + str = root.inner_text if root + + env.push_string str + env.call_success + end + sbx.put_prop_string -2, "innerText" + + sbx.push_proc 1 do |ptr| + env = Duktape::Sandbox.new ptr + msg = env.require_string 0 + env.call_success + + raise PluginException.new msg + end + sbx.put_prop_string -2, "raise" + + sbx.put_prop_string -2, "mango" + end +end