<commit_before>const AkairoModule = require('./AkairoModule');
const Argument = require('./Argument');
const { ArgumentMatches, ArgumentSplits } = require('../util/Constants');

/**
 * Options to use for command execution behavior.
 * @typedef {Object} CommandOptions
 * @prop {string[]} [aliases=[]] - Command names.
 * @prop {ArgumentOptions[]} [args=[]] - Arguments to parse.
 * @prop {ArgumentSplit} [split='plain'] - Method to split text into words.
 * @prop {string} [channelRestriction] - Restricts channel: 'guild' or 'dm'.
 * @prop {string} [category='default'] - Category ID for organization purposes.
 * @prop {boolean} [ownerOnly=false] - Whether or not to allow client owner(s) only.
 * @prop {boolean} [protected=false] - Whether or not this command cannot be disabled.
 * @prop {boolean} [editable=true] - Whether or not message edits will run this command.
 *
On an edited message, the exec function edited param will be true.
 * @prop {number} [cooldown] - The command cooldown in milliseconds.
 * @prop {number} [ratelimit=1] - Amount of command uses allowed until cooldown.
 * @prop {string|string[]|Function} [prefix] - A prefix to overwrite the global one for this command.
 *
Can be a function (message => {}).
 * @prop {RegExp|Function} [trigger] - A regex or function (message => {}) returning regex to match in messages that are NOT commands.
 *
The exec function is ((message, match, groups, edited) => {}).
 * @prop {Function} [condition] - A function ((message, edited) => {}) that returns true or false on messages that are NOT commands.
 *
The exec function is ((message, edited) => {}).
 * @prop {PromptOptions} [defaultPrompt={}] - The default prompt options.
 * @prop {Object} [options={}] - An object for custom options.
 *
Accessible with Command#options.
 * @prop {string|string[]} [description=''] - Description of the command.
 */

/**
 * The method to split text into words.
 *
plain splits word separated by whitespace. Extra whitespace is ignored.
 *
split splits word separated by whitespace.
 *
quoted is similar to plain, but counts text inside double quotes as one word.
 *
sticky is similar to quoted, but makes it so that quoted text must have a whitespace/another double quote before it to count as another word.
 *

 *
A regex or a character can be used instead (for example, a comma) to split the message by that regex or character.
 * @typedef {string} ArgumentSplit
 */

/** @extends AkairoModule */
class Command extends AkairoModule {
    /**
     * Creates a new command.
     * @param {string} id - Command ID.
     * @param {Function} exec - Function ((message, args, edited) => {}) called when command is ran.
     * @param {CommandOptions} [options={}] - Options for the command.
     */
    constructor(id, exec, options = {}) {
        super(id, exec, options);

        /**
         * Command names.
         * @type {string[]}
         */
        this.aliases = options.aliases || [];

        /**
         * Arguments for the command.
         * @type {Argument[]}
         */
        this.args = (options.args || []).map(a => new Argument(this, a));

        /**
         * The command split method.
         * @type {ArgumentSplit}
         */
        this.split = options.split || ArgumentSplits.PLAIN;

        /**
         * Usable only in this channel type.
         * @type {string}
         */
        this.channelRestriction = options.channelRestriction;

        /**
         * Usable only by the client owner.
         * @type {boolean}
         */
        this.ownerOnly = !!options.ownerOnly;

        /**
         * Whether or not this command cannot be disabled.
         * @type {boolean}
         */
        this.protected = !!options.protected;

        /**
         * Whether or not this command can be ran by an edit.
         * @type {boolean}
         */
        this.editable = !(options.editable === false);

        /**
         * Cooldown in milliseconds.
         * @type {number}
         */
        this.cooldown = options.cooldown;

        /**
         * Uses allowed before cooldown.
         * @type {number}
         */
        this.ratelimit = options.ratelimit || 1;

        /**
         * Default prompt options.
         * @type {PromptOptions}
         */
        this.defaultPrompt = options.defaultPrompt || {};

        /**
         * Custom options for the command.
         * @type {Object}
         */
        this.options = options.custom || options.options || {};

        /**
         * Description of the command.
         * @type {string}
         */
        this.description = (Array.isArray(options.description) ? options.description.join('\n') : options.description) || '';

        /**
         * Command prefix overwrite.
         * @type {?string|string[]|Function}
         */
        this.prefix = options.prefix;

        /**
         * Gets the regex trigger, if specified.
         * @method
         * @param {Message} message - Message being handled.
         * @returns {RegExp}
         */
        this.trigger = typeof options.trigger === 'function' ? options.trigger : () => options.trigger;

        /**
         * Gets the condition trigger, if specified.
         * @method
         * @param {Message} message - Message being handled.
         * @returns {boolean}
         */
        this.condition = options.condition || (() => false);

        /**
         * The ID of this command.
         * @name Command#id
         * @type {string}
         */

        /**
         * Executes the command.
         * @method
         * @name Command#exec
         * @returns {any}
         */

        /**
         * The command handler.
         * @readonly
         * @name Command#handler
         * @type {CommandHandler}
         */
    }

    /**
     * Parses text based on this command's args.
     * @param {string} content - String to parse.
     * @param {Message} [message] - Message to use.
     * @returns {Promise<Object>}
     */
    parse(content, message) {
        if (!this.args.length) return Promise.resolve({});

        const splitFuncs = {
            [ArgumentSplits.PLAIN]: () => content.match(/[^\s]+/g),
            [ArgumentSplits.SPLIT]: () => content.split(' '),
            [ArgumentSplits.QUOTED]: () => content.match(/".*?"|[^\s"]+|"/g),
            [ArgumentSplits.STICKY]: () => content.match(/[^\s"]*?".*?"|[^\s"]+|"/g)
        };

        const words = splitFuncs[this.split] ? splitFuncs[this.split]() || [] : content.split(this.split);

        const prefixes = this.args.reduce((arr, arg) => {
            if (arg.match === ArgumentMatches.PREFIX || arg.match === ArgumentMatches.FLAG) {
                if (Array.isArray(arg.prefix)) {
                    for (const p of arg.prefix) {
                        arr.push({
                            value: p.toLowerCase(),
                            flag: arg.match === ArgumentMatches.FLAG
                        });
                    }
                } else {
                    arr.push({
                        value: arg.prefix.toLowerCase(),
                        flag: arg.match === ArgumentMatches.FLAG
                    });
                }
            }

            return arr;
        }, []);

        const noPrefixWords = words.filter(w => {
            return !prefixes.some(p => {
                if (!p.flag) return w.toLowerCase().startsWith(p.value);
                return w.toLowerCase() === p.value;
            });
        });

        const parseFuncs = {
            [ArgumentMatches.WORD]: (arg, index) => {
                let word = noPrefixWords[arg.index != null ? arg.index : index] || '';

                const isQuoted = (this.split === ArgumentSplits.QUOTED || this.split === ArgumentSplits.STICKY) && /^".*"$/.test(word);
                if (isQuoted) word = word.slice(1, -1);

                return arg.cast.bind(arg, word);
            },
            [ArgumentMatches.REST]: (arg, index) => {
                const word = noPrefixWords.slice(arg.index != null ? arg.index : index).join(' ') || '';
                return arg.cast.bind(arg, word);
            },
            [ArgumentMatches.PREFIX]: arg => {
                let word = words.find(w => {
                    return Array.isArray(arg.prefix)
                    ? arg.prefix.some(p => w.toLowerCase().startsWith(p.toLowerCase()))
                    : w.toLowerCase().startsWith(arg.prefix.toLowerCase());
                }) || '';

                const prefix = prefixes.find(p => {
                    if (!p.flag) return word.toLowerCase().startsWith(p.value);
                    return word.toLowerCase() === p.value;
                });

                if (prefix) {
                    word = word.replace(prefix.value, '');
                    if (this.split === ArgumentSplits.STICKY && /^".*"$/.test(word)) word = word.slice(1, -1);
                }

                return arg.cast.bind(arg, word);
            },
            [ArgumentMatches.FLAG]: arg => {
                const word = words.find(w => {
                    return Array.isArray(arg.prefix)
                    ? arg.prefix.some(p => w.toLowerCase() === p.toLowerCase())
                    : w.toLowerCase() === arg.prefix.toLowerCase();
                }) || '';

                return () => Promise.resolve(arg.default() ? !word : !!word);
            },
            [ArgumentMatches.TEXT]: arg => {
                const word = noPrefixWords.slice(arg.index).join(' ');
                return arg.cast.bind(arg, word);
            },
            [ArgumentMatches.CONTENT]: arg => {
                const word = content.split(' ').slice(arg.index).join(' ');
                return arg.cast.bind(arg, word);
            }
        };

        const processed = {};
        let wordIndex = 0;

        const process = i => {
            if (i === this.args.length) return processed;

            const arg = this.args[i];
            const matchType = typeof arg.match === 'function' ? arg.match.call(this, message, processed) : arg.match;
            const castFunc = parseFuncs[matchType](arg, wordIndex);

            if (matchType === ArgumentMatches.WORD || matchType === ArgumentMatches.REST) wordIndex++;

            return castFunc(message, processed).then(res => {
                processed[arg.id] = res;
                return process(i + 1);
            });
        };

        return process(0);
    }

    /**
     * Disables the command.
     * @returns {boolean}
     */
    disable() {
        if (this.protected) return false;
        return super.disable();
    }

    /**
     * Reloads the command.
     * @method
     * @name Command#reload
     * @returns {Command}
     */

    /**
     * Removes the command.
     * @method
     * @name Command#remove
     * @returns {Command}
     */

    /**
     * Enables the command.
     * @method
     * @name Command#enable
     * @returns {boolean}
     */
}

module.exports = Command;
<commit_msg>Trim some things<commit_after>const AkairoModule = require('./AkairoModule');
const Argument = require('./Argument');
const { ArgumentMatches, ArgumentSplits } = require('../util/Constants');

/**
 * Options to use for command execution behavior.
 * @typedef {Object} CommandOptions
 * @prop {string[]} [aliases=[]] - Command names.
 * @prop {ArgumentOptions[]} [args=[]] - Arguments to parse.
 * @prop {ArgumentSplit} [split='plain'] - Method to split text into words.
 * @prop {string} [channelRestriction] - Restricts channel: 'guild' or 'dm'.
 * @prop {string} [category='default'] - Category ID for organization purposes.
 * @prop {boolean} [ownerOnly=false] - Whether or not to allow client owner(s) only.
 * @prop {boolean} [protected=false] - Whether or not this command cannot be disabled.
 * @prop {boolean} [editable=true] - Whether or not message edits will run this command.
 *
On an edited message, the exec function edited param will be true.
 * @prop {number} [cooldown] - The command cooldown in milliseconds.
 * @prop {number} [ratelimit=1] - Amount of command uses allowed until cooldown.
 * @prop {string|string[]|Function} [prefix] - A prefix to overwrite the global one for this command.
 *
Can be a function (message => {}).
 * @prop {RegExp|Function} [trigger] - A regex or function (message => {}) returning regex to match in messages that are NOT commands.
 *
The exec function is ((message, match, groups, edited) => {}).
 * @prop {Function} [condition] - A function ((message, edited) => {}) that returns true or false on messages that are NOT commands.
 *
The exec function is ((message, edited) => {}).
 * @prop {PromptOptions} [defaultPrompt={}] - The default prompt options.
 * @prop {Object} [options={}] - An object for custom options.
 *
Accessible with Command#options.
 * @prop {string|string[]} [description=''] - Description of the command.
 */

/**
 * The method to split text into words.
 *
plain splits word separated by whitespace. Extra whitespace is ignored.
 *
split splits word separated by whitespace.
 *
quoted is similar to plain, but counts text inside double quotes as one word.
 *
sticky is similar to quoted, but makes it so that quoted text must have a whitespace/another double quote before it to count as another word.
 *

 *
A regex or a character can be used instead (for example, a comma) to split the message by that regex or character.
 * @typedef {string} ArgumentSplit
 */

/** @extends AkairoModule */
class Command extends AkairoModule {
    /**
     * Creates a new command.
     * @param {string} id - Command ID.
     * @param {Function} exec - Function ((message, args, edited) => {}) called when command is ran.
     * @param {CommandOptions} [options={}] - Options for the command.
     */
    constructor(id, exec, options = {}) {
        super(id, exec, options);

        /**
         * Command names.
         * @type {string[]}
         */
        this.aliases = options.aliases || [];

        /**
         * Arguments for the command.
         * @type {Argument[]}
         */
        this.args = (options.args || []).map(a => new Argument(this, a));

        /**
         * The command split method.
         * @type {ArgumentSplit}
         */
        this.split = options.split || ArgumentSplits.PLAIN;

        /**
         * Usable only in this channel type.
         * @type {string}
         */
        this.channelRestriction = options.channelRestriction;

        /**
         * Usable only by the client owner.
         * @type {boolean}
         */
        this.ownerOnly = !!options.ownerOnly;

        /**
         * Whether or not this command cannot be disabled.
         * @type {boolean}
         */
        this.protected = !!options.protected;

        /**
         * Whether or not this command can be ran by an edit.
         * @type {boolean}
         */
        this.editable = !(options.editable === false);

        /**
         * Cooldown in milliseconds.
         * @type {number}
         */
        this.cooldown = options.cooldown;

        /**
         * Uses allowed before cooldown.
         * @type {number}
         */
        this.ratelimit = options.ratelimit || 1;

        /**
         * Default prompt options.
         * @type {PromptOptions}
         */
        this.defaultPrompt = options.defaultPrompt || {};

        /**
         * Custom options for the command.
         * @type {Object}
         */
        this.options = options.custom || options.options || {};

        /**
         * Description of the command.
         * @type {string}
         */
        this.description = (Array.isArray(options.description) ? options.description.join('\n') : options.description) || '';

        /**
         * Command prefix overwrite.
         * @type {?string|string[]|Function}
         */
        this.prefix = options.prefix;

        /**
         * Gets the regex trigger, if specified.
         * @method
         * @param {Message} message - Message being handled.
         * @returns {RegExp}
         */
        this.trigger = typeof options.trigger === 'function' ? options.trigger : () => options.trigger;

        /**
         * Gets the condition trigger, if specified.
         * @method
         * @param {Message} message - Message being handled.
         * @returns {boolean}
         */
        this.condition = options.condition || (() => false);

        /**
         * The ID of this command.
         * @name Command#id
         * @type {string}
         */

        /**
         * Executes the command.
         * @method
         * @name Command#exec
         * @returns {any}
         */

        /**
         * The command handler.
         * @readonly
         * @name Command#handler
         * @type {CommandHandler}
         */
    }

    /**
     * Parses text based on this command's args.
     * @param {string} content - String to parse.
     * @param {Message} [message] - Message to use.
     * @returns {Promise<Object>}
     */
    parse(content, message) {
        if (!this.args.length) return Promise.resolve({});

        const splitFuncs = {
            [ArgumentSplits.PLAIN]: () => content.match(/[^\s]+/g),
            [ArgumentSplits.SPLIT]: () => content.split(' '),
            [ArgumentSplits.QUOTED]: () => content.match(/".*?"|[^\s"]+|"/g),
            [ArgumentSplits.STICKY]: () => content.match(/[^\s"]*?".*?"|[^\s"]+|"/g)
        };

        const words = splitFuncs[this.split] ? splitFuncs[this.split]() || [] : content.split(this.split);

        const prefixes = this.args.reduce((arr, arg) => {
            if (arg.match === ArgumentMatches.PREFIX || arg.match === ArgumentMatches.FLAG) {
                if (Array.isArray(arg.prefix)) {
                    for (const p of arg.prefix) {
                        arr.push({
                            value: p.toLowerCase(),
                            flag: arg.match === ArgumentMatches.FLAG
                        });
                    }
                } else {
                    arr.push({
                        value: arg.prefix.toLowerCase(),
                        flag: arg.match === ArgumentMatches.FLAG
                    });
                }
            }

            return arr;
        }, []);

        const noPrefixWords = words.filter(w => {
            return !prefixes.some(p => {
                if (!p.flag) return w.toLowerCase().startsWith(p.value);
                return w.toLowerCase() === p.value;
            });
        });

        const parseFuncs = {
            [ArgumentMatches.WORD]: (arg, index) => {
                let word = noPrefixWords[arg.index != null ? arg.index : index] || '';

                const isQuoted = (this.split === ArgumentSplits.QUOTED || this.split === ArgumentSplits.STICKY) && /^".*"$/.test(word);
                if (isQuoted) word = word.slice(1, -1).trim();

                return arg.cast.bind(arg, word);
            },
            [ArgumentMatches.REST]: (arg, index) => {
                const word = noPrefixWords.slice(arg.index != null ? arg.index : index).join(' ') || '';
                return arg.cast.bind(arg, word);
            },
            [ArgumentMatches.PREFIX]: arg => {
                let word = words.find(w => {
                    return Array.isArray(arg.prefix)
                    ? arg.prefix.some(p => w.toLowerCase().startsWith(p.toLowerCase()))
                    : w.toLowerCase().startsWith(arg.prefix.toLowerCase());
                }) || '';

                const prefix = prefixes.find(p => {
                    if (!p.flag) return word.toLowerCase().startsWith(p.value);
                    return word.toLowerCase() === p.value;
                });

                if (prefix) {
                    word = word.replace(prefix.value, '');
                    if (this.split === ArgumentSplits.STICKY && /^".*"$/.test(word)) word = word.slice(1, -1).trim();
                }

                return arg.cast.bind(arg, word);
            },
            [ArgumentMatches.FLAG]: arg => {
                const word = words.find(w => {
                    return Array.isArray(arg.prefix)
                    ? arg.prefix.some(p => w.toLowerCase() === p.toLowerCase())
                    : w.toLowerCase() === arg.prefix.toLowerCase();
                }) || '';

                return () => Promise.resolve(arg.default() ? !word : !!word);
            },
            [ArgumentMatches.TEXT]: arg => {
                const word = noPrefixWords.slice(arg.index).join(' ');
                return arg.cast.bind(arg, word);
            },
            [ArgumentMatches.CONTENT]: arg => {
                const word = content.split(' ').slice(arg.index).join(' ');
                return arg.cast.bind(arg, word);
            }
        };

        const processed = {};
        let wordIndex = 0;

        const process = i => {
            if (i === this.args.length) return processed;

            const arg = this.args[i];
            const matchType = typeof arg.match === 'function' ? arg.match.call(this, message, processed) : arg.match;
            const castFunc = parseFuncs[matchType](arg, wordIndex);

            if (matchType === ArgumentMatches.WORD || matchType === ArgumentMatches.REST) wordIndex++;

            return castFunc(message, processed).then(res => {
                processed[arg.id] = res;
                return process(i + 1);
            });
        };

        return process(0);
    }

    /**
     * Disables the command.
     * @returns {boolean}
     */
    disable() {
        if (this.protected) return false;
        return super.disable();
    }

    /**
     * Reloads the command.
     * @method
     * @name Command#reload
     * @returns {Command}
     */

    /**
     * Removes the command.
     * @method
     * @name Command#remove
     * @returns {Command}
     */

    /**
     * Enables the command.
     * @method
     * @name Command#enable
     * @returns {boolean}
     */
}

module.exports = Command;
<|endoftext|>


<commit_before>process.env.DRSS = true
const Discord = require('discord.js')
const EventEmitter = require('events')
const listeners = require('../util/listeners.js')
const maintenance = require('../maintenance/index.js')
const ipc = require('../util/ipc.js')
const Profile = require('./db/Profile.js')
const ArticleMessage = require('./ArticleMessage.js')
const Feed = require('./db/Feed.js')
const FeedData = require('./FeedData.js')
const ArticleMessageRateLimiter = require('./ArticleMessageRateLimiter.js')
const initialize = require('../util/initialization.js')
const getConfig = require('../config.js').get
const createLogger = require('../util/logger/create.js')
const connectDb = require('../util/connectDatabase.js')
const { once } = require('events')

const DISABLED_EVENTS = [
  'TYPING_START',
  'MESSAGE_DELETE',
  'MESSAGE_UPDATE',
  'PRESENCE_UPDATE',
  'VOICE_STATE_UPDATE',
  'VOICE_SERVER_UPDATE',
  'USER_NOTE_UPDATE',
  'CHANNEL_PINS_UPDATE'
]
const STATES = {
  STOPPED: 'STOPPED',
  STARTING: 'STARTING',
  READY: 'READY'
}
const CLIENT_OPTIONS = {
  disabledEvents: DISABLED_EVENTS,
  messageCacheMaxSize: 100
}

class Client extends EventEmitter {
  constructor () {
    super()
    this.STATES = STATES
    this.state = STATES.STOPPED
    this.log = undefined
  }

  async login (token) {
    if (this.bot) {
      return this.log.warn('Cannot login when already logged in')
    }
    if (typeof token !== 'string') {
      throw new TypeError('Argument must a string')
    }
    const client = new Discord.Client(CLIENT_OPTIONS)
    try {
      this.log = createLogger('-')
      await client.login(token)
      this.log = createLogger(client.shard.ids[0].toString())
      this.bot = client
      this.shardID = client.shard.ids[0]
      this.listenToShardedEvents(client)
      if (!client.readyAt) {
        await once(client, 'ready')
        this._setup()
      } else {
        this._setup()
      }
    } catch (err) {
      this.log.error(err, 'Discord.RSS failed to start')
      ipc.send(ipc.TYPES.KILL)
    }
  }

  async connectToDatabase () {
    const config = getConfig()
    return connectDb(config.database.uri, config.database.connection)
  }

  _setup () {
    const bot = this.bot
    bot.on('error', err => {
      this.log.warn('Websocket error', err)
      const config = getConfig()
      if (config.bot.exitOnSocketIssues === true) {
        this.log.warn('Stopping all processes due to config.bot.exitOnSocketIssues')
        ipc.send(ipc.TYPES.KILL)
      } else {
        this.stop()
      }
    })
    bot.on('debug', info => this.log.debug(info))
    bot.on('resume', () => {
      this.log.info('Websocket resumed')
      this.start()
    })
    bot.on('disconnect', () => {
      this.log.warn(`SH ${this.shardID} Websocket disconnected`)
      const config = getConfig()
      if (config.bot.exitOnSocketIssues === true) {
        this.log.general.info('Stopping all processes due to config.bot.exitOnSocketIssues')
        ipc.send(ipc.TYPES.KILL)
      } else {
        this.stop()
      }
    })
    this.log.info(`Discord.RSS has logged in as "${bot.user.username}" (ID ${bot.user.id})`)
    ipc.send(ipc.TYPES.SHARD_READY, {
      guildIds: bot.guilds.cache.keyArray(),
      channelIds: bot.channels.cache.keyArray()
    })
  }

  listenToShardedEvents (bot) {
    process.on('message', async message => {
      if (!ipc.isValid(message)) {
        return
      }
      try {
        switch (message.type) {
          case ipc.TYPES.START_INIT: {
            const data = message.data
            if (data.setPresence) {
              const config = getConfig()
              bot.user.setPresence({
                status: config.bot.status,
                activity: {
                  name: config.bot.activityName,
                  type: config.bot.activityType,
                  url: config.bot.streamActivityURL || undefined
                }
              }).catch(err => this.log.warn({
                error: err
              }, 'Failed to set presence'))
            }
            this.start()
            break
          }
          case ipc.TYPES.NEW_ARTICLE:
            this.onNewArticle(message.data.newArticle, message.data.debug)
            break
          case ipc.TYPES.FINISHED_INIT:
            break
          case ipc.TYPES.SEND_USER_ALERT:
            this.sendUserAlert(message.data.channel, message.data.message)
              .catch(err => this.log.warn(`Failed to send inter-process alert to channel ${message.data.channel}`, err))
        }
      } catch (err) {
        this.log.error(err, 'client')
      }
    })
  }

  async onNewArticle (newArticle, debug) {
    const config = getConfig()
    if (config.dev === true) {
      return
    }
    const { article, feedObject } = newArticle
    const channel = this.bot.channels.cache.get(feedObject.channel)
    try {
      if (!channel) {
        this.log.debug(`No channel found for article ${article._id} of feed ${feedObject._id}`)
        return
      }
      const feedData = await FeedData.ofFeed(new Feed(feedObject))
      const articleMessage = new ArticleMessage(this.bot, article, feedData, debug)
      await ArticleMessageRateLimiter.enqueue(articleMessage)
      this.log.debug(`Sent article ${article._id} of feed ${feedObject._id}`)
    } catch (err) {
      if (err.message.includes('Rate limit')) {
        this.log.debug({
          error: err
        }, 'Ignoring rate-limited article')
        return
      }
      const article = newArticle.article
      this.log.warn({
        error: err,
        guild: channel.guild,
        channel
      }, `Failed to send article ${article.link}`)
      if (err.code === 50035) {
        channel.send(`Failed to send formatted article for article <${article.link}>.\`\`\`${err.message}\`\`\``)
          .catch(err => this.log.warn({
            error: err
          }, 'Unable to send failed-to-send message for article'))
      }
    }
  }

  async sendChannelMessage (channelID, message) {
    const channel = this.bot.channels.cache.get(channelID)
    if (!channel) {
      return
    }
    /**
     * @type {import('discord.js').GuildMember}
     */
    const guildMeMember = channel.guild.me
    if (!guildMeMember.permissionsIn(channel).has(Discord.Permissions.FLAGS.SEND_MESSAGES)) {
      this.log.warn({
        channel,
        string: message
      }, 'Failed to send Client message to channel')
      return
    }
    channel.send(message)
  }

  async sendUserAlert (channelID, message) {
    const config = getConfig()
    if (config.dev === true) {
      return
    }
    const fetchedChannel = this.bot.channels.cache.get(channelID)
    if (!fetchedChannel) {
      return
    }
    const alertMessage = `**ALERT**\n\n${message}`
    try {
      const profile = await Profile.get(fetchedChannel.guild.id)
      if (!profile) {
        return this.sendChannelMessage(channelID, alertMessage)
      }
      const alertTo = profile.alert
      for (const id of alertTo) {
        const user = this.bot.users.cache.get(id)
        if (user) {
          await user.send(alertMessage)
        }
      }
    } catch (err) {
      this.log.warn({
        error: err
      }, `Failed to send user alert to channel ${channelID}`)
      return this.sendChannelMessage(alertMessage)
    }
  }

  async start () {
    if (this.state === STATES.STARTING || this.state === STATES.READY) {
      return this.log.warn(`Ignoring start command because of ${this.state} state`)
    }
    const config = getConfig()
    this.state = STATES.STARTING
    try {
      if ((this.mongo && this.mongo.readyState !== 1) || Profile.isMongoDatabase) {
        this.mongo = await this.connectToDatabase()
      }
      await initialize.setupModels(this.mongo)
      await initialize.setupCommands()
      const uri = config.database.uri
      this.log.info(`Database URI detected as a ${uri.startsWith('mongo') ? 'MongoDB URI' : 'folder URI'}`)
      await maintenance.pruneWithBot(this.bot)
      this.state = STATES.READY
      if (config.bot.enableCommands) {
        await listeners.enableCommands(this.bot)
      }
      await initialize.setupRateLimiters(this.bot)
      this.log.info(`Commands have been ${config.bot.enableCommands ? 'enabled' : 'disabled'}.`)
      ipc.send(ipc.TYPES.INIT_COMPLETE)
      listeners.createManagers(this.bot)
      this.emit('finishInit')
    } catch (err) {
      this.log.error(err, 'Client start')
    }
  }

  stop () {
    if (this.state === STATES.STARTING || this.state === STATES.STOPPED) {
      return this.log.warn(`Ignoring stop command because of ${this.state} state`)
    }
    this.log.info('Discord.RSS has received stop command')
    clearInterval(this.maintenance)
    listeners.disableAll(this.bot)
    this.state = STATES.STOPPED
    ipc.send(ipc.TYPES.SHARD_STOPPED)
  }

  async restart () {
    if (this.state === STATES.STARTING) {
      return this.log.warn(`Ignoring restart command because of ${this.state} state`)
    }
    if (this.state === STATES.READY) {
      this.stop()
    }
    return this.start()
  }
}

module.exports = Client
<commit_msg>Use intents to greatly decrease memory usage<commit_after>process.env.DRSS = true
const Discord = require('discord.js')
const EventEmitter = require('events')
const listeners = require('../util/listeners.js')
const maintenance = require('../maintenance/index.js')
const ipc = require('../util/ipc.js')
const Profile = require('./db/Profile.js')
const ArticleMessage = require('./ArticleMessage.js')
const Feed = require('./db/Feed.js')
const FeedData = require('./FeedData.js')
const ArticleMessageRateLimiter = require('./ArticleMessageRateLimiter.js')
const initialize = require('../util/initialization.js')
const getConfig = require('../config.js').get
const createLogger = require('../util/logger/create.js')
const connectDb = require('../util/connectDatabase.js')
const { once } = require('events')

const STATES = {
  STOPPED: 'STOPPED',
  STARTING: 'STARTING',
  READY: 'READY'
}
/**
 * @type {import('discord.js').ClientOptions}
 */
const CLIENT_OPTIONS = {
  messageCacheMaxSize: 100,
  ws: {
    intents: [
      'GUILDS',
      'GUILD_MESSAGES',
      'GUILD_MESSAGE_REACTIONS'
    ]
  }
}

class Client extends EventEmitter {
  constructor () {
    super()
    this.STATES = STATES
    this.state = STATES.STOPPED
    this.log = undefined
  }

  async login (token) {
    if (this.bot) {
      return this.log.warn('Cannot login when already logged in')
    }
    if (typeof token !== 'string') {
      throw new TypeError('Argument must a string')
    }
    const client = new Discord.Client(CLIENT_OPTIONS)
    try {
      this.log = createLogger('-')
      await client.login(token)
      this.log = createLogger(client.shard.ids[0].toString())
      this.bot = client
      this.shardID = client.shard.ids[0]
      this.listenToShardedEvents(client)
      if (!client.readyAt) {
        await once(client, 'ready')
        this._setup()
      } else {
        this._setup()
      }
    } catch (err) {
      this.log.error(err, 'Discord.RSS failed to start')
      ipc.send(ipc.TYPES.KILL)
    }
  }

  async connectToDatabase () {
    const config = getConfig()
    return connectDb(config.database.uri, config.database.connection)
  }

  _setup () {
    const bot = this.bot
    bot.on('error', err => {
      this.log.warn('Websocket error', err)
      const config = getConfig()
      if (config.bot.exitOnSocketIssues === true) {
        this.log.warn('Stopping all processes due to config.bot.exitOnSocketIssues')
        ipc.send(ipc.TYPES.KILL)
      } else {
        this.stop()
      }
    })
    bot.on('debug', info => this.log.debug(info))
    bot.on('resume', () => {
      this.log.info('Websocket resumed')
      this.start()
    })
    bot.on('disconnect', () => {
      this.log.warn(`SH ${this.shardID} Websocket disconnected`)
      const config = getConfig()
      if (config.bot.exitOnSocketIssues === true) {
        this.log.general.info('Stopping all processes due to config.bot.exitOnSocketIssues')
        ipc.send(ipc.TYPES.KILL)
      } else {
        this.stop()
      }
    })
    this.log.info(`Discord.RSS has logged in as "${bot.user.username}" (ID ${bot.user.id})`)
    ipc.send(ipc.TYPES.SHARD_READY, {
      guildIds: bot.guilds.cache.keyArray(),
      channelIds: bot.channels.cache.keyArray()
    })
  }

  listenToShardedEvents (bot) {
    process.on('message', async message => {
      if (!ipc.isValid(message)) {
        return
      }
      try {
        switch (message.type) {
          case ipc.TYPES.START_INIT: {
            const data = message.data
            if (data.setPresence) {
              const config = getConfig()
              bot.user.setPresence({
                status: config.bot.status,
                activity: {
                  name: config.bot.activityName,
                  type: config.bot.activityType,
                  url: config.bot.streamActivityURL || undefined
                }
              }).catch(err => this.log.warn({
                error: err
              }, 'Failed to set presence'))
            }
            this.start()
            break
          }
          case ipc.TYPES.NEW_ARTICLE:
            this.onNewArticle(message.data.newArticle, message.data.debug)
            break
          case ipc.TYPES.FINISHED_INIT:
            break
          case ipc.TYPES.SEND_USER_ALERT:
            this.sendUserAlert(message.data.channel, message.data.message)
              .catch(err => this.log.warn(`Failed to send inter-process alert to channel ${message.data.channel}`, err))
        }
      } catch (err) {
        this.log.error(err, 'client')
      }
    })
  }

  async onNewArticle (newArticle, debug) {
    const config = getConfig()
    if (config.dev === true) {
      return
    }
    const { article, feedObject } = newArticle
    const channel = this.bot.channels.cache.get(feedObject.channel)
    try {
      if (!channel) {
        this.log.debug(`No channel found for article ${article._id} of feed ${feedObject._id}`)
        return
      }
      const feedData = await FeedData.ofFeed(new Feed(feedObject))
      const articleMessage = new ArticleMessage(this.bot, article, feedData, debug)
      await ArticleMessageRateLimiter.enqueue(articleMessage)
      this.log.debug(`Sent article ${article._id} of feed ${feedObject._id}`)
    } catch (err) {
      if (err.message.includes('Rate limit')) {
        this.log.debug({
          error: err
        }, 'Ignoring rate-limited article')
        return
      }
      const article = newArticle.article
      this.log.warn({
        error: err,
        guild: channel.guild,
        channel
      }, `Failed to send article ${article.link}`)
      if (err.code === 50035) {
        channel.send(`Failed to send formatted article for article <${article.link}>.\`\`\`${err.message}\`\`\``)
          .catch(err => this.log.warn({
            error: err
          }, 'Unable to send failed-to-send message for article'))
      }
    }
  }

  async sendChannelMessage (channelID, message) {
    const channel = this.bot.channels.cache.get(channelID)
    if (!channel) {
      return
    }
    /**
     * @type {import('discord.js').GuildMember}
     */
    const guildMeMember = channel.guild.me
    if (!guildMeMember.permissionsIn(channel).has(Discord.Permissions.FLAGS.SEND_MESSAGES)) {
      this.log.warn({
        channel,
        string: message
      }, 'Failed to send Client message to channel')
      return
    }
    channel.send(message)
  }

  async sendUserAlert (channelID, message) {
    const config = getConfig()
    if (config.dev === true) {
      return
    }
    const fetchedChannel = this.bot.channels.cache.get(channelID)
    if (!fetchedChannel) {
      return
    }
    const alertMessage = `**ALERT**\n\n${message}`
    try {
      const profile = await Profile.get(fetchedChannel.guild.id)
      if (!profile) {
        return this.sendChannelMessage(channelID, alertMessage)
      }
      const alertTo = profile.alert
      for (const id of alertTo) {
        const user = await this.bot.users.fetch(id, false)
        if (user) {
          await user.send(alertMessage)
        }
      }
    } catch (err) {
      this.log.warn({
        error: err
      }, `Failed to send user alert to channel ${channelID}`)
      return this.sendChannelMessage(alertMessage)
    }
  }

  async start () {
    if (this.state === STATES.STARTING || this.state === STATES.READY) {
      return this.log.warn(`Ignoring start command because of ${this.state} state`)
    }
    const config = getConfig()
    this.state = STATES.STARTING
    try {
      if ((this.mongo && this.mongo.readyState !== 1) || Profile.isMongoDatabase) {
        this.mongo = await this.connectToDatabase()
      }
      await initialize.setupModels(this.mongo)
      await initialize.setupCommands()
      const uri = config.database.uri
      this.log.info(`Database URI detected as a ${uri.startsWith('mongo') ? 'MongoDB URI' : 'folder URI'}`)
      await maintenance.pruneWithBot(this.bot)
      this.state = STATES.READY
      if (config.bot.enableCommands) {
        await listeners.enableCommands(this.bot)
      }
      await initialize.setupRateLimiters(this.bot)
      this.log.info(`Commands have been ${config.bot.enableCommands ? 'enabled' : 'disabled'}.`)
      ipc.send(ipc.TYPES.INIT_COMPLETE)
      listeners.createManagers(this.bot)
      this.emit('finishInit')
    } catch (err) {
      this.log.error(err, 'Client start')
    }
  }

  stop () {
    if (this.state === STATES.STARTING || this.state === STATES.STOPPED) {
      return this.log.warn(`Ignoring stop command because of ${this.state} state`)
    }
    this.log.info('Discord.RSS has received stop command')
    clearInterval(this.maintenance)
    listeners.disableAll(this.bot)
    this.state = STATES.STOPPED
    ipc.send(ipc.TYPES.SHARD_STOPPED)
  }

  async restart () {
    if (this.state === STATES.STARTING) {
      return this.log.warn(`Ignoring restart command because of ${this.state} state`)
    }
    if (this.state === STATES.READY) {
      this.stop()
    }
    return this.start()
  }
}

module.exports = Client
<|endoftext|>


<commit_before>export const AUTHENTICATION_PROMOTIONS = [
  {
    username: 'kolotusha',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/1271818/ello-regular-eced431c.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834638/ello-hdpi-71521c0e.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834638/ello-xhdpi-71521c0e.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834638/ello-optimized-71521c0e.jpg' },
    },
  },
  {
    username: 'danclarke',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/4631/ello-regular-31d9a8c4.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834637/ello-hdpi-83633f88.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834637/ello-xhdpi-83633f88.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834637/ello-optimized-83633f88.jpg' },
    },
  },
  {
    username: 'samrodriguezart',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2540242/ello-regular-f8d75deb.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834635/ello-hdpi-16a1a080.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834635/ello-xhdpi-16a1a080.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834635/ello-optimized-16a1a080.jpg' },
    },
  },
  {
    username: 'elenaeper',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2805911/ello-regular-fb28623e.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834634/ello-hdpi-f723b4f8.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834634/ello-xhdpi-f723b4f8.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834634/ello-optimized-f723b4f8.jpg' },
    },
  },
  {
    username: 'dervinbatarlo',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/1887397/ello-regular-c1d6240a.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834633/ello-hdpi-2667d4e3.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834633/ello-xhdpi-2667d4e3.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834633/ello-optimized-2667d4e3.jpg' },
    },
  },
  {
    username: 'sydneysie',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2466011/ello-regular-38aea972.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834632/ello-hdpi-517f98bb.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834632/ello-xhdpi-517f98bb.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834632/ello-optimized-517f98bb.jpg' },
    },
  },
  {
    username: 'isiope',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2160982/ello-regular-0b32ef1b.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834630/ello-hdpi-877fc6a5.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834630/ello-xhdpi-877fc6a5.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834630/ello-optimized-877fc6a5.jpg' },
    },
  },
  {
    username: 'sh-i',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/9335/ello-regular-5044772f.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834627/ello-hdpi-5e1d7ecb.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834627/ello-xhdpi-5e1d7ecb.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834627/ello-optimized-5e1d7ecb.jpg' },
    },
  },
  {
    username: 'wrdbnr',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2805926/ello-regular-03bd4eeb.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834625/ello-hdpi-329fab3f.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834625/ello-xhdpi-329fab3f.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834625/ello-optimized-329fab3f.jpg' },
    },
  },
  {
    username: 'woostaar',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/235714/ello-regular-18013c26.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834622/ello-hdpi-53df2e66.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834622/ello-xhdpi-53df2e66.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834622/ello-optimized-53df2e66.jpg' },
    },
  },
  {
    username: 'ming_nomchong',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2790069/ello-regular-286b5003.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834621/ello-hdpi-8cda840b.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834621/ello-xhdpi-8cda840b.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834621/ello-optimized-8cda840b.jpg' },
    },
  },
  {
    username: 'alconic',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/12526/ello-regular-7d3b38c3.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834619/ello-hdpi-336dfa4f.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834619/ello-xhdpi-336dfa4f.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834619/ello-optimized-336dfa4f.jpg' },
    },
  },
  {
    username: 'phigun',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/746013/ello-regular-72f5aac0.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834616/ello-hdpi-9e880ef7.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834616/ello-xhdpi-9e880ef7.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834616/ello-optimized-9e880ef7.jpg' },
    },
  },
  {
    username: 'sammearns',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/737620/ello-regular-3a7c2232.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834614/ello-hdpi-125703ff.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834614/ello-xhdpi-125703ff.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834614/ello-optimized-125703ff.jpg' },
    },
  },
  {
    username: 'l0ll3',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2489935/ello-regular-342c211d.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834612/ello-hdpi-f51a0259.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834612/ello-xhdpi-f51a0259.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834612/ello-optimized-f51a0259.jpg' },
    },
  },
  {
    username: 'bensusi',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2793217/ello-regular-3179f0f9.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834611/ello-hdpi-49df0c43.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834611/ello-xhdpi-49df0c43.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834611/ello-optimized-49df0c43.jpg' },
    },
  },
  {
    username: 'scottlistfield',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2033987/ello-regular-cad17f34.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834610/ello-hdpi-defdc55f.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834610/ello-xhdpi-defdc55f.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834610/ello-optimized-defdc55f.jpg' },
    },
  },
  {
    username: 'robincerutti',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/225302/ello-regular-78805a22.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834609/ello-hdpi-1bfeea09.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834609/ello-xhdpi-1bfeea09.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834609/ello-optimized-1bfeea09.jpg' },
    },
  },
  {
    username: 'p5art',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2181535/ello-regular-6ec6b0ea.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834607/ello-hdpi-755741fe.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834607/ello-xhdpi-755741fe.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834607/ello-optimized-755741fe.jpg' },
    },
  },
  {
    username: '29thfloor',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/1215021/ello-regular-99b03214.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834604/ello-hdpi-5f3e8ccb.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834604/ello-xhdpi-5f3e8ccb.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834604/ello-optimized-5f3e8ccb.jpg' },
    },
  },
]
<commit_msg>Update authentication images<commit_after>export const AUTHENTICATION_PROMOTIONS = [
  {
    username: 'zckrf',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/4569/ello-regular-42293606.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900700/ello-hdpi-700d2f80.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900700/ello-xhdpi-700d2f80.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3900700/ello-optimized-700d2f80.jpg' },
    },
  },
  {
    username: 'frenemy',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/264898/ello-regular-baec6461.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900699/ello-hdpi-b930acdd.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900699/ello-xhdpi-b930acdd.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3900699/ello-optimized-b930acdd.jpg' },
    },
  },
  {
    username: 'guillaumekurkdjian',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/3793/ello-regular-8f8ad28d.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900697/ello-hdpi-7abf5a42.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900697/ello-xhdpi-7abf5a42.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3900697/ello-optimized-7abf5a42.jpg' },
    },
  },
  {
    username: 'obographism',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/970479/ello-regular-3f78f8c9.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900695/ello-hdpi-34322fc3.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900695/ello-xhdpi-34322fc3.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3900695/ello-optimized-34322fc3.jpg' },
    },
  },
  {
    username: 'letasobierajski',

    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/904309/ello-regular-9a48768f.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900693/ello-hdpi-ecd0d6ef.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900693/ello-xhdpi-ecd0d6ef.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3900693/ello-optimized-ecd0d6ef.jpg' },
    },
  },
]
<|endoftext|>


<commit_before>/*
Copyright 2015 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';
var dis = require("../../../dispatcher");
var CallHandler = require("../../../CallHandler");
var MatrixClientPeg = require("../../../MatrixClientPeg");

/*
 * State vars:
 * this.state.call = MatrixCall|null
 *
 * Props:
 * this.props.room = Room (JS SDK)
 *
 * Internal state:
 * this._trackedRoom = (either from props.room or programatically set)
 */

module.exports = {

    componentDidMount: function() {
        this.dispatcherRef = dis.register(this.onAction);
        this._trackedRoom = null;
        if (this.props.room) {
            this._trackedRoom = this.props.room;
            this.showCall(this._trackedRoom.roomId);
        }
        else {
            var call = CallHandler.getAnyActiveCall();
            if (call) {
                console.log(
                    "Global CallView is now tracking active call in room %s",
                    call.roomId
                );
                this._trackedRoom = MatrixClientPeg.get().getRoom(call.roomId);
                this.showCall(call.roomId);
            }
        }
    },

    componentWillUnmount: function() {
        dis.unregister(this.dispatcherRef);
    },

    onAction: function(payload) {
        // don't filter out payloads for room IDs other than props.room because
        // we may be interested in the conf 1:1 room
        if (payload.action !== 'call_state' || !payload.room_id) {
            return;
        }
        this.showCall(payload.room_id);
    },

    showCall: function(roomId) {
        var call = CallHandler.getCallForRoom(roomId);
        if (call) {
            call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
            // N.B. the remote video element is used for playback for audio for voice calls
            call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
        }
        if (call && call.type === "video" && call.state !== 'ended') {
            this.getVideoView().getLocalVideoElement().style.display = "initial";
            this.getVideoView().getRemoteVideoElement().style.display = "initial";
        }
        else {
            this.getVideoView().getLocalVideoElement().style.display = "none";
            this.getVideoView().getRemoteVideoElement().style.display = "none";
        }
    }
};

<commit_msg>Hide the local video when in a conf call<commit_after>/*
Copyright 2015 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';
var dis = require("../../../dispatcher");
var CallHandler = require("../../../CallHandler");
var MatrixClientPeg = require("../../../MatrixClientPeg");

/*
 * State vars:
 * this.state.call = MatrixCall|null
 *
 * Props:
 * this.props.room = Room (JS SDK)
 *
 * Internal state:
 * this._trackedRoom = (either from props.room or programatically set)
 */

module.exports = {

    componentDidMount: function() {
        this.dispatcherRef = dis.register(this.onAction);
        this._trackedRoom = null;
        if (this.props.room) {
            this._trackedRoom = this.props.room;
            this.showCall(this._trackedRoom.roomId);
        }
        else {
            var call = CallHandler.getAnyActiveCall();
            if (call) {
                console.log(
                    "Global CallView is now tracking active call in room %s",
                    call.roomId
                );
                this._trackedRoom = MatrixClientPeg.get().getRoom(call.roomId);
                this.showCall(call.roomId);
            }
        }
    },

    componentWillUnmount: function() {
        dis.unregister(this.dispatcherRef);
    },

    onAction: function(payload) {
        // don't filter out payloads for room IDs other than props.room because
        // we may be interested in the conf 1:1 room
        if (payload.action !== 'call_state' || !payload.room_id) {
            return;
        }
        this.showCall(payload.room_id);
    },

    showCall: function(roomId) {
        var call = CallHandler.getCallForRoom(roomId);
        if (call) {
            call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
            // N.B. the remote video element is used for playback for audio for voice calls
            call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
        }
        if (call && call.type === "video" && call.state !== 'ended') {
            // if this call is a conf call, don't display local video as the
            // conference will have us in it
            this.getVideoView().getLocalVideoElement().style.display = (
                call.confUserId ? "none" : "initial"
            );
            this.getVideoView().getRemoteVideoElement().style.display = "initial";
        }
        else {
            this.getVideoView().getLocalVideoElement().style.display = "none";
            this.getVideoView().getRemoteVideoElement().style.display = "none";
        }
    }
};

<|endoftext|>


<commit_before>/**
 * @author Serhii Maksymchuk from Ubrainians for imCode
 * 07.05.18
 */
Imcms.define(
    "imcms-admin-panel-settings-builder",
    ["imcms-bem-builder", "imcms-cookies", "imcms-i18n-texts", "jquery", "imcms-events", "imcms-streams"],
    function (BEM, cookies, texts, $, events, sreams) {

        texts = texts.panel.settingsList;

        var specialPanelStreamPublisher = sreams.createPublisherOnTopic("prevent special panel hide");

        var settingEnabledClass = BEM.buildClass("settings-section", "setting", "enabled");
        var settingEnabledClassSelector = "." + settingEnabledClass;

        var $settings;

        function wrapWithPositionRefresh(refreshAfterMe) {
            return function () {
                refreshAfterMe.call();
                setTimeout(refreshSettingsPosition, 200);
            }
        }

        var settings = {
            small: {
                id: "small",
                text: texts.size.small,
                title: texts.size.smallTitle,
                onSettingClick: wrapWithPositionRefresh(function () {
                    $("#imcms-admin-panel").addClass("imcms-admin-panel--small")
                })
            },
            large: {
                id: "large",
                isDefault: true,
                text: texts.size.large,
                title: texts.size.largeTitle,
                onSettingClick: wrapWithPositionRefresh(function () {
                    $("#imcms-admin-panel").removeClass("imcms-admin-panel--small")
                })
            },
            auto: {
                id: "auto",
                isDefault: true,
                text: texts.appearance.auto,
                title: texts.appearance.autoTitle,
                onSettingClick: function () {
                    events.trigger("enable admin panel");
                    events.trigger("enable special panel hide");
                    $("#imcms-admin").removeClass("imcms-panel-visible");
                    console.log("panel appearance auto");
                }
            },
            hidden: {
                id: "hidden",
                text: texts.appearance.hidden,
                title: texts.appearance.hiddenTitle,
                onSettingClick: function () {
                    events.trigger("enable special panel hide");
                    console.log("panel appearance hidden");
                }
            },
            visible: {
                id: "visible",
                text: texts.appearance.visible,
                title: texts.appearance.visibleTitle,
                onSettingClick: function () {
                    specialPanelStreamPublisher.publish({
                        specialPanelHidingPrevented: true
                    });
                    events.trigger("disable admin panel");

                    $("#imcms-admin").addClass("imcms-panel-visible");
                    $("#imcms-admin-panel,#imcmsAdminSpecial").css("top", 0);
                    $("body").css("top", 0);

                    console.log("panel appearance visible");
                }
            }
        };

        var sections = {
            size: {
                id: "panel-size",
                text: texts.size.name,
                settings: [settings.small, settings.large]
            },
            appearance: {
                id: "panel-appearance",
                text: texts.appearance.name,
                settings: [settings.auto, settings.hidden, settings.visible]
            }
        };

        var allSections = [];

        for (var key in sections) {
            allSections.push(sections[key]);
        }

        function buildSection(section) {
            var savedSetting = cookies.getCookie(section.id);

            var settingsElements = section.settings.map(function (setting) {
                var attributes = {
                    id: "imcms-" + section.id + "-" + setting.id,
                    text: setting.text,
                    title: setting.title
                };

                attributes.click = function () {
                    var $setting = $(this);

                    if ($setting.hasClass(settingEnabledClass)) return;

                    $setting.parent(".settings-section")
                        .find(settingEnabledClassSelector)
                        .removeClass(settingEnabledClass);

                    $setting.addClass(settingEnabledClass);

                    setting.onSettingClick.call();
                    cookies.setCookie(section.id, setting.id);
                };

                if ((setting.id && (savedSetting === setting.id))
                    || (!savedSetting && setting.isDefault))
                {
                    attributes["class"] = settingEnabledClass;
                }

                return {
                    "setting": $("<div>", attributes)
                };
            });

            var sectionName = {"section-name": $("<div>", {text: section.text})};
            var bemOptions = {
                block: "settings-section",
                elements: [sectionName].concat(settingsElements)
            };

            return new BEM(bemOptions).buildBlockStructure("<div>");
        }

        function buildSettings(settingsButton) {
            var sections$ = allSections.map(buildSection);

            var bemOptions = {
                block: "admin-panel-settings-list",
                elements: {
                    "section": sections$
                }
            };

            return new BEM(bemOptions)
                .buildBlockStructure("<div>", {style: "display: none;"})
                .insertAfter(settingsButton);
        }

        function refreshSettingsPosition() {
            $settings && $settings.css("top", $("#imcms-admin-panel").outerHeight())
        }

        return {
            applyCurrentSettings: function () {
                allSections
                    .map(function (section) {
                        return settings[cookies.getCookie(section.id)];
                    })
                    .filter(function (setting) {
                        return setting && !setting.isDefault;
                    })
                    .forEach(function (setting) {
                        setting.onSettingClick.call();
                    });
            },
            onSettingsClicked: function () {
                $settings = ($settings || (buildSettings(this)));

                refreshSettingsPosition();
                $settings.slideToggle();
            },
            hideSettings: function () {
                $settings && $settings.slideUp(300);
            }
        }
    }
);
<commit_msg>IMCMS-290 - Add a "pin" icon to imCMS's panel: - Fixed typo.<commit_after>/**
 * @author Serhii Maksymchuk from Ubrainians for imCode
 * 07.05.18
 */
Imcms.define(
    "imcms-admin-panel-settings-builder",
    ["imcms-bem-builder", "imcms-cookies", "imcms-i18n-texts", "jquery", "imcms-events", "imcms-streams"],
    function (BEM, cookies, texts, $, events, streams) {

        texts = texts.panel.settingsList;

        var specialPanelStreamPublisher = streams.createPublisherOnTopic("prevent special panel hide");

        var settingEnabledClass = BEM.buildClass("settings-section", "setting", "enabled");
        var settingEnabledClassSelector = "." + settingEnabledClass;

        var $settings;

        function wrapWithPositionRefresh(refreshAfterMe) {
            return function () {
                refreshAfterMe.call();
                setTimeout(refreshSettingsPosition, 200);
            }
        }

        var settings = {
            small: {
                id: "small",
                text: texts.size.small,
                title: texts.size.smallTitle,
                onSettingClick: wrapWithPositionRefresh(function () {
                    $("#imcms-admin-panel").addClass("imcms-admin-panel--small")
                })
            },
            large: {
                id: "large",
                isDefault: true,
                text: texts.size.large,
                title: texts.size.largeTitle,
                onSettingClick: wrapWithPositionRefresh(function () {
                    $("#imcms-admin-panel").removeClass("imcms-admin-panel--small")
                })
            },
            auto: {
                id: "auto",
                isDefault: true,
                text: texts.appearance.auto,
                title: texts.appearance.autoTitle,
                onSettingClick: function () {
                    events.trigger("enable admin panel");
                    events.trigger("enable special panel hide");
                    $("#imcms-admin").removeClass("imcms-panel-visible");
                    console.log("panel appearance auto");
                }
            },
            hidden: {
                id: "hidden",
                text: texts.appearance.hidden,
                title: texts.appearance.hiddenTitle,
                onSettingClick: function () {
                    events.trigger("enable special panel hide");
                    console.log("panel appearance hidden");
                }
            },
            visible: {
                id: "visible",
                text: texts.appearance.visible,
                title: texts.appearance.visibleTitle,
                onSettingClick: function () {
                    specialPanelStreamPublisher.publish({
                        specialPanelHidingPrevented: true
                    });
                    events.trigger("disable admin panel");

                    $("#imcms-admin").addClass("imcms-panel-visible");
                    $("#imcms-admin-panel,#imcmsAdminSpecial").css("top", 0);
                    $("body").css("top", 0);

                    console.log("panel appearance visible");
                }
            }
        };

        var sections = {
            size: {
                id: "panel-size",
                text: texts.size.name,
                settings: [settings.small, settings.large]
            },
            appearance: {
                id: "panel-appearance",
                text: texts.appearance.name,
                settings: [settings.auto, settings.hidden, settings.visible]
            }
        };

        var allSections = [];

        for (var key in sections) {
            allSections.push(sections[key]);
        }

        function buildSection(section) {
            var savedSetting = cookies.getCookie(section.id);

            var settingsElements = section.settings.map(function (setting) {
                var attributes = {
                    id: "imcms-" + section.id + "-" + setting.id,
                    text: setting.text,
                    title: setting.title
                };

                attributes.click = function () {
                    var $setting = $(this);

                    if ($setting.hasClass(settingEnabledClass)) return;

                    $setting.parent(".settings-section")
                        .find(settingEnabledClassSelector)
                        .removeClass(settingEnabledClass);

                    $setting.addClass(settingEnabledClass);

                    setting.onSettingClick.call();
                    cookies.setCookie(section.id, setting.id);
                };

                if ((setting.id && (savedSetting === setting.id))
                    || (!savedSetting && setting.isDefault))
                {
                    attributes["class"] = settingEnabledClass;
                }

                return {
                    "setting": $("<div>", attributes)
                };
            });

            var sectionName = {"section-name": $("<div>", {text: section.text})};
            var bemOptions = {
                block: "settings-section",
                elements: [sectionName].concat(settingsElements)
            };

            return new BEM(bemOptions).buildBlockStructure("<div>");
        }

        function buildSettings(settingsButton) {
            var sections$ = allSections.map(buildSection);

            var bemOptions = {
                block: "admin-panel-settings-list",
                elements: {
                    "section": sections$
                }
            };

            return new BEM(bemOptions)
                .buildBlockStructure("<div>", {style: "display: none;"})
                .insertAfter(settingsButton);
        }

        function refreshSettingsPosition() {
            $settings && $settings.css("top", $("#imcms-admin-panel").outerHeight())
        }

        return {
            applyCurrentSettings: function () {
                allSections
                    .map(function (section) {
                        return settings[cookies.getCookie(section.id)];
                    })
                    .filter(function (setting) {
                        return setting && !setting.isDefault;
                    })
                    .forEach(function (setting) {
                        setting.onSettingClick.call();
                    });
            },
            onSettingsClicked: function () {
                $settings = ($settings || (buildSettings(this)));

                refreshSettingsPosition();
                $settings.slideToggle();
            },
            hideSettings: function () {
                $settings && $settings.slideUp(300);
            }
        }
    }
);
<|endoftext|>


<commit_before>(function() {
  'use strict';

  function sequoiaTreeDirective(Tree, BUTTONS, DEFAULT_OPTIONS){

    return {
      restrict: 'AE',
      replace: true,
      templateUrl: 'angular-sequoia.html',
      scope: {
        'treeNodes': '=sequoiaTree',
        'model': '=ngModel',
        'template': '=nodeTemplate',
        'options': '='
      },
      link: function(scope) {
        function init() {
          /* Set the default options*/
          scope.options = _.defaults(scope.options || {}, DEFAULT_OPTIONS);
          scope.canEdit = scope.options.canEdit;
          scope.inline = scope.options.inline;
          scope.allowSelect = scope.options.allowSelect;
          scope.isMultiSelect = scope.options.limit === 1 ? false : true;
          scope.model = scope.isMultiSelect ? _.isArray(scope.model) ? scope.model : [] : '';
          scope.breadcrumbs = [];
          scope.tree = new Tree(angular.copy(scope.treeNodes), scope.template);
          scope.buttons = {
            edit: scope.options.buttons.edit ? scope.options.buttons.edit : BUTTONS.edit,
            select: scope.options.buttons.select ? scope.options.buttons.select : BUTTONS.select,
            deselect: scope.options.buttons.deselect ? scope.options.buttons.deselect : BUTTONS.deselect,
            goToSubitems: scope.options.buttons.goToSubitems ? scope.options.buttons.goToSubitems : BUTTONS.goToSubitems,
            addSubitems: scope.options.buttons.addSubitems ? scope.options.buttons.addSubitems : BUTTONS.addSubitems,
            addNode: scope.options.buttons.addNode ? scope.options.buttons.addNode : BUTTONS.addNode,
            remove: scope.options.buttons.remove ? scope.options.buttons.remove : BUTTONS.remove,
            done: scope.options.buttons.done ? scope.options.buttons.done : BUTTONS.done,
            search: scope.options.buttons.search ? scope.options.buttons.search : BUTTONS.search,
            searchClear: scope.options.buttons.searchClear ? scope.options.buttons.searchClear : BUTTONS.searchClear,
            showSelected: scope.options.buttons.showSelected ? scope.options.buttons.showSelected : BUTTONS.showSelected,
            hideSelected: scope.options.buttons.hideSelected ? scope.options.buttons.hideSelected : BUTTONS.hideSelected,
            backToList: scope.options.buttons.backToList ? scope.options.buttons.backToList : BUTTONS.backToList,
            move: scope.options.buttons.move ? scope.options.buttons.move : BUTTONS.move,
            modalSelect: scope.options.buttons.modalSelect ? scope.options.buttons.modalSelect : BUTTONS.modalSelect
          };
        }

        scope.load = function(node) {
          scope.onlySelected = false;
          if(scope.tree.isValidNode(node)) {
            scope.tree.setCurrentNodes(node[scope.tree.template.nodes]);
            scope.breadcrumbs = scope.tree.breadcrumbs(node[scope.tree.template.id]);
          } else {
            scope.tree.setCurrentNodes();
            scope.breadcrumbs = [];
          }
        };

        scope.select = function(node) {
          if(node[scope.tree.template.id]) {
            if(scope.options.limit !== 0 && scope.model.length === scope.options.limit) {
              scope.notification = 'You cannot select more than ' + scope.options.limit + ' items!';
            } else {
              if(scope.isMultiSelect) {
                scope.model.push(node[scope.tree.template.id]);
              } else {
                scope.model = node[scope.tree.template.id];
              }
            }
          }
        };

        scope.deselect = function(node) {
          if(scope.isMultiSelect) {
            var index = node[scope.tree.template.id] ? _.indexOf(scope.model,node[scope.tree.template.id]) : -1;
            if(index !== -1) {
              scope.model.splice(index, 1);
            }
          } else {
            scope.model = '';
          }

        };

        scope.isSelected = function(node) {
          if(scope.tree.isValidNode(node)) {
            return scope.isMultiSelect ? _.indexOf(scope.model, node[scope.tree.template.id]) !== -1 ? true : false : scope.model === node[scope.tree.template.id];
          }
        };

        scope.toggleSelected = function() {
          if(scope.onlySelected) {
            scope.onlySelected = false;
            scope.tree.setCurrentNodes(scope.tree.getNodesInPath());
          } else {
            scope.onlySelected = true;
            var selected = scope.tree.findSelected(scope.model);
            scope.tree.setNodesInPath(scope.tree.nodes);
            scope.tree.setCurrentNodes(selected);
          }
        };

        init();

        scope.$watch('treeNodes', function(newVal) {
          if(newVal) {
            scope.tree = new Tree(angular.copy(scope.treeNodes), scope.template);
            scope.load();
          }
        }, true);

        if(!scope.inline) {
          scope.load();
        }

        /* Handle Modal */
        scope.showModal = function() {
          scope.load();
          scope.modalShown = true;
        };

        scope.closeModal = function() {
          scope.modalShown = false;
        };

        /* Handle adding and editing nodes */
        scope.toggleEditing = function(form) {
          //handle form validation
          if(scope.isEditing) {
            form.isSubmitted = true;
          }
          if(form && !form.$valid) {
            return;
          }

          scope.treeNodes = angular.copy(scope.tree.tree);

          scope.isEditing = !scope.isEditing;
        };

        scope.addNode = function(node) {
          if(scope.tree.isValidNode(node)) {
            scope.load(node);
          }

          scope.tree.nodes.push(scope.tree.newNode());
          scope.isEditing = true;
        };

        scope.remove = function(node) {
          var index = node ? _.indexOf(scope.tree.nodes, node) : -1;
          if(index !== -1) {
            scope.tree.nodes.splice(index, 1);
          }
        };

        scope.closeNotification = function() {
          scope.notification = '';
        };
      }
    };

  }

  sequoiaTreeDirective.$inject = ['SequoiaTree', 'BUTTONS'];

  angular.module('ngSequoia')
    .directive('sequoiaTree', sequoiaTreeDirective);

})();<commit_msg>Add DEFAULT_OPTIONS for injector<commit_after>(function() {
  'use strict';

  function sequoiaTreeDirective(Tree, BUTTONS, DEFAULT_OPTIONS){

    return {
      restrict: 'AE',
      replace: true,
      templateUrl: 'angular-sequoia.html',
      scope: {
        'treeNodes': '=sequoiaTree',
        'model': '=ngModel',
        'template': '=nodeTemplate',
        'options': '='
      },
      link: function(scope) {
        function init() {
          /* Set the default options*/
          scope.options = _.defaults(scope.options || {}, DEFAULT_OPTIONS);
          scope.canEdit = scope.options.canEdit;
          scope.inline = scope.options.inline;
          scope.allowSelect = scope.options.allowSelect;
          scope.isMultiSelect = scope.options.limit === 1 ? false : true;
          scope.model = scope.isMultiSelect ? _.isArray(scope.model) ? scope.model : [] : '';
          scope.breadcrumbs = [];
          scope.tree = new Tree(angular.copy(scope.treeNodes), scope.template);
          scope.buttons = {
            edit: scope.options.buttons.edit ? scope.options.buttons.edit : BUTTONS.edit,
            select: scope.options.buttons.select ? scope.options.buttons.select : BUTTONS.select,
            deselect: scope.options.buttons.deselect ? scope.options.buttons.deselect : BUTTONS.deselect,
            goToSubitems: scope.options.buttons.goToSubitems ? scope.options.buttons.goToSubitems : BUTTONS.goToSubitems,
            addSubitems: scope.options.buttons.addSubitems ? scope.options.buttons.addSubitems : BUTTONS.addSubitems,
            addNode: scope.options.buttons.addNode ? scope.options.buttons.addNode : BUTTONS.addNode,
            remove: scope.options.buttons.remove ? scope.options.buttons.remove : BUTTONS.remove,
            done: scope.options.buttons.done ? scope.options.buttons.done : BUTTONS.done,
            search: scope.options.buttons.search ? scope.options.buttons.search : BUTTONS.search,
            searchClear: scope.options.buttons.searchClear ? scope.options.buttons.searchClear : BUTTONS.searchClear,
            showSelected: scope.options.buttons.showSelected ? scope.options.buttons.showSelected : BUTTONS.showSelected,
            hideSelected: scope.options.buttons.hideSelected ? scope.options.buttons.hideSelected : BUTTONS.hideSelected,
            backToList: scope.options.buttons.backToList ? scope.options.buttons.backToList : BUTTONS.backToList,
            move: scope.options.buttons.move ? scope.options.buttons.move : BUTTONS.move,
            modalSelect: scope.options.buttons.modalSelect ? scope.options.buttons.modalSelect : BUTTONS.modalSelect
          };
        }

        scope.load = function(node) {
          scope.onlySelected = false;
          if(scope.tree.isValidNode(node)) {
            scope.tree.setCurrentNodes(node[scope.tree.template.nodes]);
            scope.breadcrumbs = scope.tree.breadcrumbs(node[scope.tree.template.id]);
          } else {
            scope.tree.setCurrentNodes();
            scope.breadcrumbs = [];
          }
        };

        scope.select = function(node) {
          if(node[scope.tree.template.id]) {
            if(scope.options.limit !== 0 && scope.model.length === scope.options.limit) {
              scope.notification = 'You cannot select more than ' + scope.options.limit + ' items!';
            } else {
              if(scope.isMultiSelect) {
                scope.model.push(node[scope.tree.template.id]);
              } else {
                scope.model = node[scope.tree.template.id];
              }
            }
          }
        };

        scope.deselect = function(node) {
          if(scope.isMultiSelect) {
            var index = node[scope.tree.template.id] ? _.indexOf(scope.model,node[scope.tree.template.id]) : -1;
            if(index !== -1) {
              scope.model.splice(index, 1);
            }
          } else {
            scope.model = '';
          }

        };

        scope.isSelected = function(node) {
          if(scope.tree.isValidNode(node)) {
            return scope.isMultiSelect ? _.indexOf(scope.model, node[scope.tree.template.id]) !== -1 ? true : false : scope.model === node[scope.tree.template.id];
          }
        };

        scope.toggleSelected = function() {
          if(scope.onlySelected) {
            scope.onlySelected = false;
            scope.tree.setCurrentNodes(scope.tree.getNodesInPath());
          } else {
            scope.onlySelected = true;
            var selected = scope.tree.findSelected(scope.model);
            scope.tree.setNodesInPath(scope.tree.nodes);
            scope.tree.setCurrentNodes(selected);
          }
        };

        init();

        scope.$watch('treeNodes', function(newVal) {
          if(newVal) {
            scope.tree = new Tree(angular.copy(scope.treeNodes), scope.template);
            scope.load();
          }
        }, true);

        if(!scope.inline) {
          scope.load();
        }

        /* Handle Modal */
        scope.showModal = function() {
          scope.load();
          scope.modalShown = true;
        };

        scope.closeModal = function() {
          scope.modalShown = false;
        };

        /* Handle adding and editing nodes */
        scope.toggleEditing = function(form) {
          //handle form validation
          if(scope.isEditing) {
            form.isSubmitted = true;
          }
          if(form && !form.$valid) {
            return;
          }

          scope.treeNodes = angular.copy(scope.tree.tree);

          scope.isEditing = !scope.isEditing;
        };

        scope.addNode = function(node) {
          if(scope.tree.isValidNode(node)) {
            scope.load(node);
          }

          scope.tree.nodes.push(scope.tree.newNode());
          scope.isEditing = true;
        };

        scope.remove = function(node) {
          var index = node ? _.indexOf(scope.tree.nodes, node) : -1;
          if(index !== -1) {
            scope.tree.nodes.splice(index, 1);
          }
        };

        scope.closeNotification = function() {
          scope.notification = '';
        };
      }
    };

  }

  sequoiaTreeDirective.$inject = ['SequoiaTree', 'BUTTONS', 'DEFAULT_OPTIONS'];

  angular.module('ngSequoia')
    .directive('sequoiaTree', sequoiaTreeDirective);

})();<|endoftext|>