Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

moderation #45

Merged
merged 10 commits into from Aug 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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