IT戦記

プログラミング、起業などについて書いているプログラマーのブログです😚

ロケーションバーに直入力するとブクマを見に行って補完してくれるコンポーネント作った

今日徹夜ぎみで作ってみた



でも

使ってみたら逆に不便だった><
勉強になったからいいや。
破棄!

もったいないので、今手元にあるソースを貼っときます

このソースを Firefox インストールディレクトリ以下の components に入れると textbox 要素で autocomplete="delicious" が使えるようになります。
開発中のものなのでバグありまくりです。
ちなみに、僕が一日に書けるコードはちょうどこのくらいです↓

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const CLASS_ID = Components.ID('{aa892eb4-ffbf-477d-9f9a-06c995ae9f85}');
const CONSTRACT_ID = '@mozilla.org/autocomplete/search;1?name=delicious';

var SocialBookmark = function(href, description, hash, tags, date) {
    this.href = href;
    this.description = description;
    this.hash = hash;
    this.tags = tags.split(/\s+/);
    this.date = date;
};

var SocialBookmarkAPI = {
    username: null,
    password: null,
    _parseBookmarkXML: function(data) {
        var posts = data.getElementsByTagName('post');
        var bookmarks = [];
        for (var i = 0, l = posts.length; i < l; i ++) {
            var post = posts[i];
            bookmarks.push(
                new SocialBookmark(
                    post.getAttribute('href'),
                    post.getAttribute('description'),
                    post.getAttribute('hash'),
                    post.getAttribute('tag'),
                    post.getAttribute('time')));
                
        }
        return bookmarks;
    },
    _parseCountXML: function(date, data) {
        var dates = data.getElementsByTagName('date');
        if (!date) {
            date = dates[dates.length - 1].getAttribute('date');
        }
        else {
            date = date.split(/T/)[0];
        }
        var count = 0;
        for (var i = 0, l = dates.length; i < l; i ++) {
            count += +dates[i].getAttribute('count');
            if (date == dates[i].getAttribute('date')) {
                break;
            }
        }
        return count;
    },
    getBookmarks: function(callback) {
        var self = this;
        this._get('https://api.del.icio.us/v1/posts/all', function(data) {
        //this._get('https://api.del.icio.us/v1/posts/recent?count=100', function(data) {
            if (data) {
                callback(self._parseBookmarkXML(data));
            }
            else {
                callback(null);
            }
        });
    },
    getBookmarksFrom: function(date, callback) {
        var self = this;
        this._get('https://api.del.icio.us/v1/posts/dates', function(data) {
            if (data) {
                var count = self._parseCountXML(date, data);
                self._get('https://api.del.icio.us/v1/posts/recent?count=' + count, function(data) {
                    if (data) {
                        callback(self._parseBookmarkXML(data));
                    }
                    else {
                        callback(null);
                    }
                });
            }
            else {
                callback(null);
            }
        });
    },
    getRecentHash: function(callback) {
        var self = this;
        this._get('https://api.del.icio.us/v1/posts/recent?count=1', function(data) {
            if (data) {
                callback(self._parseBookmarkXML(data)[0].hash);
            }
            else {
                callback(null);
            }
        });
    },
    abort: function() {
        if (this._request) {
            this._request.abort();
            this._request = null;
        }
    },
    _get: function(uri, callback) {
        dump('_get(' + uri + ')\n');
        var self = this;
        var request = this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
        request.open('GET',uri,true,this.username,this.password);
        request.send(null);
        request.onreadystatechange = function() {
            if (!self._request || self._request.readyState != 4) {
                return;
            }
            try {
                var request = self._request;
                self._request = null;
                switch (request.status) {
                case 503: // Del.icio.us のサーバから蹴られた場合
                    dump(request.status + '\n');
                    callback(request.responseXML);
                    break;
                case 200: // OK の場合
                    callback(request.responseXML);
                    break;
                default:
                    dump(request.status + '\n');
                    callback(request.responseXML);
                    break;
                }
            }
            catch(e) {
                callback(null);
            }
        };
    },
    _setBasicData: function(request, username, password) {
        request.setRequestHeader('Authorization', 'Basic ' + this._toBase64(username + ':' + password));
    },
    _setWsseData: function(request, username, password) {
        var created = this._iso8601Date(new Date());
        var nonce = (Math.random() + "").substr(2, 32);
        var data = nonce + created + (password || '');

        var converter = Cc['@mozilla.org/intl/scriptableunicodeconverter'].createInstance(Ci.nsIScriptableUnicodeConverter);
        converter.charset = 'UTF-8';
        data = converter.convertToByteArray(data, {});
        var ch = Cc['@mozilla.org/security/hash;1'].createInstance(Ci.nsICryptoHash);
        ch.init(ch.SHA1);
        ch.update(data, data.length);
        var data = ch.finish(false);

        var passwordDigest = this._toBase64(data);
        nonce = this._toBase64(nonce);

        var wsse = 'UsernameToken Username="' + username + '", PasswordDigest="' + passwordDigest + '", Created="' + created + '", Nonce="' + nonce + '"';

        request.setRequestHeader('Authorization', 'WSSE profile="UsernameToken"');
        request.setRequestHeader('X-WSSE', wsse);
    },
    _iso8601Date: function(date) {
        var datetime = date.getUTCFullYear();
        var month = String(date.getUTCMonth() + 1);
        datetime += (month.length == 1 ?  '0' + month : month);
        var day = date.getUTCDate();
        datetime += (day < 10 ? '0' + day : day);
        datetime += 'T';
        var hour = date.getUTCHours();
        datetime += (hour < 10 ? '0' + hour : hour) + ':';
        var minutes = date.getUTCMinutes();
        datetime += (minutes < 10 ? '0' + minutes : minutes) + ':';
        var seconds = date.getUTCSeconds();
        datetime += (seconds < 10 ? '0' + seconds : seconds);
        return datetime;
    },
    _toBase64: function(data) {
        const toBase64Table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
        const base64Pad = '=';

        var result = '';
        var length = data.length;

        for (var i = 0; i < (length - 2); i += 3) {
            result += toBase64Table[data.charCodeAt(i) >> 2];
            result += toBase64Table[((data.charCodeAt(i) & 0x03) << 4) + (data.charCodeAt(i+1) >> 4)];
            result += toBase64Table[((data.charCodeAt(i+1) & 0x0f) << 2) + (data.charCodeAt(i+2) >> 6)];
            result += toBase64Table[data.charCodeAt(i+2) & 0x3f];
        }

        if (length%3) {
            i = length - (length%3);
            result += toBase64Table[data.charCodeAt(i) >> 2];
            if ((length%3) == 2) {
                result += toBase64Table[((data.charCodeAt(i) & 0x03) << 4) + (data.charCodeAt(i+1) >> 4)];
                result += toBase64Table[(data.charCodeAt(i+1) & 0x0f) << 2];
                result += base64Pad;
            }
            else {
                result += toBase64Table[(data.charCodeAt(i) & 0x03) << 4];
                result += base64Pad + base64Pad;
            }
        }
        return result;
    }

};

var SocialBookmarkUrlCache = function(username, password, callback) {
    SocialBookmarkAPI.username = username;
    SocialBookmarkAPI.password = password;
    var self = this;
    this.bookmarks = [];
    this.load();
    this.hashes = {};
    this.update(function(result) {
        callback(result);
    });
};
SocialBookmarkUrlCache.prototype = {
    updateAll: function(callback) {
        var self = this;
        SocialBookmarkAPI.abort();
        SocialBookmarkAPI.getBookmarks(function(bookmarks) {
            if (bookmarks) {
                self._appendBookmarks(bookmarks);
                callback(true);
            }
            else {
                callback(false);
            }
        });
    },
    update: function(callback) {
        if (!this.loaded) {
            return this.updateAll(callback)
        }
        var self = this;
        SocialBookmarkAPI.abort();
        SocialBookmarkAPI.getRecentHash(function(hash) {
            if (hash && (!self.recentHash || self.recentHash != hash)) { // 更新されていたら
                SocialBookmarkAPI.getBookmarksFrom(self.recentDate, function(bookmarks) {
                    if (bookmarks) {
                        self._appendBookmarks(bookmarks);
                        callback(true);
                    }
                    else {
                        callback(false);
                    }
                });
            }
            else {
                callback(true);
            }
        });
    },
    _file: function() {
        var file = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties).get("ProfD", Ci.nsILocalFile);
        file.append("social_bookmark_autocomplete_cache.js");

        return file; 
    },
    load: function() {
        var file = this._file();

        if (!file.exists()) {
            return;
        }

        var data = "";
        var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
        var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);

        fstream.init(file, -1, 0, 0);
        sstream.init(fstream); 
        
        var str = sstream.read(4096);
        while (str.length > 0) {
            data += str;
            str = sstream.read(4096);
        }
        
        sstream.close();
        fstream.close();

        this.bookmarks = eval(data);
        if (this.bookmarks[0]) {
            this.recentHash = this.bookmarks[0].hash;
            this.recentDate = this.bookmarks[0].date;
        }
        this.loaded = true;
    },
    save: function() {
        var file = this._file();
        if (file.exists()) {
            file.remove(false);
        }
        var stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
        stream.init(file, 0x02 | 0x08 | 0x20, 420, -1);
        var data = this.bookmarks.toSource();
        stream.write(data, data.length);
        stream.close();
        this.loaded = true;
    },
    createSuggestAutoCompleteResult: function(searchString) {
        var self = this;
        var results = [];
        var comments = [];
        this.bookmarks.forEach(function(bm) {
            if (bm.href.indexOf(searchString, 0) != -1) {
                results.push(bm.href);
                comments.push(bm.tags.join(' '));
            }
            else if (bm.tags.some(function(tag) { return tag.indexOf(searchString) != -1 })) {
                results.push(bm.href);
                comments.push(bm.tags.join(' '));
            }
        });
        return new SocialBookmarkSuggestAutoCompleteResult(
                searchString,
                Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
                0,
                '',
                results,
                comments);
    },
    _appendBookmarks: function(bookmarks) {
        bookmarks = bookmarks.reverse();
        for (var i = 0, l = bookmarks.length; i < l; i ++) {
            this._appendBookmark(bookmarks[i]);
        }
        this.save();
        this.recentHash = this.bookmarks[0].hash;
        this.recentDate = this.bookmarks[0].date;
    },
    _appendBookmark: function(bookmark) {
        if (!this.hashes[bookmark.hash]) {
            this.hashes[bookmark.hash] = true;
            this.bookmarks.unshift(bookmark);
        }
    }
};

var SocialBookmarkSuggestAutoCompleteResult = function(
                                                searchString,
                                                searchResult,
                                                defaultIndex,
                                                errorDescription,
                                                results,
                                                comments) {
    this._searchString = searchString;
    this._searchResult = searchResult;
    this._defaultIndex = defaultIndex;
    this._errorDescription = errorDescription;
    this._matchCount = results.length;
    this._results = results;
    this._comments = comments;
};
SocialBookmarkSuggestAutoCompleteResult.prototype = {
    _className: 'SocialBookmarkSuggestAutoCompleteResult',
    get searchString() {
        return this._searchString;
    },
    get searchResult() {
        return this._searchResult;
    },
    get defaultIndex() {
        return this._defaultIndex;
    },
    get errorDescription() {
        return this._errorDescription;
    },
    get matchCount() {
        return this._matchCount;
    },
    getValueAt: function(index) {
        return this._results[index];
    },
    getCommentAt: function(index) {
        return this._comments[index];
    },
    getStyleAt: function(index) {
        return 'hoge';
    },
    removeValueAt: function(rowIndex, removeFrom) {
    },
    QueryInterface: function(iid) {
        if (!iid.equals(Ci.nsIAutoCompleteResult) &&
            !iid.equals(Ci.nsISupports)) {
            throw Cr.NS_ERROR_NO_INTERFACE;                        
        }
        return this;
    }
}

var SocialBookmarkSuggestAutoComplete = function() {
    this._busy = true;
    this._available = true;
    self = this;
    this._cache = new SocialBookmarkUrlCache('amachang', '*********', function(result) {
        if (!result) {
            self._available = false;
        }
        self._busy = false;
    });
};
SocialBookmarkSuggestAutoComplete.prototype = {
    _className: 'SocialBookmarkSuggestAutoComplete',
    startSearch: function(searchString, searchParam, previousResult, listener) {
        if (!this._busy && this._available) {
            var cache = this._cache;
            var self = this;
            cache.update(function(result) {
                if (!result) {
                    self._available = false;
                }
                else {
                    listener.onSearchResult(self, cache.createSuggestAutoCompleteResult(searchString));
                }
            });
        }
    },
    stopSearch: function() {
    },
    QueryInterface: function(iid) {
        if (!iid.equals(Ci.nsIAutoCompleteSearch) &&
            !iid.equals(Ci.nsIAutoCompleteObserver) &&
            !iid.equals(Ci.nsISupports)) {
            throw Cr.NS_ERROR_NO_INTERFACE;                        
        }
        return this;
    }
};

function NSGetModule(compMgr, fileSpec) {
    dump('HA: Call NSGetModule\n');
    return {
        registerSelf: function(compMgr, fileSpec, location, type) {
            dump('HA: Call registerSelf\n');
            compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
            compMgr.registerFactoryLocation(
                CLASS_ID,
                'Social Bookmark Suggestions',
                CONSTRACT_ID,
                fileSpec,
                location,
                type);
        },
        getClassObject: function(compMgr, cid, iid) {
            return {
                createInstance: function(outer, iid) {
                    if (outer != null) {
                        throw Cr.NS_ERROR_NO_AGGREGATION;
                    }
                    return new SocialBookmarkSuggestAutoComplete().QueryInterface(iid);
                }
            };
        }
    };
}

dump('HA: Load\n');