diff options
author | anekos | 2011-02-25 23:40:00 +0900 |
---|---|---|
committer | anekos | 2011-02-25 23:40:00 +0900 |
commit | d3f0d80f8061e3894f09fb1f63dfc8c641548665 (patch) | |
tree | 04b831748606fd68c7f8871fcf27317808838b42 /usi.js | |
parent | 0d81355ed39b363b1a81400f1106c9ceb9be0c71 (diff) | |
download | vimperator-plugins-d3f0d80f8061e3894f09fb1f63dfc8c641548665.tar.bz2 |
for Remember the milk.
Diffstat (limited to 'usi.js')
-rw-r--r-- | usi.js | 722 |
1 files changed, 722 insertions, 0 deletions
@@ -0,0 +1,722 @@ +/* NEW BSD LICENSE {{{ +Copyright (c) 2011, anekos. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + + +################################################################################### +# http://sourceforge.jp/projects/opensource/wiki/licenses%2Fnew_BSD_license # +# に参考になる日本語訳がありますが、有効なのは上記英文となります。 # +################################################################################### + +}}} */ + +// INFO {{{ +let INFO = +<> + <plugin name="usi.js" version="1.0.0" + href="http://vimpr.github.com/" + summary="for Remember The Milk." + lang="en-US" + xmlns="http://vimperator.org/namespaces/liberator"> + <author email="anekos@snca.net">anekos</author> + <license>New BSD License</license> + <project name="Vimperator" minVersion="3.0"/> + <p>See ( ◕ ‿‿ ◕ ) the completions.</p> + <item> + <tags>:usi</tags> + <spec>:usi</spec> + <description><p></p></description> + </item> + </plugin> +</>; +// }}} + +(function () { + // Constants {{{ + const AppName = 'usi'; + const APIKey = '0ac1fa2a426b535212518bf9a9e55e23'; + const APISecret = '75a925384404568d'; + const Save = storage.newMap(AppName, {store: true}); + + // }}} + + // Cache {{{ + + CacheAge = 1000 * 60; + + const StorageCache = (function () { + let store = storage.newMap(AppName + '-cache', {store: true}); + + return { + get: function (key) let (v = store.get(key)) (v && v.value), + + set: function (key, value, age) + store.set(key, {value: value, expire: new Date().getTime() + (age || CacheAge)}), + + remove: function (key) store.remove(key), + + clear: function () store.clear(), + + has: function (key) { + const Nothing = {}; + let found = store.get(key, Nothing); + return (found !== Nothing) && found && (found.expire - new Date().getTime() > 0); + } + }; + })(); + + const Cache = StorageCache; + + const CompletionCache = (function (key) { + const cache = {}; + + return { + get: function (key, args) { + return cache[key][parseInt(args.string, 10)]; + }, + + remove: function (key) { + delete cache[key]; + }, + + complete: function (key, context, args, items) { + context.compare = void 0; + context.completions = [ + [i + ': ' + name, desc] + for ([i, [name, desc, value]] in Iterator(items)) + ]; + cache[key] = items.map(function ([,, v]) v); + } + }; + + })(); + + const Transactions = (function () { + let data = storage.newArray(AppName + '-transaction', {store: true}); + + return { + __iterator__: function () Iterator(data), + + push: function (id, desc) { + data.push({id: id, desc: desc}); + }, + + pop: function (index) { + data.get(index); + data.truncate(index); + Cache.clear(); + } + }; + })(); + + // }}} + + const Utils = { // {{{ + httpGet: function (url, onComplete) { + let xhr = new XMLHttpRequest(); + xhr.open('GET', url, !!onComplete); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status == 200) + return onComplete(xhr); + }; + xhr.send(); + }, + + md5: function (str) { + function toHexString (charCode) + ("0" + charCode.toString(16)).slice(-2); + + const conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); + const crypto = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + + conv.charset = 'UTF-8'; + let result = {}; + let data = conv.convertToByteArray(str, result); + + crypto.init(crypto.MD5); + crypto.update(data, data.length); + + let binCode = crypto.finish(false); + return [toHexString(binCode.charCodeAt(i)) for(i in binCode)].join(""); + }, + + copy: function (obj) { + let result = {}; + for (let [k, v] in Iterator(obj)) + result[k] = v; + return result; + }, + + joinParams: function (params) { + return [ + encodeURIComponent(k) + '=' + encodeURIComponent(v) + for ([k, v] in Iterator(params)) + ].join('&'); + }, + + echo: function (msg) { + liberator.echo('[' + AppName + '] ' + msg); + }, + + echoerr: function (msg) { + liberator.echoerr('[' + AppName + '] ' + msg); + }, + + log: function (msg) { + liberator.log( + '[' + AppName + '] ' + ( + typeof msg == 'string' ? msg : + msg instanceof XML ? msg : msg + ) + ); + }, + + toDate: function (date) { + let dateStr = String(date); + return dateStr && new Date(String(dateStr)); + }, + + toSmartDateText: function (target) { + const Hour = 1000 * 60 * 60; + const Day = Hour * 24; + + function cutTime (date) { + return new Date(date.getYear(), date.getMonth(), date.getDate()); + } + + function beforeAfter (n) { + return (n > 0 ? 'after' : 'before'); + } + + target = Utils.toDate(target); + if (!target) + return ''; + + let now = new Date(); + let [targetDay, nowDay] = [cutTime(target), cutTime(now)]; + let dDay = (targetDay.getTime() - nowDay.getTime()) / Day; + let dHour = Math.floor((target.getTime() - now.getTime()) / Hour); + + let base = target.getHours() === 0 ? target.toLocaleDateString() + : target.toLocaleString(); + let prefix; + if (dDay == 0){ + prefix = Math.abs(dHour) + ' hours ' + beforeAfter(dHour); + } else if (dDay == -1) { + prefix = 'Yesterday'; + } else if (dDay == 1) { + prefix = 'Tomorrow'; + } else if (Math.abs(dDay) <= 7) { + prefix = Math.abs(dDay) + ' days ' + beforeAfter(dDay); + } + //Utils.log([result, dHour, dDay, targetDay, nowDay]); + return prefix ? prefix + ' - ' + base : base; + } + }; // }}} + + const Cow = { // {{{ + get: function (_params, {onComplete, onFail, cache, timeline, pre}) { // {{{ + function toResult (text) + (new XMLList(text)); + + let args = arguments; + let params = Utils.copy(_params); + let key; + + if (!onComplete) + onComplete = function () Utils.echo('Success: ' + params.method); + + if (!onFail) + onFail = function (result) { + let msg = params.method + ' was failed: ' + result.err.@msg; + Utils.echoerr(msg); + Utils.log(msg); + if (Save.get('authorized') && result.err.@code == "98") { + Save.clear(); + Save.save(); + Cow.openAuthPage(); + } + }; + + if (!pre && !Save.get('authorized')) + return Cow.getToken(function () Cow.get.apply(Cow, args)); + + + if (pre) { + // key = Utils.joinParams(params); + } else { + params.auth_token = Save.get('token'); + + if (cache && Cache.has(cache)) { + Utils.log('Get from cache: ' + cache); + return onComplete(toResult(Cache.get(cache))); + } + + if (timeline) { + let timelineValue = Save.get('timeline'); + if (timelineValue) { + params.timeline = timelineValue; + } else { + Utils.log('create timeline'); + Cow.get( + { + method: 'rtm.timelines.create', + }, + { + onComplete: function (result) { + let timeline = result.timeline; + Save.set('timeline', timeline); + Cow.get.apply(Cow, args); + } + } + ); + return; + } + } + } + + + let url = Cow.makeURL(params); + Utils.log('Get from remote: ' + url); + Utils.httpGet( + url, + function (xhr) { + let text = xhr.responseText.replace(/^<\?[^\?]+\?>/, ''); + let result = toResult(text); + if (result.@stat == 'ok') { + if (!pre) + Cache.set(cache, text); + onComplete(result); + } else { + onFail(result); + } + } + ); + }, // }}} + + makeURL: function (_params, base) { // {{{ + function makeSig (params) { + let keys = [k for ([k] in Iterator(params))]; + keys.sort(); + + let paramString = [ + k + params[k] + for ([, k] in Iterator(keys)) + ].join(''); + + return Utils.md5(APISecret + paramString); + } + + let params = Utils.copy(_params); + params.api_key = APIKey; + params.api_sig = makeSig(params); + let paramString = Utils.joinParams(params); + return (base || 'http://api.rememberthemilk.com/services/rest/') + '?' + paramString; + }, // }}} + + openAuthPage: function () { + Utils.log('openAuthPage'); + Cow.get( + { + method: 'rtm.auth.getFrob', + }, + { + pre: true, + onComplete: function (result) { + let frob = String(result.frob); + Save.set('frob', frob); + Save.save(); + Utils.log('Got frob: ' + frob); + let url = + Cow.makeURL( + { + perms: 'delete', + frob: frob + }, + 'http://www.rememberthemilk.com/services/auth/' + ); + liberator.open(url, liberator.NEW_TAB); + } + } + ); + }, + + getToken: function (onComplete) { + Utils.log('Start to get token'); + + Cow.get( + { + method: 'rtm.auth.getToken', + frob: Save.get('frob') + }, + { + pre: true, + onComplete: function (result) { + Save.set('token', String(result.auth.token)); + Save.set('user.id', String(result.auth.user.@id)); + Save.set('user.name', String(result.auth.user.@name)); + Save.set('user.fullname', String(result.auth.user.@fullname)); + Save.set('authorized', true); + Save.save(); + + onComplete(); + }, + + onFail: function (result) { + Utils.log(result); + Cow.openAuthPage(); + } + } + ); + }, + + checkAuth: function (onAuthorized) { + Cow.get( + { + method: 'rtm.auth.checkToken', + auth_token: Save.get('token') + }, + {} + ) + } + }; // }}} + + // Command maker {{{ + + function TaskActionOnComplete (text) { + return function (result) { + let echoText = text + ': ' + result.list.taskseries.@name; + let due = Utils.toSmartDateText(result.list.taskseries.task.@due); + if (due) + echoText += ' (' + due + ')'; + if (result.transaction.@undoable) + Transactions.push(result.transaction.@id, echoText); + Utils.echo(echoText); + } + } + + function SelectorCommand ({names, cache, description, action, onComplete, timeline, completionMethod, completionList}) { // {{{ + let ccKey = names + ':' + Utils.md5(Error().stack); + return new Command( + names instanceof Array ? names : [names], + description, + function (args) { + Cow.get( + action(CompletionCache.get(ccKey, args)), + { + timeline: timeline, + onComplete: onComplete + } + ); + CompletionCache.remove(ccKey); + if (typeof cache === 'string') + Cache.remove(cache); + }, + { + literal: 0, + completer: function (context, args){ + context.incomplete = true; + Cow.get( + completionMethod, + { + cache: cache || true, + onComplete: function (result) { + context.incomplete = false; + CompletionCache.complete(ccKey, context, args, completionList(result)); + } + } + ); + } + } + ); + } // }}} + + function TaskSelectorCommand ({key, method, filter, cache, names, description, onComplete}) { // {{{ + return SelectorCommand({ + key: key, + names: names, + description: description, + cache: cache || 'rtm.tasks.getList?filter=status:incomplete', + timeline: true, + action: function ([list, taskseries, task]) { + return { + method: 'rtm.' + key, + list_id: list.@id, + taskseries_id: taskseries.@id, + task_id: task.@id + }; + }, + onComplete: onComplete, + completionMethod: { + method: 'rtm.tasks.getList', + filter: filter || 'status:incomplete' + }, + completionList: function (result) { + let cs = []; + for (let [, list] in Iterator(result.tasks.list)) { + for (let [, taskseries] in Iterator(list.taskseries)) { + for (let [, task] in Iterator(taskseries.task)) { + cs.push([ + let (d = Utils.toDate(task.@due)) + (d ? d.getTime() : Infinity), + [taskseries.@name, Utils.toSmartDateText(task.@due), [list, taskseries, task]] + ]); + } + } + } + + // 現在に近い順に並べます + let n = new Date().getTime(); + cs = cs.sort(function ([a], [b]) Math.abs(a - n) - Math.abs(b - n)).map(function ([a, b]) b); + return cs; + } + }); + } // }}} + // }}} + + // Level 3 {{{ + + TaskSubCommands = [ // {{{ + // add {{{ + new Command( + ['a[dd]'], + 'Add a task', + function (args) { + Cow.get( + { + method: 'rtm.tasks.add', + parse: 1, + name: args.literalArg + }, + { + timeline: true, + onComplete: TaskActionOnComplete('Task was added') + } + ) + }, + { + literal: 0, + completer: function (context, args) { + let SmartAddCompleter = { + '#': function () { + context.incomplete = true; + Cow.get( + { + method: 'rtm.lists.getList', + }, + { + cache: 'lists.getList', + onComplete: function (result) { + context.completions = [ + [v.@name, v.@id] + for ([k, v] in Iterator(result.lists.list)) + ]; + context.incomplete = false; + } + } + ); + }, + + '*': function () { + // FIXME 数字含みのパターンをちゃんと補完する + const Items = 'daily, weekly, biweekly, monthly, yearly, after 1 day, after 1 week, after 1 year'; + context.completions = [ + [v, v] + for ([, v] in Iterator(Items.split(/,\s*/))) + ]; + }, + + '=': function () { + // FIXME 数字含みのパターンをちゃんと補完する + const Items = '1 min, 5 min, 1 hr'; + context.completions = [ + [v, v] + for ([, v] in Iterator(Items.split(/,\s*/))) + ]; + }, + + '!': function () { + context.completions = [ + ['1', 'High priority'], + ['2', 'Middle priority'], + ['3', 'Low priority'] + ]; + }, + + 'http': function () { + const Items = 'http://snca.net/, http://kurinton.net/'; + context.completions = [ + [v, v] + for ([, v] in Iterator(Items.split(/,\s*/))) + ]; + }, + }; + + // FIXME http が補完できない + let left = args.string.slice(0, context.caret); + let m = /(?:^|\s)([#!@=*^]|http)([^#!@=*^]*)$/(left); + if (m) { + let completer = SmartAddCompleter[m[1]]; + if (completer) { + context.compare = void 0; + let pos = left.length - m[1].length - m[2].length + (m[1].length == 1 ? 1 : 0); + context.advance(pos); + completer(); + return; + } + } + } + } + ), // }}} + // complete {{{ + TaskSelectorCommand({ + key: 'tasks.complete', + names: 'c[omplete]', + description: 'Complete a task', + onComplete: TaskActionOnComplete('Task was completed') + }), // }}} + // uncomplete {{{ + TaskSelectorCommand({ + key: 'tasks.uncomplete', + names: 'u[ncomplete]', + description: 'Uncomplete a task', + cache: 'rtm.tasks.getList?filter=status:completed', + filter: 'status:completed', + onComplete: TaskActionOnComplete('Task was uncompleted') + }), // }}} + // delete {{{ + TaskSelectorCommand({ + key: 'tasks.delete', + names: 'd[elete]', + description: 'Delete a task', + onComplete: TaskActionOnComplete('Task was deleted') + }), // }}} + // postpone {{{ + TaskSelectorCommand({ + key: 'tasks.postpone', + names: 'p[ostpone]', + description: 'Postpone a task', + onComplete: TaskActionOnComplete('Task was postponed') + }) // }}} + ]; // }}} + + TransactionSubCommands = [ // {{{ + let (ccKey = 'transaction/undo') + new Command( + ['undo'], + 'Undo your ooops', + function (args) { + let [i, tId] = CompletionCache.get(ccKey, args); + Cow.get( + { + method: 'rtm.transactions.undo', + transaction_id: tId + }, + { + timeline: true, + onComplete: function (result) { + Utils.echo('Task was undid.'); + Cache.clear(); + } + } + ); + }, + { + literal: 0, + completer: function (context, args) { + CompletionCache.complete( + ccKey, context, args, + [ + [t.desc, '', [i, t.id]] + for ([i, t] in Iterator(Transactions)) + ] + ); + } + } + ) + ]; // }}} + + // }}} + + // Level 2 {{{ + + MainSubCommands = [ + new Command( + ['t[ask]'], + 'Task control', + function (args) { + }, + { + subCommands: TaskSubCommands + } + ), + new Command( + ['c[ache]'], + 'Cache control', + function (args) { + }, + { + subCommands: [ + new Command( + ['clear'], + 'Clear all cache data', + function () Cache.clear() + ) + ] + } + ), + new Command( + ['tr[ansaction]', 'T'], + 'Transaction control', + function () { + }, + { + subCommands: TransactionSubCommands + } + ) + ]; + + // }}} + + // Level 1 {{{ + commands.addUserCommand( + ['usi'], + 'for Remember The Milk', + function (args) { + if (!Save.get('authorized')) + Cow.openAuthPage(); + }, + { + subCommands: MainSubCommands + }, + true + ); + // }}} + +})(); + +// vim:sw=2 ts=2 et si fdm=marker: + + |