Skip to content

Commit

Permalink
Merge pull request #45 from substack/moderation
Browse files Browse the repository at this point in the history
moderation
  • Loading branch information
noffle authored Aug 24, 2019
2 parents f676c44 + e340517 commit 756e1a7
Show file tree
Hide file tree
Showing 6 changed files with 523 additions and 10 deletions.
64 changes: 61 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ Core database, replication, swarming, and chat APIs for cabal.

> var Cabal = require('cabal-node')
### var cabal = Cabal([storage][, key][, opts])
### var cabal = Cabal([storage][, uriString][, opts])

Create a cabal p2p database using storage `storage`, which must be either a
string (filepath to directory on disk) or an instance of
[random-access-storage](https://github.com/random-access-storage/).

`key` is a hex string of the key, without any prefixes (like `cabal://`).
`uriString` is a cabal URI string, of the form `cabal://<hexkey>[?param1=value1&param2=value2`. A hexidecimal key on its own will also be understood.

If this is a new database, `key` can be omitted and will be generated.
If this is a new cabal, `key` can be omitted and will be generated.

You can pass `opts.db` as a levelup or leveldown instance to use persistent
storage for indexing instead of using memory. For example:
Expand Down Expand Up @@ -86,6 +86,39 @@ Emitted when you connect to a peer. `key` is a hex string of their public key.
Emitted when you lose a connection to a peer. `key` is a hex string of their
public key.

## Moderation

Cabal has a *subjective moderation system*.

The three roles are "admin", "moderator", and "ban/key".

Any admin/mod/ban operation can be per-channel, or cabal-wide (the `@` group).

Every user sees themselves as an administrator across the entire cabal. This
means they can grant admin or moderator powers to anyone, and ban anyone, but
only they will see its affects on their own computer.

A cabal can be instantiated with a *moderation key*. This is an additional key
to locally consider as a cabal-wide administrator (in addition to yourself).

This means that if a group of people all specify the same *moderation key*,
they will collectively see the same set of administrators, moderators, and
banned users.

#### var rs = cabal.moderation.listBans(channel)

Return a readable objectMode stream of bans for `channel`.

Each ban is an object with either a `key` or `ip` property.

To list cabal-wide bans use the special channel `@`.

#### cabal.moderation.isBanned({ ip, key, channel }, cb)

Determine whether a user identified by `ip` and/or `key` is banned on `channel`
or cabal-wide as `cb(err, banned)` for a boolean `banned`. If `channel` is
omitted, only check cabal-wide.

### Publishing

#### cabal.publish(message, opts, cb)
Expand Down Expand Up @@ -121,6 +154,31 @@ documented types include
}
```

#### mod/{add,remove}

```js
{
type: '"mod/add" or "mod/remove"',
content: {
key: 'hex string key of the user to add/remove as mod',
channel: 'channel name as a string or "@" for cabal-wide'
role: '"admin", "mod", or a custom role string'
}
}
```

#### ban/{add,remove}

```js
{
type: '"ban/add" or "ban/remove"',
content: {
key: 'hex string key of the user to ban/unban',
channel: 'channel name as a string or "@" for cabal-wide'
}
}
```

## License

AGPLv3
47 changes: 43 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ var thunky = require('thunky')
var timestamp = require('monotonic-timestamp')
var sublevel = require('subleveldown')
var crypto = require('hypercore-crypto')
var url = require('url')
var querystring = require('query-string')
var createChannelView = require('./views/channels')
var createMembershipsView = require('./views/channel-membership')
var createMessagesView = require('./views/messages')
var createTopicsView = require('./views/topics')
var createUsersView = require('./views/users')
var createModerationView = require('./views/moderation')
var swarm = require('./swarm')

var DATABASE_VERSION = 1
Expand All @@ -19,6 +22,7 @@ var MEMBERSHIPS = 'j' // j for joined memberships..? :3
var MESSAGES = 'm'
var TOPICS = 't'
var USERS = 'u'
var MODERATION = 'x'

module.exports = Cabal
module.exports.databaseVersion = DATABASE_VERSION
Expand All @@ -28,8 +32,8 @@ module.exports.databaseVersion = DATABASE_VERSION
* local nickname -> mesh interactions for a single user.
* @constructor
* @param {string|function} storage - A hyperdb compatible storage function, or a string representing the local data path.
* @param {string} key - The dat link
* @param {Object} opts -
* @param {string} key - a protocol string, optionally with url parameters
* @param {Object} opts - { modKey }
*/
function Cabal (storage, key, opts) {
if (!(this instanceof Cabal)) return new Cabal(storage, key, opts)
Expand All @@ -49,11 +53,17 @@ function Cabal (storage, key, opts) {
}

this.maxFeeds = opts.maxFeeds
this.key = key || crypto.keyPair().publicKey.toString('hex')

if (!key) this.key = generateKeyHex()
else this.key = sanitizeKey(key)
if (!isHypercoreKey(this.key)) throw new Error('invalid cabal key')

this.modKey = opts.modKey

this.db = opts.db || memdb()
this.kcore = kappa(storage, {
valueEncoding: json,
encryptionKey: this.key
encryptionKey: isHypercoreKey(this.key) ? this.key : null
})

// Create (if needed) and open local write feed
Expand All @@ -77,12 +87,17 @@ function Cabal (storage, key, opts) {
sublevel(this.db, TOPICS, { valueEncoding: json })))
this.kcore.use('users', createUsersView(
sublevel(this.db, USERS, { valueEncoding: json })))
this.kcore.use('moderation', createModerationView(
this, this.modKey,
sublevel(this.db, MODERATION, { valueEncoding: json }))
)

this.messages = this.kcore.api.messages
this.channels = this.kcore.api.channels
this.memberships = this.kcore.api.memberships
this.topics = this.kcore.api.topics
this.users = this.kcore.api.users
this.moderation = this.kcore.api.moderation
}

inherits(Cabal, events.EventEmitter)
Expand Down Expand Up @@ -196,4 +211,28 @@ Cabal.prototype._removeConnection = function (key) {
this.emit('peer-dropped', key)
}


function generateKeyHex () {
return crypto.keyPair().publicKey.toString('hex')
}

function isHypercoreKey (key) {
if (typeof key === 'string') return key.length === 64 && /^[0-9a-f]+$/.test(key)
else if (Buffer.isBuffer(key)) return key.length === 32
}

// Ensures 'key' is a hex string
function sanitizeKey (key) {
// force to hex string
if (Buffer.isBuffer(key)) {
key = key.toString('hex')
}

// remove any protocol uri prefix
if (typeof key === 'string') key = key.replace(/^.*:\/\//, '')
else key = undefined

return key
}

function noop () {}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,12 @@
"inherits": "^2.0.4",
"kappa-core": "^4.0.0",
"kappa-view-level": "^2.0.1",
"materialized-group-auth": "^1.1.1",
"memdb": "^1.3.1",
"monotonic-timestamp": "0.0.9",
"once": "^1.4.0",
"pump": "^3.0.0",
"query-string": "^6.8.2",
"randombytes": "^2.0.6",
"read-only-stream": "^2.0.0",
"strftime": "^0.10.0",
Expand Down
Loading

0 comments on commit 756e1a7

Please sign in to comment.