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

Implement channel archiving #103

Merged
merged 6 commits into from
Mar 6, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var createMessagesView = require('./views/messages')
var createTopicsView = require('./views/topics')
var createUsersView = require('./views/users')
var createModerationView = require('./views/moderation')
var createArchivingView = require('./views/channel-archiving')
var swarm = require('./swarm')

var DATABASE_VERSION = 1
Expand All @@ -22,6 +23,7 @@ var TOPICS = 't'
var USERS = 'u'
var MODERATION_AUTH = 'mx'
var MODERATION_INFO = 'my'
var ARCHIVES = 'a'

module.exports = Cabal
module.exports.databaseVersion = DATABASE_VERSION
Expand Down Expand Up @@ -96,13 +98,16 @@ function Cabal (storage, key, opts) {
sublevel(this.db, MODERATION_AUTH, { valueEncoding: json }),
sublevel(this.db, MODERATION_INFO, { valueEncoding: json })
))
this.kcore.use('archives', createArchivingView(
sublevel(this.db, ARCHIVES, { 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
this.archives = this.kcore.api.archives
}

inherits(Cabal, events.EventEmitter)
Expand Down
121 changes: 121 additions & 0 deletions views/channel-archiving.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
const EventEmitter = require('events').EventEmitter
const pump = require('pump')
const Writable = require('readable-stream').Writable
const makeView = require('kappa-view')

/*
view data structure, the value (1) doesn't matter
archive!<channelname>: '<pubkey of archiver>:<reason string>'
archive!default: 'feed..b01:unused channel'

ARCHIVE msg: leveldb PUT
UNARCHIVE msg: leveldb DEL

message schema:
{
channel: <channel>,
type: 'channel/archive' or 'channel/unarchive',
key: <pubkey of archiver>,
reason: <optional string reason for archiving>
}
*/

function getAuthorizedKeys (kcore, cb) {
return Promise.all([getKeys('admin'), getKeys('mod')]).then(res => cb(res[0].concat(res[1]).map(row => row.id)))

// collect pubkeys for cabal-wide mods or admins
function getKeys (flag) {
return new Promise((resolve, reject) => {
function processResult (err, result) {
if (err) resolve([])
resolve(result)
}
kcore.api.moderation.listByFlag({ flag, channel: '@' }, processResult)
})
}
}

module.exports = function (lvl) {
var events = new EventEmitter()

return makeView(lvl, function (db) {
return {
maxBatch: 500,
map: function (msgs, next) {
// 1. go over each msg
// 2. check if it's an archive/unarchive msg (skip if not)
// 3. accumulate a levelup PUT or DEL operation for it
// 4. write it all to leveldb as a batch
const ops = []
const seen = {}
msgs.forEach(function (msg) {
if (!sanitize(msg)) { return }
const channel = msg.value.content.channel
const reason = msg.value.content.reason || ''
const key = msg.key
const pair = channel+key // a composite key, to know if we have seen this pair
if (msg.value.type === 'channel/archive') {
ops.push({
type: 'put',
key: `archive!${channel}!${key}`,
value: `${reason}`
Copy link
Member

@hackergrrl hackergrrl Feb 22, 2021

Choose a reason for hiding this comment

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

For future development, it might be easier to store the sequence number here instead of duplicating the reason field, and then pull up that msg when answering a query. That way, if we want to add any additional data to archiving ops in the future, they too won't need to be duplicated in index storage.

EDIT: Realized that the value isn't being used right now anyways. Maybe let's just write the seq# for now (so it's recorded going forward) and we can always add logic to grab the reason etc in the future.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed this! i saved the key@seq cause i figure that would save a concatenation step during lookup, lmk if that's silly and i'll just save the seq~

})

if (!seen[pair]) events.emit('archive', channel, reason, key)
seen[pair] = true
cblgh marked this conversation as resolved.
Show resolved Hide resolved
} else if (msg.value.type === 'channel/unarchive') {
ops.push({
type: 'del',
key: `archive!${channel}!${key}`,
})
if (seen[pair]) { delete seen[pair] }
events.emit('unarchive', channel, reason, key)
}
})
if (ops.length) db.batch(ops, next)
else next()
},
api: {
// get the list of currently archived channels
// TODO: include stored values (use lvl.createValueStream() somehow?)
get: function (core, cb) {
this.ready(function () {
// query mod view to determine if archiver is either a mod or an admin
getAuthorizedKeys(core, authorizedKeys => {
const channels = []
db.createKeyStream({
gt: 'archive!!',
lt: 'archive!~'
})
.on('data', function (row) {
// structure of `row`: archive!<channel>!<key>
const pieces = row.split('!')
const channel = pieces[1]
const key = pieces[2]
if (authorizedKeys.indexOf(key) >= 0) {
channels.push(channel)
}
})
.once('end', function () {
cb(null, channels)
})
.once('error', cb)
})
})
},
events: events
}
}
})
}

// Either returns a well-formed chat message, or null.
function sanitize (msg) {
if (typeof msg !== 'object') return null
if (typeof msg.value !== 'object') return null
if (typeof msg.value.content !== 'object') return null
if (typeof msg.value.timestamp !== 'number') return null
if (typeof msg.value.type !== 'string') return null
if (typeof msg.value.content.channel !== 'string') return null
return msg
}