| 1 | // |
|---|
| 2 | // hatebuWatchDog.js - hatena bookmark watch dog - |
|---|
| 3 | // |
|---|
| 4 | // LICENSE: {{{ |
|---|
| 5 | // |
|---|
| 6 | // This software distributable under the terms of an MIT-style license. |
|---|
| 7 | // |
|---|
| 8 | // Copyright (c) 2009 snaka<snaka.gml@gmail.com> |
|---|
| 9 | // |
|---|
| 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy |
|---|
| 11 | // of this software and associated documentation files (the "Software"), to deal |
|---|
| 12 | // in the Software without restriction, including without limitation the rights |
|---|
| 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|---|
| 14 | // copies of the Software, and to permit persons to whom the Software is |
|---|
| 15 | // furnished to do so, subject to the following conditions: |
|---|
| 16 | // |
|---|
| 17 | // The above copyright notice and this permission notice shall be included in |
|---|
| 18 | // all copies or substantial portions of the Software. |
|---|
| 19 | // |
|---|
| 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|---|
| 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|---|
| 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|---|
| 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|---|
| 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|---|
| 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|---|
| 26 | // THE SOFTWARE. |
|---|
| 27 | // |
|---|
| 28 | // OSI page : http://opensource.org/licenses/mit-license.php |
|---|
| 29 | // Japanese : http://sourceforge.jp/projects/opensource/wiki/licenses%2FMIT_license |
|---|
| 30 | // |
|---|
| 31 | // }}} |
|---|
| 32 | // PLUGIN INFO: {{{ |
|---|
| 33 | let PLUGIN_INFO = |
|---|
| 34 | <VimperatorPlugin> |
|---|
| 35 | <name>{NAME}</name> |
|---|
| 36 | <description>Make notify hatebu-count when specified site's hatebu-count changed.</description> |
|---|
| 37 | <description lang="ja">指定されたサイトのはてブ数を監視、変動があったらお知らせします。</description> |
|---|
| 38 | <minVersion>2.0pre</minVersion> |
|---|
| 39 | <maxVersion>2.2pre</maxVersion> |
|---|
| 40 | <updateURL>http://svn.coderepos.org/share/lang/javascript/vimperator-plugins/trunk/hatebuWatchDog.js</updateURL> |
|---|
| 41 | <author mail="snaka.gml@gmail.com" homepage="http://vimperator.g.hatena.ne.jp/snaka72/">snaka</author> |
|---|
| 42 | <license>MIT style license</license> |
|---|
| 43 | <version>1.3.0</version> |
|---|
| 44 | <detail><![CDATA[ |
|---|
| 45 | == Subject == |
|---|
| 46 | Make notify hatebu-count when specified site's hatebu-count changed. |
|---|
| 47 | Usage is just put this script into vimperator's plugin directory. |
|---|
| 48 | |
|---|
| 49 | == Global variables == |
|---|
| 50 | g:hatebuWatchDogInterval: |
|---|
| 51 | Number. Watching interval. Default:600 Min:60 |
|---|
| 52 | g:hatebuWtachDogTargets: |
|---|
| 53 | String. Sites where it wants you to watch. |
|---|
| 54 | If you want watch only one site, you should specify like following. |
|---|
| 55 | >|| |
|---|
| 56 | :let g:hatebuWatchDogTargets = "http://d.hatena.ne.jp/snaka72/" |
|---|
| 57 | ||< |
|---|
| 58 | If you want watch more than one site, you should specify like following. |
|---|
| 59 | >|| |
|---|
| 60 | :let g:hatebuWatchDogTargets = "['http://d.hatena.ne.jp/snaka72/', 'http://vimperator.g.hatena.ne.jp/snaka72/']" |
|---|
| 61 | ||< |
|---|
| 62 | g:hatebuWatchDogAlways: |
|---|
| 63 | Boole. Make notify every time. (for debug) Default:false |
|---|
| 64 | |
|---|
| 65 | ]]></detail> |
|---|
| 66 | <detail lang="ja"><![CDATA[ |
|---|
| 67 | == 概要 == |
|---|
| 68 | 指定されたサイトの被はてブ数を監視して、その数値に変動があったらお知らせします。 |
|---|
| 69 | 使い方は、このスクリプトをVimperatorのpluginディレクトリに格納するだけです。 |
|---|
| 70 | |
|---|
| 71 | == グローバル変数 == |
|---|
| 72 | g:hatebuWatchDogInterval: |
|---|
| 73 | Number. 監視の間隔(秒). デフォルト600 設定可能な最小値:60 |
|---|
| 74 | g:hatebuWtachDogTargets: |
|---|
| 75 | String. Sites where it wants you to watch |
|---|
| 76 | 監視対象のサイトが一つだけの場合は以下のように設定します。 |
|---|
| 77 | >|| |
|---|
| 78 | :let g:hatebuWatchDogTargets = "http://d.hatena.ne.jp/snaka72/" |
|---|
| 79 | ||< |
|---|
| 80 | 監視対象のサイトがが複数の場合は以下のように設定します。 |
|---|
| 81 | >|| |
|---|
| 82 | :let g:hatebuWatchDogTargets = "['http://d.hatena.ne.jp/snaka72/', 'http://vimperator.g.hatena.ne.jp/snaka72/']" |
|---|
| 83 | ||< |
|---|
| 84 | g:hatebuWatchDogAlways: |
|---|
| 85 | Boole. 毎回報告を挙げるかどうか。デフォルト:false (主にでバッグ用) |
|---|
| 86 | |
|---|
| 87 | == ToDo == |
|---|
| 88 | - 新着ブックマークのユーザidとコメントの表示 |
|---|
| 89 | - 監視フレームワークにのっける |
|---|
| 90 | |
|---|
| 91 | ]]></detail> |
|---|
| 92 | </VimperatorPlugin>; |
|---|
| 93 | // }}} |
|---|
| 94 | |
|---|
| 95 | // Clear all watchers if started watcher exists. |
|---|
| 96 | if (plugins.hatebuWatchDog && plugins.hatebuWatchDog.stopWatching) |
|---|
| 97 | plugins.hatebuWatchDog.stopWatching(); |
|---|
| 98 | |
|---|
| 99 | let publics = plugins.hatebuWatchDog = (function() { |
|---|
| 100 | // PRIVATE //////////////////////////////////////////////////////////////{{{ |
|---|
| 101 | const libly = plugins.libly; |
|---|
| 102 | let previousValue = 0; |
|---|
| 103 | let tasks = []; |
|---|
| 104 | |
|---|
| 105 | function getCurrentValue(target, onSuccess, onFailure) { |
|---|
| 106 | // build hatebu xml-rpc request |
|---|
| 107 | let req = new libly.Request( |
|---|
| 108 | 'http://b.hatena.ne.jp/xmlrpc', |
|---|
| 109 | { |
|---|
| 110 | 'Content-Type' : 'text/xml' |
|---|
| 111 | },{ |
|---|
| 112 | postBody : <methodCall> |
|---|
| 113 | <methodName>bookmark.getTotalCount</methodName> |
|---|
| 114 | <params> |
|---|
| 115 | <param><value><string>{target}</string></value></param> |
|---|
| 116 | </params> |
|---|
| 117 | </methodCall>.toXMLString() |
|---|
| 118 | } |
|---|
| 119 | ); |
|---|
| 120 | |
|---|
| 121 | let currentValue; |
|---|
| 122 | req.addEventListener("onSuccess", function(data) { |
|---|
| 123 | liberator.log("XML-RPC request was succeeded."); |
|---|
| 124 | let resXml = new XML(data.responseText.replace(/^<\?xml version[^>]+?>/, '')); |
|---|
| 125 | currentValue = window.eval(resXml..int.toString()); |
|---|
| 126 | onSuccess(currentValue); |
|---|
| 127 | }); |
|---|
| 128 | req.addEventListener("onFailure", function(data) { |
|---|
| 129 | onFailure(); |
|---|
| 130 | }); |
|---|
| 131 | liberator.log("reauest..."); |
|---|
| 132 | req.post(); |
|---|
| 133 | liberator.log("done..."); |
|---|
| 134 | } |
|---|
| 135 | |
|---|
| 136 | function notifyAlways() |
|---|
| 137 | window.eval(liberator.globalVariables.hatebuWatchDogAlways) || false; |
|---|
| 138 | |
|---|
| 139 | function showHatebuNotification(targetSite, currentValue, delta) { |
|---|
| 140 | let title = delta >= 0 |
|---|
| 141 | ? "hatebuWatchDog\u304B\u3089\u306E\u304A\u77E5\u3089\u305B" // ordinary notification |
|---|
| 142 | : "\u6B8B\u5FF5\u306A\u304A\u77E5\u3089\u305B" // bad notification |
|---|
| 143 | let suffix = delta != 0 ? "\u306B\u306A\u308A\u307E\u3057\u305F\u3002" |
|---|
| 144 | : "\u3067\u3059\u3002"; |
|---|
| 145 | let message = "'" + targetSite + "' \u306E\u88AB\u306F\u3066\u30D6\u6570\u306F '" + |
|---|
| 146 | currentValue + "' " + suffix + " (" + getSignedNum(delta) + ")"; |
|---|
| 147 | |
|---|
| 148 | (getNotifier())(title, message, growlIcon); |
|---|
| 149 | } |
|---|
| 150 | |
|---|
| 151 | function getSignedNum(num) { |
|---|
| 152 | if (num > 0) return "+" + num; |
|---|
| 153 | if (num < 0) return "-" + Math.abs(num); |
|---|
| 154 | return "0"; |
|---|
| 155 | } |
|---|
| 156 | |
|---|
| 157 | let _notifier = null; |
|---|
| 158 | const GROWL_EXTENSION_ID = "growlgntp@brian.dunnington"; |
|---|
| 159 | |
|---|
| 160 | function getNotifier() { |
|---|
| 161 | if (_notifier) return _notifier; |
|---|
| 162 | |
|---|
| 163 | if (Application.extensions.has(GROWL_EXTENSION_ID) && |
|---|
| 164 | Application.extensions.get(GROWL_EXTENSION_ID).enabled) { |
|---|
| 165 | _notifier = publics.notify; |
|---|
| 166 | } |
|---|
| 167 | else { |
|---|
| 168 | _notifier = showAlertNotification; |
|---|
| 169 | } |
|---|
| 170 | return _notifier; |
|---|
| 171 | } |
|---|
| 172 | |
|---|
| 173 | function showAlertNotification(title, message, icon) { |
|---|
| 174 | liberator.dump("icon:" + icon); |
|---|
| 175 | Cc['@mozilla.org/alerts-service;1'] |
|---|
| 176 | .getService(Ci.nsIAlertsService) |
|---|
| 177 | .showAlertNotification( |
|---|
| 178 | null, //'chrome://mozapps/skin/downloads/downloadIcon.png', |
|---|
| 179 | title, |
|---|
| 180 | message |
|---|
| 181 | ); |
|---|
| 182 | } |
|---|
| 183 | |
|---|
| 184 | function growl() Components.classes['@growlforwindows.com/growlgntp;1'] |
|---|
| 185 | .getService().wrappedJSObject; |
|---|
| 186 | const growlIcon = "http://img.f.hatena.ne.jp/images/fotolife/s/snaka72/20090608/20090608045633.gif"; // temporary |
|---|
| 187 | |
|---|
| 188 | function growlRegister() { |
|---|
| 189 | growl().register( |
|---|
| 190 | PLUGIN_INFO.name, |
|---|
| 191 | growlIcon, |
|---|
| 192 | [ |
|---|
| 193 | {name: 'announce', displayName: 'Announce from hatebuWatchDog'}, |
|---|
| 194 | {name: 'sadlynews',displayName: 'Sadly announce from hatebuWatchdog'}, |
|---|
| 195 | {name: 'failed', displayName: 'Erroer report from hatebuWatchdog'} |
|---|
| 196 | ] |
|---|
| 197 | ); |
|---|
| 198 | } |
|---|
| 199 | |
|---|
| 200 | function getInterval() |
|---|
| 201 | window.eval(liberator.globalVariables.hatebuWatchDogInterval) || 600; // default : 10 min. |
|---|
| 202 | |
|---|
| 203 | // for debug |
|---|
| 204 | let log = liberator.log; |
|---|
| 205 | let dump = liberator.dump; |
|---|
| 206 | |
|---|
| 207 | // }}} |
|---|
| 208 | // PUBLIC ///////////////////////////////////////////////////////////////{{{ |
|---|
| 209 | let self = { |
|---|
| 210 | startWatching: function() { |
|---|
| 211 | let targets; |
|---|
| 212 | try { |
|---|
| 213 | targets = window.eval(liberator.globalVariables.hatebuWatchDogTargets); |
|---|
| 214 | } catch(e) { |
|---|
| 215 | targets = liberator.globalVariables.hatebuWatchDogTargets; |
|---|
| 216 | } |
|---|
| 217 | if (targets) { |
|---|
| 218 | if (!(targets instanceof Array)) |
|---|
| 219 | targets = [targets]; |
|---|
| 220 | let i = 1, delay = 5000; |
|---|
| 221 | log("before setTimeout()"); |
|---|
| 222 | targets.forEach(function(targetSite) { |
|---|
| 223 | setTimeout(function() { |
|---|
| 224 | publics.addTask({site : targetSite}); |
|---|
| 225 | }, delay * i++); |
|---|
| 226 | }); |
|---|
| 227 | log("after setTimeout()"); |
|---|
| 228 | } |
|---|
| 229 | else { |
|---|
| 230 | liberator.echoerr("Please set g:hatebeWatchDogTargets before watching()."); |
|---|
| 231 | } |
|---|
| 232 | }, |
|---|
| 233 | |
|---|
| 234 | addTask: function(target) { |
|---|
| 235 | dump(target.site); |
|---|
| 236 | const MINUTE = 60; // sec. |
|---|
| 237 | interval = getInterval() || (10 * MINUTE); // default 10 min. |
|---|
| 238 | interval = Math.max(interval, MINUTE); // lower limt is 1 min. |
|---|
| 239 | |
|---|
| 240 | // initialize previous value |
|---|
| 241 | target.previousValue = 0; |
|---|
| 242 | target.initialize = true; |
|---|
| 243 | publics.watching(target); |
|---|
| 244 | |
|---|
| 245 | // set watching interval |
|---|
| 246 | tasks.push(setInterval(publics.watching, 1000 * interval, target)); |
|---|
| 247 | dump({target: target, interval: interval}); |
|---|
| 248 | }, |
|---|
| 249 | |
|---|
| 250 | clearAllTasks: function() { |
|---|
| 251 | tasks.forEach(function(task) { |
|---|
| 252 | clearInterval(task); |
|---|
| 253 | }); |
|---|
| 254 | tasks = []; |
|---|
| 255 | dump("watch dog is sleeping..."); |
|---|
| 256 | }, |
|---|
| 257 | |
|---|
| 258 | watching: function(target) { |
|---|
| 259 | dump("watching..."); |
|---|
| 260 | dump(target); |
|---|
| 261 | |
|---|
| 262 | getCurrentValue( |
|---|
| 263 | target.site, |
|---|
| 264 | function(currentValue) { |
|---|
| 265 | if (target.initialize) { |
|---|
| 266 | target.initialize = false; |
|---|
| 267 | target.previousValue = currentValue; |
|---|
| 268 | return; |
|---|
| 269 | } |
|---|
| 270 | let delta = currentValue - target.previousValue; |
|---|
| 271 | if (delta || notifyAlways()) { |
|---|
| 272 | showHatebuNotification(target.site, currentValue, delta); |
|---|
| 273 | } |
|---|
| 274 | target.previousValue = currentValue; |
|---|
| 275 | if (delta > 0) { |
|---|
| 276 | liberator.dump("***hoge"); |
|---|
| 277 | self.getBookmarklistByURL(target.site) |
|---|
| 278 | .slice(0, delta) |
|---|
| 279 | .forEach(function(item) |
|---|
| 280 | self.reportBookmarkedItem(self.parseBookmarkItem(item))); |
|---|
| 281 | } |
|---|
| 282 | }, |
|---|
| 283 | function() { |
|---|
| 284 | liberator.echoerr("Cannot get current value."); |
|---|
| 285 | } |
|---|
| 286 | ); |
|---|
| 287 | }, |
|---|
| 288 | |
|---|
| 289 | notify: function(title, message) { |
|---|
| 290 | growlRegister(); |
|---|
| 291 | growl().notify( |
|---|
| 292 | PLUGIN_INFO.name, |
|---|
| 293 | 'announce', |
|---|
| 294 | title, |
|---|
| 295 | message |
|---|
| 296 | ); |
|---|
| 297 | }, |
|---|
| 298 | |
|---|
| 299 | getBookmarkListRss: function(url) { |
|---|
| 300 | return util.httpGet("http://b.hatena.ne.jp/bookmarklist.rss?url=" + encodeURIComponent(url)); |
|---|
| 301 | }, |
|---|
| 302 | |
|---|
| 303 | getBookmarklistByURL: function(url) { |
|---|
| 304 | liberator.dump("********** getBookmarklistByURL"); |
|---|
| 305 | let res = util.httpGet('http://b.hatena.ne.jp/bookmarklist.rss?url=' + encodeURIComponent(url)); |
|---|
| 306 | liberator.dump(res); |
|---|
| 307 | return self.evaluateXPath("//rss:item", res.responseXML, self.nsResolver); |
|---|
| 308 | }, |
|---|
| 309 | |
|---|
| 310 | nsResolver: { |
|---|
| 311 | lookupNamespaceURI: function(pfx) (({ |
|---|
| 312 | 'rdf' : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", |
|---|
| 313 | 'content' : "http://purl.org/rss/1.0/modules/content/", |
|---|
| 314 | 'taxo' : "http://purl.org/rss/1.0/modules/taxonomy/", |
|---|
| 315 | 'opensearch' : "http://a9.com/-/spec/opensearchrss/1.0/", |
|---|
| 316 | 'dc' : "http://purl.org/dc/elements/1.1/", |
|---|
| 317 | 'hatena' : "http://www.hatena.ne.jp/info/xmlns#", |
|---|
| 318 | 'media' : "http://search.yahoo.com/mrss" |
|---|
| 319 | })[pfx] || 'http://purl.org/rss/1.0/') |
|---|
| 320 | }, |
|---|
| 321 | |
|---|
| 322 | // reffered _libly.js |
|---|
| 323 | evaluateXPath: function(xpath, context, nsresolver) { |
|---|
| 324 | if (!xpath) return []; |
|---|
| 325 | |
|---|
| 326 | var ret = []; |
|---|
| 327 | context = context || window.content.document; |
|---|
| 328 | var nodesSnapshot = ( |
|---|
| 329 | context.ownerDocument || |
|---|
| 330 | context |
|---|
| 331 | ).evaluate( |
|---|
| 332 | xpath, |
|---|
| 333 | context, |
|---|
| 334 | nsresolver || self.nsResolver, |
|---|
| 335 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, |
|---|
| 336 | null |
|---|
| 337 | ); |
|---|
| 338 | |
|---|
| 339 | for (let i = 0, l = nodesSnapshot.snapshotLength; i < l; i++) { |
|---|
| 340 | ret.push(nodesSnapshot.snapshotItem(i)); |
|---|
| 341 | } |
|---|
| 342 | return ret; |
|---|
| 343 | }, |
|---|
| 344 | |
|---|
| 345 | parseBookmarkItem: function(item) { |
|---|
| 346 | let parsed = { |
|---|
| 347 | title: self.evaluateXPath("./rss:title", item)[0].textContent, |
|---|
| 348 | creator: self.evaluateXPath("./dc:creator", item)[0].textContent, |
|---|
| 349 | date: self.evaluateXPath("./dc:date", item)[0].textContent, |
|---|
| 350 | comment: self.evaluateXPath("./rss:description", item)[0].textContent, |
|---|
| 351 | tags: self.evaluateXPath("./dc:subject", item).map(function(i) i.textContent).join(",") |
|---|
| 352 | }; |
|---|
| 353 | return parsed; |
|---|
| 354 | }, |
|---|
| 355 | |
|---|
| 356 | reportBookmarkedItem: function(item) { |
|---|
| 357 | liberator.dump(item); |
|---|
| 358 | (getNotifier())( |
|---|
| 359 | item.title, |
|---|
| 360 | item.creator + " bookmarked at " + item.date + "\n" + |
|---|
| 361 | item.tags + ":" + item.comment, |
|---|
| 362 | 'http://www.hatena.ne.jp/users/' + item.creator.substr(0, 2) + '/' + item.creator + '/profile.gif' |
|---|
| 363 | ); |
|---|
| 364 | } |
|---|
| 365 | }; |
|---|
| 366 | // }}} |
|---|
| 367 | return self; |
|---|
| 368 | })(); |
|---|
| 369 | |
|---|
| 370 | // Awaking the watch dog. |
|---|
| 371 | publics.startWatching(); |
|---|
| 372 | liberator.dump("Watch dog is awaking ..."); |
|---|
| 373 | // vim: sw=2 ts=2 et fdm=marker |
|---|