aboutsummaryrefslogtreecommitdiffstats
path: root/hatebuWatchDog.js
blob: e4672829c9a971c092a9ee52c844572da3ff24df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84/**
 * ==VimperatorPlugin==
 * @name              Pukka
 * @description       Add bookmark to Delicious with Pukka
 * @description-ja    Pukkaを使用してDeliciousにブックマークする
 * @author            otsune info@otsune.com
 * @namespace         http://www.otsune.com/
 * @minVersion        2.0pre
 * @version           0.4
 * ==/VimperatorPlugin==
 *
 * see also http://codesorcery.net/pukka/
 *
 * Variables:
 *  g:pukka_normalizelink
 *      Specifies keys that use Pathtraq URL Normalizer
 *      usage: let g:pukka_normalizelink = true
 * Mappings:
 *  '[C-p]':
 * Commands:
 *  'pukka' or 'pu':
 *      Post bookmark to Delicious with Pukka
 *      usage: :pu[kka] [http://example.com/]
 * Options:
 *  not implemented
 */

(function() {
var useNormalizelink = liberator.globalVariables.pukka_normalizelink || true;
var buf = liberator.modules.buffer;

liberator.modules.commands
         .addUserCommand(['pukka', 'pu'], 'Post to Pukka bookmark', function(args) {
    if (!buf.title || !buf.URL || buf.URL == 'about:blank') {
        return false;
    }
    var scheme = 'pukka:';
    var title = encodeURIComponent(buf.title);
    var url = encodeURIComponent(buf.URL.toString());
    var extend = encodeURIComponent(window.content.getSelection().toString() || '');
    if (args.string) {
        url = encodeURIComponent(args.string);
    }
    liberator.open(scheme + 'url=' + url + '&title=' + title + '&extended=' + extend);
}, {
    bang: false,
    completer: function(filter) {
        var complist = [];

        complist.push([buf.URL, 'Raw URL: ' + buf.title]);

        if (useNormalizelink) {
            complist.push([getNormalizedPermalink(buf.URL), 'Normalized URL: ' + buf.title]);
        }

        // detect rel="bookmark"
        var elem;
        var relb = buf.evaluateXPath(
            '//*[contains(concat(" ", normalize-space(@rel), " "), " bookmark ")]',
            null, null, true);
        while ((elem = relb.iterateNext()) !== null) {
            complist.push([elem.toString(), '@rel="bookmark" URL: ' + elem]);
        }

        return [0, complist];
    }
});

liberator.modules.mappings
         .addUserMap([liberator.modules.modes.NORMAL], ['<C-p>'], 'Post to Pukka', function() {
    var urlarg = liberator.globalVariables.pukka_normalizelink ?
                 getNormalizedPermalink(//
//  hatebuWatchDog.js     - hatena bookmark watch dog -
//
// 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: {{{
let PLUGIN_INFO =
<VimperatorPlugin>
  <name>{NAME}</name>
  <description>Make notify hatebu-count when specified site's hatebu-count changed.</description>
  <description lang="ja">指定されたサイトのはてブ数を監視、変動があったらお知らせします。</description>
  <minVersion>2.0pre</minVersion>
  <maxVersion>2.2pre</maxVersion>
  <updateURL>http://svn.coderepos.org/share/lang/javascript/vimperator-plugins/trunk/hatebuWatchDog.js</updateURL>
  <author mail="snaka.gml@gmail.com" homepage="http://vimperator.g.hatena.ne.jp/snaka72/">snaka</author>
  <license>MIT style license</license>
  <version>1.3.0</version>
  <detail><![CDATA[
    == Subject ==
      Make notify hatebu-count when specified site's hatebu-count changed.
      Usage is just put this script into vimperator's plugin directory.

    == Global variables ==
      g:hatebuWatchDogInterval:
        Number. Watching interval. Default:600 Min:60
      g:hatebuWtachDogTargets:
        String. Sites where it wants you to watch.
        If you want watch only one site, you should specify like following.
        >||
          :let g:hatebuWatchDogTargets = "http://d.hatena.ne.jp/snaka72/"
        ||<
        If you want watch more than one site, you should specify like following.
        >||
          :let g:hatebuWatchDogTargets = "['http://d.hatena.ne.jp/snaka72/', 'http://vimperator.g.hatena.ne.jp/snaka72/']"
        ||<
      g:hatebuWatchDogAlways:
        Boole. Make notify every time. (for debug) Default:false

  ]]></detail>
  <detail lang="ja"><![CDATA[
    == 概要 ==
      指定されたサイトの被はてブ数を監視してその数値に変動があったらお知らせします
      使い方はこのスクリプトをVimperatorのpluginディレクトリに格納するだけです

    == グローバル変数 ==
      g:hatebuWatchDogInterval:
        Number. 監視の間隔(). デフォルト600 設定可能な最小値:60
      g:hatebuWtachDogTargets:
        String. Sites where it wants you to watch
        監視対象のサイトが一つだけの場合は以下のように設定します
        >||
          :let g:hatebuWatchDogTargets = "http://d.hatena.ne.jp/snaka72/"
        ||<
        監視対象のサイトがが複数の場合は以下のように設定します
        >||
          :let g:hatebuWatchDogTargets = "['http://d.hatena.ne.jp/snaka72/', 'http://vimperator.g.hatena.ne.jp/snaka72/']"
        ||<
      g:hatebuWatchDogAlways:
        Boole. 毎回報告を挙げるかどうかデフォルト:false 主にでバッグ用

    == ToDo ==
      - 新着ブックマークのユーザidとコメントの表示
      - 監視フレームワークにのっける

    ]]></detail>
  </VimperatorPlugin>;
// }}}

// Clear all watchers if started watcher exists.
if (plugins.hatebuWatchDog && plugins.hatebuWatchDog.stopWatching)
  plugins.hatebuWatchDog.stopWatching();

let publics = plugins.hatebuWatchDog = (function() {
  // PRIVATE //////////////////////////////////////////////////////////////{{{
  const libly = plugins.libly;
  let previousValue = 0;
  let tasks = [];

  function getCurrentValue(target, onSuccess, onFailure) {
    // build hatebu xml-rpc request
    let req = new libly.Request(
      'http://b.hatena.ne.jp/xmlrpc',
      {
        'Content-Type' : 'text/xml'
      },{
        postBody : <methodCall>
                     <methodName>bookmark.getTotalCount</methodName>
                     <params>
                       <param><value><string>{target}</string></value></param>
                     </params>
                   </methodCall>.toXMLString()
      }
    );

    let currentValue;
    req.addEventListener("onSuccess", function(data) {
      liberator.log("XML-RPC request was succeeded.");
      let resXml = new XML(data.responseText.replace(/^<\?xml version[^>]+?>/, ''));
      currentValue = window.eval(resXml..int.toString());
      onSuccess(currentValue);
    });
    req.addEventListener("onFailure", function(data) {
      onFailure();
    });
    liberator.log("reauest...");
    req.post();
    liberator.log("done...");
  }

  function notifyAlways()
    window.eval(liberator.globalVariables.hatebuWatchDogAlways) || false;

  function showHatebuNotification(targetSite, currentValue, delta) {
    let title = delta >= 0
              ? "hatebuWatchDog\u304B\u3089\u306E\u304A\u77E5\u3089\u305B"  // ordinary notification
              : "\u6B8B\u5FF5\u306A\u304A\u77E5\u3089\u305B"                // bad notification
    let suffix = delta != 0 ? "\u306B\u306A\u308A\u307E\u3057\u305F\u3002"
                            : "\u3067\u3059\u3002";
    let message = "'" + targetSite + "' \u306E\u88AB\u306F\u3066\u30D6\u6570\u306F '" +
                  currentValue + "' " + suffix + " (" + getSignedNum(delta) + ")";

    (getNotifier())(title, message, growlIcon);
  }

  function getSignedNum(num) {
    if (num > 0) return "+" + num;
    if (num < 0) return "-" + Math.abs(num);
    return "0";
  }

  let _notifier = null;
  const GROWL_EXTENSION_ID = "growlgntp@brian.dunnington";

  function getNotifier() {
    if (_notifier) return _notifier;

    if (Application.extensions.has(GROWL_EXTENSION_ID) &&
        Application.extensions.get(GROWL_EXTENSION_ID).enabled) {
      _notifier = publics.notify;
    }
    else {
      _notifier = showAlertNotification;
    }
    return _notifier;
  }

  function showAlertNotification(title, message, icon) {
    liberator.dump("icon:" + icon);
    Cc['@mozilla.org/alerts-service;1']
    .getService(Ci.nsIAlertsService)
    .showAlertNotification(
      null, //'chrome://mozapps/skin/downloads/downloadIcon.png',
      title,
      message
    );
  }

  function growl() Components.classes['@growlforwindows.com/growlgntp;1']
                   .getService().wrappedJSObject;
  const growlIcon = "http://img.f.hatena.ne.jp/images/fotolife/s/snaka72/20090608/20090608045633.gif";  // temporary

  function growlRegister() {
    growl().register(
      PLUGIN_INFO.name,
      growlIcon,
      [
        {name: 'announce', displayName: 'Announce from hatebuWatchDog'},
        {name: 'sadlynews',displayName: 'Sadly announce from hatebuWatchdog'},
        {name: 'failed',   displayName: 'Erroer report from hatebuWatchdog'}
      ]
    );
  }

  function getInterval()
    window.eval(liberator.globalVariables.hatebuWatchDogInterval) || 600; // default : 10 min.

  // for debug
  let log  = liberator.log;
  let dump = liberator.dump;

  // }}}
  // PUBLIC ///////////////////////////////////////////////////////////////{{{
  let self = {
    startWatching: function() {
      let targets;
      try {
        targets = window.eval(liberator.globalVariables.hatebuWatchDogTargets);
      } catch(e) {
        targets = liberator.globalVariables.hatebuWatchDogTargets;
      }
      if (targets) {
        if (!(targets instanceof Array))
          targets = [targets];
        let i = 1, delay = 5000;
        log("before setTimeout()");
        targets.forEach(function(targetSite) {
            setTimeout(function() {
              publics.addTask({site : targetSite});
            }, delay * i++);
        });
        log("after setTimeout()");
      }
      else {
        liberator.echoerr("Please set g:hatebeWatchDogTargets before watching().");
      }
    },

    addTask: function(target) {
      dump(target.site);
      const MINUTE = 60; // sec.
      interval = getInterval() || (10 * MINUTE);       // default 10 min.
      interval = Math.max(interval, MINUTE);      // lower limt is 1 min.

      // initialize previous value
      target.previousValue = 0;
      target.initialize = true;
      publics.watching(target);

      // set watching interval
      tasks.push(setInterval(publics.watching, 1000 * interval, target));
      dump({target: target, interval: interval});
    },

    clearAllTasks: function() {
      tasks.forEach(function(task) {
          clearInterval(task);
      });
      tasks = [];
      dump("watch dog is sleeping...");
    },

    watching: function(target) {
      dump("watching...");
      dump(target);

      getCurrentValue(
        target.site,
        function(currentValue) {
          if (target.initialize) {
            target.initialize = false;
            target.previousValue = currentValue;
            return;
          }
          let delta =  currentValue - target.previousValue;
          if (delta || notifyAlways()) {
            showHatebuNotification(target.site, currentValue, delta);
          }
          target.previousValue = currentValue;
          if (delta > 0) {
            liberator.dump("***hoge");
            self.getBookmarklistByURL(target.site)
            .slice(0, delta)
            .forEach(function(item)
                      self.reportBookmarkedItem(self.parseBookmarkItem(item)));
          }
        },
        function() {
          liberator.echoerr("Cannot get current value.");
        }
      );
    },

    notify: function(title, message) {
      growlRegister();
      growl().notify(
        PLUGIN_INFO.name,
        'announce',
        title,
        message
      );
    },

    getBookmarkListRss: function(url) {
      return util.httpGet("http://b.hatena.ne.jp/bookmarklist.rss?url=" + encodeURIComponent(url));
    },

    getBookmarklistByURL: function(url) {
      liberator.dump("********** getBookmarklistByURL");
      let res = util.httpGet('http://b.hatena.ne.jp/bookmarklist.rss?url=' + encodeURIComponent(url));
      liberator.dump(res);
      return self.evaluateXPath("//rss:item", res.responseXML, self.nsResolver);
    },

    nsResolver: {
        lookupNamespaceURI: function(pfx) (({
          'rdf'         : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
          'content'     : "http://purl.org/rss/1.0/modules/content/",
          'taxo'        : "http://purl.org/rss/1.0/modules/taxonomy/",
          'opensearch'  : "http://a9.com/-/spec/opensearchrss/1.0/",
          'dc'          : "http://purl.org/dc/elements/1.1/",
          'hatena'      : "http://www.hatena.ne.jp/info/xmlns#",
          'media'       : "http://search.yahoo.com/mrss"
        })[pfx] || 'http://purl.org/rss/1.0/')
    },

    // reffered  _libly.js
    evaluateXPath: function(xpath, context, nsresolver) {
      if (!xpath) return [];

      var ret = [];
      context = context || window.content.document;
      var nodesSnapshot = (
        context.ownerDocument ||
        context
      ).evaluate(
        xpath,
        context,
        nsresolver || self.nsResolver,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
        null
      );

      for (let i = 0, l = nodesSnapshot.snapshotLength; i < l; i++) {
          ret.push(nodesSnapshot.snapshotItem(i));
      }
      return ret;
    },

    parseBookmarkItem: function(item) {
      let parsed = {
        title: self.evaluateXPath("./rss:title", item)[0].textContent,
        creator: self.evaluateXPath("./dc:creator", item)[0].textContent,
        date: self.evaluateXPath("./dc:date", item)[0].textContent,
        comment: self.evaluateXPath("./rss:description", item)[0].textContent,
        tags: self.evaluateXPath("./dc:subject", item).map(function(i) i.textContent).join(",")
      };
      return parsed;
    },

    reportBookmarkedItem: function(item) {
      liberator.dump(item);
      (getNotifier())(
          item.title,
          item.creator + " bookmarked at " + item.date + "\n" +
          item.tags + ":" + item.comment,
          'http://www.hatena.ne.jp/users/' + item.creator.substr(0, 2) + '/' + item.creator + '/profile.gif'
      );
    }
  };
  // }}}
  return self;
})();

// Awaking the watch dog.
publics.startWatching();
liberator.dump("Watch dog is awaking ...");
// vim: sw=2 ts=2 et fdm=marker