<commit_before>import RouteNode from 'route-node/modules/RouteNode'

let nameToIDs = name => {
    return name.split('.').reduce((ids, name) => {
        ids.push(ids.length ? ids[ids.length - 1] + '.' + name : name)
        return ids
    }, [])
}

let makeState = (name, params, path) => ({name, params, path})

/**
 * Create a new Router5 instance
 * @class
 * @param {RouteNode[]|Object[]|RouteNode|Object} routes The router routes
 * @param {Object} [opts={}] The router options: useHash, defaultRoute and defaultParams can be specified.
 * @return {Router5} The router instance
 */
export default class Router5 {
    constructor(routes, opts = {}) {
        this.started = false
        this._cbs = {}
        this._cmps = {}
        this._canAct = {}
        this.lastStateAttempt = null
        this.lastKnownState = null
        this.rootNode  = routes instanceof RouteNode ? routes : new RouteNode('', '', routes)
        this.options = {
            useHash: false,
            hashPrefix: ''
        }
        Object.keys(opts).forEach(opt => this.options[opt] = opts[opt])
        this.base = window.location.pathname.replace(/^\/$/, '')

        return this
    }

    /**
     * Set an option value
     * @param  {String} opt The option to set
     * @param  {*}      val The option value
     * @return {Router5}    The Router5 instance
     */
    setOption(opt, val) {
        this.options[opt] = val
        return this
    }

    /**
     * Add route(s)
     * @param  {RouteNode[]|Object[]|RouteNode|Object} routes Route(s) to add
     * @return {Router5}  The Router5 instance
     */
    add(routes) {
        this.rootNode.add(routes)
        return this
    }

    /**
     * Add a route to the router.
     * @param {String}   name        The route name
     * @param {String}   path        The route path
     * @param {Function} canActivate A function to determine if the route can be activated.
     *                               It will be invoked during a transition with `toState`
     *                               and `fromState` parameters.
     * @return {Router5}             The Router5 instance
     */
    addNode(name, path, canActivate) {
        try {
            this.rootNode.addNode(name, path)
            this._canAct[name] = canActivate
        } catch (e) {}
        return this
    }

    /**
     * @private
     */
    onPopState(evt) {
        // Do nothing if no state or if last know state is poped state (it should never happen)
        let state = evt.state || this.matchPath(this.getLocation())
        if (!state) return
        if (this.lastKnownState && this.areStatesEqual(state, this.lastKnownState)) return

        let canTransition = this._transition(state, this.lastKnownState)
        if (!canTransition) {
            let url = this.buildUrl(this.lastKnownState.name, this.lastKnownState.params)
            window.history.pushState(this.lastKnownState, '', url)
        }
    }

    /**
     * Start the router
     * @return {Router5} The router instance
     */
    start() {
        if (this.started) return this
        this.started = true

        // Try to match starting path name
        let startPath = this.getLocation();
        let startState = this.matchPath(startPath)

        if (startState) {
            this.lastKnownState = startState
            window.history.replaceState(this.lastKnownState, '', this.buildUrl(startState.name, startState.params))
        } else if (this.options.defaultRoute) {
            this.navigate(this.options.defaultRoute, this.options.defaultParams, {replace: true})
        }
        // Listen to popstate
        window.addEventListener('popstate', this.onPopState.bind(this))
        return this
    }

    /**
     * Stop the router
     * @return {Router5} The router instance
     */
    stop() {
        if (!this.started) return this
        this.started = false

        window.removeEventListener('popstate', this.onPopState.bind(this))
        return this
    }

    /**
     * Return the current state object
     * @return {Object} The current state
     */
    getState() {
        return this.lastKnownState
        // return window.history.state
    }

    /**
     * Whether or not the given route name with specified params is active.
     * @param  {String}   name             The route name
     * @param  {Object}   [params={}]      The route parameters
     * @param  {Boolean}  [equality=false] If set to false (default), isActive will return true
     *                                     if the provided route name and params are descendants
     *                                     of the active state.
     * @return {Boolean}                   Whether nor not the route is active
     */
    isActive(name, params = {}, strictEquality = false) {
        let activeState = this.getState()

        if (!activeState) return false

        if (strictEquality || activeState.name === name) {
            return this.areStatesEqual(makeState(name, params), activeState)
        } else {
            return this.areStatesDescendants(makeState(name, params), activeState)
        }
    }

    /**
     * @private
     */
    areStatesEqual(state1, state2) {
        return state1.name === state2.name &&
           Object.keys(state1.params).length === Object.keys(state2.params).length &&
           Object.keys(state1.params).every(p => state1.params[p] === state2.params[p])
    }

    /**
     * Whether two states are descendants
     * @param  {Object} parentState The parent state
     * @param  {Object} childState  The child state
     * @return {Boolean}            Whether the two provided states are related
     */
    areStatesDescendants(parentState, childState) {
        let regex = new RegExp('^' + parentState.name + '\\.(.*)$')
        if (!regex.test(childState.name)) return false
        // If child state name extends parent state name, and all parent state params
        // are in child state params.
        return Object.keys(parentState.params).every(p => parentState.params[p] === childState.params[p])
    }

    /**
     * @private
     */
    _invokeListeners(name, newState, oldState) {
        if (!this._cbs[name]) return
        this._cbs[name].forEach(cb => cb(newState, oldState))
    }

    /**
     * @private
     */
    _addListener(name, cb) {
        let normalizedName = name.replace(/^(\*|\^|=)/, '')
        if (normalizedName) {
            let segments = this.rootNode.getSegmentsByName(normalizedName)
            if (!segments) console.warn(`No route found for ${normalizedName}, listener might never be called!`)
        }
        if (!this._cbs[name]) this._cbs[name] = []
        this._cbs[name].push(cb)
        return this
    }

    /**
     * @private
     */
    _removeListener(name, cb) {
        if (this._cbs[name]) this._cbs[name] = this._cbs[name].filter(callback => callback !== cb)
        return this
    }

    /**
     * Add a route change listener
     * @param {Function} cb The listener to add
     * @return {Router5} The router instance
     */
    addListener(cb) {
        return this._addListener('*', cb)
    }

    /**
     * Remove a route change listener
     * @param  {Function} cb The listener to remove
     * @return {Router5} The router instance
     */
    removeListener(cb) {
        return this._removeListener('*', cb)
    }

    /**
     * Add a node change listener
     * @param {String}   name The route segment full name
     * @param {Function} cb   The listener to add
     * @return {Router5} The router instance
     */
    addNodeListener(name, cb) {
        return this._addListener('^' + name, cb);
    }

    /**
     * Remove a node change listener
     * @param {String}   name The route segment full name
     * @param {Function} cb   The listener to remove
     * @return {Router5} The router instance
     */
    removeNodeListener(name, cb) {
        return this._removeListener('^' + name, cb);
    }

    /**
     * Add a route change listener
     * @param {String}   name The route name to listen to
     * @param {Function} cb   The listener to add
     * @return {Router5} The router instance
     */
    addRouteListener(name, cb) {
        return this._addListener('=' + name, cb)
    }

    /**
     * Remove a route change listener
     * @param {String}   name The route name to listen to
     * @param {Function} cb   The listener to remove
     * @return {Router5} The router instance
     */
    removeRouteListener(name, cb) {
        return this._removeListener('=' + name, cb)
    }

    /**
     * Register an active component for a specific route segment
     * @param  {String} name      The route segment full name
     * @param  {Object} component The component instance
     */
    registerComponent(name, component) {
        if (this._cmps[name]) console.warn(`A component was alread registered for route node ${name}.`)
        this._cmps[name] = component
        return this
    }

    /**
     * Deregister an active component
     * @param  {String} name The route segment full name
     * @return {Router5} The router instance
     */
    deregisterComponent(name) {
        delete this._cmps[name]
    }

    /**
     * [registerCanActivate description]
     * @param  {String}   name        The route name to register the canActivate method for
     * @param  {Function} canActivate The canActivate function. It should return `true`, `false`
     *                                or a promise
     * @return {Router5}  The router instance
     */
    canActivate(name, canActivate) {
        if (this._canAct[name]) console.warn(`A canActivate was alread registered for route node ${name}.`)
        this._canAct[name] = canActivate
        return this
    }

    /**
     * @private
     */
    getLocation() {
        let path = this.options.useHash
            ? window.location.hash.replace(new RegExp('^#' + this.options.hashPrefix), '')
            : window.location.pathname.replace(new RegExp('^' + this.base), '')
        return path + window.location.search;
    }

    /**
     * Generates an URL from a route name and route params.
     * The generated URL will be prefixed by hash if useHash is set to true
     * @param  {String} route  The route name
     * @param  {Object} params The route params (key-value pairs)
     * @return {String}        The built URL
     */
    buildUrl(route, params) {
        return (this.options.useHash ? window.location.pathname + '#' + this.options.hashPrefix : this.base) +
            this.rootNode.buildPath(route, params)
    }

    /**
     * Build a path from a route name and route params
     * The generated URL will be prefixed by hash if useHash is set to true
     * @param  {String} route  The route name
     * @param  {Object} params The route params (key-value pairs)
     * @return {String}        The built Path
     */
    buildPath(route, params) {
        return this.rootNode.buildPath(route, params)
    }

    /**
     * Match a path against the route tree.
     * @param  {String} path   The path / URL to match
     * @return {Object}        The matched state object (null if no match)
     */
    matchPath(path) {
        let match = this.rootNode.matchPath(path)
        return match ? makeState(match.name, match.params, path) : null
    }

    /**
     * @private
     */
    _transition(toState, fromState) {
        if (!fromState) {
            this.lastKnownState = toState
            this._invokeListeners('*', toState, fromState)
            return true
        }

        let i
        let cannotDeactivate = false
        let cannotActivate = false
        let fromStateIds = nameToIDs(fromState.name)
        let toStateIds   = nameToIDs(toState.name)
        let maxI = Math.min(fromStateIds.length, toStateIds.length)

        for (i = 0; i < maxI; i += 1) {
            if (fromStateIds[i] !== toStateIds[i]) break
        }

        cannotDeactivate =
            fromStateIds.slice(i).reverse()
                .map(id => this._cmps[id])
                .filter(comp => comp && comp.canDeactivate)
                .some(comp => !comp.canDeactivate(toState, fromState))

        if (!cannotDeactivate) {
            cannotActivate = toStateIds.slice(i)
                .map(id => this._canAct[id])
                .filter(canAct => canAct)
                .some(canAct => !canAct(toState, fromState))
        }

        if (!cannotDeactivate && !cannotActivate) {
            this.lastKnownState = toState
            this._invokeListeners('^' + (i > 0 ? fromStateIds[i - 1] : ''), toState, fromState)
            this._invokeListeners('=' + toState.name, toState, fromState)
            this._invokeListeners('*', toState, fromState)
        }

        return !cannotDeactivate && !cannotActivate
    }

    /**
     * Navigate to a specific route
     * @param  {String} name   The route name
     * @param  {Object} [params={}] The route params
     * @param  {Object} [opts={}]   The route options (replace, reload)
     * @return {Boolean}       Whether or not transition was allowed
     */
    navigate(name, params = {}, opts = {}) {
        if (!this.started) return

        let path  = this.buildPath(name, params)
        let url  = this.buildUrl(name, params)

        if (!path) throw new Error(`Could not find route "${name}"`)

        this.lastStateAttempt = makeState(name, params, path)
        let sameStates = this.lastKnownState ? this.areStatesEqual(this.lastKnownState, this.lastStateAttempt) : false

        // Do not proceed further if states are the same and no reload
        // (no desactivation and no callbacks)
        if (sameStates && !opts.reload) return

        // Transition and amend history
        let canTransition = this._transition(this.lastStateAttempt, this.lastKnownState)

        if (canTransition && !sameStates) {
            window.history[opts.replace ? 'replaceState' : 'pushState'](this.lastStateAttempt, '', url)
        }

        return canTransition
    }
}
<commit_msg>feat: limit node listeners to one per node<commit_after>import RouteNode from 'route-node/modules/RouteNode'

let nameToIDs = name => {
    return name.split('.').reduce((ids, name) => {
        ids.push(ids.length ? ids[ids.length - 1] + '.' + name : name)
        return ids
    }, [])
}

let makeState = (name, params, path) => ({name, params, path})

/**
 * Create a new Router5 instance
 * @class
 * @param {RouteNode[]|Object[]|RouteNode|Object} routes The router routes
 * @param {Object} [opts={}] The router options: useHash, defaultRoute and defaultParams can be specified.
 * @return {Router5} The router instance
 */
export default class Router5 {
    constructor(routes, opts = {}) {
        this.started = false
        this._cbs = {}
        this._cmps = {}
        this._canAct = {}
        this.lastStateAttempt = null
        this.lastKnownState = null
        this.rootNode  = routes instanceof RouteNode ? routes : new RouteNode('', '', routes)
        this.options = {
            useHash: false,
            hashPrefix: ''
        }
        Object.keys(opts).forEach(opt => this.options[opt] = opts[opt])
        this.base = window.location.pathname.replace(/^\/$/, '')

        return this
    }

    /**
     * Set an option value
     * @param  {String} opt The option to set
     * @param  {*}      val The option value
     * @return {Router5}    The Router5 instance
     */
    setOption(opt, val) {
        this.options[opt] = val
        return this
    }

    /**
     * Add route(s)
     * @param  {RouteNode[]|Object[]|RouteNode|Object} routes Route(s) to add
     * @return {Router5}  The Router5 instance
     */
    add(routes) {
        this.rootNode.add(routes)
        return this
    }

    /**
     * Add a route to the router.
     * @param {String}   name        The route name
     * @param {String}   path        The route path
     * @param {Function} canActivate A function to determine if the route can be activated.
     *                               It will be invoked during a transition with `toState`
     *                               and `fromState` parameters.
     * @return {Router5}             The Router5 instance
     */
    addNode(name, path, canActivate) {
        try {
            this.rootNode.addNode(name, path)
            this._canAct[name] = canActivate
        } catch (e) {}
        return this
    }

    /**
     * @private
     */
    onPopState(evt) {
        // Do nothing if no state or if last know state is poped state (it should never happen)
        let state = evt.state || this.matchPath(this.getLocation())
        if (!state) return
        if (this.lastKnownState && this.areStatesEqual(state, this.lastKnownState)) return

        let canTransition = this._transition(state, this.lastKnownState)
        if (!canTransition) {
            let url = this.buildUrl(this.lastKnownState.name, this.lastKnownState.params)
            window.history.pushState(this.lastKnownState, '', url)
        }
    }

    /**
     * Start the router
     * @return {Router5} The router instance
     */
    start() {
        if (this.started) return this
        this.started = true

        // Try to match starting path name
        let startPath = this.getLocation();
        let startState = this.matchPath(startPath)

        if (startState) {
            this.lastKnownState = startState
            window.history.replaceState(this.lastKnownState, '', this.buildUrl(startState.name, startState.params))
        } else if (this.options.defaultRoute) {
            this.navigate(this.options.defaultRoute, this.options.defaultParams, {replace: true})
        }
        // Listen to popstate
        window.addEventListener('popstate', this.onPopState.bind(this))
        return this
    }

    /**
     * Stop the router
     * @return {Router5} The router instance
     */
    stop() {
        if (!this.started) return this
        this.started = false

        window.removeEventListener('popstate', this.onPopState.bind(this))
        return this
    }

    /**
     * Return the current state object
     * @return {Object} The current state
     */
    getState() {
        return this.lastKnownState
        // return window.history.state
    }

    /**
     * Whether or not the given route name with specified params is active.
     * @param  {String}   name             The route name
     * @param  {Object}   [params={}]      The route parameters
     * @param  {Boolean}  [equality=false] If set to false (default), isActive will return true
     *                                     if the provided route name and params are descendants
     *                                     of the active state.
     * @return {Boolean}                   Whether nor not the route is active
     */
    isActive(name, params = {}, strictEquality = false) {
        let activeState = this.getState()

        if (!activeState) return false

        if (strictEquality || activeState.name === name) {
            return this.areStatesEqual(makeState(name, params), activeState)
        } else {
            return this.areStatesDescendants(makeState(name, params), activeState)
        }
    }

    /**
     * @private
     */
    areStatesEqual(state1, state2) {
        return state1.name === state2.name &&
           Object.keys(state1.params).length === Object.keys(state2.params).length &&
           Object.keys(state1.params).every(p => state1.params[p] === state2.params[p])
    }

    /**
     * Whether two states are descendants
     * @param  {Object} parentState The parent state
     * @param  {Object} childState  The child state
     * @return {Boolean}            Whether the two provided states are related
     */
    areStatesDescendants(parentState, childState) {
        let regex = new RegExp('^' + parentState.name + '\\.(.*)$')
        if (!regex.test(childState.name)) return false
        // If child state name extends parent state name, and all parent state params
        // are in child state params.
        return Object.keys(parentState.params).every(p => parentState.params[p] === childState.params[p])
    }

    /**
     * @private
     */
    _invokeListeners(name, newState, oldState) {
        if (!this._cbs[name]) return
        this._cbs[name].forEach(cb => cb(newState, oldState))
    }

    /**
     * @private
     */
    _addListener(name, cb, replace) {
        let normalizedName = name.replace(/^(\*|\^|=)/, '')
        if (normalizedName) {
            let segments = this.rootNode.getSegmentsByName(normalizedName)
            if (!segments) console.warn(`No route found for ${normalizedName}, listener might never be called!`)
        }
        if (!this._cbs[name]) this._cbs[name] = []
        this._cbs[name] = (replace ? [] : this._cbs[name]).concat(cb)
        return this
    }

    /**
     * @private
     */
    _removeListener(name, cb) {
        if (this._cbs[name]) this._cbs[name] = this._cbs[name].filter(callback => callback !== cb)
        return this
    }

    /**
     * Add a route change listener
     * @param {Function} cb The listener to add
     * @return {Router5} The router instance
     */
    addListener(cb) {
        return this._addListener('*', cb)
    }

    /**
     * Remove a route change listener
     * @param  {Function} cb The listener to remove
     * @return {Router5} The router instance
     */
    removeListener(cb) {
        return this._removeListener('*', cb)
    }

    /**
     * Add a node change listener
     * @param {String}   name The route segment full name
     * @param {Function} cb   The listener to add
     * @return {Router5} The router instance
     */
    addNodeListener(name, cb) {
        return this._addListener('^' + name, cb, true);
    }

    /**
     * Remove a node change listener
     * @param {String}   name The route segment full name
     * @param {Function} cb   The listener to remove
     * @return {Router5} The router instance
     */
    removeNodeListener(name, cb) {
        return this._removeListener('^' + name, cb);
    }

    /**
     * Add a route change listener
     * @param {String}   name The route name to listen to
     * @param {Function} cb   The listener to add
     * @return {Router5} The router instance
     */
    addRouteListener(name, cb) {
        return this._addListener('=' + name, cb)
    }

    /**
     * Remove a route change listener
     * @param {String}   name The route name to listen to
     * @param {Function} cb   The listener to remove
     * @return {Router5} The router instance
     */
    removeRouteListener(name, cb) {
        return this._removeListener('=' + name, cb)
    }

    /**
     * Register an active component for a specific route segment
     * @param  {String} name      The route segment full name
     * @param  {Object} component The component instance
     */
    registerComponent(name, component) {
        if (this._cmps[name]) console.warn(`A component was alread registered for route node ${name}.`)
        this._cmps[name] = component
        return this
    }

    /**
     * Deregister an active component
     * @param  {String} name The route segment full name
     * @return {Router5} The router instance
     */
    deregisterComponent(name) {
        delete this._cmps[name]
    }

    /**
     * [registerCanActivate description]
     * @param  {String}   name        The route name to register the canActivate method for
     * @param  {Function} canActivate The canActivate function. It should return `true`, `false`
     *                                or a promise
     * @return {Router5}  The router instance
     */
    canActivate(name, canActivate) {
        if (this._canAct[name]) console.warn(`A canActivate was alread registered for route node ${name}.`)
        this._canAct[name] = canActivate
        return this
    }

    /**
     * @private
     */
    getLocation() {
        let path = this.options.useHash
            ? window.location.hash.replace(new RegExp('^#' + this.options.hashPrefix), '')
            : window.location.pathname.replace(new RegExp('^' + this.base), '')
        return path + window.location.search;
    }

    /**
     * Generates an URL from a route name and route params.
     * The generated URL will be prefixed by hash if useHash is set to true
     * @param  {String} route  The route name
     * @param  {Object} params The route params (key-value pairs)
     * @return {String}        The built URL
     */
    buildUrl(route, params) {
        return (this.options.useHash ? window.location.pathname + '#' + this.options.hashPrefix : this.base) +
            this.rootNode.buildPath(route, params)
    }

    /**
     * Build a path from a route name and route params
     * The generated URL will be prefixed by hash if useHash is set to true
     * @param  {String} route  The route name
     * @param  {Object} params The route params (key-value pairs)
     * @return {String}        The built Path
     */
    buildPath(route, params) {
        return this.rootNode.buildPath(route, params)
    }

    /**
     * Match a path against the route tree.
     * @param  {String} path   The path / URL to match
     * @return {Object}        The matched state object (null if no match)
     */
    matchPath(path) {
        let match = this.rootNode.matchPath(path)
        return match ? makeState(match.name, match.params, path) : null
    }

    /**
     * @private
     */
    _transition(toState, fromState) {
        if (!fromState) {
            this.lastKnownState = toState
            this._invokeListeners('*', toState, fromState)
            return true
        }

        let i
        let cannotDeactivate = false
        let cannotActivate = false
        let fromStateIds = nameToIDs(fromState.name)
        let toStateIds   = nameToIDs(toState.name)
        let maxI = Math.min(fromStateIds.length, toStateIds.length)

        for (i = 0; i < maxI; i += 1) {
            if (fromStateIds[i] !== toStateIds[i]) break
        }

        cannotDeactivate =
            fromStateIds.slice(i).reverse()
                .map(id => this._cmps[id])
                .filter(comp => comp && comp.canDeactivate)
                .some(comp => !comp.canDeactivate(toState, fromState))

        if (!cannotDeactivate) {
            cannotActivate = toStateIds.slice(i)
                .map(id => this._canAct[id])
                .filter(canAct => canAct)
                .some(canAct => !canAct(toState, fromState))
        }

        if (!cannotDeactivate && !cannotActivate) {
            this.lastKnownState = toState
            this._invokeListeners('^' + (i > 0 ? fromStateIds[i - 1] : ''), toState, fromState)
            this._invokeListeners('=' + toState.name, toState, fromState)
            this._invokeListeners('*', toState, fromState)
        }

        return !cannotDeactivate && !cannotActivate
    }

    /**
     * Navigate to a specific route
     * @param  {String} name   The route name
     * @param  {Object} [params={}] The route params
     * @param  {Object} [opts={}]   The route options (replace, reload)
     * @return {Boolean}       Whether or not transition was allowed
     */
    navigate(name, params = {}, opts = {}) {
        if (!this.started) return

        let path  = this.buildPath(name, params)
        let url  = this.buildUrl(name, params)

        if (!path) throw new Error(`Could not find route "${name}"`)

        this.lastStateAttempt = makeState(name, params, path)
        let sameStates = this.lastKnownState ? this.areStatesEqual(this.lastKnownState, this.lastStateAttempt) : false

        // Do not proceed further if states are the same and no reload
        // (no desactivation and no callbacks)
        if (sameStates && !opts.reload) return

        // Transition and amend history
        let canTransition = this._transition(this.lastStateAttempt, this.lastKnownState)

        if (canTransition && !sameStates) {
            window.history[opts.replace ? 'replaceState' : 'pushState'](this.lastStateAttempt, '', url)
        }

        return canTransition
    }
}
<|endoftext|>


<commit_before>var Discord = require('discord.js');
var client = new Discord.Client();

var mongoose = require('mongoose');
var request = require('request');
var cheerio = require('cheerio');
var sha1 = require('sha1');

var stats = require('./stats');
var badges = require('./badges');

var Site = mongoose.model('site');

client.on('ready', function() {
	client.user.setUsername('BadgeBot');
	client.user.setGame('n!help');
	console.log('BadgeBot is active!');
});

var commands = [
    {
        name: 'link',
        desc: 'Links your account to your neocities account',
        example: 'Use your neocities website name: e.g. `n!link kyledrake`',
        action: function(msg, args) {
            var site = args[0];
            if (site) {
            	console.log('Generating site data...');
	            stats.get(site, function(statObject,_,cache) {
	            	if (statObject) {
		            	badges.add(statObject, site, function() {
		            		if (hasDiscordCode(site,msg.author,cache)) {
								badges.award(site, 'discord', function() {
								    msg.channel.send(msg.author + ' has linked their account to '+site+'! Enjoy the badge!');
								});
								Site.update({name:site}, {
								    discord: msg.author
								}, function(err) {
								    if (err) return console.log(err);
								    console.log('Put discord name in database!');
								});
		            		}
		            		else {
		            			msg.channel.send("To make sure "+site+" is actually you, post this as a message on your neocities page (you can delete it later):\n\n`"+sha1(msg.author)+"`\n\nType `n!link "+site+"` again once you're done.");
		            		}
		            	});
	            	}
	            	else {
	            		msg.channel.send("That website doesn't exist!");
	            	}
	            },true);
            }
            else {
            	msg.channel.send(this.example);
            }
        }
    },
    {
        name: 'badges',
        desc: "Lists all of your badges, or lists someone else's badges",
        example: '`n!badges` or `n!badges kyledrake`',
        action: function(msg, args) {
        	if (args[0]) {
        		var site = args[0];
            	msg.channel.send('Please wait while we get '+site+"'s badges...").then(function(message) {
                    displayBadges(site,message);
            	});
        	}
        	else {
	        	checkIfLinked(msg, function(sites) {
	            	msg.channel.send('Please wait while we get your badges...').then(function(message) {
	                    displayBadges(sites[0].name,message);
	            	});
	        	});
        	}
        }
    },
    {
    	name: 'stats',
    	desc: "Lists all of your stats, or lists someone else's stats",
    	example: '`n!stats` or `n!stats kyledrake`',
    	action: function(msg, args) {
        	if (args[0]) {
        		var site = args[0];
            	msg.channel.send('Please wait while we get '+site+"'s stats...").then(function(message) {
                    displayStats(site,message);
            	});
        	}
        	else {
	        	checkIfLinked(msg, function(sites) {
	            	msg.channel.send('Please wait while we get your stats...').then(function(message) {
	                    displayStats(sites[0].name,message);
	            	});
	        	});
        	}
    	}
    },
    {
    	name: 'random',
    	desc: "Lists the stats of a random neocities site",
    	example: '`n!random`',
    	action: function(msg, args) {
    		console.log('Getting a random website...');
    		request('https://neocities.org/browse?page='+~~(Math.random()*100), function(err, response, body) {
    			if (err) return console.log(err);

    			var $ = cheerio.load(body);
    			var site = $('.website-Gallery li').eq(~~(Math.random()*100)).attr('id').replace('username_','');

    			console.log('Found site: '+site+'!');

    			msg.channel.send('Please wait while we get '+site+"'s stats...").then(function(message) {
                    displayStats(site,message);
            	});
    		});
    	}
    }
];

function displayStats(site,message) {
	stats.get(site, function(statObject) {
		if (statObject) {
			var statArray = [];
			var statsToShow = ['rank','views','followers','updates','tips'];
			for (var i=0;i<statsToShow.length;i++) {
				var emojiToShow = 'thinking';
				var statToShow = statsToShow[i];
				var statValue = statObject[statToShow];

				if (statToShow=='rank') emojiToShow = 'trophy';
				if (statToShow=='views') emojiToShow = 'eyes';
				if (statToShow=='followers') emojiToShow = 'white_check_mark';
				if (statToShow=='updates') emojiToShow = 'construction';
				if (statToShow=='tips') emojiToShow = 'moneybag';

				if (statToShow=='rank') {
					switch (statValue%10) {
						case 1:
							statValue += 'st';
							break;
						case 2:
							statValue += 'nd';
							break;
						case 3:
							statValue += 'rd';
							break;
						default:
							statValue += 'th';
							break;
					}
				}

				statArray.push(':'+emojiToShow+':  '+capital(statToShow)+': '+statValue);
			}

		    message.edit({embed: {
		    	color: 16762454,
		        title: '__**'+capital(site)+"'s Stats**__",
		        url: 'https://neocities.org/site/'+site,
		        description: statArray.join('\n')
		    }});
		}
		else {
			message.edit("That website doesn't exist!");
		}
	});
}

function displayBadges(site,message) {
	stats.get(site, function(statObject) {
		if (statObject) {
			badges.add(statObject, site, function(data) {
			    message.edit({embed: {
			    	color: 16762454,
			        title: '__**'+capital(site)+"'s Badges**__",
		        	url: 'https://neocities.org/site/'+site,
			        description: data.badges.map(function(badge) {
	    		        return ':medal:  '+capital(badge)+' - '+badges.description(badge);
	    		    }).join('\n')
			    }});
			});
		}
		else {
			message.edit("That website doesn't exist!");
		}
	});
}

function checkIfLinked(msg,cb) {
	Site.find({discord:msg.author}, function(err, sites) {
        if (err) return console.log(err);

        if (sites.length==0) {
            msg.channel.send('You are not linked to your neocities account! Link your account with the `n!link` command.');
        }
        else {
        	cb(sites);
        }
    });
}

function hasDiscordCode(site,discordName,cache) {
	console.log('Checking for discord code...');
	console.log('Discord ID: '+discordName);
	console.log('Discord code: '+sha1(discordName));

	var $ = cheerio.load(cache.site);

	var hasCode = false;
	$('.comment').each(function() {
		if ($(this).find('.title .text a').text()==site && $(this).find('.content').first().text().trim()==sha1(discordName)) {
			console.log('Discord code found!');
			hasCode = true;
			return;
		}
	});
	return hasCode;
}

function capital(str) {
	return str[0].toUpperCase()+str.substr(1,str.length);
}

var commandStart = 'n!';
client.on('message', function(msg) {
	if (msg.content.match(/^(n!).+/)) {
		var command = msg.content.split(' ')[0].replace(commandStart,'');
		var args = msg.content.split(' ');
		args.shift();

		for (var i=0;i<commands.length;i++) {
		    if (command == commands[i].name) {
		        commands[i].action(msg, args);
		    }
		}
		if (command == 'help') {
		    var helpArray = [];
    		for (var i=0;i<commands.length;i++) {
    		    var command = commands[i];
    		    helpArray.push({
    		        name: 'n!'+command.name,
    		        value: command.desc+'\n('+command.example+')'
    		    });
    		}
    		msg.channel.send({embed: {
    		    color: 16762454,
    		    fields: helpArray
    		}});
		}
	}
});

var config = require('./config');
client.login(config.discordKey);<commit_msg>BadgeBot only displays rank if the website is in the top 100<commit_after>var Discord = require('discord.js');
var client = new Discord.Client();

var mongoose = require('mongoose');
var request = require('request');
var cheerio = require('cheerio');
var sha1 = require('sha1');

var stats = require('./stats');
var badges = require('./badges');

var Site = mongoose.model('site');

client.on('ready', function() {
	client.user.setUsername('BadgeBot');
	client.user.setGame('n!help');
	console.log('BadgeBot is active!');
});

var commands = [
    {
        name: 'link',
        desc: 'Links your account to your neocities account',
        example: 'Use your neocities website name: e.g. `n!link kyledrake`',
        action: function(msg, args) {
            var site = args[0];
            if (site) {
            	console.log('Generating site data...');
	            stats.get(site, function(statObject,_,cache) {
	            	if (statObject) {
		            	badges.add(statObject, site, function() {
		            		if (hasDiscordCode(site,msg.author,cache)) {
								badges.award(site, 'discord', function() {
								    msg.channel.send(msg.author + ' has linked their account to '+site+'! Enjoy the badge!');
								});
								Site.update({name:site}, {
								    discord: msg.author
								}, function(err) {
								    if (err) return console.log(err);
								    console.log('Put discord name in database!');
								});
		            		}
		            		else {
		            			msg.channel.send("To make sure "+site+" is actually you, post this as a message on your neocities page (you can delete it later):\n\n`"+sha1(msg.author)+"`\n\nType `n!link "+site+"` again once you're done.");
		            		}
		            	});
	            	}
	            	else {
	            		msg.channel.send("That website doesn't exist!");
	            	}
	            },true);
            }
            else {
            	msg.channel.send(this.example);
            }
        }
    },
    {
        name: 'badges',
        desc: "Lists all of your badges, or lists someone else's badges",
        example: '`n!badges` or `n!badges kyledrake`',
        action: function(msg, args) {
        	if (args[0]) {
        		var site = args[0];
            	msg.channel.send('Please wait while we get '+site+"'s badges...").then(function(message) {
                    displayBadges(site,message);
            	});
        	}
        	else {
	        	checkIfLinked(msg, function(sites) {
	            	msg.channel.send('Please wait while we get your badges...').then(function(message) {
	                    displayBadges(sites[0].name,message);
	            	});
	        	});
        	}
        }
    },
    {
    	name: 'stats',
    	desc: "Lists all of your stats, or lists someone else's stats",
    	example: '`n!stats` or `n!stats kyledrake`',
    	action: function(msg, args) {
        	if (args[0]) {
        		var site = args[0];
            	msg.channel.send('Please wait while we get '+site+"'s stats...").then(function(message) {
                    displayStats(site,message);
            	});
        	}
        	else {
	        	checkIfLinked(msg, function(sites) {
	            	msg.channel.send('Please wait while we get your stats...').then(function(message) {
	                    displayStats(sites[0].name,message);
	            	});
	        	});
        	}
    	}
    },
    {
    	name: 'random',
    	desc: "Lists the stats of a random neocities site",
    	example: '`n!random`',
    	action: function(msg, args) {
    		console.log('Getting a random website...');
    		request('https://neocities.org/browse?page='+~~(Math.random()*100), function(err, response, body) {
    			if (err) return console.log(err);

    			var $ = cheerio.load(body);
    			var site = $('.website-Gallery li').eq(~~(Math.random()*100)).attr('id').replace('username_','');

    			console.log('Found site: '+site+'!');

    			msg.channel.send('Please wait while we get '+site+"'s stats...").then(function(message) {
                    displayStats(site,message);
            	});
    		});
    	}
    }
];

function displayStats(site,message) {
	stats.get(site, function(statObject) {
		if (statObject) {
			var statArray = [];
			var statsToShow = ['rank','views','followers','updates','tips'];
			for (var i=0;i<statsToShow.length;i++) {
				var emojiToShow = 'thinking';
				var statToShow = statsToShow[i];
				var statValue = statObject[statToShow];

				if (statToShow=='rank') emojiToShow = 'trophy';
				if (statToShow=='views') emojiToShow = 'eyes';
				if (statToShow=='followers') emojiToShow = 'white_check_mark';
				if (statToShow=='updates') emojiToShow = 'construction';
				if (statToShow=='tips') emojiToShow = 'moneybag';

				if (statToShow=='rank') {
					switch (statValue%10) {
						case 1:
							statValue += 'st';
							break;
						case 2:
							statValue += 'nd';
							break;
						case 3:
							statValue += 'rd';
							break;
						default:
							statValue += 'th';
							break;
					}
				}

				if (statToShow!='rank' || statValue!='0th') statArray.push(':'+emojiToShow+':  '+capital(statToShow)+': '+statValue);
			}

		    message.edit({embed: {
		    	color: 16762454,
		        title: '__**'+capital(site)+"'s Stats**__",
		        url: 'https://neocities.org/site/'+site,
		        description: statArray.join('\n')
		    }});
		}
		else {
			message.edit("That website doesn't exist!");
		}
	});
}

function displayBadges(site,message) {
	stats.get(site, function(statObject) {
		if (statObject) {
			badges.add(statObject, site, function(data) {
			    message.edit({embed: {
			    	color: 16762454,
			        title: '__**'+capital(site)+"'s Badges**__",
		        	url: 'https://neocities.org/site/'+site,
			        description: data.badges.map(function(badge) {
	    		        return ':medal:  '+capital(badge)+' - '+badges.description(badge);
	    		    }).join('\n')
			    }});
			});
		}
		else {
			message.edit("That website doesn't exist!");
		}
	});
}

function checkIfLinked(msg,cb) {
	Site.find({discord:msg.author}, function(err, sites) {
        if (err) return console.log(err);

        if (sites.length==0) {
            msg.channel.send('You are not linked to your neocities account! Link your account with the `n!link` command.');
        }
        else {
        	cb(sites);
        }
    });
}

function hasDiscordCode(site,discordName,cache) {
	console.log('Checking for discord code...');
	console.log('Discord ID: '+discordName);
	console.log('Discord code: '+sha1(discordName));

	var $ = cheerio.load(cache.site);

	var hasCode = false;
	$('.comment').each(function() {
		if ($(this).find('.title .text a').text()==site && $(this).find('.content').first().text().trim()==sha1(discordName)) {
			console.log('Discord code found!');
			hasCode = true;
			return;
		}
	});
	return hasCode;
}

function capital(str) {
	return str[0].toUpperCase()+str.substr(1,str.length);
}

var commandStart = 'n!';
client.on('message', function(msg) {
	if (msg.content.match(/^(n!).+/)) {
		var command = msg.content.split(' ')[0].replace(commandStart,'');
		var args = msg.content.split(' ');
		args.shift();

		for (var i=0;i<commands.length;i++) {
		    if (command == commands[i].name) {
		        commands[i].action(msg, args);
		    }
		}
		if (command == 'help') {
		    var helpArray = [];
    		for (var i=0;i<commands.length;i++) {
    		    var command = commands[i];
    		    helpArray.push({
    		        name: 'n!'+command.name,
    		        value: command.desc+'\n('+command.example+')'
    		    });
    		}
    		msg.channel.send({embed: {
    		    color: 16762454,
    		    fields: helpArray
    		}});
		}
	}
});

var config = require('./config');
client.login(config.discordKey);<|endoftext|>


<commit_before>import makeDebug from 'debug';

const debug = makeDebug('playing:content-services:documents:events');

const createActivity = async function (app, document, verb, message) {
  const svcFeeds = app.service('feeds');
  const svcActivities = app.service('activities');
  if (!document.creator) return; // skip feeds without actor

  const feed = await svcFeeds.get(`${document.type}:${document.id}`);
  if (feed) {
    await svcActivities.create({
      feed: feed.id,
      actor: `user:${document.creator}`,
      verb: verb,
      object: `${document.type}:${document.id}`,
      foreignId: `${document.type}:${document.id}`,
      message: message,
      title: document.title,
      cc: [`user:${document.creator}`]
    });
  }
};

// subscribe to document.create events
export default function (app, options) {
  app.trans.add({
    pubsub$: true,
    topic: 'playing.events',
    cmd: 'document.create'
  }, (resp) => {
    const document = resp.event;
    if (document && document.type) {
      debug('document.create event', document.type, document.id);
      createActivity(app, document, 'documentCreated', 'created the document');
    }
  });
}
<commit_msg>using feeds.createActivity<commit_after>import makeDebug from 'debug';
import { helpers as feeds } from 'playing-feed-services';

const debug = makeDebug('playing:content-services:documents:events');

const createActivity = async function (app, document, verb, message) {
  if (!document.creator) return; // skip feeds without actor

  const activity = {
    actor: `user:${document.creator}`,
    verb: verb,
    object: `${document.type}:${document.id}`,
    foreignId: `${document.type}:${document.id}`,
    message: message,
    title: document.title,
    cc: [`user:${document.creator}`]
  };

  // add to document's activity log
  await feeds.createActivity(app, 'feeds')(`${document.type}:${document.id}`, activity);
};

// subscribe to document.create events
export default function (app, options) {
  app.trans.add({
    pubsub$: true,
    topic: 'playing.events',
    cmd: 'document.create'
  }, (resp) => {
    const document = resp.event;
    if (document && document.type) {
      debug('document.create event', document.type, document.id);
      createActivity(app, document, 'document.create', 'created the document');
    }
  });
}
<|endoftext|>


<commit_before>/* See license.txt for terms of usage */

// ********************************************************************************************* //
// Constants

var EXPORTED_SYMBOLS = ["FBTrace"];

// ********************************************************************************************* //
// Firebug Trace - FBTrace

var scope = {};

try
{
    Components.utils["import"]("resource://fbtrace/firebug-trace-service.js", scope);
}
catch (err)
{
    scope.traceConsoleService =
    {
        getTracer: function(prefDomain)
        {
            var TraceAPI = ["dump", "sysout", "setScope", "matchesNode", "time", "timeEnd"];
            var TraceObj = {};
            for (var i=0; i<TraceAPI.length; i++)
                TraceObj[TraceAPI[i]] = function() {};
            return TraceObj;
        }
    };
}

var FBTrace = scope.traceConsoleService.getTracer("extensions.firebug");

// ********************************************************************************************* //
<commit_msg>Do not export from fbtrace module<commit_after>/* See license.txt for terms of usage */

// ********************************************************************************************* //
// Constants

var EXPORTED_SYMBOLS = [];

// ********************************************************************************************* //
// Firebug Trace - FBTrace

var scope = {};

try
{
    Components.utils["import"]("resource://fbtrace/firebug-trace-service.js", scope);
}
catch (err)
{
    scope.traceConsoleService =
    {
        getTracer: function(prefDomain)
        {
            var TraceAPI = ["dump", "sysout", "setScope", "matchesNode", "time", "timeEnd"];
            var TraceObj = {};
            for (var i=0; i<TraceAPI.length; i++)
                TraceObj[TraceAPI[i]] = function() {};
            return TraceObj;
        }
    };
}

var FBTrace = scope.traceConsoleService.getTracer("extensions.firebug");

// ********************************************************************************************* //
<|endoftext|>


<commit_before>/*
   Copyright (c) 2007-9, iUI Project Members
   See LICENSE.txt for licensing terms
 */

(function() {

var slideSpeed = 20;
var slideInterval = 0;

var currentPage = null;
var currentDialog = null;
var currentWidth = 0;
var currentHash = location.hash;
var hashPrefix = "#_";
var pageHistory = [];
var newPageCount = 0;
var checkTimer;
var hasOrientationEvent = false;
var portraitVal = "portrait";
var landscapeVal = "landscape";

// *************************************************************************************************

window.iui =
{
    showPage: function(page, backwards)
    {
        if (page)
        {
            if (currentDialog)
            {
                currentDialog.removeAttribute("selected");
                currentDialog = null;
            }

            if (hasClass(page, "dialog"))
                showDialog(page);
            else
            {
                var fromPage = currentPage;
                currentPage = page;

                if (fromPage)
                    setTimeout(slidePages, 0, fromPage, page, backwards);
                else
                    updatePage(page, fromPage);
            }
        }
    },

    showPageById: function(pageId)
    {
        var page = $(pageId);
        if (page)
        {
            var index = pageHistory.indexOf(pageId);
            var backwards = index != -1;
            if (backwards)
                pageHistory.splice(index, pageHistory.length);

            iui.showPage(page, backwards);
        }
    },

    showPageByHref: function(href, args, method, replace, cb)
    {
        var req = new XMLHttpRequest();
        req.onerror = function()
        {
            if (cb)
                cb(false);
        };

        req.onreadystatechange = function()
        {
            if (req.readyState == 4)
            {
                if (replace)
                    replaceElementWithSource(replace, req.responseText);
                else
                {
                    var frag = document.createElement("div");
                    frag.innerHTML = req.responseText;
                    iui.insertPages(frag.childNodes);
                }
                if (cb)
                    setTimeout(cb, 1000, true);
            }
        };

        if (args)
        {
            req.open(method || "GET", href, true);
            req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            req.setRequestHeader("Content-Length", args.length);
            req.send(args.join("&"));
        }
        else
        {
            req.open(method || "GET", href, true);
            req.send(null);
        }
    },

    insertPages: function(nodes)
    {
        var targetPage;
        for (var i = 0; i < nodes.length; ++i)
        {
            var child = nodes[i];
            if (child.nodeType == 1)
            {
                if (!child.id)
                    child.id = "__" + (++newPageCount) + "__";

                var clone = $(child.id);
                if (clone)
                    clone.parentNode.replaceChild(child, clone);
                else
                    document.body.appendChild(child);

                if (child.getAttribute("selected") == "true" || !targetPage)
                    targetPage = child;

                --i;
            }
        }

        if (targetPage)
            iui.showPage(targetPage);
    },

    getSelectedPage: function()
    {
        for (var child = document.body.firstChild; child; child = child.nextSibling)
        {
            if (child.nodeType == 1 && child.getAttribute("selected") == "true")
                return child;
        }
    },
    isNativeUrl: function(href)
    {
        for(var i = 0; i < iui.nativeUrlPatterns.length; i++)
        {
            if(href.match(iui.nativeUrlPatterns[i])) return true;
        }
        return false;
    },
    nativeUrlPatterns: [
        new RegExp("^http:\/\/maps.google.com\/maps\?"),
        new RegExp("^mailto:"),
        new RegExp("^tel:"),
        new RegExp("^http:\/\/www.youtube.com\/watch\\?v="),
        new RegExp("^http:\/\/www.youtube.com\/v\/"),
        new RegExp("^javascript:"),

    ]
};

// *************************************************************************************************

addEventListener("load", function(event)
{
    var page = iui.getSelectedPage();
    if (page)
        iui.showPage(page);

    setTimeout(preloadImages, 0);
    setTimeout(checkOrientAndLocation, 0);
    checkTimer = setInterval(checkOrientAndLocation, 300);
}, false);

addEventListener("unload", function(event)
{
	return;
}, false);

addEventListener("click", function(event)
{
    var link = findParent(event.target, "a");
    if (link)
    {
        function unselect() { link.removeAttribute("selected"); }

        if (link.href && link.hash && link.hash != "#")
        {
            link.setAttribute("selected", "true");
            iui.showPage($(link.hash.substr(1)));
            setTimeout(unselect, 500);
        }
        else if (link == $("backButton"))
            history.back();
        else if (link.getAttribute("type") == "submit")
            submitForm(findParent(link, "form"));
        else if (link.getAttribute("type") == "cancel")
            cancelDialog(findParent(link, "form"));
        else if (link.target == "_replace")
        {
            link.setAttribute("selected", "progress");
            iui.showPageByHref(link.href, null, null, link, unselect);
        }
        else if (iui.isNativeUrl(link.href))
        {
            return;
        }
        else if (link.target == "_webapp")
        {
            location.href = link.href;
        }
        else if (!link.target)
        {
            link.setAttribute("selected", "progress");
            iui.showPageByHref(link.href, null, null, null, unselect);
        }
        else
            return;

        event.preventDefault();
    }
}, true);

addEventListener("click", function(event)
{
    var div = findParent(event.target, "div");
    if (div && hasClass(div, "toggle"))
    {
        div.setAttribute("toggled", div.getAttribute("toggled") != "true");
        event.preventDefault();
    }
}, true);

function orientChangeHandler()
{
  var orientation=window.orientation;
  switch(orientation)
  {
    case 0:
        setOrientation(portraitVal);
        break;

    case 90:
    case -90:
        setOrientation(landscapeVal);
        break;
  }
}

if (typeof window.onorientationchange == "object")
{
    window.onorientationchange=orientChangeHandler;
    hasOrientationEvent = true;
    setTimeout(orientChangeHandler, 0);
}

function checkOrientAndLocation()
{
    if (!hasOrientationEvent)
    {
      if (window.innerWidth != currentWidth)
      {
          currentWidth = window.innerWidth;
          var orient = currentWidth == 320 ? portraitVal : landscapeVal;
          setOrientation(orient);
      }
    }

    if (location.hash != currentHash)
    {
        var pageId = location.hash.substr(hashPrefix.length);
        iui.showPageById(pageId);
    }
}

function setOrientation(orient)
{
    document.body.setAttribute("orient", orient);
    setTimeout(scrollTo, 100, 0, 1);
}

function showDialog(page)
{
    currentDialog = page;
    page.setAttribute("selected", "true");

    if (hasClass(page, "dialog") && !page.target)
        showForm(page);
}

function showForm(form)
{
    form.onsubmit = function(event)
    {
        event.preventDefault();
        submitForm(form);
    };

    form.onclick = function(event)
    {
        if (event.target == form && hasClass(form, "dialog"))
            cancelDialog(form);
    };
}

function cancelDialog(form)
{
    form.removeAttribute("selected");
}

function updatePage(page, fromPage)
{
    if (!page.id)
        page.id = "__" + (++newPageCount) + "__";

    location.href = currentHash = hashPrefix + page.id;
    pageHistory.push(page.id);

    var pageTitle = $("pageTitle");
    if (page.title)
        pageTitle.innerHTML = page.title;

    if (page.localName.toLowerCase() == "form" && !page.target)
        showForm(page);

    var backButton = $("backButton");
    if (backButton)
    {
        var prevPage = $(pageHistory[pageHistory.length-2]);
        if (prevPage && !page.getAttribute("hideBackButton"))
        {
            backButton.style.display = "inline";
            backButton.innerHTML = prevPage.title ? prevPage.title : "Back";
        }
        else
            backButton.style.display = "none";
    }
}

function slidePages(fromPage, toPage, backwards)
{
    var axis = (backwards ? fromPage : toPage).getAttribute("axis");

    clearInterval(checkTimer);

    if (canDoSlideAnim() && axis != 'y')
    {
      slide2(fromPage, toPage, backwards, slideDone);
    }
    else
    {
      slide1(fromPage, toPage, backwards, axis, slideDone);
    }

    function slideDone()
    {
//      console.log("slideDone");
      if (!hasClass(toPage, "dialog"))
          fromPage.removeAttribute("selected");
      checkTimer = setInterval(checkOrientAndLocation, 300);
      setTimeout(updatePage, 0, toPage, fromPage);
      fromPage.removeEventListener('webkitTransitionEnd', slideDone, false);
    }
}

function canDoSlideAnim()
{
  return (typeof WebKitCSSMatrix == "object");
}

function slide1(fromPage, toPage, backwards, axis, cb)
{
    if (axis == "y")
        (backwards ? fromPage : toPage).style.top = "100%";
    else
        toPage.style.left = "100%";

    scrollTo(0, 1);
    toPage.setAttribute("selected", "true");
    var percent = 100;
    slide();
    var timer = setInterval(slide, slideInterval);

    function slide()
    {
        percent -= slideSpeed;
        if (percent <= 0)
        {
            percent = 0;
            clearInterval(timer);
            cb();
        }

        if (axis == "y")
        {
            backwards
                ? fromPage.style.top = (100-percent) + "%"
                : toPage.style.top = percent + "%";
        }
        else
        {
            fromPage.style.left = (backwards ? (100-percent) : (percent-100)) + "%";
            toPage.style.left = (backwards ? -percent : percent) + "%";
        }
    }
}

//function durationInt(dur)
//{
//  var val = parseFloat(dur);
//  return (dur.indexOf('ms') == -1) ? val * 1000 : val;
//}

function slide2(fromPage, toPage, backwards, cb)
{
  toPage.style.webkitTransitionDuration = '0ms'; // Turn off transitions to set toPage start offset
  // fromStart is always 0% and toEnd is always 0%
  // iPhone won't take % width on toPage
  var toStart = 'translateX(' + (backwards ? '-' : '') + window.innerWidth +  'px)';
  var fromEnd = 'translateX(' + (backwards ? '100%' : '-100%') + ')';
  toPage.style.webkitTransform = toStart;
  toPage.setAttribute("selected", "true");
  toPage.style.webkitTransitionDuration = '';   // Turn transitions back on
//  var duration = durationInt(window.getComputedStyle(toPage, null).webkitTransitionDuration);
  function startTrans()
  {
    fromPage.style.webkitTransform = fromEnd;
    toPage.style.webkitTransform = 'translateX(0%)'; //toEnd
//    setTimeout(cb, duration);
  }
  fromPage.addEventListener('webkitTransitionEnd', cb, false);
  setTimeout(startTrans, 0);
}

function preloadImages()
{
    var preloader = document.createElement("div");
    preloader.id = "preloader";
    document.body.appendChild(preloader);
}

function submitForm(form)
{
    iui.showPageByHref(form.action || "POST", encodeForm(form), form.method);
}

function encodeForm(form)
{
    function encode(inputs)
    {
        for (var i = 0; i < inputs.length; ++i)
        {
            if (inputs[i].name)
                args.push(inputs[i].name + "=" + escape(inputs[i].value));
        }
    }

    var args = [];
    encode(form.getElementsByTagName("input"));
    encode(form.getElementsByTagName("textarea"));
    encode(form.getElementsByTagName("select"));
    return args;
}

function findParent(node, localName)
{
    while (node && (node.nodeType != 1 || node.localName.toLowerCase() != localName))
        node = node.parentNode;
    return node;
}

function hasClass(self, name)
{
    var re = new RegExp("(^|\\s)"+name+"($|\\s)");
    return re.exec(self.getAttribute("class")) != null;
}

function replaceElementWithSource(replace, source)
{
    var page = replace.parentNode;
    var parent = replace;
    while (page.parentNode != document.body)
    {
        page = page.parentNode;
        parent = parent.parentNode;
    }

    var frag = document.createElement(parent.localName);
    frag.innerHTML = source;

    page.removeChild(parent);

    while (frag.firstChild)
        page.appendChild(frag.firstChild);
}

function $(id) { return document.getElementById(id); }
function ddd() { console.log.apply(console, arguments); }

})();
<commit_msg>Allow links to external pages with hash/anchor see Issue #82<commit_after>/*
   Copyright (c) 2007-9, iUI Project Members
   See LICENSE.txt for licensing terms
 */

(function() {

var slideSpeed = 20;
var slideInterval = 0;

var currentPage = null;
var currentDialog = null;
var currentWidth = 0;
var currentHash = location.hash;
var hashPrefix = "#_";
var pageHistory = [];
var newPageCount = 0;
var checkTimer;
var hasOrientationEvent = false;
var portraitVal = "portrait";
var landscapeVal = "landscape";

// *************************************************************************************************

window.iui =
{
    showPage: function(page, backwards)
    {
        if (page)
        {
            if (currentDialog)
            {
                currentDialog.removeAttribute("selected");
                currentDialog = null;
            }

            if (hasClass(page, "dialog"))
                showDialog(page);
            else
            {
                var fromPage = currentPage;
                currentPage = page;

                if (fromPage)
                    setTimeout(slidePages, 0, fromPage, page, backwards);
                else
                    updatePage(page, fromPage);
            }
        }
    },

    showPageById: function(pageId)
    {
        var page = $(pageId);
        if (page)
        {
            var index = pageHistory.indexOf(pageId);
            var backwards = index != -1;
            if (backwards)
                pageHistory.splice(index, pageHistory.length);

            iui.showPage(page, backwards);
        }
    },

    showPageByHref: function(href, args, method, replace, cb)
    {
        var req = new XMLHttpRequest();
        req.onerror = function()
        {
            if (cb)
                cb(false);
        };

        req.onreadystatechange = function()
        {
            if (req.readyState == 4)
            {
                if (replace)
                    replaceElementWithSource(replace, req.responseText);
                else
                {
                    var frag = document.createElement("div");
                    frag.innerHTML = req.responseText;
                    iui.insertPages(frag.childNodes);
                }
                if (cb)
                    setTimeout(cb, 1000, true);
            }
        };

        if (args)
        {
            req.open(method || "GET", href, true);
            req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            req.setRequestHeader("Content-Length", args.length);
            req.send(args.join("&"));
        }
        else
        {
            req.open(method || "GET", href, true);
            req.send(null);
        }
    },

    insertPages: function(nodes)
    {
        var targetPage;
        for (var i = 0; i < nodes.length; ++i)
        {
            var child = nodes[i];
            if (child.nodeType == 1)
            {
                if (!child.id)
                    child.id = "__" + (++newPageCount) + "__";

                var clone = $(child.id);
                if (clone)
                    clone.parentNode.replaceChild(child, clone);
                else
                    document.body.appendChild(child);

                if (child.getAttribute("selected") == "true" || !targetPage)
                    targetPage = child;

                --i;
            }
        }

        if (targetPage)
            iui.showPage(targetPage);
    },

    getSelectedPage: function()
    {
        for (var child = document.body.firstChild; child; child = child.nextSibling)
        {
            if (child.nodeType == 1 && child.getAttribute("selected") == "true")
                return child;
        }
    },
    isNativeUrl: function(href)
    {
        for(var i = 0; i < iui.nativeUrlPatterns.length; i++)
        {
            if(href.match(iui.nativeUrlPatterns[i])) return true;
        }
        return false;
    },
    nativeUrlPatterns: [
        new RegExp("^http:\/\/maps.google.com\/maps\?"),
        new RegExp("^mailto:"),
        new RegExp("^tel:"),
        new RegExp("^http:\/\/www.youtube.com\/watch\\?v="),
        new RegExp("^http:\/\/www.youtube.com\/v\/"),
        new RegExp("^javascript:"),

    ]
};

// *************************************************************************************************

addEventListener("load", function(event)
{
    var page = iui.getSelectedPage();
    if (page)
        iui.showPage(page);

    setTimeout(preloadImages, 0);
    setTimeout(checkOrientAndLocation, 0);
    checkTimer = setInterval(checkOrientAndLocation, 300);
}, false);

addEventListener("unload", function(event)
{
	return;
}, false);

addEventListener("click", function(event)
{
    var link = findParent(event.target, "a");
    if (link)
    {
        function unselect() { link.removeAttribute("selected"); }

        if (link.href && link.hash && link.hash != "#" && !link.target)
        {
            link.setAttribute("selected", "true");
            iui.showPage($(link.hash.substr(1)));
            setTimeout(unselect, 500);
        }
        else if (link == $("backButton"))
            history.back();
        else if (link.getAttribute("type") == "submit")
            submitForm(findParent(link, "form"));
        else if (link.getAttribute("type") == "cancel")
            cancelDialog(findParent(link, "form"));
        else if (link.target == "_replace")
        {
            link.setAttribute("selected", "progress");
            iui.showPageByHref(link.href, null, null, link, unselect);
        }
        else if (iui.isNativeUrl(link.href))
        {
            return;
        }
        else if (link.target == "_webapp")
        {
            location.href = link.href;
        }
        else if (!link.target)
        {
            link.setAttribute("selected", "progress");
            iui.showPageByHref(link.href, null, null, null, unselect);
        }
        else
            return;

        event.preventDefault();
    }
}, true);

addEventListener("click", function(event)
{
    var div = findParent(event.target, "div");
    if (div && hasClass(div, "toggle"))
    {
        div.setAttribute("toggled", div.getAttribute("toggled") != "true");
        event.preventDefault();
    }
}, true);

function orientChangeHandler()
{
  var orientation=window.orientation;
  switch(orientation)
  {
    case 0:
        setOrientation(portraitVal);
        break;

    case 90:
    case -90:
        setOrientation(landscapeVal);
        break;
  }
}

if (typeof window.onorientationchange == "object")
{
    window.onorientationchange=orientChangeHandler;
    hasOrientationEvent = true;
    setTimeout(orientChangeHandler, 0);
}

function checkOrientAndLocation()
{
    if (!hasOrientationEvent)
    {
      if (window.innerWidth != currentWidth)
      {
          currentWidth = window.innerWidth;
          var orient = currentWidth == 320 ? portraitVal : landscapeVal;
          setOrientation(orient);
      }
    }

    if (location.hash != currentHash)
    {
        var pageId = location.hash.substr(hashPrefix.length);
        iui.showPageById(pageId);
    }
}

function setOrientation(orient)
{
    document.body.setAttribute("orient", orient);
    setTimeout(scrollTo, 100, 0, 1);
}

function showDialog(page)
{
    currentDialog = page;
    page.setAttribute("selected", "true");

    if (hasClass(page, "dialog") && !page.target)
        showForm(page);
}

function showForm(form)
{
    form.onsubmit = function(event)
    {
        event.preventDefault();
        submitForm(form);
    };

    form.onclick = function(event)
    {
        if (event.target == form && hasClass(form, "dialog"))
            cancelDialog(form);
    };
}

function cancelDialog(form)
{
    form.removeAttribute("selected");
}

function updatePage(page, fromPage)
{
    if (!page.id)
        page.id = "__" + (++newPageCount) + "__";

    location.href = currentHash = hashPrefix + page.id;
    pageHistory.push(page.id);

    var pageTitle = $("pageTitle");
    if (page.title)
        pageTitle.innerHTML = page.title;

    if (page.localName.toLowerCase() == "form" && !page.target)
        showForm(page);

    var backButton = $("backButton");
    if (backButton)
    {
        var prevPage = $(pageHistory[pageHistory.length-2]);
        if (prevPage && !page.getAttribute("hideBackButton"))
        {
            backButton.style.display = "inline";
            backButton.innerHTML = prevPage.title ? prevPage.title : "Back";
        }
        else
            backButton.style.display = "none";
    }
}

function slidePages(fromPage, toPage, backwards)
{
    var axis = (backwards ? fromPage : toPage).getAttribute("axis");

    clearInterval(checkTimer);

    if (canDoSlideAnim() && axis != 'y')
    {
      slide2(fromPage, toPage, backwards, slideDone);
    }
    else
    {
      slide1(fromPage, toPage, backwards, axis, slideDone);
    }

    function slideDone()
    {
//      console.log("slideDone");
      if (!hasClass(toPage, "dialog"))
          fromPage.removeAttribute("selected");
      checkTimer = setInterval(checkOrientAndLocation, 300);
      setTimeout(updatePage, 0, toPage, fromPage);
      fromPage.removeEventListener('webkitTransitionEnd', slideDone, false);
    }
}

function canDoSlideAnim()
{
  return (typeof WebKitCSSMatrix == "object");
}

function slide1(fromPage, toPage, backwards, axis, cb)
{
    if (axis == "y")
        (backwards ? fromPage : toPage).style.top = "100%";
    else
        toPage.style.left = "100%";

    scrollTo(0, 1);
    toPage.setAttribute("selected", "true");
    var percent = 100;
    slide();
    var timer = setInterval(slide, slideInterval);

    function slide()
    {
        percent -= slideSpeed;
        if (percent <= 0)
        {
            percent = 0;
            clearInterval(timer);
            cb();
        }

        if (axis == "y")
        {
            backwards
                ? fromPage.style.top = (100-percent) + "%"
                : toPage.style.top = percent + "%";
        }
        else
        {
            fromPage.style.left = (backwards ? (100-percent) : (percent-100)) + "%";
            toPage.style.left = (backwards ? -percent : percent) + "%";
        }
    }
}

//function durationInt(dur)
//{
//  var val = parseFloat(dur);
//  return (dur.indexOf('ms') == -1) ? val * 1000 : val;
//}

function slide2(fromPage, toPage, backwards, cb)
{
  toPage.style.webkitTransitionDuration = '0ms'; // Turn off transitions to set toPage start offset
  // fromStart is always 0% and toEnd is always 0%
  // iPhone won't take % width on toPage
  var toStart = 'translateX(' + (backwards ? '-' : '') + window.innerWidth +  'px)';
  var fromEnd = 'translateX(' + (backwards ? '100%' : '-100%') + ')';
  toPage.style.webkitTransform = toStart;
  toPage.setAttribute("selected", "true");
  toPage.style.webkitTransitionDuration = '';   // Turn transitions back on
//  var duration = durationInt(window.getComputedStyle(toPage, null).webkitTransitionDuration);
  function startTrans()
  {
    fromPage.style.webkitTransform = fromEnd;
    toPage.style.webkitTransform = 'translateX(0%)'; //toEnd
//    setTimeout(cb, duration);
  }
  fromPage.addEventListener('webkitTransitionEnd', cb, false);
  setTimeout(startTrans, 0);
}

function preloadImages()
{
    var preloader = document.createElement("div");
    preloader.id = "preloader";
    document.body.appendChild(preloader);
}

function submitForm(form)
{
    iui.showPageByHref(form.action || "POST", encodeForm(form), form.method);
}

function encodeForm(form)
{
    function encode(inputs)
    {
        for (var i = 0; i < inputs.length; ++i)
        {
            if (inputs[i].name)
                args.push(inputs[i].name + "=" + escape(inputs[i].value));
        }
    }

    var args = [];
    encode(form.getElementsByTagName("input"));
    encode(form.getElementsByTagName("textarea"));
    encode(form.getElementsByTagName("select"));
    return args;
}

function findParent(node, localName)
{
    while (node && (node.nodeType != 1 || node.localName.toLowerCase() != localName))
        node = node.parentNode;
    return node;
}

function hasClass(self, name)
{
    var re = new RegExp("(^|\\s)"+name+"($|\\s)");
    return re.exec(self.getAttribute("class")) != null;
}

function replaceElementWithSource(replace, source)
{
    var page = replace.parentNode;
    var parent = replace;
    while (page.parentNode != document.body)
    {
        page = page.parentNode;
        parent = parent.parentNode;
    }

    var frag = document.createElement(parent.localName);
    frag.innerHTML = source;

    page.removeChild(parent);

    while (frag.firstChild)
        page.appendChild(frag.firstChild);
}

function $(id) { return document.getElementById(id); }
function ddd() { console.log.apply(console, arguments); }

})();
<|endoftext|>


<commit_before>'use strict';

const
    MWBot = require('mwbot'),
    config = require('config'),
    toMarkdown = require('to-markdown'),
    BaseModule = require('./base_module');

const wiki = new MWBot({
    apiUrl: 'https://wiki.guildwars2.com/api.php'
});

class GeneralModule extends BaseModule {
    constructor(bot, config, filename) {
        super(bot, config, filename);
        this.name = 'General';
    }

    cmd_help() {
        return {
            id: 'help',
            command: config.get('modules.general.command_help'),
            deliver: config.get('modules.general.deliver_help'),
            cooldown: 'none',
            help: 'Shows information about how to use commands, with optionally a command as argument to get more detailed information.',
            short_help: 'Shows information about how to use commands',
            params: [
                {
                    id: 'id',
                    help: 'The command',
                    optional: true
                }
            ],
            on_command: (message, params) => {
                const modules = this.bot.getModules();

                if (params.length > 0) {
                    // Reply with help for a specific command
                    const commandName = params[0].replace(new RegExp(`^${config.get('discord.command_prefix')}?(.*)$`), '$1');
                    let command;
                    for (module of modules) {
                        const foundCommand = module.commands.find(command => command.command == commandName);
                        if (foundCommand) {
                            command = foundCommand;
                            command.filename = module.filename;
                            break;
                        }
                    }

                    // TODO: Disabled for now as I don't have a way to detect server role assignments in a DM yet... (they are separate)
                    //if (!this.checkPermission(message, `${command.filename}.${command.id}`)) return;

                    if (!command) {
                        return `I do not recognize the command \`${config.get('discord.command_prefix')}${commandName}\`.` +
                            `Type \`${config.get('discord.command_prefix')}${config.get('modules.general.command_help')}\` to see the list of commands.`;
                    }
                    return `\n${this.formatCommandHelp(command)}`;
                } else {
                    // Reply with general help
                    const help = [];
                    modules.forEach(module => {
                        const moduleHelp = this.formatModuleHelp(message, module);
                        if (moduleHelp) {
                            help.push(moduleHelp);
                        }
                    });
                    return `\n${help.join('\n\n')}`;
                }
            }
        };
    }

    cmd_sourceCode() {
        return {
            id: 'source',
            command: config.get('modules.general.command_source'),
            cooldown: 'global',
            help: 'Shows the link to the source code of this bot.',
            short_help: 'Shows the link to the source code of this bot',
            on_command: () => {
                return 'You can find the source code at https://github.com/Archomeda/raiders-inn-bot.';
            }
        };
    }

    cmd_welcome() {
        return {
            id: 'welcome',
            command: config.get('modules.general.command_welcome'),
            deliver: 'mention',
            cooldown: 'user',
            help: 'Welcomes a person to the server and directs him or her to the read first channel.',
            short_help: 'Welcomes a person to the server',
            channel_type: 'text',
            on_command: message => {
                return (
                    `Welcome to The Raiders Inn! Be sure to head over to ${message.guild.channels.get(config.get('modules.general.welcome_channel'))} to get started. ` +
                    "Our rules and general information is listed there. Don't forget to assign yourself to a server!\n\nEnjoy your stay!"
                );
            }
        };
    }

    cmd_wiki() {
        return {
            id: 'wiki',
            command: config.get('modules.general.command_wiki'),
            cooldown: 'user',
            help: 'Searches the wiki for an article and returns a summary and the article link if found.',
            short_help: 'Searches the wiki for an article',
            params: [
                {
                    id: 'terms',
                    help: 'Search terms'
                }
            ],
            on_command: (message, params) => {
                if (!params || params.length === 0) {
                    return 'Please provide a wiki article title or search terms.';
                }
                const terms = params[0];

                // Search with nearmatch first
                return wiki.request({
                    action: 'query',
                    list: 'search',
                    srsearch: terms,
                    srwhat: 'nearmatch'
                }).then(response => {
                    if (response && response.query.search.length > 0) {
                        return response;
                    }

                    // No results, search with title
                    return wiki.request({
                        action: 'query',
                        list: 'search',
                        srsearch: terms,
                        srwhat: 'title'
                    });
                }).then(response => {
                    if (response && response.query.search.length > 0) {
                        // Found our article, get it
                        return wiki.request({
                            action: 'parse',
                            page: response.query.search[0].title,
                            redirects: true,
                            prop: 'text'
                        });
                    }
                }).catch(err => {
                    // Make sure we have sane errors
                    if (err.code === 'missingtitle') {
                        throw new Error('not found');
                    } else if (err.info) {
                        throw new Error(err.info);
                    }
                    throw err;
                }).then(response => {
                    if (response && response.parse.text['*']) {
                        // We have our article
                        let text = response.parse.text['*'];
                        const title = response.parse.title;

                        // Construct message
                        text = this.formatWikiText(text).split('\n')[0].trim();
                        const url = encodeURI(`https://wiki.guildwars2.com/wiki/${title}`);
                        if (text) {
                            text += `\n\nMore: ${url}`;
                        } else {
                            text = `${title}: ${url}`;
                        }
                        return text;
                    }
                    throw new Error('not found');
                }).catch(err => {
                    // Capture errors and construct proper fail message
                    switch (err.message) {
                        case 'not found':
                            return 'Your request did not come up with any results. Try using different search terms.';
                        case 'no title':
                            return 'Please provide a wiki article title or search terms.';
                        default:
                            throw err;
                    }
                });
            }
        }
    }

    formatCommandChannelFilter(command) {
        let text = [];
        if (command.channel_type) {
            if (command.channel_type.indexOf('dm') > -1 && command.channel_type.indexOf('text') === -1) {
                text.push('DM only');
            } else if (command.channel_type.indexOf('dm') === -1 && command.channel_type.indexOf('text') > -1) {
                if (command.channels && command.channels.length > 0) {
                    text.push('specific server channels only');
                } else {
                    text.push('server channels only');
                }
            }
        }
        let deliver = command.deliver || ['text'];
        if (!Array.isArray(deliver)) deliver = [deliver];
        if (deliver.indexOf('mention') > -1) {
            text.push('mentionable');
        }
        return text.join(', ');
    }

    formatModuleHelp(message, module) {
        const name = module.name || module.constructor.name.replace(/(.*?)(Module)?/, '$1');
        const commands = [];
        module.commands.forEach(command => {
            // TODO: Disabled for now as I don't have a way to detect server role assignments in a DM yet... (they are separate)
            //if (!this.checkPermission(message, `${module.filename}.${command.id}`)) return;

            const commandText = `\`${config.get('discord.command_prefix')}${command.command}\``;
            const helpText = command.short_help;
            if (!helpText) return;

            let text;
            if (command.channel_type && command.channel_type.length > 0) {
                const extraText = this.formatCommandChannelFilter(command);
                text = `${commandText} - ${helpText} *(${extraText})*`;
            } else {
                text = `${commandText} - ${helpText}`;
            }
            commands.push(text);
        });
        if (commands.length === 0) {
            return;
        }
        return `__**${name}**__\n${commands.join('\n')}`;
    }

    formatCommandHelp(command) {
        let invocation = `${config.get('discord.command_prefix')}${command.command} `;
        const params = [];
        if (command.params) {
            command.params.forEach(param => {
                const paramText = `\`${param.id}\``;
                const helpText = param.help;
                let text;
                if (param.optional) {
                    const extraText = 'optional';
                    text = `${paramText} - ${helpText} *(${extraText})*`;
                } else {
                    text = `${paramText} - ${helpText}`;
                }
                params.push(text);
                invocation += (param.optional ? '[' : '') + param.id + (param.optional ? ']' : '') + ' ';
            });
        }
        const extraText = this.formatCommandChannelFilter(command);
        if (extraText) return `\`\`\`${invocation}\`\`\`\n**(${extraText})**\n${command.help}\n\n${params.join('\n')}`;
        else return `\`\`\`${invocation}\`\`\`\n${command.help}\n\n${params.join('\n')}`;
    }

    formatWikiText(text) {
        return toMarkdown(text, {
            converters: [
                {
                    // Convert various stuff to plain-text
                    filter: ['a', 'small', 'span'],
                    replacement: (innerHTML, node) => node.style.display !== 'none' ? innerHTML : ''
                },
                {
                    // Filter out all unwanted tags
                    filter: node => !node.nodeName.match(/^(b|strong|i|em|s|del|p)$/i),
                    replacement: () => ''
                }
            ]
        })
    }
}

module.exports = GeneralModule;
<commit_msg>Change welcome message<commit_after>'use strict';

const
    MWBot = require('mwbot'),
    config = require('config'),
    toMarkdown = require('to-markdown'),
    BaseModule = require('./base_module');

const wiki = new MWBot({
    apiUrl: 'https://wiki.guildwars2.com/api.php'
});

class GeneralModule extends BaseModule {
    constructor(bot, config, filename) {
        super(bot, config, filename);
        this.name = 'General';
    }

    cmd_help() {
        return {
            id: 'help',
            command: config.get('modules.general.command_help'),
            deliver: config.get('modules.general.deliver_help'),
            cooldown: 'none',
            help: 'Shows information about how to use commands, with optionally a command as argument to get more detailed information.',
            short_help: 'Shows information about how to use commands',
            params: [
                {
                    id: 'id',
                    help: 'The command',
                    optional: true
                }
            ],
            on_command: (message, params) => {
                const modules = this.bot.getModules();

                if (params.length > 0) {
                    // Reply with help for a specific command
                    const commandName = params[0].replace(new RegExp(`^${config.get('discord.command_prefix')}?(.*)$`), '$1');
                    let command;
                    for (module of modules) {
                        const foundCommand = module.commands.find(command => command.command == commandName);
                        if (foundCommand) {
                            command = foundCommand;
                            command.filename = module.filename;
                            break;
                        }
                    }

                    // TODO: Disabled for now as I don't have a way to detect server role assignments in a DM yet... (they are separate)
                    //if (!this.checkPermission(message, `${command.filename}.${command.id}`)) return;

                    if (!command) {
                        return `I do not recognize the command \`${config.get('discord.command_prefix')}${commandName}\`.` +
                            `Type \`${config.get('discord.command_prefix')}${config.get('modules.general.command_help')}\` to see the list of commands.`;
                    }
                    return `\n${this.formatCommandHelp(command)}`;
                } else {
                    // Reply with general help
                    const help = [];
                    modules.forEach(module => {
                        const moduleHelp = this.formatModuleHelp(message, module);
                        if (moduleHelp) {
                            help.push(moduleHelp);
                        }
                    });
                    return `\n${help.join('\n\n')}`;
                }
            }
        };
    }

    cmd_sourceCode() {
        return {
            id: 'source',
            command: config.get('modules.general.command_source'),
            cooldown: 'global',
            help: 'Shows the link to the source code of this bot.',
            short_help: 'Shows the link to the source code of this bot',
            on_command: () => {
                return 'You can find the source code at https://github.com/Archomeda/raiders-inn-bot.';
            }
        };
    }

    cmd_welcome() {
        return {
            id: 'welcome',
            command: config.get('modules.general.command_welcome'),
            deliver: 'mention',
            cooldown: 'user',
            help: 'Welcomes a person to the server and directs him or her to the read first channel.',
            short_help: 'Welcomes a person to the server',
            channel_type: 'text',
            on_command: message => {
                return (
                    `Welcome to The Raiders Inn! Be sure to head over to ${message.guild.channels.get(config.get('modules.general.welcome_channel'))} to get started. ` +
                    "You will find our rules and general information there. Don't forget to assign yourself to a server!\n\nEnjoy your stay!"
                );
            }
        };
    }

    cmd_wiki() {
        return {
            id: 'wiki',
            command: config.get('modules.general.command_wiki'),
            cooldown: 'user',
            help: 'Searches the wiki for an article and returns a summary and the article link if found.',
            short_help: 'Searches the wiki for an article',
            params: [
                {
                    id: 'terms',
                    help: 'Search terms'
                }
            ],
            on_command: (message, params) => {
                if (!params || params.length === 0) {
                    return 'Please provide a wiki article title or search terms.';
                }
                const terms = params[0];

                // Search with nearmatch first
                return wiki.request({
                    action: 'query',
                    list: 'search',
                    srsearch: terms,
                    srwhat: 'nearmatch'
                }).then(response => {
                    if (response && response.query.search.length > 0) {
                        return response;
                    }

                    // No results, search with title
                    return wiki.request({
                        action: 'query',
                        list: 'search',
                        srsearch: terms,
                        srwhat: 'title'
                    });
                }).then(response => {
                    if (response && response.query.search.length > 0) {
                        // Found our article, get it
                        return wiki.request({
                            action: 'parse',
                            page: response.query.search[0].title,
                            redirects: true,
                            prop: 'text'
                        });
                    }
                }).catch(err => {
                    // Make sure we have sane errors
                    if (err.code === 'missingtitle') {
                        throw new Error('not found');
                    } else if (err.info) {
                        throw new Error(err.info);
                    }
                    throw err;
                }).then(response => {
                    if (response && response.parse.text['*']) {
                        // We have our article
                        let text = response.parse.text['*'];
                        const title = response.parse.title;

                        // Construct message
                        text = this.formatWikiText(text).split('\n')[0].trim();
                        const url = encodeURI(`https://wiki.guildwars2.com/wiki/${title}`);
                        if (text) {
                            text += `\n\nMore: ${url}`;
                        } else {
                            text = `${title}: ${url}`;
                        }
                        return text;
                    }
                    throw new Error('not found');
                }).catch(err => {
                    // Capture errors and construct proper fail message
                    switch (err.message) {
                        case 'not found':
                            return 'Your request did not come up with any results. Try using different search terms.';
                        case 'no title':
                            return 'Please provide a wiki article title or search terms.';
                        default:
                            throw err;
                    }
                });
            }
        }
    }

    formatCommandChannelFilter(command) {
        let text = [];
        if (command.channel_type) {
            if (command.channel_type.indexOf('dm') > -1 && command.channel_type.indexOf('text') === -1) {
                text.push('DM only');
            } else if (command.channel_type.indexOf('dm') === -1 && command.channel_type.indexOf('text') > -1) {
                if (command.channels && command.channels.length > 0) {
                    text.push('specific server channels only');
                } else {
                    text.push('server channels only');
                }
            }
        }
        let deliver = command.deliver || ['text'];
        if (!Array.isArray(deliver)) deliver = [deliver];
        if (deliver.indexOf('mention') > -1) {
            text.push('mentionable');
        }
        return text.join(', ');
    }

    formatModuleHelp(message, module) {
        const name = module.name || module.constructor.name.replace(/(.*?)(Module)?/, '$1');
        const commands = [];
        module.commands.forEach(command => {
            // TODO: Disabled for now as I don't have a way to detect server role assignments in a DM yet... (they are separate)
            //if (!this.checkPermission(message, `${module.filename}.${command.id}`)) return;

            const commandText = `\`${config.get('discord.command_prefix')}${command.command}\``;
            const helpText = command.short_help;
            if (!helpText) return;

            let text;
            if (command.channel_type && command.channel_type.length > 0) {
                const extraText = this.formatCommandChannelFilter(command);
                text = `${commandText} - ${helpText} *(${extraText})*`;
            } else {
                text = `${commandText} - ${helpText}`;
            }
            commands.push(text);
        });
        if (commands.length === 0) {
            return;
        }
        return `__**${name}**__\n${commands.join('\n')}`;
    }

    formatCommandHelp(command) {
        let invocation = `${config.get('discord.command_prefix')}${command.command} `;
        const params = [];
        if (command.params) {
            command.params.forEach(param => {
                const paramText = `\`${param.id}\``;
                const helpText = param.help;
                let text;
                if (param.optional) {
                    const extraText = 'optional';
                    text = `${paramText} - ${helpText} *(${extraText})*`;
                } else {
                    text = `${paramText} - ${helpText}`;
                }
                params.push(text);
                invocation += (param.optional ? '[' : '') + param.id + (param.optional ? ']' : '') + ' ';
            });
        }
        const extraText = this.formatCommandChannelFilter(command);
        if (extraText) return `\`\`\`${invocation}\`\`\`\n**(${extraText})**\n${command.help}\n\n${params.join('\n')}`;
        else return `\`\`\`${invocation}\`\`\`\n${command.help}\n\n${params.join('\n')}`;
    }

    formatWikiText(text) {
        return toMarkdown(text, {
            converters: [
                {
                    // Convert various stuff to plain-text
                    filter: ['a', 'small', 'span'],
                    replacement: (innerHTML, node) => node.style.display !== 'none' ? innerHTML : ''
                },
                {
                    // Filter out all unwanted tags
                    filter: node => !node.nodeName.match(/^(b|strong|i|em|s|del|p)$/i),
                    replacement: () => ''
                }
            ]
        })
    }
}

module.exports = GeneralModule;
<|endoftext|>