diff --git a/.gitignore b/.gitignore index 91e11cd..e174d68 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Unlicense node_modules +package-lock.json pnpm-lock.yaml coverage -.nyc_output \ No newline at end of file +.nyc_output diff --git a/README.md b/README.md index b078872..14b6721 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ main classDef default fill:#3f506c,stroke:#3f506c,color:#fff; ``` -How "classic" scuttlebutt worked - each device has one `main` feed with all messages +_How "classic" scuttlebutt worked - each device has one `main` feed with all messages_ ```mermaid graph TB @@ -37,19 +37,20 @@ classDef root fill:#8338ec,stroke:#8338ec,color:#fff; classDef default fill:#3a86ff,stroke:#3a86ff,color:#fff; classDef legacy fill:#3f506c,stroke:#3f506c,color:#fff; ``` -How scuttlebutt works with metafeeds - each device now has a `root` metafeed, +_How scuttlebutt works with metafeeds - each device now has a `root` metafeed, whose sole responsibility is to announce (point to) subfeeds that you publish content to. A subfeed can also be a metafeed, which then allows the existence of -"sub-subfeeds". +"sub-subfeeds"._ -This means that when you first meet a peer you can replicate their `root` metafeed -and, having discovered their subfeeds, replicate just their `aboutMe` and `contacts` feeds -to get enough info to place them socially. Once you decide you want to follow them you may -replicate their other subfeeds. +This means that when you first meet a peer you can replicate their `root` +metafeed and, having discovered their subfeeds, replicate just their `aboutMe` +and `contacts` feeds to get enough info to place them socially. Once you decide +you want to follow them you may replicate their other subfeeds. -_NOTE: The ideal state is that all content is split out into subfeeds. -To add backwards compatability for devices that have already posted a lot of posts to their -classic `main` feed, this library will auto-link that main feed in as a "subfeed" of our root._ +_NOTE: The ideal state is that all content is split out into subfeeds. To add +backwards compatability for devices that have already posted a lot of posts to +their classic `main` feed, this library will auto-link that main feed in as a +"subfeed" of our root._ ## Installation @@ -74,117 +75,59 @@ Add this plugin like this: ## Example usage -Let's start by creating a **root metafeed** with `findOrCreate(cb)`, necessary -for using this module. There can only be one _root_ metafeed, so even if you -call `findOrCreate(cb)` many times, it will not create duplicates, it will just -load the root metafeed. +We create a subfeed for `about` messages under our `root` feed using +`findOrCreate`. This will only create the subfeed if there is no existing +subfeed that matches the criteria. ```js -sbot.metafeeds.findOrCreate((err, root) => { - // ... -}) -``` +const details = { feedpurpose: 'aboutMe' } +sbot.metafeeds.findOrCreate(details, (err, aboutMeFeed) => { + console.log(aboutMeFeed) -Now we create a subfeed for `about` messages under our `root` feed using -`findOrCreate(parent, isFeed, details, cb)`. This will only create the subfeed -if there is no existing subfeed that matches the criteria`isFeed`: - -```js -sbot.metafeeds.findOrCreate((err, root) => { - // Find an existing feed - const isFeed = (feed) => feed.feedpurpose === 'aboutMe' - // Details for creating a subfeed if it doesn't already exist: - const details = { feedpurpose: 'aboutMe', feedformat: 'classic' } - - sbot.metafeeds.findOrCreate(root, isFeed, details, (err, aboutMeFeed) => { - // ... - }) + // }) ``` -The `details` argument is a *FeedDetails* object and it always requires -`feedpurpose` (any string) and `feedformat` (either `'classic'` for normal -ed25519 SSB feeds, or `'bendybutt-v1'` if you want a metafeed). The `isFeed` -function also takes a *FeedDetails* object as input. +The `details` argument is an object used to find (or create) a subfeed under +your "root feed". (It actually nests it under a couple of subfeeds, to handle +versioning, and sparse replication, but you generally don't need to know the +details). Once you have a *FeedDetails* object, like `aboutMeFeed`, you can publish on the new subfeed: ```js -sbot.metafeeds.findOrCreate((err, root) => { - const isFeed = (feed) => feed.feedpurpose === 'aboutMe' - const details = { feedpurpose: 'aboutMe', feedformat: 'classic' } - - sbot.metafeeds.findOrCreate(root, isFeed, details, (err, aboutMeFeed) => { - const content = { - type: 'about', - name: 'baba yaga' - description: 'lives in a hutt in the forest, swing by sometime!' - } - sbot.db.publishAs(aboutMeFeed.keys, content, (err, msg) => { - // ... - }) +const details = { feedpurpose: 'aboutMe' } +sbot.metafeeds.findOrCreate(details, (err, aboutMeFeed) => { + console.log(aboutMeFeed) + + const content = { + type: 'about', + name: 'baba yaga' + description: 'lives in a hutt in the forest, swing by sometime!' + } + sbot.db.create({ keys: aboutMeFeed.keys, content }, (err, msg) => { + console.log(msg) }) }) ``` ## API -### `sbot.metafeeds.findOrCreate(cb)` - -Calls back with your `root` Metafeed object which has the form: - -```js -{ - metafeed: null, - subfeed: 'ssb:feed/bendybutt-v1/sxK3OnHxdo7yGZ-28HrgpVq8nRBFaOCEGjRE4nB7CO8=', - feedpurpose: 'root', - feedformat: 'bendybutt-v1', - seed: , - keys: { - curve: 'ed25519', - public: 'sxK3OnHxdo7yGZ+28HrgpVq8nRBFaOCEGjRE4nB7CO8=.ed25519', - private: 'SOEx7hA9vRHrli0PZwNJ8jijH+PShmlrzz/JAKI7v6SzErc6cfF2jvIZn7bweuClWrydEEVo4IQaNETicHsI7w==.ed25519', - id: 'ssb:feed/bendybutt-v1/sxK3OnHxdo7yGZ-28HrgpVq8nRBFaOCEGjRE4nB7CO8=' - }, - metadata: {} -``` - -Meaning: -- `metafeed` - the id of the feed this is underneath. As this is the topmost feed, this is empty -- `subfeed` - the id of this feed, same as `keys.id` -- `feedpurpose` - a human readable ideally unique handle for this feed -- `feedformat` - the feed format ("classic" or "bendybutt-v1" are current options) -- `seed` - the data from which is use to derive the `keys` and `id` of this feed. -- `keys` - cryptographic keys used for signing messages published by this feed (see [ssb-keys]) -- `metadata` - additional data - -NOTES: -- the `root` metafeed is unique - you have only one, and it has no metafeed (it's at the top!) -- if you have a legacy `main` feed, this will also set that up as a subfeed of your `root` feed. - -### `sbot.metafeeds.findOrCreate(metafeed, isFeed, details, cb)` - -Looks for the first subfeed of `metafeed` that satisfies the condition in `isFeed`, -or creates it matching the properties in `details`. +### `sbot.metafeeds.findOrCreate(details, cb)` -This is strictly concerned with meta feeds and sub feeds that **you own**, not -with those that belong to other peers. +Looks for the first subfeed of `metafeed` that matches `details`, or creates +one which matches these. This creates feeds following the +[v1 tree structure](https://github.com/ssbc/ssb-meta-feeds-spec#v1). Arguments: -- `metafeed` - the metafeed you are finding/ creating under, can be: - - *FeedDetails* object (as returned by `findOrCreate()` or `getRoot()`) - - *null* which is short-hand for the `rootFeed` (this will be created if doesn't exist) -- `isFeed` - method you use to find an existing *FeedDetails*, can be: - - *function* of shape `(FeedDetails) => boolean` - - *null* - this method will then return an arbitrary subfeed under provided `metafeed` -- `details` - used to create a new subfeed if a match for an existing one is not found, can be - - *Object*: - - `details.feedpurpose` *String* any string to characterize the purpose of this new subfeed - - `details.feedformat` *String* either `'classic'` or `'bendybutt-v1'` - - `details.metadata` *Object* (optional) - for containing other data - - if `details.metadata.recps` is used, the subfeed announcement will be encrypted - - *null* - only allowed if `metafeed` is null (i.e. the details of the `root` FeedDetails) +- `details` *Object* where + - `details.feedpurpose` *String* any string to characterize the purpose of this new subfeed + - `details.feedformat` *String* (optional) + - either `'classic'` or `'bendybutt-v1'` + - default: `'classic'` + - `details.metadata` *Object* (optional) - for containing other data + - if `details.metadata.recps` is used, the subfeed announcement will be encrypted - `cb` *function* delivers the response, has signature `(err, FeedDetails)`, where FeedDetails is ```js { @@ -206,34 +149,29 @@ Arguments: } ``` -### `sbot.metafeeds.findById(feedId, cb)` +Meaning: +- `metafeed` - the id of the feed this is underneath +- `subfeed` - the id of this feed, same as `keys.id` +- `feedpurpose` - a human readable ideally unique handle for this feed +- `feedformat` - the feed format ("classic" or "bendybutt-v1" are current options) +- `seed` - the data from which is use to derive the `keys` and `id` of this feed. +- `keys` - cryptographic keys used for signing messages published by this feed (see [ssb-keys]) +- `metadata` - object containing additional data -Given a `feedId` that is presumed to be a subfeed of some meta feed, this API -fetches the *Details* object describing that feed, which is of form: +NOTES: +- if you have a legacy `main` feed, this will also set that up as a subfeed of your `root` feed. -```js -{ - metafeed, - feedpurpose, - feedformat, - id, - // seed - // keys - metadata -} -``` -NOTE - may include `seed`, `keys` if this is one of your feeds. +### `sbot.metafeeds.findOrCreate(cb)` -### `sbot.metafeeds.findByIdSync(feedId)` +Fetches the **root metafeed** details. There can only be one _root_ metafeed, +so even if you call `findOrCreate(cb)` many times, it will not create duplicates, +it will just load the root metafeed. -Similar to `findById`, but returns synchronously. :warning: Note, in order to -use this API, you **must** call `sbot.metafeeds.loadState(cb)` first, and wait -for `cb` to be called. +Callsback with your `root` FeedDetails object (see `findOrCreate(details, cb)`) -You can also call `sbot.metafeeds.ensureLoaded(feedId, cb)` on an individual -basis to make sure that `findByIdSync` will operate at the correct time when the -`feedId`'s metadata has been processed in the local database. +NOTES: +- `metafeed = null` - the root metafeed is the topmost metafeed ### `sbot.metafeeds.branchStream(opts)` @@ -296,73 +234,9 @@ The `opts` argument can have the following properties: in the results; if `true`, only tombstoned branches are included; if `null`, all branches are included regardless of tombstoning. (Default: `null`) +### Advanced API -### `sbot.metafeeds.findAndTombstone(metafeed, isFeed, reason, cb)` - -_Looks for the first subfeed of `metafeed` that satisfies the condition in -`isFeed` and, if found, tombstones it with the string `reason`. - -This is strictly concerned with meta feeds and sub feeds that **you own**, not -with those that belong to other peers. - -Arguments: -- `metafeed` *FeedDetails* object (as returned by e.g. `findOrCreate()`, `getRoot()`). -- `isFeed` *function* of the shape `(FeedDetails) => Boolean`. -- `reason` *String* - describes why the found feed is being tombstoned. - -The callback is called with `true` on the 2nd argument if tombstoning suceeded, -or called with an error object on the 1st argument if it failed. - - -### `sbot.metafeeds.getRoot(cb)` - -Looks for the root meta feed declared by your main feed, and returns it (as -`{ seed, keys}`) via the callback `cb` if it exists. - -If it does not exist, this API will **not** create the root meta feed. - - -## Validation - -Exposed via the internal API. - -### `isValid(msg, hmacKey)` - -_Validate a single meta feed message._ - -Extracts the `contentSection` from the given `msg` object and calls -`validateSingle()` to perform validation checks. - -If provided, the `hmacKey` is also given as input to the `validateSingle()` -function call. `hmacKey` may be `null` or a valid HMAC key supplied as a -`Buffer` or `string`. - -The response is a boolean: `true` if validation is successful, `false` if -validation fails in any way. Note that this function does not return the -underlying cause of the validation failure. - -### `validateSingle(contentSection, hmacKey)` - -_Validate a single meta feed message `contentSection` according to the criteria -defined in the [specification](https://github.com/ssb-ngi-pointer/ssb-meta-feed-spec#usage-of-bendy-butt-feed-format)._ - -`contentSection` must be an array of `content` and `contentSignature`. If a -`string` is provided (representing an encrypted message, for instance) an error -will be returned; an encrypted `contentSection` cannot be validated. - -`hmacKey` may be `null` or a valid HMAC key supplied as a `Buffer` or `string`. - -The response will be `undefined` (for successful validation) or an `Error` -object with a `message` describing the error. - -### `validateMetafeedAnnounce(msg)` - -_Validates a `metafeed/announce` message expected to be published on "main" -feeds which are in the classic format, but are signed by a meta feed according -to the [ssb meta feed spec]._ - -The response will be `undefined` (for successful validation) or an `Error` -object with a `message` describing the error. +For lower level API docs, [see here](./README_ADVANCED.md). ## License diff --git a/README_ADVANCED.md b/README_ADVANCED.md new file mode 100644 index 0000000..f65ca02 --- /dev/null +++ b/README_ADVANCED.md @@ -0,0 +1,139 @@ + + +# Advanced API + +Most people using this module should not need to access these methods. +Some of them are low level and there for testing, some are for people wanting to step off +the recommended path. + +### `sbot.metafeeds.advanced.findOrCreate(metafeed, isFeed, details, cb)` + +Looks for the first subfeed of `metafeed` that satisfies the condition in `isFeed`, +or creates it matching the properties in `details`. + +This is strictly concerned with meta feeds and sub feeds that **you own**, not +with those that belong to other peers. + +Arguments: +- `metafeed` - the metafeed you are finding/ creating under, can be: + - *FeedDetails* object (as returned by `findOrCreate()` or `getRoot()`) + - *null* which is short-hand for the `rootFeed` (this will be created if doesn't exist) +- `isFeed` - method you use to find an existing *FeedDetails*, can be: + - *function* of shape `(FeedDetails) => boolean` + - *null* - this method will then return an arbitrary subfeed under provided `metafeed` +- `details` - used to create a new subfeed if a match for an existing one is not found, can be + - *Object*: + - `details.feedpurpose` *String* any string to characterize the purpose of this new subfeed + - `details.feedformat` *String* either `'classic'` or `'bendybutt-v1'` + - `details.metadata` *Object* (optional) - for containing other data + - if `details.metadata.recps` is used, the subfeed announcement will be encrypted + - *null* - only allowed if `metafeed` is null (i.e. the details of the `root` FeedDetails) +- `cb` *function* delivers the response, has signature `(err, FeedDetails)`, where FeedDetails is + ```js + { + metafeed: 'ssb:feed/bendybutt-v1/sxK3OnHxdo7yGZ-28HrgpVq8nRBFaOCEGjRE4nB7CO8=', + subfeed: '@I5TBH6BuCvMkSAWJXKwa2FEd8y/fUafkQ1z19PyXzbE=.ed25519', + feedpurpose: 'chess', + feedformat: 'classic', + seed: + keys: { + curve: 'ed25519', + public: 'I5TBH6BuCvMkSAWJXKwa2FEd8y/fUafkQ1z19PyXzbE=.ed25519', + private: 'Mxa+LL16ws7HZhetR9FbsIOsAeud+ii+9KDUisXkq08jlMEfoG4K8yRIBYlcrBrYUR3zL99Rp+RDXPX0/JfNsQ==.ed25519', + id: '@I5TBH6BuCvMkSAWJXKwa2FEd8y/fUafkQ1z19PyXzbE=.ed25519' + }, + metadata: { // example + notes: 'private testing of chess dev', + recps: ['@I5TBH6BuCvMkSAWJXKwa2FEd8y/fUafkQ1z19PyXzbE=.ed25519'] + }, + } + ``` + +### `sbot.metafeeds.advanced.findById(feedId, cb)` + +Given a `feedId` that is presumed to be a subfeed of some meta feed, this API +fetches the *Details* object describing that feed, which is of form: + +```js +{ + metafeed, + feedpurpose, + feedformat, + id, + // seed + // keys + metadata +} +``` + +NOTE - may include `seed`, `keys` if this is one of your feeds. + +### `sbot.metafeeds.advanced.findAndTombstone(metafeed, isFeed, reason, cb)` + +_Looks for the first subfeed of `metafeed` that satisfies the condition in +`isFeed` and, if found, tombstones it with the string `reason`. + +This is strictly concerned with meta feeds and sub feeds that **you own**, not +with those that belong to other peers. + +Arguments: +- `metafeed` *FeedDetails* object (as returned by e.g. `findOrCreate()`, `getRoot()`). +- `isFeed` *function* of the shape `(FeedDetails) => Boolean`. +- `reason` *String* - describes why the found feed is being tombstoned. + +The callback is called with `true` on the 2nd argument if tombstoning suceeded, +or called with an error object on the 1st argument if it failed. + + +### `sbot.metafeeds.advanced.getRoot(cb)` + +Looks for the root meta feed declared by your main feed, and returns it (as +`{ seed, keys}`) via the callback `cb` if it exists. + +If it does not exist, this API will **not** create the root meta feed. + + +### `sbot.metafeeds.validate.isValid(msg, hmacKey)` + +_Validate a single meta feed message._ + +Extracts the `contentSection` from the given `msg` object and calls +`validateSingle()` to perform validation checks. + +If provided, the `hmacKey` is also given as input to the `validateSingle()` +function call. `hmacKey` may be `null` or a valid HMAC key supplied as a +`Buffer` or `string`. + +The response is a boolean: `true` if validation is successful, `false` if +validation fails in any way. Note that this function does not return the +underlying cause of the validation failure. + +### `sbot.metafeeds.validate.validateSingle(contentSection, hmacKey)` + +_Validate a single meta feed message `contentSection` according to the criteria +defined in the [specification](https://github.com/ssb-ngi-pointer/ssb-meta-feed-spec#usage-of-bendy-butt-feed-format)._ + +`contentSection` must be an array of `content` and `contentSignature`. If a +`string` is provided (representing an encrypted message, for instance) an error +will be returned; an encrypted `contentSection` cannot be validated. + +`hmacKey` may be `null` or a valid HMAC key supplied as a `Buffer` or `string`. + +The response will be `undefined` (for successful validation) or an `Error` +object with a `message` describing the error. + +### `sbot.metafeeds.validate.validateMetafeedAnnounce(msg)` + +_Validates a `metafeed/announce` message expected to be published on "main" +feeds which are in the classic format, but are signed by a meta feed according +to the [ssb meta feed spec]._ + +The response will be `undefined` (for successful validation) or an `Error` +object with a `message` describing the error. + + + diff --git a/api.js b/api.js index c200a20..edeefca 100644 --- a/api.js +++ b/api.js @@ -3,9 +3,24 @@ // SPDX-License-Identifier: LGPL-3.0-only const run = require('promisify-tuple') +const deepEqual = require('fast-deep-equal') const debug = require('debug')('ssb:meta-feeds') +const pickShard = require('./pick-shard') const alwaysTrue = () => true +const BB1 = 'bendybutt-v1' +const v1Details = { feedpurpose: 'v1', feedformat: BB1 } +const v1Visit = detailsToVisit(v1Details) + +function detailsToVisit(details) { + return (feed) => { + return ( + feed.feedpurpose === details.feedpurpose && + feed.feedformat === details.feedformat && + deepEqual(feed.metadata, details.metadata || {}) + ) + } +} exports.init = function (sbot, config) { function filter(metafeed, visit, maybeCB) { @@ -210,11 +225,39 @@ exports.init = function (sbot, config) { } } + function commonFindOrCreate(details, cb) { + if (!details.feedformat) details.feedformat = 'classic' + + findOrCreate((err, rootFeed) => { + if (err) return cb(err) + + findOrCreate(rootFeed, v1Visit, v1Details, (err, v1Feed) => { + if (err) return cb(err) + + const shardDetails = { + feedpurpose: pickShard(rootFeed.keys.id, details.feedpurpose), + feedformat: BB1, + } + const shardVisit = detailsToVisit(shardDetails) + + findOrCreate(v1Feed, shardVisit, shardDetails, (err, shardFeed) => { + if (err) return cb(err) + + findOrCreate(shardFeed, detailsToVisit(details), details, cb) + }) + }) + }) + } + return { - getRoot, - findOrCreate, - findAndTombstone, - findById, branchStream, + findOrCreate: commonFindOrCreate, + + advanced: { + getRoot, + findOrCreate, + findAndTombstone, + findById, + }, } } diff --git a/index.js b/index.js index 68fcb06..2f28f7e 100644 --- a/index.js +++ b/index.js @@ -2,19 +2,19 @@ // // SPDX-License-Identifier: LGPL-3.0-only +const API = require('./api') const Keys = require('./keys') const Messages = require('./messages') +const Lookup = require('./lookup') const Query = require('./query') -const API = require('./api') const Validate = require('./validate') -const FeedsLookup = require('./feeds-lookup') exports.name = 'metafeeds' exports.init = function (sbot, config) { const messages = Messages.init(sbot, config) const query = Query.init(sbot, config) - const lookup = FeedsLookup.init(sbot, config) + const lookup = Lookup.init(sbot, config) const api = API.init(sbot, config) return { diff --git a/feeds-lookup.js b/lookup.js similarity index 100% rename from feeds-lookup.js rename to lookup.js diff --git a/messages.js b/messages.js index 02a0c42..67a1054 100644 --- a/messages.js +++ b/messages.js @@ -39,7 +39,7 @@ exports.init = function init(sbot) { return { feedFormat: 'bendybutt-v1', keys: mfKeys, - contentKeys: feedKeys, + contentKeys: feedKeys, // see ssb-bendy-butt/format.js content, encryptionFormat: 'box2', // in case metadata.recps is set } diff --git a/package.json b/package.json index 28d0ca6..6a7e376 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "bencode": "^2.0.2", "bipf": "^1.9.0", "debug": "^4.3.0", + "fast-deep-equal": "^3.1.3", "futoin-hkdf": "^1.4.2", "is-canonical-base64": "^1.1.1", "p-defer": "^3.0.0", @@ -38,8 +39,8 @@ "rimraf": "^3.0.2", "secret-stack": "^6.4.0", "ssb-bendy-butt": "^1.0.1", - "ssb-db2": "^6.1.1", "ssb-caps": "^1.1.0", + "ssb-db2": "^6.1.1", "tap-arc": "^0.3.5", "tape": "^5.6.0" }, diff --git a/pick-shard.js b/pick-shard.js new file mode 100644 index 0000000..3f24aed --- /dev/null +++ b/pick-shard.js @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2022 Mix Irving +// +// SPDX-License-Identifier: LGPL-3.0-only + +const bfe = require('ssb-bfe') +const crypto = require('crypto') + +module.exports = function pickShard(rootFeedId, idString) { + const buf = Buffer.concat([bfe.encode(rootFeedId), bfe.encode(idString)]) + + const hash = crypto.createHash('sha256') + hash.update(buf) + + return hash.digest('hex')[0] +} diff --git a/test/api.test.js b/test/api.test.js index b162c98..519ea0f 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -30,16 +30,16 @@ function testReadAndPersisted(t, sbot, testRead) { /* Tests */ -test('getRoot() when there is nothing', (t) => { +test('advanced.getRoot() when there is nothing', (t) => { const sbot = Testbot() - sbot.metafeeds.getRoot((err, found) => { + sbot.metafeeds.advanced.getRoot((err, found) => { t.error(err, 'no err for find()') t.notOk(found, 'nothing found') sbot.close(true, t.end) }) }) -test('findOrCreate(null, null, null, cb)', (t) => { +test('advanced.findOrCreate(null, null, null, cb)', (t) => { const sbot = Testbot() sbot.db.query( where(author(sbot.id)), @@ -47,7 +47,7 @@ test('findOrCreate(null, null, null, cb)', (t) => { if (err) throw err t.equals(msgs.length, 0, 'empty db') - sbot.metafeeds.findOrCreate(null, null, null, (err, mf) => { + sbot.metafeeds.advanced.findOrCreate(null, null, null, (err, mf) => { t.error(err, 'no err for findOrCreate()') // t.equals(mf.feeds.length, 1, '1 sub feed in the root metafeed') // t.equals(mf.feeds[0].feedpurpose, 'main', 'it is the main feed') @@ -59,10 +59,10 @@ test('findOrCreate(null, null, null, cb)', (t) => { ) }) -test('findOrCreate(cb)', (t) => { +test('advanced.findOrCreate(cb)', (t) => { const sbot = Testbot() - sbot.metafeeds.findOrCreate((err, mf) => { + sbot.metafeeds.advanced.findOrCreate((err, mf) => { t.error(err, 'no err for findOrCreate()') // t.equals(mf.feeds.length, 1, '1 sub feed in the root metafeed') // t.equals(mf.feeds[0].feedpurpose, 'main', 'it is the main feed') @@ -72,19 +72,19 @@ test('findOrCreate(cb)', (t) => { }) }) -test('findOrCreate is idempotent', (t) => { +test('advanced.findOrCreate is idempotent', (t) => { const sbot = Testbot() - sbot.metafeeds.findOrCreate(null, null, null, (err, mf) => { + sbot.metafeeds.advanced.findOrCreate(null, null, null, (err, mf) => { t.error(err, 'no err for findOrCreate()') t.ok(mf, 'got a metafeed') - sbot.metafeeds.getRoot((err, mf) => { + sbot.metafeeds.advanced.getRoot((err, mf) => { t.error(err, 'no err for getRoot()') t.equals(mf.seed.toString('hex').length, 64, 'seed length is okay') t.equals(typeof mf.keys.id, 'string', 'key seems okay') const originalSeed = mf.seed.toString('hex') const originalID = mf.keys.id - sbot.metafeeds.findOrCreate((err, mf) => { + sbot.metafeeds.advanced.findOrCreate((err, mf) => { t.error(err, 'no err for findOrCreate(null, ...)') t.equals(mf.seed.toString('hex'), originalSeed, 'same seed') t.equals(mf.keys.id, originalID, 'same ID') @@ -95,14 +95,14 @@ test('findOrCreate is idempotent', (t) => { }) }) -test('findOrCreate() a sub feed', (t) => { +test('advanced.findOrCreate() a sub feed', (t) => { const sbot = Testbot() - sbot.metafeeds.findOrCreate(null, null, null, (err, mf) => { - sbot.metafeeds.getRoot((err, mf) => { + sbot.metafeeds.advanced.findOrCreate(null, null, null, (err, mf) => { + sbot.metafeeds.advanced.getRoot((err, mf) => { t.error(err, 'gets rootFeed') // lets create a new chess feed - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate( mf, (f) => f.feedpurpose === 'chess', { @@ -122,11 +122,11 @@ test('findOrCreate() a sub feed', (t) => { test('all FeedDetails have same format', (t) => { const sbot = Testbot() - sbot.metafeeds.findOrCreate(null, null, null, (err, mf) => { - t.error(err, 'no err') - sbot.metafeeds.getRoot((err, mf) => { + sbot.metafeeds.advanced.findOrCreate(null, null, null, (err, mf) => { + if (err) throw err + sbot.metafeeds.advanced.getRoot((err, mf) => { if (err) throw err - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate( null, () => true, {}, @@ -139,7 +139,7 @@ test('all FeedDetails have same format', (t) => { 'getRoot and findOrCreate return the same root FeedDetails' ) - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate( mf, (f) => f.feedpurpose === 'chess', { @@ -162,10 +162,10 @@ test('all FeedDetails have same format', (t) => { }) }) -test('findOrCreate() a subfeed under a sub meta feed', (t) => { +test('advanced.findOrCreate() a subfeed under a sub meta feed', (t) => { const sbot = Testbot() - sbot.metafeeds.findOrCreate(null, null, null, (err, rootMF) => { - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate(null, null, null, (err, rootMF) => { + sbot.metafeeds.advanced.findOrCreate( rootMF, (f) => f.feedpurpose === 'indexes', { feedpurpose: 'indexes', feedformat: 'bendybutt-v1' }, @@ -177,7 +177,7 @@ test('findOrCreate() a subfeed under a sub meta feed', (t) => { 'has a bendy butt SSB URI' ) - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate( indexesMF, (f) => f.feedpurpose === 'index', { @@ -208,8 +208,8 @@ test('findOrCreate() a subfeed under a sub meta feed', (t) => { // - indexes // - about async function setupTree(sbot) { - const rootMF = await p(sbot.metafeeds.findOrCreate)() - const chessF = await p(sbot.metafeeds.findOrCreate)( + const rootMF = await p(sbot.metafeeds.advanced.findOrCreate)() + const chessF = await p(sbot.metafeeds.advanced.findOrCreate)( rootMF, (f) => f.feedpurpose === 'chess', { @@ -218,12 +218,12 @@ async function setupTree(sbot) { metadata: { score: 0 }, } ) - const indexesMF = await p(sbot.metafeeds.findOrCreate)( + const indexesMF = await p(sbot.metafeeds.advanced.findOrCreate)( rootMF, (f) => f.feedpurpose === 'indexes', { feedpurpose: 'indexes', feedformat: 'bendybutt-v1' } ) - const indexF = await p(sbot.metafeeds.findOrCreate)( + const indexF = await p(sbot.metafeeds.advanced.findOrCreate)( indexesMF, (f) => f.feedpurpose === 'index', { @@ -236,16 +236,16 @@ async function setupTree(sbot) { return { rootMF, chessF, indexesMF, indexF } } -test('findById', (t) => { +test('advanced.findById', (t) => { const sbot = Testbot() setupTree(sbot).then(({ indexF, indexesMF }) => { - sbot.metafeeds.findById(null, (err, details) => { + sbot.metafeeds.advanced.findById(null, (err, details) => { t.match(err.message, /feedId should be provided/, 'error about feedId') t.notOk(details) testReadAndPersisted(t, sbot, (t, sbot, cb) => { - sbot.metafeeds.findById(indexF.keys.id, (err, details) => { + sbot.metafeeds.advanced.findById(indexF.keys.id, (err, details) => { if (err) return cb(err) t.deepEquals(Object.keys(details), [ @@ -302,7 +302,7 @@ test('branchStream', (t) => { }) }) -test('findAndTombstone and tombstoning branchStream', (t) => { +test('advanced.findAndTombstone and tombstoning branchStream', (t) => { const sbot = Testbot() setupTree(sbot).then(({ rootMF }) => { @@ -367,7 +367,7 @@ test('findAndTombstone and tombstoning branchStream', (t) => { }) ) - sbot.metafeeds.findAndTombstone( + sbot.metafeeds.advanced.findAndTombstone( rootMF, (f) => f.feedpurpose === 'chess', 'This game is too good', @@ -378,19 +378,19 @@ test('findAndTombstone and tombstoning branchStream', (t) => { }) }) -test('findOrCreate() recps', (t) => { +test('advanced.findOrCreate (metadata.recps)', (t) => { const sbot = Testbot() - const testkey = Buffer.from( + const ownKey = Buffer.from( '30720d8f9cbf37f6d7062826f6decac93e308060a8aaaa77e6a4747f40ee1a76', 'hex' ) - sbot.box2.setOwnDMKey(testkey) + sbot.box2.setOwnDMKey(ownKey) testReadAndPersisted(t, sbot, (t, sbot, cb) => { - sbot.metafeeds.findOrCreate((err, mf) => { + sbot.metafeeds.advanced.findOrCreate((err, mf) => { if (err) return cb(err) - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate( mf, (f) => f.feedpurpose === 'private', { @@ -410,3 +410,67 @@ test('findOrCreate() recps', (t) => { }) }) }) + +// sugary top level API + +test('findOrCreate', (t) => { + const sbot = Testbot() + + const details = { + feedpurpose: 'chess', + // feedformat: 'classic', optional + } + + sbot.metafeeds.findOrCreate(details, (err, chessF) => { + if (err) throw err + t.equal(chessF.feedpurpose, details.feedpurpose, 'creates feed') + + sbot.metafeeds.findOrCreate(details, (err, chessF2) => { + if (err) throw err + t.deepEqual(chessF, chessF2, 'finds feed') + + pull( + sbot.metafeeds.branchStream({ root: null, old: true, live: false }), + pull.collect((err, branches) => { + if (err) throw err + + t.equal(branches.length, 5, 'correct number of feeds created') + // root, v1, shard, chess (AND MAIN) + + const purposePath = branches + .pop() + .map((f) => f[1] && f[1].feedpurpose) + t.deepEqual(purposePath, [null, 'v1', purposePath[2], 'chess']) + // TODO it would be nice for testing that we could deterministically know the shard + // but I don't know how to fix the "seed" that the root feed is derived from + + sbot.close(true, t.end) + }) + ) + }) + }) +}) + +test('findOrCreate (metadata.recps)', (t) => { + const sbot = Testbot() + + const ownKey = Buffer.from( + '30720d8f9cbf37f6d7062826f6decac93e308060a8aaaa77e6a4747f40ee1a76', + 'hex' + ) + sbot.box2.setOwnDMKey(ownKey) + + const details = { + feedpurpose: 'chess', + metadata: { + recps: [sbot.id], + }, + } + + sbot.metafeeds.findOrCreate(details, (err, chessF) => { + if (err) throw err + + t.deepEqual(chessF.metadata.recps, [sbot.id], 'creates encrypted subfee') + sbot.close(true, t.end) + }) +}) diff --git a/test/pick-shard.test.js b/test/pick-shard.test.js new file mode 100644 index 0000000..354af4a --- /dev/null +++ b/test/pick-shard.test.js @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2022 Mix Irving +// +// SPDX-License-Identifier: Unlicense + +const test = require('tape') +const Keys = require('ssb-keys') + +const pickShard = require('../pick-shard') + +test('pick-shard', (t) => { + const rootFeedId = Keys.generate(null, null, 'bendybutt-v1').id + + t.equal( + pickShard(rootFeedId, 'dog'), + pickShard(rootFeedId, 'dog'), + 'is deterministic' + ) + + const validShards = new Set('0123456789abcdef'.split('')) + let pass = true + // NOTE these are all Strings + for (let i = 0; i < 1600; i++) { + const shard = pickShard(rootFeedId, `test-${i}`) + if (!validShards.has(shard)) pass = false + } + t.equal(pass, true, 'picked shards are nibbles') + + t.end() +})