From c5fe0ff17ecc591b2e803828cac64f651226370d Mon Sep 17 00:00:00 2001 From: haad Date: Sun, 17 Jan 2016 18:10:16 +0800 Subject: [PATCH] Add .remove() functionality, add more test (channel info) --- HashCacheItem.js | 15 ++++-- OrbitClient.js | 103 +++++++++++++++++++++++++------------ README.md | 28 +++++----- examples/readMessages.js | 2 +- examples/writeMessages.js | 26 ++++++++-- test/orbit-client-tests.js | 99 +++++++++++++++++++++++++++-------- 6 files changed, 197 insertions(+), 76 deletions(-) diff --git a/HashCacheItem.js b/HashCacheItem.js index 2fe3553..aef3d83 100644 --- a/HashCacheItem.js +++ b/HashCacheItem.js @@ -1,9 +1,15 @@ 'use strict'; -var encryption = require('./Encryption'); +let encryption = require('./Encryption'); + +let HashCacheOps = { + Add: "ADD", + Delete: "DELETE" +} class HashCacheItem { - constructor(sequenceNumber, targetHash, metaInfo) { + constructor(operation, sequenceNumber, targetHash, metaInfo) { + this.op = operation; this.seq = sequenceNumber; this.target = targetHash; this.meta = metaInfo; @@ -11,8 +17,8 @@ class HashCacheItem { } class EncryptedHashCacheItem extends HashCacheItem { - constructor(sequenceNumber, targetHash, metaInfo, publicKey, privateKey, salt) { - super(sequenceNumber, targetHash, metaInfo); + constructor(operation, sequenceNumber, targetHash, metaInfo, publicKey, privateKey, salt) { + super(operation, sequenceNumber, targetHash, metaInfo); this.pubkey = publicKey; try { this.target = encryption.encrypt(targetHash, privateKey, publicKey); @@ -26,6 +32,7 @@ class EncryptedHashCacheItem extends HashCacheItem { } module.exports = { + HashCacheOps: HashCacheOps, HashCacheItem: HashCacheItem, EncryptedHashCacheItem: EncryptedHashCacheItem }; \ No newline at end of file diff --git a/OrbitClient.js b/OrbitClient.js index 69e80b2..b8573c5 100644 --- a/OrbitClient.js +++ b/OrbitClient.js @@ -6,6 +6,7 @@ var ipfsDaemon = require('./ipfs-daemon'); var ipfsAPI = require('./ipfs-api-promised'); var HashCache = require('./HashCacheClient'); var HashCacheItem = require('./HashCacheItem').EncryptedHashCacheItem; +var HashCacheOps = require('./HashCacheItem').HashCacheOps; var MetaInfo = require('./MetaInfo'); var ItemTypes = require('./ItemTypes'); var Keystore = require('./Keystore'); @@ -24,12 +25,18 @@ class OrbitClient { } channel(hash, password) { + if(password === undefined) password = ''; return { + info: (options) => this._info(hash, password), iterator: (options) => this._iterator(hash, password, options), - send: (text, options) => { + put: (text, options) => { this.sequences[hash] = !this.sequences[hash] ? this._getChannelSequence(hash, password) : this.sequences[hash] + 1; return this._send(hash, password, text, options); }, + remove: (targetHash) => { + this.sequences[hash] = !this.sequences[hash] ? this._getChannelSequence(hash, password) : this.sequences[hash] + 1; + return this._remove(hash, password, targetHash); + }, delete: () => this._delete(hash, password), setMode: (mode) => this._setMode(hash, password, mode) } @@ -75,11 +82,11 @@ class OrbitClient { // Slice the array var startIndex = 0; var endIndex = messages.length; - if(limit > -1) { + if(limit < 0) { + endIndex = messages.length - (gt ? 1 : 0); + } else { startIndex = Math.max(0, messages.length - limit); endIndex = messages.length - ((gt || lt) ? 1 : 0); - } else if(limit === -1) { - endIndex = messages.length - (gt ? 1 : 0); } messages = messages.slice(startIndex, endIndex) @@ -100,16 +107,14 @@ class OrbitClient { return this; }, next: () => { - var item = { value: messages[currentIndex], done: false }; - if(currentIndex < messages.length) + var item = { value: null, done: true }; + if(currentIndex < messages.length) { + item = { value: messages[currentIndex], done: false }; currentIndex ++; - else - item = { value: null, done: true }; + } return item; }, - collect: () => { - return messages; - } + collect: () => messages } return iterator; @@ -119,40 +124,62 @@ class OrbitClient { var item = null; if(hash) { item = await (ipfsAPI.getObject(this.ipfs, hash)); - var data = JSON.parse(item.Data); + let data = JSON.parse(item.Data); // verify - var verified = Encryption.verify(data.target, data.pubkey, data.sig, data.seq, password); + const verified = Encryption.verify(data.target, data.pubkey, data.sig, data.seq, password); if(!verified) throw "Item '" + hash + "' has the wrong signature" // decrypt - var targetDec = Encryption.decrypt(data.target, privkey, 'TODO: pubkey'); - var metaDec = Encryption.decrypt(data.meta, privkey, 'TODO: pubkey'); + const targetDec = Encryption.decrypt(data.target, privkey, 'TODO: pubkey'); + const metaDec = Encryption.decrypt(data.meta, privkey, 'TODO: pubkey'); data.target = targetDec; data.meta = JSON.parse(metaDec); - item.Data = data; + + if(data.op === HashCacheOps.Add) { + const payload = await (ipfsAPI.getObject(this.ipfs, data.target)); + const contentEnc = JSON.parse(payload.Data)["content"]; + const contentDec = Encryption.decrypt(contentEnc, privkey, 'TODO: pubkey'); + item.Payload = contentDec; + } + + item.Data = data; } return item; } - _fetchRecursive(hash, password, amount, last, currentDepth) { + // TEMP + _contains(src, e) { + let contains = false; + src.forEach((a) => { + if(a.toString() === e.toString()) contains = true; + }); + return contains; + } + + _fetchRecursive(hash, password, amount, last, currentDepth, deleted) { var res = []; + var deletedItems = deleted ? deleted : []; if(!currentDepth) currentDepth = 0; - if(!last && amount > -1 && currentDepth >= amount) - return res; - var message = await (this._fetchOne(hash, password)); - res.push({ hash: hash, item: message }); - currentDepth ++; + if(message.Data.op === HashCacheOps.Add && !this._contains(deletedItems, hash)) { + res.push({ hash: hash, item: message }); + currentDepth ++; + } else if(message.Data.op === HashCacheOps.Delete) { + deletedItems.push(message.Data.target); + } if((last && hash === last)) return res; + if(!last && amount > -1 && currentDepth >= amount) + return res; + if(message && message.Links[0]) { - var next = this._fetchRecursive(message.Links[0].Hash, password, amount, last, currentDepth); + var next = this._fetchRecursive(message.Links[0].Hash, password, amount, last, currentDepth, deletedItems); res = res.concat(next); } @@ -165,29 +192,34 @@ class OrbitClient { return await (ipfsAPI.putObject(this.ipfs, JSON.stringify(post))); } - _createMessage(channel, password, post) { + _createMessage(channel, password, target, operation) { var seq = this.sequences[channel]; var size = -1; var metaInfo = new MetaInfo(ItemTypes.Message, size, new Date().getTime()); - var hcItem = new HashCacheItem(seq, post.Hash, metaInfo, pubkey, privkey, password); + var hcItem = new HashCacheItem(operation, seq, target, metaInfo, pubkey, privkey, password); var item = await (ipfsAPI.putObject(this.ipfs, JSON.stringify(hcItem))); var newHead = { Hash: item.Hash }; if(seq > 0) { - var iter = this._iterator(channel, password); - var prevHead = iter.next().value; - var headItem = await (ipfsAPI.getObject(this.ipfs, prevHead.hash)); + var prevHead = await(this.client.linkedList(channel, password).head()); + var headItem = await (ipfsAPI.getObject(this.ipfs, prevHead.head)); seq = JSON.parse(headItem.Data)["seq"] + 1; - newHead = await (ipfsAPI.patchObject(this.ipfs, item.Hash, prevHead.hash)) + newHead = await (ipfsAPI.patchObject(this.ipfs, item.Hash, prevHead.head)) + this.sequences[channel] = seq; } - return newHead; } _send(channel, password, text, options) { // TODO: check options for what type to publish as (text, snippet, file, etc.) var post = this._publish(text); - var message = this._createMessage(channel, password, post); + var message = this._createMessage(channel, password, post.Hash, HashCacheOps.Add); + await(this.client.linkedList(channel, password).add(message.Hash)); + return message.Hash; + } + + _remove(channel, password, target) { + var message = this._createMessage(channel, password, target, HashCacheOps.Delete); await(this.client.linkedList(channel, password).add(message.Hash)) return message.Hash; } @@ -204,7 +236,12 @@ class OrbitClient { m.push(modes); else m = modes; - return await(this.client.linkedList(channel, password).setMode(m)) + var res = await(this.client.linkedList(channel, password).setMode(m)); + return res.modes; + } + + _info(channel, password) { + return await(this.client.linkedList(channel, password).head()) } _connect(host, username, password) { @@ -215,9 +252,7 @@ class OrbitClient { name: this.client.info.name, config: this.client.info.config }; - return; } - } class OrbitClientFactory { diff --git a/README.md b/README.md index 4c159e2..96a5701 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,9 @@ Client library to interact with orbit-server. Implements the levelDOWN API witho orbit-server uses linked lists on top of IPFS. orbit-server not *yet* released, working on it. ### TODO -- local caching of messages -- channel.send() - - API for sending a file -- use HTTPS instead of HTTP (channel password are sent in plaintext atm) -- API for fetching message content +- Tests for .remove(...) +- Local caching of messages +- Use HTTPS instead of HTTP (channel password are sent in plaintext atm) - API for fetching user info - OrbitNetwork + channel system (join, part, pub/sub) @@ -23,14 +21,16 @@ orbit-server uses linked lists on top of IPFS. orbit-server not *yet* released, channel(name, password) - .send(text) + .put(text) + + .remove(hash) .iterator([options]) - .delete() - .setMode(modes) + .delete() + ## Usage ```javascript var async = require('asyncawait/async'); @@ -43,13 +43,15 @@ async(() => { var orbit = OrbitClient.connect(host, username, password); // OrbitClient var channelName = 'hello-world'; - var channelPwd = ''; // Send a message - var head = orbit.channel(channelName, channelPwd).send('hello'); // + var head = orbit.channel(channelName).send('hello'); // + + // Delete a message + orbit.channel(channelName).remove(head); // Iterator options - var options = { limit: -1 }; // fetch all messages + var options = { limit: -1 }; // fetch all messages, default is 1 // { // gt: , // gte: , @@ -60,7 +62,7 @@ async(() => { // } // Get messages - var iter = orbit.channel(channelName, channelPwd).iterator(options); // Symbol.iterator + var iter = orbit.channel(channelName).iterator(options); // Symbol.iterator var next = iter.next(); // { value: , done: false|true} // OR: @@ -70,7 +72,7 @@ async(() => { // Set modes var password = 'hello'; var channelModes; - channelModes = orbit.channel(channel, channelPwd).setMode({ mode: "+r", params: { password: password } }); // { modes: { r: { password: 'hello' } } } + channelModes = orbit.channel(channel).setMode({ mode: "+r", params: { password: password } }); // { modes: { r: { password: 'hello' } } } channelModes = orbit.channel(channel, password).setMode({ mode: "+w", params: { ops: [orbit.user.id] } }); // { modes: { ... } } channelModes = orbit.channel(channel, password).setMode({ mode: "-r" }); // { modes: { ... } } channelModes = orbit.channel(channel, '').setMode({ mode: "-w" }); // { modes: {} } diff --git a/examples/readMessages.js b/examples/readMessages.js index 032c3ea..9c3330e 100644 --- a/examples/readMessages.js +++ b/examples/readMessages.js @@ -29,7 +29,7 @@ let run = (async(() => { var iter = orbit.channel(channel, '').iterator(options); for(let i of iter) { - console.log(i.item.Data.seq, i.hash, "ts: " + i.item.Data.meta.ts); + console.log(i.item.Data.seq, i.item.Data.op, i.hash, "ts: " + i.item.Data.meta.ts, i.item.Payload); } console.log("Fetch messages took " + timer.stop() + "ms"); diff --git a/examples/writeMessages.js b/examples/writeMessages.js index a323ee9..52b14e2 100644 --- a/examples/writeMessages.js +++ b/examples/writeMessages.js @@ -18,16 +18,36 @@ let run = (async(() => { // Delete channel and its data var result = orbit.channel(channel, '').delete(); + // Add the first message and delete it immediately + orbit.channel(channel, '').put("hello world!"); + var e = orbit.channel(channel, '').iterator({ limit: -1 }).collect()[0].hash; + orbit.channel(channel, '').remove(e); + var messages = 100; - var i = 0; - while(i < messages) { + var i = 1; + while(i <= messages) { var timer = new Timer(true); // Send a message - var head = orbit.channel(channel, '').send(JSON.stringify({ omg: "hello" })); + // var head = orbit.channel(channel, '').send(JSON.stringify({ omg: "hello" })); + var head = orbit.channel(channel, '').put("hello world " + i); console.log(i, head, timer.stop() + "ms"); + + if(i === 4) { + console.log("remove", head); + orbit.channel(channel, '').remove(head); + } + i ++; } + var items = orbit.channel(channel, '').iterator({ limit: -1 }).collect(); + orbit.channel(channel, '').remove(items[2].hash); // 97 + orbit.channel(channel, '').remove(items[3].hash); // 96 + orbit.channel(channel, '').remove(items[66].hash); // 34 + orbit.channel(channel, '').remove(items[items.length - 10].hash); // 11 + orbit.channel(channel, '').remove(items[items.length - 9].hash); // 10 + orbit.channel(channel, '').remove(items[items.length - 8].hash); // 9 + } catch(e) { console.error("error:", e); console.error(e.stack); diff --git a/test/orbit-client-tests.js b/test/orbit-client-tests.js index 7684075..2c7fc0f 100644 --- a/test/orbit-client-tests.js +++ b/test/orbit-client-tests.js @@ -35,7 +35,7 @@ describe('Orbit Client', () => { let head = ''; let second = ''; let items = []; - let channel = 'abc1'; + let channel = 'abcde'; before(function(done) { // logger.setLevel('ERROR'); @@ -58,17 +58,21 @@ describe('Orbit Client', () => { // } var start = () => new Promise(async((resolve, reject) => { orbit = OrbitClient.connect(host, username, password); - orbit.channel(channel, 'hello').setMode({ mode: "-r" }) + // orbit.channel(channel, 'hello').setMode({ mode: "-r" }) resolve(); })); start().then(done); }); after(function(done) { + var deleteChannel = () => new Promise(async((resolve, reject) => { + orbit.channel(channel, '').delete(); + resolve(); + })); + deleteChannel().then(done); // server.shutdown(); // httpServer.close(); // rmDir(serverConfig.userDataPath); - done(); }); /* TESTS */ @@ -80,11 +84,54 @@ describe('Orbit Client', () => { assert.equal(orbit.network.id, 'anon-test'); assert.equal(orbit.network.name, 'Anonymous Networks TEST'); assert.notEqual(orbit.network.config.SupernodeRouting, null); - assert.equal(orbit.network.config.Bootstrap.length, 3); + assert.notEqual(orbit.network.config.Bootstrap.length, 0); done(); })); }); + describe('Info', function() { + it('gets channel info on empty channel', async((done) => { + var info = orbit.channel(channel, '').info(); + assert.notEqual(info, null); + assert.equal(info.head, null); + assert.notEqual(info.modes, null); + done(); + })); + + it('gets channel info on an existing channel', async((done) => { + var msg = orbit.channel(channel, '').put('hello'); + var info = orbit.channel(channel, '').info(); + assert.notEqual(info, null); + assert.equal(info.head, msg); + assert.notEqual(info.modes, null); + assert.equal(info.modes.r, null); + done(); + })); + + it('gets channel info when channel has modes set', async((done) => { + try { + orbit.channel(channel).delete(); + var mode = { + mode: "+r", + params: { + password: 'password' + } + }; + var res = orbit.channel(channel, '').setMode(mode) + var info = orbit.channel(channel, 'password').info(); + assert.notEqual(info, null); + assert.equal(info.head, null); + assert.equal(JSON.stringify(info.modes), JSON.stringify(res)); + orbit.channel(channel, 'password').delete(); + } catch(e) { + orbit.channel(channel, 'password').delete(); + assert.equal(e, null); + } + done(); + })); + + }); + describe('Delete', function() { it('deletes a channel from the database', async((done) => { var result = orbit.channel(channel, '').delete(); @@ -110,7 +157,7 @@ describe('Orbit Client', () => { describe('Insert', function() { it('adds an item to an empty channel', async((done) => { try { - head = orbit.channel(channel, '').send('hello'); + head = orbit.channel(channel, '').put('hello'); assert.notEqual(head, null); assert.equal(head.startsWith('Qm'), true); assert.equal(head.length, 46); @@ -122,7 +169,7 @@ describe('Orbit Client', () => { it('adds a new item to a channel with one item', async((done) => { try { - second = orbit.channel(channel, '').send('hello'); + second = orbit.channel(channel, '').put('hello'); assert.notEqual(second, null); assert.notEqual(second, head); assert.equal(second.startsWith('Qm'), true); @@ -136,7 +183,7 @@ describe('Orbit Client', () => { it('adds five items', async((done) => { for(var i = 0; i < 5; i ++) { try { - var s = orbit.channel(channel, '').send('hello'); + var s = orbit.channel(channel, '').put('hello'); assert.notEqual(s, null); assert.equal(s.startsWith('Qm'), true); assert.equal(s.length, 46); @@ -151,7 +198,7 @@ describe('Orbit Client', () => { try { var msg = new Buffer(512); msg.fill('a') - var s = orbit.channel(channel, '').send(msg.toString()); + var s = orbit.channel(channel, '').put(msg.toString()); assert.notEqual(s, null); assert.equal(s.startsWith('Qm'), true); assert.equal(s.length, 46); @@ -172,7 +219,7 @@ describe('Orbit Client', () => { var result = orbit.channel(channel, '').delete(); var iter = orbit.channel(channel, '').iterator(); for(var i = 0; i < itemCount; i ++) { - var s = orbit.channel(channel, '').send('hello'); + var s = orbit.channel(channel, '').put('hello' + i); items.push(s); } resolve(); @@ -183,8 +230,15 @@ describe('Orbit Client', () => { describe('Defaults', function() { it('returns an iterator', async((done) => { var iter = orbit.channel(channel, '').iterator(); + var next = iter.next().value; assert.notEqual(iter, null); - assert.notEqual(iter.next, null); + assert.notEqual(next, null); + assert.notEqual(next.item, null); + assert.notEqual(next.item.Links, null); + assert.notEqual(next.item.Data, null); + assert.equal(next.item.Data.seq, 4); + assert.notEqual(next.item.Payload, null); + assert.equal(next.item.Payload, 'hello4'); done(); })); @@ -205,6 +259,7 @@ describe('Orbit Client', () => { var second = iter.next().value; assert.equal(first.hash, items[items.length - 1]); assert.equal(second, null); + assert.equal(first.item.Payload, 'hello4'); done(); })); }); @@ -214,6 +269,8 @@ describe('Orbit Client', () => { var iter = orbit.channel(channel, '').iterator({ limit: -1 }); var messages = iter.collect(); assert.equal(messages.length, items.length); + assert.equal(messages[messages.length - 1].item.Payload, 'hello0'); + assert.equal(messages[0].item.Payload, 'hello4'); done(); })); @@ -458,9 +515,9 @@ describe('Orbit Client', () => { password: password } }; - var res = orbit.channel(channel, '').setMode(mode) - assert.notEqual(res.modes.r, null); - assert.equal(res.modes.r.password, password); + var modes = orbit.channel(channel, '').setMode(mode) + assert.notEqual(modes.r, null); + assert.equal(modes.r.password, password); } catch(e) { assert.equal(e, null); } @@ -469,7 +526,7 @@ describe('Orbit Client', () => { it('can\'t read with wrong password', async((done) => { try { - var res = orbit.channel(channel, '').iterator(); + var modes = orbit.channel(channel, '').iterator(); assert.equal(true, false); } catch(e) { assert.equal(e, 'Unauthorized'); @@ -485,9 +542,9 @@ describe('Orbit Client', () => { ops: [orbit.user.id] } }; - var res = orbit.channel(channel, password).setMode(mode); - assert.notEqual(res.modes.w, null); - assert.equal(res.modes.w.ops[0], orbit.user.id); + var modes = orbit.channel(channel, password).setMode(mode); + assert.notEqual(modes.w, null); + assert.equal(modes.w.ops[0], orbit.user.id); } catch(e) { assert.equal(e, null); } @@ -501,8 +558,8 @@ describe('Orbit Client', () => { it('removes write mode', async((done) => { try { - var res = orbit.channel(channel, password).setMode({ mode: "-w" }); - assert.equal(res.modes.w, null); + var modes = orbit.channel(channel, password).setMode({ mode: "-w" }); + assert.equal(modes.w, null); } catch(e) { assert.equal(e, null); } @@ -511,8 +568,8 @@ describe('Orbit Client', () => { it('removes read mode', async((done) => { try { - var res = orbit.channel(channel, password).setMode({ mode: "-r" }); - assert.equal(res.modes.r, null); + var modes = orbit.channel(channel, password).setMode({ mode: "-r" }); + assert.equal(modes.r, null); } catch(e) { assert.equal(e, null); }