// Vimperator plugin: "Update Haiku" // Last Change: 12-Jan-2009. Jan 2008 // License: Creative Commons // Maintainer: mattn - http://mattn.kaoriya.net/ // // The script allows you to update Haiku status from Vimperator. // // Commands: // :haiku some thing text // post "some thing text" to keyword 'id:username' on Hatena Haiku. // :haiku #keyword some thing text // post "some thing text" to keyword 'id:keyword' on Hatena Haiku. // :haiku!/ // show public timeline. // :haiku! someone // show someone's statuses. // :haiku! album // show album timeline. // :haiku!+ someone // fav someone's last status.. mean put Hatena Star. // :haiku!- someone // un-fav someone's last status.. mean remove Hatena Star. // :haiku! #keyword // show the keyword timeline. var PLUGIN_INFO = {NAME} Hatena Haiku Client mattn Creative Commons 2.0a1 2.0 https://github.com/vimpr/vimperator-plugins/raw/master/haiku.js ; (function(){ liberator.plugins.haiku = { get cache() statuses }; var passwordManager = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); var CLIENT_NAME = encodeURIComponent(config.name + "::plugin::haiku.js"); var evalFunc = window.eval; var statuses = null; try { var sandbox = new Components.utils.Sandbox("about:blank"); if (Components.utils.evalInSandbox("true", sandbox) === true) { evalFunc = function(text) { return Components.utils.evalInSandbox(text, sandbox); } } } catch (e) { liberator.log('warning: haiku.js is working with unsafe sandbox.'); } function sprintf(format){ var i = 1, re = /%s/, result = "" + format; while (re.test(result) && i < arguments.length) result = result.replace(re, arguments[i++]); return result; } function sayHaiku(username, password, stat){ var keyword = ''; var user = '', id = ''; if (stat.match(/^#([^ ].+)\s+(.*)$/)) [keyword, stat] = [RegExp.$1, RegExp.$2]; else if (stat.match(/^@([^\s#]+)(?:#(\d+))?\s+(.*)$/)) [user, id, stat] = [RegExp.$1, RegExp.$2, RegExp.$3]; stat = stat.split("\\n").map(function(str) encodeURIComponent(str)).join("\n"); //liberator.log({keyword:keyword,user:user,id:id,stat:stat},0); if (user && !(id && isValidStatusID(id))){ id = getStatusIDFromUserID(user); if (!id) stat = "@" + user + "\n" + stat; } var xhr = new XMLHttpRequest(); xhr.open("POST", "http://h.hatena.ne.jp/api/statuses/update.json", false, username, password); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); var senddata = [ "status=", stat, keyword ? "&keyword=" + encodeURIComponent(keyword) : id ? "&in_reply_to_status_id=" + id : "", "&source=" + CLIENT_NAME ].join(''); //liberator.log('xhr.send(' + senddata +')',0); xhr.send(senddata); } function favHaiku(username, password, user){ var xhr = new XMLHttpRequest(); xhr.open("POST", "http://h.hatena.ne.jp/api/statuses/user_timeline/" + user + ".json", false, username, password); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.send(null); xhr.open("POST", "http://h.hatena.ne.jp/api/favorites/create/" + evalFunc(xhr.responseText)[0].id + '.json', false, username, password); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.send(null); } function unfavHaiku(username, password, user){ var xhr = new XMLHttpRequest(); xhr.open("POST", "http://h.hatena.ne.jp/api/statuses/user_timeline/" + user + ".json?count=1", false, username, password); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.send(null); xhr.open("POST", "http://h.hatena.ne.jp/api/favorites/destroy/" + evalFunc(xhr.responseText)[0].id + '.json', false, username, password); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.send(null); } function isValidStatusID(id){ if (!statuses) return false; return statuses.some(function(status) status.id == id); } function getStatusIDFromUserID(userid){ if (!statuses) return null; return statuses.filter(function(status) status.in_reply_to_user_id == userid)[0].id; } function getTimelineURLFromTarget(target){ if (target == "/"){ return "http://h.hatena.ne.jp/api/statuses/public_timeline.json"; } else if (target == "album"){ return "http://h.hatena.ne.jp/api/statuses/album.json"; } else if (/^#(.+)/.test(target)){ return "http://h.hatena.ne.jp/api/statuses/keyword_timeline/" + encodeURIComponent(RegExp.$1) + ".json"; } else if (/^@?(.+)/.test(target)){ return "http://h.hatena.ne.jp/api/statuses/user_timeline/" + RegExp.$1 + ".json"; } return "http://h.hatena.ne.jp/api/statuses/friends_timeline.json"; } function statusToXML(statuses){ var html = ; statuses.forEach(function(status) { var text = status.text; var keyword = status.keyword; var star = status.favorited > 0 ? <>{'x' + status.favorited} : <>; var replies = <>; if (text.indexOf(keyword+"=") == 0) text = status.text.substr(keyword.length + 1); text = convert(text); keyword = convert(keyword); if (status.replies.length > 0){ replies =
; status.replies.forEach(function(rep){ replies.* += <>
{rep.user.screen_name} {rep.user.name}
{rep.text.substr(keyword.length)}
; }); } html += <>
{status.user.screen_name} {status.user.name}‬ {star} : {keyword}
{text}
{replies}
; }); return html; } function showFollowersStatus(username, password, target){ var xhr = new XMLHttpRequest(); var endPoint = getTimelineURLFromTarget(target); xhr.open("POST", endPoint, false, username, password); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.send(null); statuses = evalFunc(xhr.responseText); var html = statusToXML(statuses); liberator.log(html.toXMLString(), 0); liberator.echo(html, true); } function convert(str){ function createHTML(all){ var str = all; if (all.indexOf("id:") == 0){ str = '' + all + ''; } else if (/\.(?:jpe?g|gif|png|bmp)$/.test(all)){ str = ''; } else if (/^http:\/\/(?:[^.]+\.)?youtube\.com\/(?:watch\?(?:[^&]+&)*v=|v\/)([^&=#?;\/]+)/.test(all)){ var url = "http://www.youtube.com/v/" + RegExp.$1 + "&fs=1"; str = '' + url + '' + '
'+ ''+ '
'; } else if (/^http:\/\/[^.]+\.nicovideo\.jp\/watch\/([-\w]+)$/.test(all)){ str = ''; } else if (all.charAt(0) == "["){ var keyword = all.substring(2, all.length -2); str = '' + keyword + ''; } return str; } var str = str.replace(/&/g,"&") .replace(//g,">") .replace(/\n/g,"
") .replace(/\[\[[^\]]+\]\]|https?:\/\/[-\w!#$%&'()*+,.\/:;=?@~]+|id:[a-zA-Z][-\w]{1,30}[a-zA-Z\d]/g, createHTML); return new XMLList(str); } commands.addUserCommand(["haiku"], "Change Haiku status", function(args){ var special = args.bang; var password; var username; try { var logins = passwordManager.findLogins({}, 'http://h.hatena.ne.jp', null, 'http://h.hatena.ne.jp (API)'); if (logins.length) [username, password] = [logins[0].username, logins[0].password]; else { var ps = Cc['@mozilla.org/embedcomp/prompt-service;1'].getService(Ci.nsIPromptService); var [user,pass] = [{ value : '' }, { value : '' }]; var ret = ps.promptUsernameAndPassword( window, 'http://h.hatea.ne.jp (API)', 'Enter username and password.\nyou can get "password" from\n\thttp://h.hatena.ne.jp/api#auth', user, pass, null, {}); if(ret){ username = user.value; password = pass.value.replace(/@.*$/, ''); var nsLoginInfo = new Components.Constructor( '@mozilla.org/login-manager/loginInfo;1', Ci.nsILoginInfo, 'init'); loginInfo = new nsLoginInfo('http://h.hatena.ne.jp', null, 'http://h.hatena.ne.jp (API)', username, password, '', ''); passwordManager.addLogin(loginInfo); } else throw 'Haiku: account not found'; } } catch (ex){ liberator.echoerr(ex); } var arg = args.string.replace(/%URL%/g, buffer.URL) .replace(/%TITLE%/g, buffer.title); if (special && arg.match(/^\+\s*(.*)/)) favHaiku(username, password, RegExp.$1) else if (special && arg.match(/^-\s*(.*)/)) unfavHaiku(username, password, RegExp.$1) else if (special || arg.length == 0) showFollowersStatus(username, password, arg) else sayHaiku(username, password, arg); }, { bang: true, hereDoc: true, completer: function(context, args){ if (!statuses) return; var matches= context.filter.match(/^([@#]|[-+]\s*)(\S*)$/); if (!matches) return; var list = []; var [prefix, target] = [matches[1],matches[2]]; switch (prefix.charAt(0)){ case "+": case "-": if (!args.bang) return; case "@": context.title = ["ID","Entry"]; if (args.bang) list = statuses.map(function(entry) ["@" + entry.user.id, entry.text]); else list = statuses.map(function(entry) ["@" + entry.user.id + "#" + entry.id, entry.text]); break; case "#": context.title = ["Keyword","Entry"]; list = statuses.map(function(entry) ["#" + entry.keyword, entry.text]); break; } if (target){ list = list.filter(function($_) $_[0].indexOf(target) >= 0); } context.completions = list; } } ); })(); // vim:sw=4 ts=4 et: 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
let INFO =
<plugin name="zip-de-download" version="0.7.0"
        href=""
        summary="ZIPでダウンロードするお"
        xmlns="http://vimperator.org/namespaces/liberator">
  <author email="teramako@gmail.com">teramako</author>
  <license href="http://opensource.org/licenses/mit-license.php">MIT</license>
  <project name="Vimperator" minVersion="2.3"/>
  <p xmlns={XHTML}>
    特定ページの画像とかのURLを取ってきて一気にZIPにしてダウンロードするお
    <code style='font-family: sans-serif !important;'><![CDATA[
             /)
           ///)
          ,.=''"/   
   /     i f ,.r='"-'つ____   こまけぇこたぁいいんだよ!!
  /      /   _,.-‐'~/⌒  ⌒\
       ,i   ,二ニ⊃( ●). (●)\
   /        il゙フ::::::⌒(__人__)⌒::::: 
      ,  ,!,!|     |r-|     |
     / iトヾヽ_/ィ"\      `ー'´     /
    ]]></code>
  </p>
  <item>
    <tags>:zipd :zipdownload</tags>
    <spec>:zipd<oa>ownload</oa> <oa>-l<oa>ist</oa></oa> <oa>-f<oa>ilter</oa>=filter</oa> <a>downloadPath</a></spec>
    <description>
      <p>
        <a>downloadPath</a>へZIPでアーカイブする。
        <a>downloadPath</a>がディレクトリの場合、"ページタイトル.zip"となる。
        省略された場合以下の順に値を見てそのディレクトリへダウンロードされる
        <ul>
          <li>g:zipDownloadDir (liberator.globalVariables.zipDownloadDir)</li>
          <li>browser.download.lastDir (Preference)</li>
          <li>ホームディレクトリ</li>
        </ul>
      </p>
      <p>
        <oa>-l<oa>ist</oa></oa>オプションを指定すると、ダウンロードされるURLをリストする。
        (ダウンロードはされない)
      </p>
      <p>
        <oa>-f<oa>ilter</oa></oa>オプションを指定すると、マッチするURLのアイテムのみダウンロードする。
      </p>
    </description>
  </item>
  <item>
    <tags>g:zipDownloadDir</tags>
    <spec><oa>g:</oa>zipDownloadDir</spec>
    <spec>liberator.globalVariables.zipDownloadDir</spec>
    <description>
      <p>ダウンロード先ディレクトリ<a>downloadPath</a>を省略した場合に、使用される。</p>
      <p>
        <code><ex>:let g:zipDownloadDir="~/downloads"</ex></code>
      </p>
    </description>
  </item>
  <item>
    <tags>g:zipDownloadFilter</tags>
    <spec><oa>g:</oa>zipDownloadFilter</spec>
    <spec>liberator.globalVariables.zipDownloadFilter</spec>
    <description>
      <p>デフォルトのフィルタ<a>filter</a>を省略した場合に、使用される。</p>
      <p>
        <code><ex>:let g:zipDownloadFilter="\.(jpe?g|gif|png)$"</ex></code>
      </p>
    </description>
  </item>
  <item>
    <tags>plugins.zipDeDownload.SITE_INFO</tags>
    <spec>plugins.zipDeDownload.SITE_INFO</spec>
    <description>
      <p>
        ページ毎の設定詳細はコードを見よ(見れば分かると思う)
      </p>
    </description>
  </item>
</plugin>;

// FIXME: 将来的には、storageに入れるべき
// FIXME: あと、それぞれダウンロード先を指定できた方が良い(?)
// XXX: WeData化してもOK
let SITE_INFO = [
  {
    label: "みんくちゃんねる",
    site: "http://minkch\\.com/archives/.*\\.html",
    xpath: '//a[img[@class="pict"]]|//div/img[@class="pict"]',
    filter: "\\.(jpe?g|gif|png)$"
  }, {
    label: "カナ速",
    site: "http://kanasoku\\.blog82\\.fc2\\.com/blog-entry-.*\\.html",
    xpath: '//div[@class="entry_body"]//a[img]',
    filter: "\\.(jpe?g|gif|png)$"
  }, {
    label: "がぞう~速報",
    site: "http://stalker\\.livedoor\\.biz/archives/.*\\.html",
    xpath: '//div[@class="main" or @class="mainmore"]//a/img[@class="pict"]/..',
    filter: "\\.(jpe?g|gif|png)$"
  }, {
    label: "ギャルゲーブログ",
    site: "http://suiseisekisuisui\\.blog107\\.fc2\\.com/blog-entry-.*.html",
    xpath: '//div[@class="ently_text"]/a[img]',
    filter: "\\.(jpe?g|gif|png)$"
  }, {
    label: "わくてか速報",
    site: "http://blog\\.livedoor\\.jp/wakusoku/archives/.*\\.html",
    xpath: '//div[@class="article-body-inner" or @class="article-body-more"]//a[//img[@class="pict"]]',
    filter: "\\.(jpe?g|gif|png)$"
  }, {
    label: "らばQ",
    site: "http://labaq\\.com/archives/.*\\.html",
    xpath: '//img[@class="pict"]',
  }, {
    labe: "【2ch】ニュー速クオリティ",
    site: "http://news4vip\\.livedoor\\.biz/archives/.*\\.html",
    xpath: '//a[img[@class="pict"]] | //div/img[@class="pict"]',
    filter: "\\.(jpe?g|gif|png)$"
  }, {
    label: "ねとねた",
    site: "http://vitaminabcdefg\\.blog6\\.fc2\\.com/blog-entry-.*\\.html",
    xpath: '//div[@class="mainEntryBody" or @class="mainEntryMore"]//a[img]',
    filter: "\\.(jpe?g|gif|png)$"
  }, {
    label: "PINK速報",
    site: "http://pinkimg\\.blog57\\.fc2\\.com/blog-entry-.*\\.html",
    xpath: '//div[@class="entry_text"]/a[img]',
    filter: "\\.(jpe?g|gif|png)$"
  }
];

(function(){
  // nsIZipWriter#open io-flags
  const PR_RDONLY      = 0x01;
  const PR_WRONLY      = 0x02;
  const PR_RDWR        = 0x04;
  const PR_CREATE_FILE = 0x08;
  const PR_APPEND      = 0x10;
  const PR_TRUNCATE    = 0x20;
  const PR_SYNC        = 0x40;
  const PR_EXCL        = 0x80;

  const mimeService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
  const zipWriter = Components.Constructor("@mozilla.org/zipwriter;1", "nsIZipWriter");

  function getFile(aFile){
    return liberator.modules.io.File(aFile);
  }
  function createChannel(url){
    return liberator.modules.services.get("io").newChannel(url, "UTF-8", null);
  }
  function getEntryName(uri, mimeType){
    let mime;
    try {
      mime = mimeService.getTypeFromURI(uri);
    } catch(e) {
      liberator.reportError(e);
    };
    let ext = mimeService.getPrimaryExtension(mime ? mime : mimeType, null)
    let name = uri.path.split("/").pop();
    name = (name ? name : "index") + (mime ? "" : "." + ext);
    return name;
  }
  function getDownloadDirectory(){
    let path = liberator.globalVariables.zipDownloadDir ||
               liberator.modules.options.getPref("browser.download.lastDir", null) ||
               liberator.modules.services.get("directory").get("Home", Ci.nsIFile).path;
    return getFile(path);
  }
  function fixFilename(filename){
    const badChars = /[\\\/:;\*\?\"\<\>\|\#]/g;
    return liberator.has('windows') ? filename.replace(badChars, '_') :  filename;
  }
  function getXPathFromExtensions(exts){
    function getXPath(elem){
      if (!elem)
        return '';

      // 連番かもしれない id は無視する
      let id = elem.getAttribute('id');
      if (id && !/\d/(id))
        return 'id("' + id + '")';

      return getXPath(elem.parentNode) + '/' + elem.tagName.toLowerCase();
    }

    let extPattern = RegExp('(' + exts.join('|')+')(\\W|$)');

    let links =
      Array.slice( content.document.querySelectorAll('a')).filter(
        function (link) (link.href && extPattern(link.href)));

    let xs = {};
    for each(let link in links){
      let xpath = getXPath(link);
      if (xs[xpath])
        xs[xpath]++;
      else
        xs[xpath] = 1;
    }

    let result = null, max = 0;
    for(let [xpath, count] in Iterator(xs)){
      if (count > max)
        [result, max] = [xpath, count];
    }
    return result;
  }
  function extensionValidator(vs)
    vs && vs.every(function (v) /^[\da-zA-Z]+$/(v));

  let self = {
    downloadZip: function(path, urls, comment, isAppend){
      let zipW = new zipWriter();
      let urls = [url for each(url in urls)];
      liberator.assert(urls.length > 0, "None of URLs");

      if (!(/\.zip$/i).test(path)){
        path += ".zip";
      }
      let zipFile = getFile(path);
      if (isAppend && zipFile.exists()){
        zipW.open(zipFile, PR_RDWR | PR_APPEND);
      } else {
        zipW.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
      }

      if (comment)
        zipW.comment = comment;

      let i = 0;
      for each(let url in urls){
        let ch = createChannel(url);
        try {
          let stream = ch.open();
          let entryName = ("000" + ++i).slice(-3) + "-" + getEntryName(ch.URI, ch.contentType);
          liberator.echomsg("zip: " + url + " to " + entryName, 3);
          zipW.addEntryStream(entryName, Date.now() * 1000, Ci.nsIZipWriter.COMPRESSION_DEFAULT, stream, false);
        } catch (e) {
          // XXX エラー分を通知すべき?
          liberator.log('zip-de-download: error: ' + e);
        }
      }
      zipW.close();
      return zipFile;
    },
    getInfoFromBuffer: function(){
      for each(data in SITE_INFO){
        let reg = new RegExp(data.site);
        if (reg.test(liberator.modules.buffer.URL)){
          return data;
        }
      }
      return null;
    },
    getURLs: function(info){
      let filter = new RegExp(info.filter || liberator.globalVariables.zipDownloadFilter || ".");
      let i = 0;
      for (let elm in liberator.modules.util.evaluateXPath(info.xpath, content.document)){
        let url;
        if (elm instanceof Ci.nsIDOMHTMLAnchorElement)
          url = elm.href;
        else if (elm instanceof Ci.nsIDOMHTMLImageElement)
          url = elm.src;
        else
          continue;

        if (filter.test(url))
          yield url;
      }
    },
    download: function(zipPath, listOnly, option){
      let info = this.getInfoFromBuffer() || {};
      if (option){
        let infoBuf = {};
        for (let key in info){
          infoBuf[key] = info[key];
        }
        for (let key in option){
          infoBuf[key] = option[key];
        }
        info = infoBuf;
      }
      liberator.assert(info.xpath, "not registered in SITE_INFO");

      let urls = this.getURLs(info);
      let title = fixFilename(liberator.modules.buffer.title);
      let comment = [title, liberator.modules.buffer.URL].join("\n");
      let file;
      if (!zipPath){
        file = getDownloadDirectory();
        file.append(title + ".zip");
      } else {
        file = getFile(zipPath);
        if (file.exists() && file.isDirectory()){
          file.append(title + ".zip");
        }
      }
      if (listOnly){
        return [file, urls, comment];
      }
      return this.downloadZip(file.path, urls, comment, info.append);
    }
  };

  // ---------------------------------------------------
  // Commands
  // ---------------------------------------------------
  liberator.modules.commands.addUserCommand(
    ["zipd[ownload]"], "download and archive to ZIP",
    function (arg){
      let option = {}
      option.append = ("-append" in arg);
      if ("-auto-detect" in arg){
        option.xpath = getXPathFromExtensions(arg["-auto-detect"]);
      }
      if ("-xpath" in arg){
        option.xpath = arg["-xpath"];
      }
      if ("-filter" in arg){
        option.filter = arg["-filter"];
      }
      if ("-list" in arg){
        let [file, urls, comment] = self.download(arg[0], true, option);
        let xml = <>
          <h1><span>Download :</span><span>{file.path}</span></h1>
          <p>{comment}</p>
          <ol>
            {liberator.modules.template.map(urls, function(url) <li>{url}</li>)}
          </ol>
          <br/>
        </>;
        liberator.echo(xml, true);
        return;
      }
      liberator.echo("Started DownloadZip");
      let zipFile = self.download(arg[0], false, option);
      liberator.echo("Completed DownloadZip: " + zipFile.path);
    }, {
      argCount: "?",
      literal: true,
      options: [
        [["-list", "-l"], liberator.modules.commands.OPTION_NOARG],
        [["-append", "-a"], liberator.modules.commands.OPTION_NOARG],
        [["-xpath", "-x"], liberator.modules.commands.OPTION_STRING],
        [["-auto-detect", "-d"], liberator.modules.commands.OPTION_LIST, extensionValidator,
         [["jpeg,jpg,png", "images"]]],
        [["-filter", "-f"], liberator.modules.commands.OPTION_STRING]
      ],
      completer: liberator.modules.completion.file
    }, true);

  util.extend(__context__, self);
})();
// vim: sw=2 ts=2 et: