// 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 http://svn.coderepos.org/share/lang/javascript/vimperator-plugins/trunk/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: d='n325' href='#n325'>325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
//
//  pino.js  - Open livedoor Reader (and clone server) pinned items -
//
// LICENSE: {{{
//
// This software distributable under the terms of an MIT-style license.
//
// Copyright (c) 2009 snaka<snaka.gml@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// OSI page : http://opensource.org/licenses/mit-license.php
// Japanese : http://sourceforge.jp/projects/opensource/wiki/licenses%2FMIT_license
//
// }}}
// PLUGIN INFO: {{{
var PLUGIN_INFO =
<VimperatorPlugin>
  <name>{NAME}</name>
  <description>Open livedoor Reader pinned items</description>
  <description lang="ja">livedoor Reader でピンを立てたページを開く</description>
  <minVersion>2.0</minVersion>
  <maxVersion>2.1</maxVersion>
  <updateURL>http://svn.coderepos.org/share/lang/javascript/vimperator-plugins/trunk/pino.js</updateURL>
  <require type="plugin">_libly.js</require>
  <author mail="snaka.gml@gmail.com" homepage="http://vimperator.g.hatena.ne.jp/snaka72/">snaka</author>
  <license>MIT style license</license>
  <version>1.3.2</version>
  <detail><![CDATA[
    == Subject ==
    Open livedoor Reader pinned items.

    == Commands ==
    :[count]pino
      Following option is avilable.
      -list:
        Show pinned item list.

    == Global variables ==
    g:pinoOpenItemsCount:
      default: 5

    g:pinoOpenBehavior:
      default: liberator.NEW_BACKGROUND_TAB

    g:pinoAscendingOrder:
      default: "false"

    g:pinoBaseURL:
      If you want to use fastladder, set "http://fastladder.com" into this variable.
      default: "http://reader.livedoor.com"

    == API ==
    plugins.pino.items():
      Return pinned items list array.
      Each item is following structure.
      >||
      {
         created_on : (create date),
         link : (url),
         title : (page title)
      }
      ||<

    plugins.pino.shift():
      Return first item and remove pin.

    plugins.pino.remove(link):
      Remove pin from item that matched by 'link'.

  ]]></detail>
  <detail lang="ja"><![CDATA[
    == 概要 ==
    livedoor Reader でピンを立てた記事をVimperatorのコマンドラインから開く
    ことができます

    == コマンド ==
    :[count]pino:
      そのまま<Enter>で先頭のn件デフォルト5件グローバル変数で調整可能
      をバックグラウンドのタブで開きます
      <TAB>で補完候補の一覧にピンを立てた記事の一覧から選択することもできます
      count を指定するとその件数だけ開きます
      以下のオプションが指定可能です
      -list:
        ピンの一覧を表示します

    == グローバル変数 ==
    g:pinoOpenItemsCount:
      一度に開くピンの数
      default: 5

    g:pinoOpenBehavior:
      ピンを開くときの挙動liberator.open()の第2引数として使用する値
      参考http://wiki.livedoor.jp/shin_yan/d/liberator%282%2e0%29#content_34
      default: liberator.NEW_BACKGROUND_TAB

    g:pinoAscendingOrder:
      ピンの一覧の表示順を昇順古い順とするかどうか
      default: "false" 新しい順

    g:pinoBaseURL:
      fastladder を使う場合はこの変数を "http://fastladder.com" とする
      default: "http://reader.livedoor.com"

    == API ==
    plugins.pino.items():
      ピンの一覧を配列で取得する
      ピンのデータ構造は以下のとおりとなっている
      >||
      {
         created_on : (create date),
         link : (url),
         title : (page title)
      }
      ||<

    plugins.pino.shift():
      先頭のピンを取得してそのピンを一覧から削除する

    plugins.pino.remove(link):
      linkに該当するピンを一覧から削除する

  ]]></detail>
</VimperatorPlugin>;
// }}}
let self = liberator.plugins.pino = (function() {
  // COMMAND /////////////////////////////////////////////////////// {{{
  commands.addUserCommand(
    ["pinneditemopen", "pino"],
    "Open livedoor Reader(and clone server) pinned item",
    function(args) {
      let pins = new Pins();
      let items = pins.items();
      if (!items || items.length == 0) {
        liberator.echo("Pinned item doesn't exists.");
        return;
      }

      if (args["-list"]) {
        //let items = pins.items();
        let list = <div>{items.length} items.
                    <ul>{
                      [<li><a href={i.link}>{i.title}</a><br/></li>
                        for each (i in items)
                      ].reduce(function(a, b) a + b)
                    }</ul>
                   </div>;
        liberator.echo(list, commandline.FORCE_MULTILINE);
        return;
      }

      if (args.string == "") {
        let pin;
        let max = (args.count >= 1) ? args.count : openItemsCount();
        for(let i = 0; i < max; i++) {
          if (!(pin = pins.shift()))
            break;
          setTimeout(function(link) liberator.open(link, openBehavior()), 200 * i, pin.link);
        }
      }
      else {
        liberator.open(args.string, openBehavior());
        pins.remove(args.string);
      }
    },
    {
      literal: 0,
      count: true,
      completer: function(context) {
        var pins = new Pins();
        context.title = ["url", "title"];
        context.completions = [
          [i.link, i.title] for each (i in pins.items())
        ];
      },
      options: [
        [["-list", "-l"], commands.OPTION_NOARG]
      ]
    },
    true    // for Debug
  );
  // }}}
  // GLOBAL VARIABLES ////////////////////////////////////////////// {{{
  var gv = liberator.globalVariables;
  function openItemsCount()
    gv.pinoOpenItemsCount || 5;

  function ascending()
    window.eval(gv.pinoAscendingOrder) == true; // default: false

  function openBehavior()
    window.eval(gv.pinoOpenBehavior) || liberator.NEW_BACKGROUND_TAB;

  function baseURL()
    gv.pinoBaseURL || "http://reader.livedoor.com";

  // }}}
  // CLASS ///////////////////////////////////////////////////////// {{{

  function Pins() {
    this.cache = null;
    this.apiKey = getLDRApiKey();
    this.sortOrder = ascending()
                      ? function(a, b) (a.created_on < b.created_on ? -1 : 1)
                      : function(a, b) (a.created_on > b.created_on ? -1 : 1);
  }
  Pins.prototype = {
    items : function() {
      let result = this.cache
              ? this.cache
              : this.cache = this._getPinnedItems();
      return (result || []).sort(this.sortOrder);
    },

    shift : function() {
      if (this.items().length == 0)
        return null;
      var pin = this.items().shift();
      this.remove(pin.link);
      return pin;
    },

    remove : function(link) {
      var unescapedLink = unescapeHTML(link);
      var request = new libly.Request(
        baseURL() + "/api/pin/remove",
        {
          //Cookie: "reader_sid=" + this.apiKey,
          //Referer: "http://reader.livedoor.com/reader/"
        },
        {
          postBody: toQuery({link: unescapedLink, ApiKey: this.apiKey})
        }
      );

      request.addEventListener("onSuccess", function(data) {
        liberator.log("Removed pin from '" + link + "' was succeeded.");
      });
      request.addEventListener("onFailure", function(data) {
        liberator.echoerr("Cannot remove pin");
      });
      request.post();
    },

    _getPinnedItems : function() {
      var result = null;
      var request = new libly.Request(
          baseURL() + "/api/pin/all",
          null,
          {
            asynchronous: false,
            postBody: toQuery({ApiKey: this.apiKey})
          }
      );

      request.addEventListener("onSuccess", function(data) {
        if (isLoginPage(data)) {
          liberator.echoerr("Can't get pinned list. Maybe you should login to livedoor.");
          return;
        }
        result = liberator.eval(data.responseText);
      });
      request.addEventListener("onFailure", function(data) {
        liberator.echoerr("Can't get pinned list!!!");
      });
      request.post();

      return result;
    },
  }
  // }}}
  // FUNCTIONS ///////////////////////////////////////////////////// {{{
  var libly = plugins.libly;

  function getLDRApiKey() {
    var ioService = Cc["@mozilla.org/network/io-service;1"]
                    .getService(Ci.nsIIOService);
    var uri = ioService.newURI(baseURL(), null, null);
    var channel = ioService.newChannelFromURI(uri);
    var cookie = Cc["@mozilla.org/cookieService;1"]
                .getService(Ci.nsICookieService)
                .getCookieString(uri, channel);
    var apiKey = cookie.match(/reader_sid=([^;]+)/);
    return apiKey ? apiKey[1]: null;
  }

  function unescapeHTML(source) {
    var result = source;
    [
      [/&lt;/g,  "<"],
      [/&gt;/g,  ">"],
      [/&amp;/g, "&"]
    ].forEach( function(rule) {
      result = result.replace(rule[0], rule[1]);
    });
    return result;
  }

  function toQuery(source)
    [encodeURIComponent(i) + "=" + encodeURIComponent(source[i])
        for (i in source)
    ].join('&');

  function isLoginPage(response)
    response.responseText.substr(0, 5) == '<?xml'

  // }}}
  // API /////////////////////////////////////////////////////////// {{{
  return {
    items : function()
      (new Pins).items(),

    shift : function()
      (new Pins).shift(),

    head : function()   // @deprecated
      self.shift(),

    remove : function(link)
      (new Pins).remove(link),
  };
  // }}}
})();
// for backward compatibility
self.api = {};
for each (p in "items head remove".split(' '))
  self.api[p] = self[p];
// vim: ts=2 sw=2 et fdm=marker