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

add moderation class, new cmd /moderation, and fix cmd output #50

Merged
merged 4 commits into from
May 26, 2020
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"level": "^6.0.0",
"memdb": "^1.3.1",
"mkdirp": "^0.5.1",
"monotonic-timestamp": "0.0.9",
"pump": "^3.0.0",
"qrcode": "^1.4.4",
"random-access-memory": "^3.1.1",
Expand Down
5 changes: 5 additions & 0 deletions src/cabal-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const EventEmitter = require('events')
const debug = require("debug")("cabal-client")
const { VirtualChannelDetails, ChannelDetails } = require("./channel-details")
const User = require("./user")
const Moderation = require("./moderation")
const collect = require('collect-stream')
const { nextTick } = process

Expand Down Expand Up @@ -61,6 +62,7 @@ class CabalDetails extends EventEmitter {
}
}
this.key = cabal.key
this.moderation = new Moderation(this.core)

this.channels = {
'!status': new VirtualChannelDetails("!status"),
Expand Down Expand Up @@ -111,10 +113,12 @@ class CabalDetails extends EventEmitter {
var m = /^\/(\w+)(?:\s+(.*))?/.exec(line.trimRight())
if (m && this._commands[m[1]] && typeof this._commands[m[1]].call === 'function') {
this._commands[m[1]].call(this, this._res, m[2])
this._emitUpdate("command", { command: m[1], arg: m[2] || '' })
} else if (m && this._aliases[m[1]]) {
var key = this._aliases[m[1]]
if (this._commands[key]) {
this._commands[key].call(this, this._res, m[2])
this._emitUpdate("command", { command: key, arg: m[2] || ''})
} else {
this._res.info(`command for alias ${m[1]} => ${key} not found`)
cb()
Expand Down Expand Up @@ -248,6 +252,7 @@ class CabalDetails extends EventEmitter {
*/
addStatusMessage(message, channel=this.chname) {
if (!this.channels[channel]) return
debug(channel)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover debug output?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hehe oops :^)

this.channels[channel].addVirtualMessage(message)
this._emitUpdate("status-message", { channel, message })
}
Expand Down
5 changes: 3 additions & 2 deletions src/channel-details.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const collect = require('collect-stream')
const timestamp = require("monotonic-timestamp")
const { stableSort, merge } = require('./util')

class ChannelDetailsBase {
Expand Down Expand Up @@ -90,7 +91,7 @@ class ChannelDetailsBase {
const virtualMessages = this.getVirtualMessages(opts)
var cmp = (a, b) => {
// sort by timestamp
let diff = parseInt(a.value.timestamp) - parseInt(b.value.timestamp)
let diff = parseFloat(a.value.timestamp) - parseFloat(b.value.timestamp)
// if timestamp was the same, and messages are by same author, sort by seqno
if (diff === 0
&& a.key && b.key && a.key === b.key
Expand Down Expand Up @@ -125,7 +126,7 @@ class ChannelDetailsBase {
msg = {
key: this.name,
value: {
timestamp: msg.timestamp || Date.now(),
timestamp: msg.timestamp || timestamp(),
type: msg.type || "status",
content: {
text: msg.text
Expand Down
1 change: 1 addition & 0 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ class Client {
let cabalPromise
let dnsFailed = false
if (typeof key === 'string') {
key = key.trim()
cabalPromise = this.resolveName(key).then((resolvedKey) => {
if (resolvedKey === null) {
dnsFailed = true
Expand Down
96 changes: 63 additions & 33 deletions src/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ module.exports = {
})
}
},
share: {
help: () => 'print a cabal key with you as admin. useful for sending to friends',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussion point: do we want /share to share with you as admin, or with you + whoever is in your admin/mod-keys?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@noffle good discussion point! my reasoning was as follows:

what i reckoned was that it would get so absurdly long as to be useless if we shared all the admin + modkeys, which is why i opted for sharing oneself as an admin; reasoning that would have a similar effect

(at least that was what i was thinking at the time; given that the passed-in admin/modkeys don't actually issue admin/mods as statements then they won't be transitively be available)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! I wasn't sure where to continue this convo :)

As I understand it, most people have one admin key. So a /share that used your admin keys should also just include one key, right? It feels like it might get confusing if inviting another user gives them a different moderation view than yours.

call: (cabal, res, arg) => {
const adminkey = `cabal://${cabal.key}?admin=${cabal.user.key}`
res.info(adminkey, { data: { adminkey }})
res.end()
}
},
ids: {
help: () => 'toggle showing ids at the end of nicks. useful for moderation',
call: (cabal, res, arg) => {
Expand Down Expand Up @@ -215,6 +223,36 @@ module.exports = {
})
}
},
moderation: {
help: () => 'display moderation help and commands',
call: (cabal, res, arg) => {
const baseCmds = ["hide", "mod", "admin"]
const extraCmds = ["inspect", "ids"]
const debugCmds = ["flag", "flags"]
res.info("moderation commands")
res.info("\nbasic actions. the basic actions will be published to your log")
res.info("USAGE /<cmd> NICK{.PUBKEY} {REASON...}")
baseCmds.forEach((base) => {
res.info(`/${base}: ${module.exports[base].help()}`)
const reverse = `un${base}`
res.info(`/${reverse}: ${module.exports[reverse].help()}`)
})
res.info("\nlisting applied moderation actions. local actions (i.e. not published)")
baseCmds.forEach((base) => {
const list = `${base}s`
res.info(`/${list}: ${module.exports[list].help()}`)
})
res.info("\nadditional commands. local actions")
extraCmds.forEach((cmd) => {
res.info(`/${cmd}: ${module.exports[cmd].help()}`)
})
res.info("\ndebug commands")
debugCmds.forEach((cmd) => {
res.info(`/${cmd}: ${module.exports[cmd].help()}`)
})
res.end()
}
},
hide: {
help: () => 'hide a user from a channel or the whole cabal',
call: (cabal, res, arg) => {
Expand Down Expand Up @@ -481,50 +519,42 @@ function flagCmd (cmd, cabal, res, arg) {
return res.end()
}
id = keys[0]
var fname = /^un/.test(cmd) ? 'removeFlags' : 'addFlags'
var type = /^un/.test(cmd) ? 'remove' : 'add'
var flag = cmd.replace(/^un/,'')
cabal.core.moderation[fname]({
id,
channel,
flags: [flag],
reason: args.slice(1).join(' ')
}, (err) => {
if (err) { res.error(err) }
else {
var reason = args.slice(1).join(' ')
cabal.moderation._flagCmd(flag, type, channel, id, reason).then(() => {
res.info(`${/^un/.test(cmd) ? 'removed' : 'added'} flag ${flag} for ${id}`)
res.end()
}
})
}).catch((err) => { res.error(err) })
}

function listCmd (cmd, cabal, res, arg) {
var args = arg ? arg.split(/\s+/) : []
var channel = '@'
pump(
cabal.core.moderation.listByFlag({ flag: cmd, channel }),
to.obj(write, end)
)
function write (row, enc, next) {
if (/^[0-9a-f]{64}@\d+$/.test(row.key)) {
cabal.core.getMessage(row.key, function (err, doc) {
if (err) return res.error(err)
res.info(Object.assign({}, row, {
text: `${cmd}: ${getPeerName(cabal, row.id)}: `

cabal.moderation._listCmd(cmd, channel).then((keys) => {
if (keys.length === 0) {
res.info(`you don't have any ${cmd}s`)
res.end()
return
}
keys.forEach((key) => {
if (/^[0-9a-f]{64}@\d+$/.test(key)) {
cabal.core.getMessage(key, function (err, doc) {
if (err) return res.error(err)
res.info(Object.assign({}, {
text: `${cmd}: ${getPeerName(cabal, key)}: `
+ (doc.timestamp ? strftime(' [%F %T] ', new Date(doc.timestamp)) : '')
+ (doc.content && doc.content.reason || '')
}))
})
} else {
res.info(Object.assign({}, {
text: `${cmd}: ${getPeerName(cabal, key)}`
}))
})
} else {
res.info(Object.assign({}, row, {
text: `${cmd}: ${getPeerName(cabal, row.id)}`
}))
}
next()
}
function end (next) {
res.end()
next()
}
}
})
})
}

function ucfirst (s) {
Expand Down
108 changes: 108 additions & 0 deletions src/moderation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const pump = require('pump')
const to = require('to2')

class Moderation {
constructor (core) {
this.core = core
}

getAdmins (channel) {
return this._listCmd('admin', channel)
}

getMods (channel) {
return this._listCmd('mod', channel)
}

getHides (channel) {
return this._listCmd('hide', channel)
}

getBlocks (channel) {
return this._listCmd('block', channel)
}

hide (id, opts) {
opts = opts || {}
return this.setFlag('hide', 'add', opts.channel, id, opts.reason)
}

unhide (id, opts) {
opts = opts || {}
return this.setFlag('hide', 'remove', opts.channel, id, opts.reason)
}

block (id, opts) {
opts = opts || {}
return this.setFlag('block', 'add', opts.channel, id, opts.reason)
}

unblock (id, opts) {
opts = opts || {}
return this.setFlag('block', 'remove', opts.channel, id, opts.reason)
}

addAdmin (id, opts) {
opts = opts || {}
return this.setFlag('admin', 'add', id, opts.channel, opts.reason)
}

removeAdmin (id, opts) {
opts = opts || {}
return this.setFlag('admin', 'remove', opts.channel, id, opts.reason)
}

addMod (id, opts) {
opts = opts || {}
return this.setFlag('mod', 'add', opts.channel, id, opts.reason)
}

removeMod (id, opts) {
opts = opts || {}
return this.setFlag('mod', 'remove', opts.channel, id, opts.reason)
}

setFlag (flag, type, channel='@', id, reason='') {
// a list of [[id, reason]] was passed in
if (typeof id[Symbol.iterator] === 'function') {
const promises = id.map((entry) => { return this._flagCmd(flag, type, channel, entry[0], entry[1]) })
return Promise.all(promises)
}
return this._flagCmd(flag, type, id, channel, reason)
}

_flagCmd (flag, type, channel='@', id, reason='') {
const fname = (type === 'add' ? 'addFlags' : 'removeFlags')
return new Promise((resolve, reject) => {
this.core.moderation[fname]({
id,
channel,
flags: [flag],
reason
}, (err) => {
if (err) { return reject(err) }
else { resolve() }
})
})
}

_listCmd (cmd, channel='@') {
const keys = []
return new Promise((resolve, reject) => {
const write = (row, enc, next) => {
keys.push(row.id)
next()
}
const end = (next) => {
next()
resolve(keys)
}
pump(
this.core.moderation.listByFlag({ flag: cmd, channel }),
to.obj(write, end)
)
})
}
}

module.exports = Moderation