Skip to content
This repository has been archived by the owner on Jan 19, 2022. It is now read-only.

Commit

Permalink
3.10.0: New connections scheme, new commands, clearer readme
Browse files Browse the repository at this point in the history
  • Loading branch information
Bjornskjald committed Sep 11, 2018
2 parents cffd4ef + 113962e commit bf3ee93
Show file tree
Hide file tree
Showing 41 changed files with 863 additions and 633 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## [3.10.0] - 2018-09-09
### Added
- Read-only channels
- Commands: `eval`, `info`, `add`, `remove`
- Hiding automatically created channels for non-admins

### Changed
- Renamed channels.yml to connections.yml
- Changed format of connections.yml
- Commands: `link`, `unlink`

## [3.9.3] - 2018-09-05
### Added
- Truncating message when exceeds 2000 chars on Discord
Expand Down
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@
**[FAQ](../../wiki/faq)**  
**[Config Generator](https://miscord.net/config-generator.html)**  

<br>
# :memo: Wiki

<a href="https://miscord.net/">
<img src="../gh-pages/img/screenshot.png" style="max-width: 80%">
</a>
**Miscord has its own wiki [here](../../wiki), any information is most likely to be there**

# :wrench: Setup

## Installation
- [NPM install (recommended method)](../../wiki/install#npm)
- [Binary packages](../../releases/latest)
- [Docker install](../../wiki/install#docker)

## Configuration

**Follow a guide [here](../../wiki/Creating-a-Discord-bot) to get the Discord token**
Expand All @@ -33,25 +36,34 @@ Default location of config file:

**You can use config generator [here](https://miscord.net/config-generator.html)**

**Example config: see [here](config.example.json)**

**See all config properties [here](../../wiki/configuration)**

## Installation
- [NPM install (recommended method)](../../wiki/install#npm)
- [Binary packages](../../releases/latest)
- [Docker install](../../wiki/install#docker)
**Next step: connections configuration - see [here](../../wiki/Connections.yml)**

# :electric_plug: Running

## Binaries/NPM install

Run `miscord` in the console.
_If you store your config somewhere else, you can run it with `miscord --config {path}`_
_If you store your config somewhere else, you can run it with `miscord --config {path}`_ like this:
*In this example config is stored in a folder called miscord in your home directory*

```
miscord --config ~/miscord/config.json
```

## Local install

Enter the Miscord directory where you cloned it (`cd miscord`)
Run it using `npm start`.
_If you store your config somewhere else, you can run it with `npm start -- --config {path}` (note the `--` before `--config`)_
_If you store your config somewhere else, you can run it with `npm start -- --config {path}` (note the `--` before `--config`)_ like this:
*In this example config is stored in a folder called miscord in your home directory*

```
npm start -- --config ~/miscord/config.json
```

# :warning: Disclaimer

Expand All @@ -62,4 +74,4 @@ All product and company names are trademarks™ or registered® trademarks of th
Facebook and the Facebook logo are trademarks or registered trademarks of Facebook, Inc., used under license agreement.

# :scroll: License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details.
140 changes: 72 additions & 68 deletions lib/ConnectionsManager.js
Original file line number Diff line number Diff line change
@@ -1,117 +1,121 @@
Object.fromMap = map => Object.assign(...map.map((v, k) => ({[k]: v})))
const yaml = require('js-yaml')
const fs = require('fs')
const log = require('npmlog')
const { promisify } = require('util')
const { Collection } = require('discord.js')
const path = require('path')
const { getThread, getChannelName } = require('./messenger')

class ConnectionsManager {
constructor () {
this.path = path.join(config.path, 'channels.yml')
if (fs.existsSync(path.join(config.path, 'channels.yml'))) {
fs.renameSync(path.join(config.path, 'channels.yml'), path.join(config.path, 'connections.yml'))
}

this.path = path.join(config.path, 'connections.yml')
}

async load () {
try {
// load file and parse YAML
let parsed = yaml.safeLoad(fs.readFileSync(this.path, 'utf8'))

if (!parsed) throw new Error('not an error')

// throw error when channels.yaml is empty (TODO)
if (typeof parsed !== 'object') throw new Error('channels.yml\'s type is not "object"')
// throw error when connections.yml is empty
if (typeof parsed !== 'object') throw new Error('connections.yml\'s type is not "object"')

// create Collection from these channels
this.channels = new Collection()
Object.entries(parsed).forEach(
([discord, messenger]) => {
if (discord.startsWith('_')) return
const channel = this.findChannel(discord)
if (typeof messenger === 'string') return this.channels.set(messenger, channel)
messenger.forEach(threadID => this.channels.set(threadID, channel))
}
)
this.list = new Collection()
await Promise.all(Object.entries(parsed).map(async ([key, value]) => {
log.verbose('ConnectionsManager: init', 'key: %s, value: %s', key, value)
if (typeof value === 'string' || value.some(el => typeof el === 'string')) return this.parseOldFormat(key, value)
value = await Promise.all(value.map(async endpoint => {
if (!endpoint.type || !endpoint.id) throw new Error('type or id missing on endpoint ' + JSON.stringify(endpoint))
if (endpoint.name) return endpoint
if (endpoint.type === 'discord') endpoint.name = discord.client.channels.get(endpoint.id).name
else endpoint.name = await getChannelName(await getThread(endpoint.id), false)
return endpoint
}))
this.list.set(key, value)
}))
await this.save()
} catch (err) {
if (err.message !== 'not an error' && err.code !== 'ENOENT') throw err
this.channels = new Collection()
this.list = new Collection()
await this.save()
}
}

async parseOldFormat (key, value) {
if (key.startsWith('_')) return log.verbose('CM: parseOldFormat', 'Ignoring ', key)
if (value === '0') return log.verbose('CM: parseOldFormat', 'Ignoring ', key)

this.save()
const connection = [ { type: 'discord', id: key, name: discord.client.channels.get(key).name } ]
if (typeof value === 'string') connection.push({ type: 'messenger', id: value, name: await getChannelName(await getThread(value), false) })
else await Promise.all(value.map(async threadID => connection.push({ type: 'messenger', id: threadID, name: await getChannelName(await getThread(threadID), false) })))

log.verbose('CM: parseOldFormat', 'Migrated connection:', JSON.stringify(connection))
return this.list.set(discord.client.channels.get(key).name, connection)
}

async getChannel (id, name) {
async getChannels (id, name) {
log.verbose('CM: getChannel', 'Looking up channel with name %s and ID %s', name, id)

// try to get channel by id
let channel = this.channels.get(id)
log.silly('CM: getChannel: existing channel', channel)

// if found, change name and return
if (channel) return (config.discord.renameChannels && this.getThreadIDs(channel.id).length === 1) ? (channel.name === name ? channel : channel.edit({ name })) : channel
// try to get channels by id
const channels = this.getConnection(id)
.filter(endpoint => endpoint.type === 'discord')
.map(channel => { let c = discord.client.channels.get(channel.id); c.readonly = channel.readonly; return c })
log.silly('CM: getChannel: existing channels', channels)

// if found
if (channels.length) {
// if: renaming is disabled/it's not one-to-one/name matches - return original channels
if (!config.discord.renameChannels || this.getThreads(id).length > 1 || channels.length > 1 || channels[0].name === name) return channels.filter(el => !el.readonly)
// rename and return as array
return [await channels[0].edit({ name })].filter(el => !el.readonly)
}

if (!config.discord.createChannels) return log.verbose('CM: getChannel', 'Channel creation disabled, skipping.')
if (!config.discord.createChannels) return log.verbose('CM: getChannel', 'Channel creation disabled, skipping. Name: %s, id: %s', name, id) || []

// if not found, create new channel with specified name and set its parent
channel = await discord.guilds[0].createChannel(name, 'text')
const channel = await discord.guilds[0].createChannel(name, 'text')
await channel.overwritePermissions(discord.guilds[0].roles.find(role => role.name === '@everyone').id, { VIEW_CHANNEL: false })
log.verbose('CM: getChannel', 'Channel created, name: %s, id: %s', name, channel.id)
// TODO: the code below could crash if category user provided is not in the same guild
if (discord.category) await channel.setParent(discord.category)

// save newly created channel in the channels map
this.channels.set(id, channel)
this.list.set(name, [
{ type: 'discord', id: channel.id, name },
{ type: 'messenger', id, name }
])
await this.save()

log.silly('getChannel: new channel', channel)
return channel
return [ channel ]
}

getThreads (id) {
return this.getConnection(id).filter(endpoint => endpoint.type === 'messenger')
}

has (nameOrID) {
return this.channels.find(discord => nameOrID === discord.name || nameOrID === discord.id) || this.channels.has(nameOrID)
getConnection (id) {
return this.list.find(connection => connection.some(endpoint => endpoint.id === id)) || []
}

getThreadIDs (nameOrID) {
return this.channels.filter(discord => discord.id === nameOrID || discord.name === nameOrID).map((value, key) => key)
has (id) {
return this.list.some(connection => connection.some(endpoint => endpoint.id === id))
}

save () {
let obj = {
__comment: 'This is your channels.yml file. Lines beginning with "_" are created by Miscord just for readability, they\'re not needed if you\'re editing this file by hand. You can add your own comments by adding lines starting with "#"'
}
for (const [messenger, discord] of this.channels.entries()) {
let otherChannels = this.channels.filter(another => another.id === discord.id).map((value, key) => key)
obj[discord.id] = otherChannels.length > 1 ? otherChannels : messenger
obj['_' + discord.id] = discord.name
__comment: 'This is your connections.yml file. More info at https://github.com/miscord/miscord/wiki/Connections.yml'
}
if (this.list.size) obj = Object.assign(obj, Object.fromMap(this.list))
return promisify(fs.writeFile)(this.path, yaml.safeDump(obj), 'utf8')
}

findChannel (nameOrID) {
return discord.client.channels.find(channel => channel.name === nameOrID || channel.id === nameOrID) || log.warn('CM', 'Discord channel %s has not been mapped properly', nameOrID)
}

add (discord, messenger) {
const channel = this.findChannel(discord)
if (!channel) return `Channel ${discord} not found!`
if (typeof messenger === 'string') this.channels.set(messenger, channel)
else messenger.forEach(threadID => this.channels.set(threadID, channel))
this.save()
let plural = typeof messenger !== 'string'
return `Thread${plural ? 's' : ''} ${plural ? messenger.join(', ') : messenger} ${plural ? 'were' : 'was'} linked to #${channel.name}!`
}

remove (messenger) {
this.channels.delete(messenger)
this.save()
return `Thread ${messenger} was removed from channel map!`
}

getPrintable () {
var getFBLink = id => `[\`${id}\`](https://facebook.com/messages/t/${id})`
let arr = []
for (let [messenger, discord] of this.channels.entries()) {
let otherChannels = this.channels.filter(another => another.id === discord.id).map((value, key) => key)

messenger = otherChannels.length > 1 ? otherChannels.map(getFBLink) : getFBLink(messenger)
discord = `<#${discord.id}>`
arr.push(`${discord}: ${messenger}`)
}
return [ ...new Set(arr) ].join('\n')
}
}

module.exports = ConnectionsManager
13 changes: 13 additions & 0 deletions lib/commands/add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const Command = require('./Command')

module.exports = new Command({
argc: 1,
usage: `add <connection name>`,
example: `add test-connection`
}, (argv, reply) => {
const name = argv[0]
if (connections.list.has(name)) return `Connection \`${name}\` already exists!`
connections.list.set(name, [])
connections.save()
reply(`Connection \`${name}\` was added successfully!`)
})
33 changes: 33 additions & 0 deletions lib/commands/eval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const Command = require('./Command')
// shamelessly stolen from takidelfin/8rniczka
// full credits to @takidelfin

module.exports = new Command({
argc: 1,
usage: `eval <code>`,
example: `eval config.messenger.whitelist = ['123']`,
allowMoreArguments: true
}, (argv, reply) => {
try {
const code = argv.join(' ')
let evaled = eval(code) // eslint-disable-line

if (typeof evaled !== 'string') evaled = require('util').inspect(evaled)
if (evaled.length >= 2000) {
for (let i = 0; i < evaled.length; i += 2000) {
reply(evaled.substring(i, i + 2000), { code: 'xl' })
}
return
}
reply(evaled, { code: 'xl' })
} catch (err) {
const embed = {
title: 'Miscord eval() error',
description: 'Whoops, looks like eval() died :(\n```\n' + clean(err) + '\n```',
color: 16741749
}
reply({ embed })
}
})

const clean = text => typeof text === 'string' ? text.replace(/`/g, '`' + String.fromCharCode(8203)).replace(/@/g, '@' + String.fromCharCode(8203)) : text
9 changes: 7 additions & 2 deletions lib/commands/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ module.exports = new Command((_, reply) => {
reply(`Commands available:
- \`set\` - sets value in the config
- \`get\` - gets value from the config
- \`link\` - links one or more Facebook threads to a Discord channel
- \`unlink\` - removes existing link from the channel map
- \`add\` - adds a new connection
- \`rename\` - renames an existing connection
- \`remove\` - removes an existing connection
- \`list\` - shows existing connections
- \`info\` - shows endpoints of an existing connection
- \`link\` - adds an endpoint to an existing connection
- \`unlink\` - removes an endpoint from an existing connection
- \`readonly\` - toggles read-only status
- \`showConfig\` - shows the entire config
- \`help\` - shows this message
- \`quit\` - exits Miscord`)
Expand Down
9 changes: 8 additions & 1 deletion lib/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ module.exports = {
get: require('./get'),
link: require('./link'),
unlink: require('./unlink'),
add: require('./add'),
remove: require('./remove'),
delete: require('./remove'),
list: require('./list'),
info: require('./info'),
showConfig: require('./showConfig'),
help: require('./help'),
quit: require('./quit')
quit: require('./quit'),
eval: require('./eval'),
rename: require('./rename'),
readonly: require('./readonly')
}
18 changes: 18 additions & 0 deletions lib/commands/info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const Command = require('./Command')
const getFBLink = id => `[\`${id}\`](https://facebook.com/messages/t/${id})`
const e = endpoint => `\`${endpoint.type}\`: ${endpoint.type === 'messenger' ? getFBLink(endpoint.id) : `<#${endpoint.id}>`}${endpoint.readonly ? ' (readonly)' : ''}`

module.exports = new Command({
argc: 1,
usage: `info <connection name/Discord channel ID/Facebook thread ID>`,
example: `info test-connection`
}, async (argv, reply) => {
let name = argv[0]
let connection = connections.list.get(name)
if (!connection) connection = connections.list.filter(connection => connection.some(endpoint => endpoint.id === name)).map((value, key) => { name = key; return value })[0]
if (!connection) return reply(`Connection \`${name}\` not found!`)
reply({ embed: {
title: `Connection: ${name}`,
description: `${connection.map(e).join('\n')}`
}})
})
Loading

0 comments on commit bf3ee93

Please sign in to comment.