<commit_before>"use strict";

const fs = require('fs');
const path = require('path');
const util = require('util');

const Module = require('../Core/API/Module');

const Types = require('../queue/types');
const LocalMusicSearch = require('../queue/search');
const Utils = require('../queue/utils');
const QueuedMedia = require('../queue/queued-media');

class MusicPlayer extends Module {
  constructor(container) {
    super(container);

    this.bot = this.container.get('bot');
    this.redisClient = this.container.get('redisBrain');
    this.settings = this.container.get('settings');

    this.currentlyPlaying = null;
    this.voiceConnection = null;
    this.recordCache = new Map();

    this.QueuedMedia = QueuedMedia;

    this.hears(/np/i, this.onNowPlaying.bind(this));
    this.hears(/li/i, this.onDisplayPlaylist.bind(this));
    this.hears(/queue/i, this.queueItems.bind(this));
    this.hears(/next/i, this.skipSong.bind(this));
    this.hears(/yt/i, this.onYoutube.bind(this));
    this.hears(/sel/i, this.onSelectSearchResult.bind(this));

    if (MusicPlayer.useMPD) {
      this.search = new LocalMusicSearch(this.container);
      this.hears(/search/i, this.onSearch.bind(this));
    }

    QueuedMedia.initialize(this.container);
  }

  shutdown() {
    if (MusicPlayer.useMPD) {
      this.search.shutdown();
    }
  }

  getRedisKey(guildId, scope) {
    return `cardinal.${guildId}.${scope}`;
  }

  onNowPlaying(m) {
    if (this.currentlyPlaying === null) {
      return m.reply('No queued song.');
    }
    return m.channel.sendMessage(this.currentlyPlaying.printString());
  }

  onDisplayPlaylist(m) {
    const key = this.getRedisKey(m.guild.id, 'music_queue');
    let msg = '';

    return this.redisClient.llenAsync(key).then(([len]) => {
      if (this.currentlyPlaying) {
        msg += 'Currently Playing:\n';
        msg += this.currentlyPlaying.printString();
        msg += '\n\n';
      }

      msg += 'Playlist:\n';
      if (!len) {
        msg += '- Nothing!\n';
        return m.channel.sendMessage(msg);
      } else {
        return this.redisClient.lrangeAsync(key, 0, len).then(([list]) => {
          let promise = Promise.resolve();

          const entries = list.map((entry) => Utils.formatInfo(entry));
          const msgs = this.printItems(entries);
          msgs.forEach((msg) => {
            promise = promise.then(() => m.channel.sendMessage(msg));
          });

          return promise.catch((err) => {
            this.logger.error('error sending messages', err);
          });
        }).catch((err) => {
          if (err) {
            this.logger.error('error redis lrange', err);
            return m.reply('I could not get the queue playlist from Redis.');
          }
        });
      }
    }).catch((err) => {
      if (err) {
        this.logger.error('error redis llen', err);
        return m.reply('I could not get the queue playlist length from Redis.');
      }
    });
  }

  printItems(entries) {
    const msgs = [];
    let currentMsg = '';

    for (const text of entries) {
      if (currentMsg.length + text.length >= 2000) {
        msgs.push(currentMsg);
        currentMsg = '';
      }
      currentMsg += `${text}\n`;
    }

    if (currentMsg.length > 0) {
      msgs.push(currentMsg);
    }

    return msgs;
  }

  queueItems(m, args) {
    const promises = [];

    for (const url of args) {
      promises.push(this.queueItem(m, url));
    }

    return Promise.all(promises);
  }

  queueItem(m, url) {
    this.logger.debug('QUEUE_ITEM', url);
    if (!url) {
      this.logger.debug('no valid url');
      return m.reply('Please give me a URL to play');
    }

    const guildId = m.guild.id;

    return Utils.fetchYoutubeInfo(url).then((obj) => {
      this.logger.debug('fetchYoutubeInfo promise resolve');

      const promises = [];

      if (obj['_type'] === 'playlist') {
        for (const entry of obj.entries) {
          promises.push(this.queueAddItem(m, entry));
        }
      } else {
        promises.push(this.queueAddItem(m, obj));
      }

      return Promise.all(promises).then((records) => this.queuePostAddItem(m, records));
    }).catch((err) => {
      if (err) {
        this.logger.debug('error pulling youtube data', err.stack);
      }

      return m.reply('Sorry, I was not able to queue that song.');
    });
  }

  queueAddItem(m, obj) {
    const fields = [
      { from: 'title' },
      { from: 'display_id', to: 'id' },
      { from: 'duration' },
      { from: 'acodec', to: 'encoding' },
      { from: 'url' },
    ];
    const info = fields.reduce((info, field) => {
      info[field.to || field.from] = obj[field.from];
      return info;
    }, {});
    // const formats = [obj.formats.find(elem => elem.format_id === obj.format_id)];

    const record = Object.assign({
      type: Types.YTDL,
      ownerId: m.author.id,
      guildId: m.guild.id,
    }, info /* , formats */);

    return this.queueSave(m.guild.id, record);
  }

  queueSave(guildId, record) {
    const redisKey = this.getRedisKey(guildId, 'music_queue');

    return this.redisClient.rpushAsync(redisKey, JSON.stringify(record)).then(() => {
      this.logger.debug('saved to redis');

      return record;
    });
  }

  queuePostAddItem(m, records) {
    console.log(records);

    let promise = Promise.resolve();

    const entries = records.map((entry) => `Added ${Utils.formatInfo(entry)}`);
    const msgs = this.printItems(entries);
    msgs.forEach((msg) => {
      promise = promise.then(() => m.channel.sendMessage(msg));
    });

    return promise.then(() => this.handleQueued(m.guild, m.author, m.channel)).then((res) => {
      this.logger.debug('queueSave promise resolve', res)
    });
  }

  onSearch(m, args) {
    const text = args.join(' ');

    return this.search.byAnyField(text).then((results) =>
      results.slice(0, 5).map((entry) => {
        const filePath = path.join(this.settings.mpd.baseDirectory, entry.file);
        return { id: filePath, title: entry.file };
      })
    ).then((entries) => {
      const redisKey = this.getRedisKey(m.guild.id, `${m.channel.id}.search`);
      this.redisClient.set(redisKey, JSON.stringify(entries));

      const response = entries.map((item, i) => `**${i + 1}:** ${item.title}`).join('\n');
      return m.channel.sendMessage(response);
    });
  }

  onYoutube(m, args) {
    const text = args.join(' ');

    return Utils.searchYoutube(text).then((results) => {
      const useful = results.items.map((item) => {
        return { id: item.id.videoId, title: item.snippet.title };
      });

      const redisKey = this.getRedisKey(m.guild.id, `${m.channel.id}.search`);
      this.redisClient.set(redisKey, JSON.stringify(useful));

      const response = useful.map((item, i) => `**${i + 1}:** ${item.title}`).join('\n');
      return m.channel.sendMessage(response);
    });
  }

  onSelectSearchResult(m, args) {
    if (args < 1) {
      return m.reply('Please supply an index to play from search results');
    }

    const index = parseInt(args[0]);

    if (isNaN(index) || index < 1) {
      return m.reply('Please supply a valid index');
    }

    const position = index - 1;
    const redisKey = this.getRedisKey(m.guild.id, `${m.channel.id}.search`);

    return this.redisClient.getAsync(redisKey).then(([ result ]) => {
      if (!result) {
        return m.reply('There is no previous search query');
      }
      const entries = JSON.parse(result);

      return this.queueItem(m, [entries[position].id]);
    });
  }

  skipSong(m) {
    if (m.author.id !== '142098955818369024') {
      return m.reply('You are not authorized to perform this action.');
    }

    const key = this.getRedisKey(m.guild.id, 'music_queue');

    return this.redisClient.llenAsync(key).then(([len]) => {
      let author = m.author;

      if (len === 0) {
        if (!this.currentlyPlaying) {
          return m.reply('No currently playing song');
        }
        // this.currentlyPlaying can be assumed to be true since the previous test
        // failed
        else if (!this.currentlyPlaying.stream) {
          return m.reply('For some reason this song does not have a stream associated with it');
        }

        author = null;
      }

      if (this.currentlyPlaying) {
        this.currentlyPlaying.stopPlaying();
        this.currentlyPlaying = null;
      }

      return this.handleQueued(m.guild, author);
    });
  }

  playNext(guild, author, channel) {
    this.logger.debug('playNext');

    const authorVoiceChannel = (guild && author) ? author.getVoiceChannel(guild) : null;
    const redisKey = this.getRedisKey(guild.id, 'music_queue');

    return this.redisClient.lpopAsync(redisKey).then(JSON.parse).then((record) => {
      const next = new QueuedMedia(this, record || {});
      this.currentlyPlaying = next;

      if (!authorVoiceChannel && channel) {
        channel.sendMessage(`${author.mention}, Can you please join a voice channel I can play to?`);

        this.queuedDonePlaying(this.currentlyPlaying);

        return false;
      } else if (this.voiceConnection && !this.voiceConnection.disposed && this.voiceConnection.canStream) {
        this.currentlyPlaying.play(this.voiceConnection);
      } else {
        return authorVoiceChannel.join(false, false).then((info) => {
          this.logger.debug(`joined voice chat: ${info.voiceSocket.voiceServerURL}@${info.voiceSocket.mode}`);

          this.voiceConnection = info.voiceConnection;

          this.currentlyPlaying.play(this.voiceConnection);

          return true;
        }).catch((err) => {
          this.logger.debug('failed to join voice chat', err);

          if (err.message === 'Missing permission' && authorVoiceChannel) {
            channel.sendMessage(`${author.mention}, I do not have permission to join the '${authorVoiceChannel.name}' voice channel`);
          }

          return this.queuedDonePlaying(this.currentlyPlaying);
        });
      }

      return true;
    }).catch((err) => {
      if (err) {
        this.logger.debug('error getting next item from redis', err);
      }
      throw err;
    });
  }

  handleQueued(guild, author, channel) {
    this.logger.debug('handleQueued');

    if (!guild) {
      this.logger.debug('no guild provided');
      return;
    }

    const redisKey = this.getRedisKey(guild.id, 'music_queue');

    return this.redisClient.llenAsync(redisKey).then(([len]) => {
      this.logger.debug(`redis ${redisKey}`, len);

      if (this.currentlyPlaying === null && len > 0) {
        return this.playNext(guild, author, channel);
      } else if (len === 0 && this.voiceConnection && !this.voiceConnection.disposed) {
        this.logger.debug('handleQueued disconnect');

        this.voiceConnection.disconnect();
        this.voiceConnection = null;
      }
    });
  }

  queuedDonePlaying(queued) {
    if (queued === this.currentlyPlaying) {
      const guild = this.bot.client.Guilds.get(queued.guildId);

      this.currentlyPlaying = null;
      this.handleQueued(guild);
    }
  }
}

module.exports = MusicPlayer;
<commit_msg>MusicPlayer: add todo for object spread<commit_after>"use strict";

const fs = require('fs');
const path = require('path');
const util = require('util');

const Module = require('../Core/API/Module');

const Types = require('../queue/types');
const LocalMusicSearch = require('../queue/search');
const Utils = require('../queue/utils');
const QueuedMedia = require('../queue/queued-media');

class MusicPlayer extends Module {
  constructor(container) {
    super(container);

    this.bot = this.container.get('bot');
    this.redisClient = this.container.get('redisBrain');
    this.settings = this.container.get('settings');

    this.currentlyPlaying = null;
    this.voiceConnection = null;
    this.recordCache = new Map();

    this.QueuedMedia = QueuedMedia;

    this.hears(/np/i, this.onNowPlaying.bind(this));
    this.hears(/li/i, this.onDisplayPlaylist.bind(this));
    this.hears(/queue/i, this.queueItems.bind(this));
    this.hears(/next/i, this.skipSong.bind(this));
    this.hears(/yt/i, this.onYoutube.bind(this));
    this.hears(/sel/i, this.onSelectSearchResult.bind(this));

    if (MusicPlayer.useMPD) {
      this.search = new LocalMusicSearch(this.container);
      this.hears(/search/i, this.onSearch.bind(this));
    }

    QueuedMedia.initialize(this.container);
  }

  shutdown() {
    if (MusicPlayer.useMPD) {
      this.search.shutdown();
    }
  }

  getRedisKey(guildId, scope) {
    return `cardinal.${guildId}.${scope}`;
  }

  onNowPlaying(m) {
    if (this.currentlyPlaying === null) {
      return m.reply('No queued song.');
    }
    return m.channel.sendMessage(this.currentlyPlaying.printString());
  }

  onDisplayPlaylist(m) {
    const key = this.getRedisKey(m.guild.id, 'music_queue');
    let msg = '';

    return this.redisClient.llenAsync(key).then(([len]) => {
      if (this.currentlyPlaying) {
        msg += 'Currently Playing:\n';
        msg += this.currentlyPlaying.printString();
        msg += '\n\n';
      }

      msg += 'Playlist:\n';
      if (!len) {
        msg += '- Nothing!\n';
        return m.channel.sendMessage(msg);
      } else {
        return this.redisClient.lrangeAsync(key, 0, len).then(([list]) => {
          let promise = Promise.resolve();

          const entries = list.map((entry) => Utils.formatInfo(entry));
          const msgs = this.printItems(entries);
          msgs.forEach((msg) => {
            promise = promise.then(() => m.channel.sendMessage(msg));
          });

          return promise.catch((err) => {
            this.logger.error('error sending messages', err);
          });
        }).catch((err) => {
          if (err) {
            this.logger.error('error redis lrange', err);
            return m.reply('I could not get the queue playlist from Redis.');
          }
        });
      }
    }).catch((err) => {
      if (err) {
        this.logger.error('error redis llen', err);
        return m.reply('I could not get the queue playlist length from Redis.');
      }
    });
  }

  printItems(entries) {
    const msgs = [];
    let currentMsg = '';

    for (const text of entries) {
      if (currentMsg.length + text.length >= 2000) {
        msgs.push(currentMsg);
        currentMsg = '';
      }
      currentMsg += `${text}\n`;
    }

    if (currentMsg.length > 0) {
      msgs.push(currentMsg);
    }

    return msgs;
  }

  queueItems(m, args) {
    const promises = [];

    for (const url of args) {
      promises.push(this.queueItem(m, url));
    }

    return Promise.all(promises);
  }

  queueItem(m, url) {
    this.logger.debug('QUEUE_ITEM', url);
    if (!url) {
      this.logger.debug('no valid url');
      return m.reply('Please give me a URL to play');
    }

    const guildId = m.guild.id;

    return Utils.fetchYoutubeInfo(url).then((obj) => {
      this.logger.debug('fetchYoutubeInfo promise resolve');

      const promises = [];

      if (obj['_type'] === 'playlist') {
        for (const entry of obj.entries) {
          promises.push(this.queueAddItem(m, entry));
        }
      } else {
        promises.push(this.queueAddItem(m, obj));
      }

      return Promise.all(promises).then((records) => this.queuePostAddItem(m, records));
    }).catch((err) => {
      if (err) {
        this.logger.debug('error pulling youtube data', err.stack);
      }

      return m.reply('Sorry, I was not able to queue that song.');
    });
  }

  queueAddItem(m, obj) {
    const fields = [
      { from: 'title' },
      { from: 'display_id', to: 'id' },
      { from: 'duration' },
      { from: 'acodec', to: 'encoding' },
      { from: 'url' },
    ];
    const info = fields.reduce((info, field) => {
      info[field.to || field.from] = obj[field.from];
      return info;
    }, {});
    // const formats = [obj.formats.find(elem => elem.format_id === obj.format_id)];

    // TODO: Use "Object Spread" when it is implemented
    const record = Object.assign({
      type: Types.YTDL,
      ownerId: m.author.id,
      guildId: m.guild.id,
    }, info /* , formats */);

    return this.queueSave(m.guild.id, record);
  }

  queueSave(guildId, record) {
    const redisKey = this.getRedisKey(guildId, 'music_queue');

    return this.redisClient.rpushAsync(redisKey, JSON.stringify(record)).then(() => {
      this.logger.debug('saved to redis');

      return record;
    });
  }

  queuePostAddItem(m, records) {
    console.log(records);

    let promise = Promise.resolve();

    const entries = records.map((entry) => `Added ${Utils.formatInfo(entry)}`);
    const msgs = this.printItems(entries);
    msgs.forEach((msg) => {
      promise = promise.then(() => m.channel.sendMessage(msg));
    });

    return promise.then(() => this.handleQueued(m.guild, m.author, m.channel)).then((res) => {
      this.logger.debug('queueSave promise resolve', res)
    });
  }

  onSearch(m, args) {
    const text = args.join(' ');

    return this.search.byAnyField(text).then((results) =>
      results.slice(0, 5).map((entry) => {
        const filePath = path.join(this.settings.mpd.baseDirectory, entry.file);
        return { id: filePath, title: entry.file };
      })
    ).then((entries) => {
      const redisKey = this.getRedisKey(m.guild.id, `${m.channel.id}.search`);
      this.redisClient.set(redisKey, JSON.stringify(entries));

      const response = entries.map((item, i) => `**${i + 1}:** ${item.title}`).join('\n');
      return m.channel.sendMessage(response);
    });
  }

  onYoutube(m, args) {
    const text = args.join(' ');

    return Utils.searchYoutube(text).then((results) => {
      const useful = results.items.map((item) => {
        return { id: item.id.videoId, title: item.snippet.title };
      });

      const redisKey = this.getRedisKey(m.guild.id, `${m.channel.id}.search`);
      this.redisClient.set(redisKey, JSON.stringify(useful));

      const response = useful.map((item, i) => `**${i + 1}:** ${item.title}`).join('\n');
      return m.channel.sendMessage(response);
    });
  }

  onSelectSearchResult(m, args) {
    if (args < 1) {
      return m.reply('Please supply an index to play from search results');
    }

    const index = parseInt(args[0]);

    if (isNaN(index) || index < 1) {
      return m.reply('Please supply a valid index');
    }

    const position = index - 1;
    const redisKey = this.getRedisKey(m.guild.id, `${m.channel.id}.search`);

    return this.redisClient.getAsync(redisKey).then(([ result ]) => {
      if (!result) {
        return m.reply('There is no previous search query');
      }
      const entries = JSON.parse(result);

      return this.queueItem(m, [entries[position].id]);
    });
  }

  skipSong(m) {
    if (m.author.id !== '142098955818369024') {
      return m.reply('You are not authorized to perform this action.');
    }

    const key = this.getRedisKey(m.guild.id, 'music_queue');

    return this.redisClient.llenAsync(key).then(([len]) => {
      let author = m.author;

      if (len === 0) {
        if (!this.currentlyPlaying) {
          return m.reply('No currently playing song');
        }
        // this.currentlyPlaying can be assumed to be true since the previous test
        // failed
        else if (!this.currentlyPlaying.stream) {
          return m.reply('For some reason this song does not have a stream associated with it');
        }

        author = null;
      }

      if (this.currentlyPlaying) {
        this.currentlyPlaying.stopPlaying();
        this.currentlyPlaying = null;
      }

      return this.handleQueued(m.guild, author);
    });
  }

  playNext(guild, author, channel) {
    this.logger.debug('playNext');

    const authorVoiceChannel = (guild && author) ? author.getVoiceChannel(guild) : null;
    const redisKey = this.getRedisKey(guild.id, 'music_queue');

    return this.redisClient.lpopAsync(redisKey).then(JSON.parse).then((record) => {
      const next = new QueuedMedia(this, record || {});
      this.currentlyPlaying = next;

      if (!authorVoiceChannel && channel) {
        channel.sendMessage(`${author.mention}, Can you please join a voice channel I can play to?`);

        this.queuedDonePlaying(this.currentlyPlaying);

        return false;
      } else if (this.voiceConnection && !this.voiceConnection.disposed && this.voiceConnection.canStream) {
        this.currentlyPlaying.play(this.voiceConnection);
      } else {
        return authorVoiceChannel.join(false, false).then((info) => {
          this.logger.debug(`joined voice chat: ${info.voiceSocket.voiceServerURL}@${info.voiceSocket.mode}`);

          this.voiceConnection = info.voiceConnection;

          this.currentlyPlaying.play(this.voiceConnection);

          return true;
        }).catch((err) => {
          this.logger.debug('failed to join voice chat', err);

          if (err.message === 'Missing permission' && authorVoiceChannel) {
            channel.sendMessage(`${author.mention}, I do not have permission to join the '${authorVoiceChannel.name}' voice channel`);
          }

          return this.queuedDonePlaying(this.currentlyPlaying);
        });
      }

      return true;
    }).catch((err) => {
      if (err) {
        this.logger.debug('error getting next item from redis', err);
      }
      throw err;
    });
  }

  handleQueued(guild, author, channel) {
    this.logger.debug('handleQueued');

    if (!guild) {
      this.logger.debug('no guild provided');
      return;
    }

    const redisKey = this.getRedisKey(guild.id, 'music_queue');

    return this.redisClient.llenAsync(redisKey).then(([len]) => {
      this.logger.debug(`redis ${redisKey}`, len);

      if (this.currentlyPlaying === null && len > 0) {
        return this.playNext(guild, author, channel);
      } else if (len === 0 && this.voiceConnection && !this.voiceConnection.disposed) {
        this.logger.debug('handleQueued disconnect');

        this.voiceConnection.disconnect();
        this.voiceConnection = null;
      }
    });
  }

  queuedDonePlaying(queued) {
    if (queued === this.currentlyPlaying) {
      const guild = this.bot.client.Guilds.get(queued.guildId);

      this.currentlyPlaying = null;
      this.handleQueued(guild);
    }
  }
}

module.exports = MusicPlayer;
<|endoftext|>


<commit_before>'use strict';
const errors = require('feathers-errors');
const mongoose = require('mongoose');
const logger = require('winston');
const _ = require('lodash');
const KeysModel = require('../services/keys/model');
// Add any common hooks you want to share across services in here.

// don't require authentication for internal requests
exports.ifNotLocal = function (hookForRemoteRequests) {
	return function (hook) {
		if (typeof(hook.params.provider) != 'undefined') {	// meaning it's not a local call
			// Call the specified hook
			return hookForRemoteRequests.call(this, hook);
		}
	};
};

exports.isAdmin = function (options) {
	return hook => {
		if (!(hook.params.user.permissions || []).includes('ADMIN')) {
			throw new errors.Forbidden('you are not an administrator');
		}

		return Promise.resolve(hook);
	};
};

exports.isSuperHero = function (options) {
	return hook => {
		const userService = hook.app.service('/users/');
		return userService.find({query: {_id: (hook.params.account.userId || ""), $populate: 'roles'}})
			.then(user => {
				user.data[0].roles = Array.from(user.data[0].roles);
				if (!(user.data[0].roles.filter(u => (u.name === 'superhero')).length > 0)) {
					throw new errors.Forbidden('you are not a superhero, sorry...');
				}
				return Promise.resolve(hook);
			});
	};
};

exports.hasPermission = function (permissionName) {
	return hook => {
		// If it was an internal call then skip this hook
		if (!hook.params.provider) {
			return hook;
		}

		// If an api key was provided, skip
		if ((hook.params.headers || {})["x-api-key"]) {
			return KeysModel.findOne({key: hook.params.headers["x-api-key"]})
				.then(res => {
					if (!res)
						throw new errors.NotAuthenticated('API Key is invalid');
					return Promise.resolve(hook);
				})
				.catch(err => {
					throw new errors.NotAuthenticated('API Key is invalid.');
				});
		}
		// If test then skip too
		if (process.env.NODE_ENV === 'test')
			return Promise.resolve(hook);

		// Otherwise check for user permissions
		const service = hook.app.service('/users/');
		return service.get({_id: (hook.params.account.userId || "")})
			.then(user => {
				user.permissions = Array.from(user.permissions);

				if (!(user.permissions || []).includes(permissionName)) {
					throw new errors.Forbidden(`You don't have the permission ${permissionName}.`);
				}
				return Promise.resolve(hook);
			});
	};
};

// non hook releated function
exports.hasPermissionNoHook = function (hook, userId, permissionName) {
	const service = hook.app.service('/users/');
	return service.get({_id: (userId || "")})
		.then(user => {
			user.permissions = Array.from(user.permissions);
			return (user.permissions || []).includes(permissionName);
		});
};

exports.hasRoleNoHook = function (hook, userId, roleName, account = false) {
	const userService = hook.app.service('/users/');
	const accountService = hook.app.service('/accounts/');
	if (account) {
		return accountService.get(userId)
			.then(account => {
				return userService.find({query: {_id: (account.userId || ""), $populate: 'roles'}})
					.then(user => {
						user.data[0].roles = Array.from(user.data[0].roles);

						return (user.data[0].roles.filter(u => (u.name === roleName)).length > 0);
					});
			});
	} else {
		return userService.find({query: {_id: (userId || ""), $populate: 'roles'}})
			.then(user => {
				user.data[0].roles = Array.from(user.data[0].roles);

				return (user.data[0].roles.filter(u => (u.name === roleName)).length > 0);
			});
	}
};

// resolves IDs of objects from serviceName specified by *key* instead of their *_id*
exports.resolveToIds = (serviceName, path, key, hook) => {
	// get ids from a probably really deep nested path
	const service = hook.app.service(serviceName);

	let values = deepValue(hook, path) || [];
	if (typeof values == 'string') values = [values];

	if (!values.length) return;

	let resolved = values.map(value => {
		if (!mongoose.Types.ObjectId.isValid(value)) {
			return _resolveToId(service, key, value);
		} else {
			return Promise.resolve(value);
		}
	});

	return Promise.all(resolved)
		.then(values => {
			deepValue(hook, path, values);
		});
};

exports.permitGroupOperation = (hook) => {
	if (!hook.id) {
		throw new errors.Forbidden('Operation on this service requires an id!');
	}
	return Promise.resolve(hook);
};

const _resolveToId = (service, key, value) => {
	let query = {};
	query[key] = value;
	return service.find({query})
		.then(results => {
			const result = results.data[0];
			if (!result) throw new TypeError(`No records found where ${key} is ${value}.`);
			return result._id;
		});
};

const deepValue = (obj, path, newValue) => {
	path = path.split('.');
	const len = path.length - 1;

	let i;
	for (i = 0; i < len; i++) {
		obj = obj[path[i]];
	}

	if (newValue) obj[path[i]] = newValue;
	return obj[path[i]];
};

exports.computeProperty = function (Model, functionName, variableName) {
	return (hook) => {
		return Model.findById(hook.result._id)	// get the model instance to call functions etc  TODO make query results not lean
			.then(modelInstance => modelInstance[functionName]())	// compute that property
			.then(result => {
				hook.result[variableName] = Array.from(result);		// save it in the resulting object
			})
			.catch(e => logger.error(e))
			.then(_ => Promise.resolve(hook));

	};
};

exports.mapPaginationQuery = (hook) => {
	if ((hook.params.query || {}).$limit === '-1') {
		hook.params.paginate = false;
		delete hook.params.query.$limit;
		return Promise.resolve(hook);
	}
};

exports.restrictToCurrentSchool = hook => {
	let userService = hook.app.service("users");
	return userService.find({
		query: {
			_id: hook.params.account.userId,
			$populate: 'roles'
		}
	}).then(res => {
		let access = false;
		res.data[0].roles.map(role => {
			if (role.name === 'superhero')
				access = true;
		});
		if (access)
			return hook;
		hook.params.query.schoolId = res.data[0].schoolId;
		return hook;
	});
};

//TODO: hooks $or condition gets overwritten if set, check first
exports.restrictToUsersOwnCourses = hook => {
	let userService = hook.app.service('users');
	return userService.find({
		query: {
			_id: hook.params.account.userId,
			$populate: 'roles'
		}
	}).then(res => {
		let access = false;
		res.data[0].roles.map(role => {
			if (role.name === 'admin' || role.name === 'superhero' )
				access = true;
		});
		if (access)
			return hook;

		if (hook.method ==="get") {
			let courseService = hook.app.service('courses');
			return courseService.get(hook.id).then(course => {
				if (!(_.some(course.userIds, u => JSON.stringify(u) === JSON.stringify(hook.params.account.userId))) &&
					!(_.some(course.teacherIds, u => JSON.stringify(u) === JSON.stringify(hook.params.account.userId))) &&
					!(_.some(course.substitutionIds, u => JSON.stringify(u) === JSON.stringify(hook.params.account.userId)))) {
					throw new errors.Forbidden('You are not in that course.');
				}
			});
		} /*else {
			hook.params.query.$or = [
				{ userIds: res.data[0]._id },
				{ teacherIds: res.data[0]._id },
				{ substitutionIds: res.data[0]._id }
			];
		}*/
		return hook;
	});
};

// meant to be used as an after hook
exports.denyIfNotCurrentSchool = ({errorMessage = 'Die angefragte Ressource gehört nicht zur eigenen Schule!'}) =>
	hook => {
		let userService = hook.app.service("users");
		return userService.find({
			query: {
				_id: hook.params.account.userId,
				$populate: 'roles'
			}
		}).then(res => {
			let access = false;
			res.data[0].roles.map(role => {
				if (role.name === 'superhero')
					access = true;
			});
			if (access)
				return hook;
			let requesterSchoolId = res.data[0].schoolId;
			let requestedUserSchoolId = (hook.result || {}).schoolId;
			if (!requesterSchoolId.equals(requestedUserSchoolId)) {
				return Promise.reject(new errors.Forbidden(errorMessage));
			}
			return hook;
		});
	};

exports.checkSchoolOwnership = hook => {
	let userId = hook.params.account.userId;
	let objectId = hook.id;
	let service = hook.path;

	let genericService = hook.app.service(service);
	let userService = hook.app.service('users');

	return Promise.all([userService.get(userId), genericService.get(objectId)])
		.then(res => {
			if (res[0].schoolId.equals(res[1].schoolId))
				return hook;
			else
				throw new errors.Forbidden('You do not have valid permissions to access this.');
		});
};
<commit_msg>server-side course filter to show only user courses<commit_after>'use strict';
const errors = require('feathers-errors');
const mongoose = require('mongoose');
const logger = require('winston');
const _ = require('lodash');
const KeysModel = require('../services/keys/model');
// Add any common hooks you want to share across services in here.

// don't require authentication for internal requests
exports.ifNotLocal = function (hookForRemoteRequests) {
	return function (hook) {
		if (typeof(hook.params.provider) != 'undefined') {	// meaning it's not a local call
			// Call the specified hook
			return hookForRemoteRequests.call(this, hook);
		}
	};
};

exports.isAdmin = function (options) {
	return hook => {
		if (!(hook.params.user.permissions || []).includes('ADMIN')) {
			throw new errors.Forbidden('you are not an administrator');
		}

		return Promise.resolve(hook);
	};
};

exports.isSuperHero = function (options) {
	return hook => {
		const userService = hook.app.service('/users/');
		return userService.find({query: {_id: (hook.params.account.userId || ""), $populate: 'roles'}})
			.then(user => {
				user.data[0].roles = Array.from(user.data[0].roles);
				if (!(user.data[0].roles.filter(u => (u.name === 'superhero')).length > 0)) {
					throw new errors.Forbidden('you are not a superhero, sorry...');
				}
				return Promise.resolve(hook);
			});
	};
};

exports.hasPermission = function (permissionName) {
	return hook => {
		// If it was an internal call then skip this hook
		if (!hook.params.provider) {
			return hook;
		}

		// If an api key was provided, skip
		if ((hook.params.headers || {})["x-api-key"]) {
			return KeysModel.findOne({key: hook.params.headers["x-api-key"]})
				.then(res => {
					if (!res)
						throw new errors.NotAuthenticated('API Key is invalid');
					return Promise.resolve(hook);
				})
				.catch(err => {
					throw new errors.NotAuthenticated('API Key is invalid.');
				});
		}
		// If test then skip too
		if (process.env.NODE_ENV === 'test')
			return Promise.resolve(hook);

		// Otherwise check for user permissions
		const service = hook.app.service('/users/');
		return service.get({_id: (hook.params.account.userId || "")})
			.then(user => {
				user.permissions = Array.from(user.permissions);

				if (!(user.permissions || []).includes(permissionName)) {
					throw new errors.Forbidden(`You don't have the permission ${permissionName}.`);
				}
				return Promise.resolve(hook);
			});
	};
};

// non hook releated function
exports.hasPermissionNoHook = function (hook, userId, permissionName) {
	const service = hook.app.service('/users/');
	return service.get({_id: (userId || "")})
		.then(user => {
			user.permissions = Array.from(user.permissions);
			return (user.permissions || []).includes(permissionName);
		});
};

exports.hasRoleNoHook = function (hook, userId, roleName, account = false) {
	const userService = hook.app.service('/users/');
	const accountService = hook.app.service('/accounts/');
	if (account) {
		return accountService.get(userId)
			.then(account => {
				return userService.find({query: {_id: (account.userId || ""), $populate: 'roles'}})
					.then(user => {
						user.data[0].roles = Array.from(user.data[0].roles);

						return (user.data[0].roles.filter(u => (u.name === roleName)).length > 0);
					});
			});
	} else {
		return userService.find({query: {_id: (userId || ""), $populate: 'roles'}})
			.then(user => {
				user.data[0].roles = Array.from(user.data[0].roles);

				return (user.data[0].roles.filter(u => (u.name === roleName)).length > 0);
			});
	}
};

// resolves IDs of objects from serviceName specified by *key* instead of their *_id*
exports.resolveToIds = (serviceName, path, key, hook) => {
	// get ids from a probably really deep nested path
	const service = hook.app.service(serviceName);

	let values = deepValue(hook, path) || [];
	if (typeof values == 'string') values = [values];

	if (!values.length) return;

	let resolved = values.map(value => {
		if (!mongoose.Types.ObjectId.isValid(value)) {
			return _resolveToId(service, key, value);
		} else {
			return Promise.resolve(value);
		}
	});

	return Promise.all(resolved)
		.then(values => {
			deepValue(hook, path, values);
		});
};

exports.permitGroupOperation = (hook) => {
	if (!hook.id) {
		throw new errors.Forbidden('Operation on this service requires an id!');
	}
	return Promise.resolve(hook);
};

const _resolveToId = (service, key, value) => {
	let query = {};
	query[key] = value;
	return service.find({query})
		.then(results => {
			const result = results.data[0];
			if (!result) throw new TypeError(`No records found where ${key} is ${value}.`);
			return result._id;
		});
};

const deepValue = (obj, path, newValue) => {
	path = path.split('.');
	const len = path.length - 1;

	let i;
	for (i = 0; i < len; i++) {
		obj = obj[path[i]];
	}

	if (newValue) obj[path[i]] = newValue;
	return obj[path[i]];
};

exports.computeProperty = function (Model, functionName, variableName) {
	return (hook) => {
		return Model.findById(hook.result._id)	// get the model instance to call functions etc  TODO make query results not lean
			.then(modelInstance => modelInstance[functionName]())	// compute that property
			.then(result => {
				hook.result[variableName] = Array.from(result);		// save it in the resulting object
			})
			.catch(e => logger.error(e))
			.then(_ => Promise.resolve(hook));

	};
};

exports.mapPaginationQuery = (hook) => {
	if ((hook.params.query || {}).$limit === '-1') {
		hook.params.paginate = false;
		delete hook.params.query.$limit;
		return Promise.resolve(hook);
	}
};

exports.restrictToCurrentSchool = hook => {
	let userService = hook.app.service("users");
	return userService.find({
		query: {
			_id: hook.params.account.userId,
			$populate: 'roles'
		}
	}).then(res => {
		let access = false;
		res.data[0].roles.map(role => {
			if (role.name === 'superhero')
				access = true;
		});
		if (access)
			return hook;
		hook.params.query.schoolId = res.data[0].schoolId;
		return hook;
	});
};

//TODO: hooks $or condition gets overwritten if set, check first
exports.restrictToUsersOwnCourses = hook => {
	let userService = hook.app.service('users');
	return userService.find({
		query: {
			_id: hook.params.account.userId,
			$populate: 'roles'
		}
	}).then(res => {
		let access = false;
		res.data[0].roles.map(role => {
			if (role.name === 'admin' || role.name === 'superhero' )
				access = true;
		});
		if (access)
			return hook;

		if (hook.method === "get") {
			let courseService = hook.app.service('courses');
			return courseService.get(hook.id).then(course => {
				if (!(_.some(course.userIds, u => JSON.stringify(u) === JSON.stringify(hook.params.account.userId))) &&
					!(_.some(course.teacherIds, u => JSON.stringify(u) === JSON.stringify(hook.params.account.userId))) &&
					!(_.some(course.substitutionIds, u => JSON.stringify(u) === JSON.stringify(hook.params.account.userId)))) {
					throw new errors.Forbidden('You are not in that course.');
				}
			});
		} else {
			if (typeof(hook.params.query.$or) === 'undefined') {
				hook.params.query.$or = [
					{ userIds: res.data[0]._id },
					{ teacherIds: res.data[0]._id },
					{ substitutionIds: res.data[0]._id }
				];
			}
		}
		return hook;
	});
};

// meant to be used as an after hook
exports.denyIfNotCurrentSchool = ({errorMessage = 'Die angefragte Ressource gehört nicht zur eigenen Schule!'}) =>
	hook => {
		let userService = hook.app.service("users");
		return userService.find({
			query: {
				_id: hook.params.account.userId,
				$populate: 'roles'
			}
		}).then(res => {
			let access = false;
			res.data[0].roles.map(role => {
				if (role.name === 'superhero')
					access = true;
			});
			if (access)
				return hook;
			let requesterSchoolId = res.data[0].schoolId;
			let requestedUserSchoolId = (hook.result || {}).schoolId;
			if (!requesterSchoolId.equals(requestedUserSchoolId)) {
				return Promise.reject(new errors.Forbidden(errorMessage));
			}
			return hook;
		});
	};

exports.checkSchoolOwnership = hook => {
	let userId = hook.params.account.userId;
	let objectId = hook.id;
	let service = hook.path;

	let genericService = hook.app.service(service);
	let userService = hook.app.service('users');

	return Promise.all([userService.get(userId), genericService.get(objectId)])
		.then(res => {
			if (res[0].schoolId.equals(res[1].schoolId))
				return hook;
			else
				throw new errors.Forbidden('You do not have valid permissions to access this.');
		});
};
<|endoftext|>


<commit_before>/**
 * 同步遍历指定路径目录
 * @param {String} dir 目录地址
 * @param {Function} callback 回调
 */
function traverseSync(dir, callback) {
  const fs = require('fs');
  const path = require('path');
  fs.readdirSync(dir).forEach((file) => {
    const pathname = path.join(dir, file);

    if (fs.statSync(pathname).isDirectory()) {
      traverse(pathname, callback);
    } else {
      callback && callback(pathname);
    }
  });
}

/**
 * 异步遍历指定路径目录
 * @param {Stirng} dir 目录地址
 * @param {Function} callback 回调
 * @param {Function} finish 遍历结束回调
 * @param {Boolean} isDeepTraverse 是否遍历子文件夹
 */
function traverse(dir, callback, finish, isDeepTraverse = true) {
  const fs = require('fs');
  const path = require('path');

  fs.readdir(dir, (err, files) => {
    if (err) {
      throw err;
    }
    (function next(i) {
      if (i < files.length) {
        const pathname = path.join(dir, files[i]);

        fs.stat(pathname, function(err, stats) {
          if (stats.isDirectory() && isDeepTraverse) {
            traverse(pathname, callback, function() {
              next(i + 1);
            });
          } else {
            callback && callback(pathname);

            next(i + 1);
          }
        });
      } else {
        finish && finish();
      }
    })(0);
  });
}

/**
 * 发送邮件（需收件方开启smtp）
 * @param {Object} options receiver-收件人 title-标题 text-文本格式内容 html-html格式内容
 */
function sendEmail(options) {
  const mail = require('nodemailer');

  const smtpTransport = mail.createTransport({
    host: 'smtp.qq.com',
    secureConnection: true,
    port: 587, //腾讯端口为465或587
    auth: {
      user: 'tenfyma@foxmail.com',
      pass: 'otiiohqmkqnfbeii'
    }
  });

  //设置邮箱内容
  const mailOptions = {
    from: options.from || '马腾飞 <tenfyma@foxmail.com>',
    to: options.to, //收件人，多个收件人用逗号隔开
    subject: options.title, //标题
    text: options.text, //文本格式内容
    html: options.html //html格式内容
    /*attachments: [{ //上传附件
      filename: 'json',
      path: './add.html'
    }]*/
  };

  smtpTransport.sendMail(mailOptions, (error, response) => {
    let status = '';
    if (error) {
      console.log(error);
      status = error;
    } else {
      console.log('success' + response.response);
      status = '200';
    }
    smtpTransport.close();
    return status;
  });
}

/**
 * 获取变量类型
 * @param {*} variable 变量
 * @return {String} 变量类型
 */
function typeOf(variable) {
  return Object.prototype.toString
    .call(variable)
    .match(/(?<=\[object\s).+(?=\])/gi)[0]
    .toLowerCase();
}

/**
 * 将字符串进行正则转义，使之可以构造正则对象
 * @param {String} string 待转义字符串
 * @return {String} 转义后字符串
 */
function escapeRegExp(string) {
  // $&表示整个被匹配的字符串
  return string.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$&');
}

/**
 * 加法（纯函数）
 * @param  {number} args 数字
 * @return {Object} 如果含参数，返回该方法，否则返回计算结果
 */

function add(...args) {
  const fn = function(...arg_fn) {
    if (arg_fn.length === 0) {
      return args.reduce((a, b) => a + b);
    }
    return add.apply(null, args.concat(arg_fn));
  };
  fn.valueOf = function() {
    return args.reduce((a, b) => a + b);
  };
  fn.toString = function() {
    return args.reduce((a, b) => a + b);
  };
  return fn;
}

exports = module.exports = {
  traverse,
  traverseSync,
  sendEmail,
  typeOf,
  escapeRegExp,
  add,
};
<commit_msg>增加一个生成随机数的方法<commit_after>/**
 * 同步遍历指定路径目录
 * @param {String} dir 目录地址
 * @param {Function} callback 回调
 */
function traverseSync(dir, callback) {
  const fs = require('fs');
  const path = require('path');
  fs.readdirSync(dir).forEach((file) => {
    const pathname = path.join(dir, file);

    if (fs.statSync(pathname).isDirectory()) {
      traverse(pathname, callback);
    } else {
      callback && callback(pathname);
    }
  });
}

/**
 * 异步遍历指定路径目录
 * @param {Stirng} dir 目录地址
 * @param {Function} callback 回调
 * @param {Function} finish 遍历结束回调
 * @param {Boolean} isDeepTraverse 是否遍历子文件夹
 */
function traverse(dir, callback, finish, isDeepTraverse = true) {
  const fs = require('fs');
  const path = require('path');

  fs.readdir(dir, (err, files) => {
    if (err) {
      throw err;
    }
    (function next(i) {
      if (i < files.length) {
        const pathname = path.join(dir, files[i]);

        fs.stat(pathname, function(err, stats) {
          if (stats.isDirectory() && isDeepTraverse) {
            traverse(pathname, callback, function() {
              next(i + 1);
            });
          } else {
            callback && callback(pathname);

            next(i + 1);
          }
        });
      } else {
        finish && finish();
      }
    })(0);
  });
}

/**
 * 发送邮件（需收件方开启smtp）
 * @param {Object} options receiver-收件人 title-标题 text-文本格式内容 html-html格式内容
 */
function sendEmail(options) {
  const mail = require('nodemailer');

  const smtpTransport = mail.createTransport({
    host: 'smtp.qq.com',
    secureConnection: true,
    port: 587, //腾讯端口为465或587
    auth: {
      user: 'tenfyma@foxmail.com',
      pass: 'otiiohqmkqnfbeii'
    }
  });

  //设置邮箱内容
  const mailOptions = {
    from: options.from || '马腾飞 <tenfyma@foxmail.com>',
    to: options.to, //收件人，多个收件人用逗号隔开
    subject: options.title, //标题
    text: options.text, //文本格式内容
    html: options.html //html格式内容
    /*attachments: [{ //上传附件
      filename: 'json',
      path: './add.html'
    }]*/
  };

  smtpTransport.sendMail(mailOptions, (error, response) => {
    let status = '';
    if (error) {
      console.log(error);
      status = error;
    } else {
      console.log('success' + response.response);
      status = '200';
    }
    smtpTransport.close();
    return status;
  });
}

/**
 * 获取变量类型
 * @param {*} variable 变量
 * @return {String} 变量类型
 */
function typeOf(variable) {
  return Object.prototype.toString
    .call(variable)
    .match(/(?<=\[object\s).+(?=\])/gi)[0]
    .toLowerCase();
}

/**
 * 将字符串进行正则转义，使之可以构造正则对象
 * @param {String} string 待转义字符串
 * @return {String} 转义后字符串
 */
function escapeRegExp(string) {
  // $&表示整个被匹配的字符串
  return string.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$&');
}

/**
 * 加法（纯函数）
 * @param  {number} args 数字
 * @return {Object} 如果含参数，返回该方法，否则返回计算结果
 */

function add(...args) {
  const fn = function(...arg_fn) {
    if (arg_fn.length === 0) {
      return args.reduce((a, b) => a + b);
    }
    return add.apply(null, args.concat(arg_fn));
  };
  fn.valueOf = function() {
    return args.reduce((a, b) => a + b);
  };
  fn.toString = function() {
    return args.reduce((a, b) => a + b);
  };
  return fn;
}

//产生随机整数
function randomBetween(min, max, type = 2) {
  //如果数值输反了自行纠正
  if (min === max) {
    return min;
  } else if (min > max) {
    [min, max] = [max, min];
  }

  const diff = max - min;

  if (type === 0) {
    //不包含边界值
    return Math.round(Math.random() * (diff - 2) + min + 1);
  } else if (type === -1) {
    //包含左边界值
    return Math.floor(Math.random() * diff + min, 10);
  } else if (type === 1) {
    //包含右边界值
    return Math.round(Math.random() * (diff - 1) + min) + 1;
  } else {
    //包含边界值，默认类型
    return Math.round(Math.random() * diff + min);
  }
}

exports = module.exports = {
  traverse,
  traverseSync,
  sendEmail,
  typeOf,
  escapeRegExp,
  add,
  randomBetween
};
<|endoftext|>


<commit_before>(function(root) {

    var hyve = (typeof require == 'function' && !(typeof define == 'function' && define.amd)) ? require('../src/hyve.core.js') : root.hyve

    hyve.feeds['flickr'] = {
        methods : ['search', 'friends'],
        interval : 10000,
        result_type : 'date-posted-desc',  // date-posted-asc, date-posted-desc, date-taken-asc, date-taken-desc, interestingness-desc, interestingness-asc, relevance
        api_key: '',
        auth_token: '',
        api_sig: '',
        oauth_version: '1.0',
        url_suffix_auth : 'rest/?method=flickr.photos.search&',
        url_suffix_anon : 'feeds/photos_public.gne?',
        feed_urls : {
            search: 'http://api.flickr.com/services/{{url_suffix}}&per_page=20&format=json{{#&sort=#result_type}}&tagmode=all&tags={{query}}{{#&jsoncallback=#callback}}&content_type=1&extras=date_upload,date_taken,owner_name,geo,tags,views,url_m,url_b{{#&api_key=#api_key}}',
            friends: 'http://api.flickr.com/services/rest/?method=flickr.photos.getContactsPhotos&extras=date_upload%2Cdate_taken%2Cowner_name%2Cgeo%2Ctags%2Cviews%2Curl_m%2Curl_t&format=json&nojsoncallback=1'
        },
        format_url : function(query){
            var url_suffix
            if (this.api_key){
                url_suffix = this.url_suffix_auth
            } else {
                url_suffix = this.url_suffix_anon
            }
            return {   query: query
                     , url_suffix: url_suffix
                     , result_type: this.result_type
                     , api_key: this.api_key
                     , auth_token: this.auth_token
                     , api_sig: this.api_sig
            }
        },
        parsers : {

            search: function(data, query, callback){

                var api_key = hyve.feeds.flickr.api_key

                if (!this.items_seen){
                    this.items_seen = {}
                }
                var items
                if (api_key){
                    items = data.photos.photo
                } else {
                    items = data.items
                }
                items && items.forEach(function(item){
                    var id, thumbnail, source_img, userid, username, source
                    if (api_key){
                        id = item.id
                        if (item.url_m){
                            thumbnail = item.url_m
                            source_img = item.url_m.replace('.jpg','_b.jpg')
                        }
                        username = item.ownername
                        userid = item.owner
                    } else {
                        id = item.media.m
                        thumbnail = item.media.m
                        source_img = item.media.m.replace('_m','_b')
                        source = item.media.m.replace('_m','_b')
                        username = item.author
                        userid = item.author_id
                    }
                    var weight = 0
                    if (item.views){
                        weight = item.views
                    }
                    if (!this.items_seen[id]){
                        this.items_seen[id] = true
                        hyve.process({
                            'service' : 'flickr',
                            'type' : 'image',
                            'query' : query,
                            'user' : {
                                'id' : userid,
                                'name' : username,
                                'avatar' : ''
                            },
                            'id' : id,
                            'date' : item.dateupload,
                            'text' : item.title,
                            'source' : item.link,
                            'source_img' : source_img,
                            'thumbnail': thumbnail,
                            'weight' : weight
                        },callback)
                    }
                }, this)
            },

            friends: function(data, query, callback) {
                 if (!this.items_seen){
                    this.items_seen = {}
                }
                if (data.photos != undefined){
                    var items = data.photos.photo
                    items.forEach(function(item) {
                        if (!this.items_seen[item.id]) {
                            this.items_seen[item.id] = true

                            source_url = 'http://flickr.com/photos/'+item.owner+'/'+ item.id
                            source_img = 'http://farm'+ item.farm + '.staticflickr.com/' + item.server + '/' + item.id + '_' + item.secret + '.jpg'

                            hyve.process({
                                'service' : 'flickr',
                                'type' : 'image',
                                'query' : query,
                                'user' : {
                                    'id' : item.owner,
                                    'name' : item.username,
                                    'avatar' : ''
                                },
                                'id' : item.id,
                                'date' : item.dateupload,
                                'text' : item.title,
                                'source' : source_url,
                                'source_img' : source_img,
                                'thumbnail': item.url_t,
                                'weight' : item.views
                            }, callback)
                        }
                    }, this)
                }
            }
        }
    }

})(this)
<commit_msg>fix flickr friends url<commit_after>(function(root) {

    var hyve = (typeof require == 'function' && !(typeof define == 'function' && define.amd)) ? require('../src/hyve.core.js') : root.hyve

    hyve.feeds['flickr'] = {
        methods : ['search', 'friends'],
        interval : 10000,
        result_type : 'date-posted-desc',  // date-posted-asc, date-posted-desc, date-taken-asc, date-taken-desc, interestingness-desc, interestingness-asc, relevance
        api_key: '',
        auth_token: '',
        api_sig: '',
        oauth_version: '1.0',
        url_suffix_auth : 'rest/?method=flickr.photos.search&',
        url_suffix_anon : 'feeds/photos_public.gne?',
        feed_urls : {
            search: 'http://api.flickr.com/services/{{url_suffix}}&per_page=20&format=json{{#&sort=#result_type}}&tagmode=all&tags={{query}}{{#&jsoncallback=#callback}}&content_type=1&extras=date_upload,date_taken,owner_name,geo,tags,views,url_m,url_b{{#&api_key=#api_key}}',
            friends: 'http://api.flickr.com/services/rest/?method=flickr.photos.getContactsPhotos&api_key={{ api_key }}&format=json&auth_token={{ auth_token }}&api_sig={{ api_sig }}&format=json&nojsoncallback=1&count=25'
        },
        format_url : function(query){
            var url_suffix
            if (this.api_key){
                url_suffix = this.url_suffix_auth
            } else {
                url_suffix = this.url_suffix_anon
            }
            return {   query: query
                     , url_suffix: url_suffix
                     , result_type: this.result_type
                     , api_key: this.api_key
                     , auth_token: this.auth_token
                     , api_sig: this.api_sig
            }
        },
        parsers : {

            search: function(data, query, callback){

                var api_key = hyve.feeds.flickr.api_key

                if (!this.items_seen){
                    this.items_seen = {}
                }
                var items
                if (api_key){
                    items = data.photos.photo
                } else {
                    items = data.items
                }
                items && items.forEach(function(item){
                    var id, thumbnail, source_img, userid, username, source
                    if (api_key){
                        id = item.id
                        if (item.url_m){
                            thumbnail = item.url_m
                            source_img = item.url_m.replace('.jpg','_b.jpg')
                        }
                        username = item.ownername
                        userid = item.owner
                    } else {
                        id = item.media.m
                        thumbnail = item.media.m
                        source_img = item.media.m.replace('_m','_b')
                        source = item.media.m.replace('_m','_b')
                        username = item.author
                        userid = item.author_id
                    }
                    var weight = 0
                    if (item.views){
                        weight = item.views
                    }
                    if (!this.items_seen[id]){
                        this.items_seen[id] = true
                        hyve.process({
                            'service' : 'flickr',
                            'type' : 'image',
                            'query' : query,
                            'user' : {
                                'id' : userid,
                                'name' : username,
                                'avatar' : ''
                            },
                            'id' : id,
                            'date' : item.dateupload,
                            'text' : item.title,
                            'source' : item.link,
                            'source_img' : source_img,
                            'thumbnail': thumbnail,
                            'weight' : weight
                        },callback)
                    }
                }, this)
            },

            friends: function(data, query, callback) {
                console.log('inside friends')
                 if (!this.items_seen){
                    this.items_seen = {}
                }
                if (data.photos != undefined){
                    var items = data.photos.photo
                    items.forEach(function(item) {
                        if (!this.items_seen[item.id]) {
                            this.items_seen[item.id] = true

                            source_url = 'http://flickr.com/photos/'+item.owner+'/'+ item.id
                            source_img = 'http://farm'+ item.farm + '.staticflickr.com/' + item.server + '/' + item.id + '_' + item.secret + '.jpg'

                            hyve.process({
                                'service' : 'flickr',
                                'type' : 'image',
                                'query' : query,
                                'user' : {
                                    'id' : item.owner,
                                    'name' : item.username,
                                    'avatar' : ''
                                },
                                'id' : item.id,
                                'date' : item.dateupload,
                                'text' : item.title,
                                'source' : source_url,
                                'source_img' : source_img,
                                'thumbnail': item.url_t,
                                'weight' : item.views
                            }, callback)
                        }
                    }, this)
                }
            }
        }
    }

})(this)
<|endoftext|>


<commit_before>import $ from 'jquery'
import '../jquery-extensions'

import Garnish from 'garnish'
import Craft from 'craft'

import NS from '../namespace'

import BlockType from './BlockType'
import Group from './Group'
import Block from './Block'
import Buttons from './Buttons'

import renderTemplate from './templates/input.twig'
import '../twig-extensions'
import './styles/input.scss'

const _defaults = {
	namespace: [],
	blockTypes: [],
	groups: [],
	blocks: [],
	inputId: null,
	maxBlocks: 0
}

export default Garnish.Base.extend({

	_templateNs: [],

	init(settings = {})
	{
		settings = Object.assign({}, _defaults, settings)

		this._templateNs = NS.parse(settings.namespace)
		this._blockTypes = []
		this._groups = []
		this._blocks = []
		this._maxBlocks = settings.maxBlocks

		NS.enter(this._templateNs)

		this.$container = $('#' + settings.inputId).append(renderTemplate({
			blockTypes: settings.blockTypes
		}))

		NS.leave()

		for(let btInfo of settings.blockTypes)
		{
			let blockType = new BlockType(btInfo)

			this._blockTypes.push(blockType)
			this._blockTypes[blockType.getHandle()] = blockType
		}

		for(let gInfo of settings.groups)
		{
			let group = new Group(gInfo)

			this._groups.push(group)
		}

		const $neo = this.$container.find('[data-neo]')
		this.$blocksContainer = $neo.filter('[data-neo="container.blocks"]')
		this.$buttonsContainer = $neo.filter('[data-neo="container.buttons"]')

		this._buttons = new Buttons({
			blockTypes: this.getBlockTypes(),
			groups: this.getGroups(),
			maxBlocks: this.getMaxBlocks()
		})

		this.$buttonsContainer.append(this._buttons.$container)
		this._buttons.on('newBlock', e => this['@newBlock'](e))
		this._buttons.initUi()

		this._blockSort = new Garnish.DragSort(null, {
			container: this.$blocksContainer,
			handle: '[data-neo-b="button.move"]',
			axis: 'y',
			filter: () =>
			{
				// Only return all the selected items if the target item is selected
				if(this._blockSort.$targetItem.hasClass('is-selected'))
				{
					return this._blockSelect.getSelectedItems()
				}
				else
				{
					return this._blockSort.$targetItem
				}
			},
			collapseDraggees: true,
			magnetStrength: 4,
			helperLagBase: 1.5,
			helperOpacity: 0.9,
			onSortChange: () => this._updateBlockOrder()
		})

		this._blockSelect = new Garnish.Select(this.$blocksContainer, null, {
			multi: true,
			vertical: true,
			handle: '[data-neo-b="select"], [data-neo-b="button.toggler"]',
			checkboxMode: true,
			selectedClass: 'is-selected sel'
		});

		for(let bInfo of settings.blocks)
		{
			let blockType = this._blockTypes[bInfo.blockType]

			bInfo.namespace = [...this._templateNs, bInfo.id]
			bInfo.blockType = new BlockType({
				name: blockType.getName(),
				handle: blockType.getHandle(),
				maxBlocks: blockType.getMaxBlocks(),
				childBlocks: blockType.getChildBlocks(),
				tabs: bInfo.tabs
			})
			bInfo.buttons = new Buttons({
				blockTypes: blockType.getChildBlockItems(this.getItems()),
				groups: this.getGroups(),
				maxBlocks: this.getMaxBlocks()
			})

			let block = new Block(bInfo)
			this.addBlock(block, -1, bInfo.level|0)
		}
	},

	addBlock(block, index = -1, level = 0)
	{
		const blockCount = this._blocks.length
		index = (index >= 0 ? Math.max(0, Math.min(index, blockCount)) : blockCount)

		const prevBlock = index > 0 ? this._blocks[index - 1] : false
		const nextBlock = index < blockCount ? this._blocks[index] : false

		if(!prevBlock)
		{
			this.$blocksContainer.prepend(block.$container)
			this._blocks.unshift(block)
		}
		else
		{
			const minLevel = nextBlock ? nextBlock.getLevel() : 0
			const maxLevel = prevBlock.getLevel() + (prevBlock.getBlockType().isParent() ? 1 : 0)

			level = Math.max(minLevel, Math.min(level, maxLevel))

			const prevBlockOnLevel = this._findPrevBlockOnLevel(index, level)

			if(prevBlockOnLevel)
			{
				prevBlockOnLevel.$container.after(block.$container)
			}
			else
			{
				prevBlock.$blocksContainer.prepend(block.$container)
			}

			this._blocks.splice(index, 0, block)
		}

		block.setLevel(level)

		this._blockSort.addItems(block.$container)
		this._blockSelect.addItems(block.$container)

		block.initUi()
		block.on('destroy.input',         e => this._blockBatch(block, b => this.removeBlock(b)))
		block.on('toggleEnabled.input',   e => this._blockBatch(block, b => b.toggleEnabled(e.enabled)))
		block.on('toggleExpansion.input', e => this._blockBatch(block, b => b.toggleExpansion(e.expanded)))
		block.on('newBlock.input',        e => this['@newBlock'](Object.assign(e, {index: this._blocks.indexOf(block) + 1})))
		block.on('addBlockAbove.input',   e => this['@addBlockAbove'](e))

		this._destroyTempButtons()
		this._updateButtons()
		this._updateBlockOrder()

		this.trigger('addBlock', {
			block: block,
			index: index
		})
	},

	removeBlock(block)
	{
		const childBlocks = this._findChildBlocks(this._blocks.indexOf(block))
		for(let childBlock of childBlocks)
		{
			this.removeBlock(childBlock)
		}

		block.$container.remove()
		block.off('.input')

		this._blocks = this._blocks.filter(b => b !== block)
		this._blockSort.removeItems(block.$container)
		this._blockSelect.removeItems(block.$container)

		this._destroyTempButtons()
		this._updateButtons()

		this.trigger('removeBlock', {
			block: block
		})
	},

	getBlockByElement($block)
	{
		return this._blocks.find(block => block.$container.is($block))
	},

	getBlocks()
	{
		return Array.from(this._blocks)
	},

	getBlockTypes()
	{
		return Array.from(this._blockTypes)
	},

	getGroups()
	{
		return Array.from(this._groups)
	},

	getItems()
	{
		return [...this.getBlockTypes(), ...this.getGroups()].sort((a, b) => a.getSortOrder() - b.getSortOrder())
	},

	getMaxBlocks()
	{
		return this._maxBlocks
	},

	getSelectedBlocks()
	{
		const $selectedBlocks = this._blockSelect.getSelectedItems()
		return this._blocks.filter(block => block.$container.is($selectedBlocks))
	},

	_updateBlockOrder()
	{
		const blocks = []

		this._blockSort.$items.each((index, element) =>
		{
			const block = this.getBlockByElement(element)
			blocks.push(block)
		})

		this._blocks = blocks
	},

	_updateButtons()
	{
		const blocks = this.getBlocks()
		this._buttons.updateButtonStates(blocks)

		if(this._tempButtons)
		{
			this._tempButtons.updateButtonStates(blocks)
		}

		for(let block of blocks)
		{
			let buttons = block.getButtons()
			if(buttons)
			{
				buttons.updateButtonStates(blocks)
			}
		}
	},

	_blockBatch(block, callback)
	{
		const blocks = block.isSelected() ? this.getSelectedBlocks() : [block]

		for(let b of blocks)
		{
			callback(b)
		}
	},

	_destroyTempButtons()
	{
		if(this._tempButtons)
		{
			this._tempButtons.off('newBlock')
			this._tempButtons.$container.remove()
			this._tempButtons = null
		}
	},

	_findPrevBlockOnLevel(index, level)
	{
		const blocks = this._blocks

		let block = blocks[--index]
		let lowestLevel = Number.MAX_VALUE

		while(block)
		{
			let blockLevel = block.getLevel()

			if(blockLevel < lowestLevel)
			{
				if(blockLevel === level)
				{
					return block
				}

				lowestLevel = blockLevel
			}

			block = this._blocks[--index]
		}

		return false
	},

	_findChildBlocks(index)
	{
		const blocks = this._blocks
		const block = blocks[index]
		const childBlocks = []

		if(block)
		{
			const level = block.getLevel()

			let currentBlock = blocks[++index]
			while(currentBlock && currentBlock.getLevel() > level)
			{
				if(currentBlock.getLevel() === level + 1)
				{
					childBlocks.push(currentBlock)
				}

				currentBlock = blocks[++index]
			}
		}

		return childBlocks
	},

	'@newBlock'(e)
	{
		const blockId = Block.getNewId()
		const block = new Block({
			namespace: [...this._templateNs, blockId],
			blockType: e.blockType,
			id: blockId,
			buttons: new Buttons({
				blockTypes: e.blockType.getChildBlockItems(this.getItems()),
				groups: this.getGroups(),
				maxBlocks: this.getMaxBlocks()
			})
		})

		this.addBlock(block, e.index, e.level)
	},

	'@addBlockAbove'(e)
	{
		this._destroyTempButtons()

		const block = e.block
		const buttons = new Buttons({
			blockTypes: this.getBlockTypes(),
			groups: this.getGroups(),
			maxBlocks: this.getMaxBlocks(),
			blocks: this.getBlocks()
		})

		block.$container.before(buttons.$container)
		buttons.on('newBlock', e =>
		{
			this['@newBlock']({
				blockType: e.blockType,
				index: this._blocks.indexOf(block),
				level: block.getLevel()
			})
		})

		buttons.initUi()

		this._tempButtons = buttons
	}
})
<commit_msg>Selected blocks now includes child blocks<commit_after>import $ from 'jquery'
import '../jquery-extensions'

import Garnish from 'garnish'
import Craft from 'craft'

import NS from '../namespace'

import BlockType from './BlockType'
import Group from './Group'
import Block from './Block'
import Buttons from './Buttons'

import renderTemplate from './templates/input.twig'
import '../twig-extensions'
import './styles/input.scss'

const _defaults = {
	namespace: [],
	blockTypes: [],
	groups: [],
	blocks: [],
	inputId: null,
	maxBlocks: 0
}

export default Garnish.Base.extend({

	_templateNs: [],

	init(settings = {})
	{
		settings = Object.assign({}, _defaults, settings)

		this._templateNs = NS.parse(settings.namespace)
		this._blockTypes = []
		this._groups = []
		this._blocks = []
		this._maxBlocks = settings.maxBlocks

		NS.enter(this._templateNs)

		this.$container = $('#' + settings.inputId).append(renderTemplate({
			blockTypes: settings.blockTypes
		}))

		NS.leave()

		for(let btInfo of settings.blockTypes)
		{
			let blockType = new BlockType(btInfo)

			this._blockTypes.push(blockType)
			this._blockTypes[blockType.getHandle()] = blockType
		}

		for(let gInfo of settings.groups)
		{
			let group = new Group(gInfo)

			this._groups.push(group)
		}

		const $neo = this.$container.find('[data-neo]')
		this.$blocksContainer = $neo.filter('[data-neo="container.blocks"]')
		this.$buttonsContainer = $neo.filter('[data-neo="container.buttons"]')

		this._buttons = new Buttons({
			blockTypes: this.getBlockTypes(),
			groups: this.getGroups(),
			maxBlocks: this.getMaxBlocks()
		})

		this.$buttonsContainer.append(this._buttons.$container)
		this._buttons.on('newBlock', e => this['@newBlock'](e))
		this._buttons.initUi()

		this._blockSort = new Garnish.DragSort(null, {
			container: this.$blocksContainer,
			handle: '[data-neo-b="button.move"]',
			axis: 'y',
			filter: () =>
			{
				// Only return all the selected items if the target item is selected
				if(this._blockSort.$targetItem.hasClass('is-selected'))
				{
					return this._blockSelect.getSelectedItems()
				}
				else
				{
					return this._blockSort.$targetItem
				}
			},
			collapseDraggees: true,
			magnetStrength: 4,
			helperLagBase: 1.5,
			helperOpacity: 0.9,
			onSortChange: () => this._updateBlockOrder()
		})

		this._blockSelect = new Garnish.Select(this.$blocksContainer, null, {
			multi: true,
			vertical: true,
			handle: '[data-neo-b="select"], [data-neo-b="button.toggler"]',
			checkboxMode: true,
			selectedClass: 'is-selected sel'
		});

		for(let bInfo of settings.blocks)
		{
			let blockType = this._blockTypes[bInfo.blockType]

			bInfo.namespace = [...this._templateNs, bInfo.id]
			bInfo.blockType = new BlockType({
				name: blockType.getName(),
				handle: blockType.getHandle(),
				maxBlocks: blockType.getMaxBlocks(),
				childBlocks: blockType.getChildBlocks(),
				tabs: bInfo.tabs
			})
			bInfo.buttons = new Buttons({
				blockTypes: blockType.getChildBlockItems(this.getItems()),
				groups: this.getGroups(),
				maxBlocks: this.getMaxBlocks()
			})

			let block = new Block(bInfo)
			this.addBlock(block, -1, bInfo.level|0)
		}
	},

	addBlock(block, index = -1, level = 0)
	{
		const blockCount = this._blocks.length
		index = (index >= 0 ? Math.max(0, Math.min(index, blockCount)) : blockCount)

		const prevBlock = index > 0 ? this._blocks[index - 1] : false
		const nextBlock = index < blockCount ? this._blocks[index] : false

		if(!prevBlock)
		{
			this.$blocksContainer.prepend(block.$container)
			this._blocks.unshift(block)
		}
		else
		{
			const minLevel = nextBlock ? nextBlock.getLevel() : 0
			const maxLevel = prevBlock.getLevel() + (prevBlock.getBlockType().isParent() ? 1 : 0)

			level = Math.max(minLevel, Math.min(level, maxLevel))

			const prevBlockOnLevel = this._findPrevBlockOnLevel(index, level)

			if(prevBlockOnLevel)
			{
				prevBlockOnLevel.$container.after(block.$container)
			}
			else
			{
				prevBlock.$blocksContainer.prepend(block.$container)
			}

			this._blocks.splice(index, 0, block)
		}

		block.setLevel(level)

		this._blockSort.addItems(block.$container)
		this._blockSelect.addItems(block.$container)

		block.initUi()
		block.on('destroy.input',         e => this._blockBatch(block, b => this.removeBlock(b)))
		block.on('toggleEnabled.input',   e => this._blockBatch(block, b => b.toggleEnabled(e.enabled)))
		block.on('toggleExpansion.input', e => this._blockBatch(block, b => b.toggleExpansion(e.expanded)))
		block.on('newBlock.input',        e => this['@newBlock'](Object.assign(e, {index: this._blocks.indexOf(block) + 1})))
		block.on('addBlockAbove.input',   e => this['@addBlockAbove'](e))

		this._destroyTempButtons()
		this._updateButtons()
		this._updateBlockOrder()

		this.trigger('addBlock', {
			block: block,
			index: index
		})
	},

	removeBlock(block)
	{
		const childBlocks = this._findChildBlocks(this._blocks.indexOf(block))
		for(let childBlock of childBlocks)
		{
			this.removeBlock(childBlock)
		}

		block.$container.remove()
		block.off('.input')

		this._blocks = this._blocks.filter(b => b !== block)
		this._blockSort.removeItems(block.$container)
		this._blockSelect.removeItems(block.$container)

		this._destroyTempButtons()
		this._updateButtons()

		this.trigger('removeBlock', {
			block: block
		})
	},

	getBlockByElement($block)
	{
		return this._blocks.find(block => block.$container.is($block))
	},

	getBlocks()
	{
		return Array.from(this._blocks)
	},

	getBlockTypes()
	{
		return Array.from(this._blockTypes)
	},

	getGroups()
	{
		return Array.from(this._groups)
	},

	getItems()
	{
		return [...this.getBlockTypes(), ...this.getGroups()].sort((a, b) => a.getSortOrder() - b.getSortOrder())
	},

	getMaxBlocks()
	{
		return this._maxBlocks
	},

	getSelectedBlocks()
	{
		const $selectedBlocks = this._blockSelect.getSelectedItems()
		return this._blocks.filter(block => block.$container.closest($selectedBlocks).length > 0)
	},

	_updateBlockOrder()
	{
		const blocks = []

		this._blockSort.$items.each((index, element) =>
		{
			const block = this.getBlockByElement(element)
			blocks.push(block)
		})

		this._blocks = blocks
	},

	_updateButtons()
	{
		const blocks = this.getBlocks()
		this._buttons.updateButtonStates(blocks)

		if(this._tempButtons)
		{
			this._tempButtons.updateButtonStates(blocks)
		}

		for(let block of blocks)
		{
			let buttons = block.getButtons()
			if(buttons)
			{
				buttons.updateButtonStates(blocks)
			}
		}
	},

	_blockBatch(block, callback)
	{
		const blocks = block.isSelected() ? this.getSelectedBlocks() : [block]

		for(let b of blocks)
		{
			callback(b)
		}
	},

	_destroyTempButtons()
	{
		if(this._tempButtons)
		{
			this._tempButtons.off('newBlock')
			this._tempButtons.$container.remove()
			this._tempButtons = null
		}
	},

	_findPrevBlockOnLevel(index, level)
	{
		const blocks = this._blocks

		let block = blocks[--index]
		let lowestLevel = Number.MAX_VALUE

		while(block)
		{
			let blockLevel = block.getLevel()

			if(blockLevel < lowestLevel)
			{
				if(blockLevel === level)
				{
					return block
				}

				lowestLevel = blockLevel
			}

			block = this._blocks[--index]
		}

		return false
	},

	_findChildBlocks(index)
	{
		const blocks = this._blocks
		const block = blocks[index]
		const childBlocks = []

		if(block)
		{
			const level = block.getLevel()

			let currentBlock = blocks[++index]
			while(currentBlock && currentBlock.getLevel() > level)
			{
				if(currentBlock.getLevel() === level + 1)
				{
					childBlocks.push(currentBlock)
				}

				currentBlock = blocks[++index]
			}
		}

		return childBlocks
	},

	'@newBlock'(e)
	{
		const blockId = Block.getNewId()
		const block = new Block({
			namespace: [...this._templateNs, blockId],
			blockType: e.blockType,
			id: blockId,
			buttons: new Buttons({
				blockTypes: e.blockType.getChildBlockItems(this.getItems()),
				groups: this.getGroups(),
				maxBlocks: this.getMaxBlocks()
			})
		})

		this.addBlock(block, e.index, e.level)
	},

	'@addBlockAbove'(e)
	{
		this._destroyTempButtons()

		const block = e.block
		const buttons = new Buttons({
			blockTypes: this.getBlockTypes(),
			groups: this.getGroups(),
			maxBlocks: this.getMaxBlocks(),
			blocks: this.getBlocks()
		})

		block.$container.before(buttons.$container)
		buttons.on('newBlock', e =>
		{
			this['@newBlock']({
				blockType: e.blockType,
				index: this._blocks.indexOf(block),
				level: block.getLevel()
			})
		})

		buttons.initUi()

		this._tempButtons = buttons
	}
})
<|endoftext|>


<commit_before>/*
 * MelonJS Game Engine
 * Copyright (C) 2012, Olivier BIOT
 * http://www.melonjs.org
 *
 */

(function($, undefined) {

	/**
	 * There is no constructor function for me.input.
	 * @final
	 * @memberOf me
	 * @constructor Should not be called by the user.
	 */
	me.input = (function() {

		// hold public stuff in our singletong
		var obj = {};

		/*---------------------------------------------

			PRIVATE STUFF

		  ---------------------------------------------*/

		// list of binded keys
		var KeyBinding = [];

		// corresponding actions
		var keyStatus = [];

		// lock enable flag for keys
		var keyLock = [];
		// actual lock status of each key
		var keyLocked = [];

		// callback function for mouse & gyro
		var mouseEventCB = null;
		var gyroEventCB = null;

		// some usefull flags
		var keyboardInitialized = false;
		var mouseInitialized = false;

		/**
		 * enable keyboard event
		 * @private
		 */

		function enableKeyboardEvent(enable) {
			if (enable) {
				// Event Management
				if (!keyboardInitialized) {
					$.addEventListener('keydown', keydown, false);
					$.addEventListener('keyup', keyup, false);
				}
			} else {
				// remove the even listeners
				$.removeEventListener('keydown', keydown, false);
				$.removeEventListener('keyup', keyup, false);
			}
			keyboardInitialized = enable;
		};

		/**
		 * prevent event propagation
		 * @private
		 */
		function preventDefault(e) {
			e.stopPropagation();
			if (e.preventDefault)
				e.preventDefault();
			e.returnValue = false;
			// this is apprently needed on some platforms
			e.cancelBubble = true;
		};

		/**
		 * key down event
		 * @private
		 */
		function keydown(e, keyCode) {

			var action = KeyBinding[keyCode || e.keyCode || e.which];

			if (action) {
				//console.log(e, action);

				//console.log(action);
				if (!keyLocked[action]) {
					keyStatus[action] = true;
					// lock the key if requested
					keyLocked[action] = keyLock[action];
				}
				// prevent event propagation
				preventDefault(e);
				return false;
			}
			return true;
		};

		/**
		 * key up event
		 * @private
		 */
		function keyup(e, keyCode) {

			var action = KeyBinding[keyCode || e.keyCode || e.which];

			//console.log(e, action);

			if (action) {

				keyStatus[action] = false;
				keyLocked[action] = false;
				// prevent the event propagation
				preventDefault(e);
				return false;
			}
			return true;

		}

		/**
		 * translate Mouse Coordinates
		 * @private
		 */
		function updateMouseCoords(x, y) {
			obj.mouse.pos.set(x,y);
			obj.mouse.pos.sub(obj.mouse.offset);
			return obj.mouse.pos;
		};

		/**
		 * mouse event management (mousemove)
		 * @private
		 */
		function onMouseMove(e) {
			// update mouse position
			updateMouseCoords(e.pageX, e.pageY);
			preventDefault(e);
		};

		/**
		 * mouse event management (mousedown, mouseup)
		 * @private
		 */
		function onMouseEvent(e) {
			// propagate the event to the callback with x,y coords
			mouseEventCB(obj.mouse.pos);

			// check if mouse is mapped to a key
			var keycode = me.input.mouse.bind[event.button];
			if (keycode) {
				if (e.type=="mousedown")
					keydown(e, keycode);
				else // (e.type=="mouseup")
					keyup(e, keycode);
			}
		};

		/* ---

				 event management (Gyroscopic)

				---										*/
		function onGyroEvent(event) {
			// http://www.mobilexweb.com/samples/ball.html
			// http://www.mobilexweb.com/blog/safari-ios-accelerometer-websockets-html5
			//
			// ax = event.accelerationIncludingGravity.x;
			// ay = event.accelerationIncludingGravity.y;

			// use acceleration instead on iphone4
			// event.accelerationIncludingGravity.x
			// event.accelerationIncludingGravity.y
			// event.accelerationIncludingGravity.z

			// Gyroscope
			// window.ondeviceorientation = function(event) {
			// event.alpha
			// event.beta
			// event.gamma
			//}
		}
		;

		/*---------------------------------------------

			PUBLIC STUFF

		  ---------------------------------------------*/

		obj.mouse = {
			// mouse position
			pos : null,
			// canvas offset
			offset : null,
			// button constants (W3C)
			LEFT : 0,
			MIDDLE: 1,
			RIGHT: 2,
			// bind list for mouse buttons
			bind: [3]
		}

		/**
		 * list of mappable keys :
		 * LEFT, UP, RIGHT, DOWN, ENTER, SHIFT, CTRL, ALT, PAUSE, ESC, ESCAPE, [0..9], [A..Z]
		 * @public
		 * @enum {number}
		 * @name me.input#KEY
		 */
		obj.KEY = {
			'LEFT' : 37,
			'UP' : 38,
			'RIGHT' : 39,
			'DOWN' : 40,
			'ENTER' : 13,
			'SHIFT' : 16,
			'CTRL' : 17,
			'ALT' : 18,
			'PAUSE' : 19,
			'ESC' : 27,
			'SPACE' : 32,
			'NUM0' : 48,
			'NUM1' : 49,
			'NUM2' : 50,
			'NUM3' : 51,
			'NUM4' : 52,
			'NUM5' : 53,
			'NUM6' : 54,
			'NUM7' : 55,
			'NUM8' : 56,
			'NUM9' : 57,
			'A' : 65,
			'B' : 66,
			'C' : 67,
			'D' : 68,
			'E' : 69,
			'F' : 70,
			'G' : 71,
			'H' : 72,
			'I' : 73,
			'J' : 74,
			'K' : 75,
			'L' : 76,
			'M' : 77,
			'N' : 78,
			'O' : 79,
			'P' : 80,
			'Q' : 81,
			'R' : 82,
			'S' : 83,
			'T' : 84,
			'U' : 85,
			'V' : 86,
			'W' : 87,
			'X' : 88,
			'Y' : 89,
			'Z' : 90,
		};

		/**
		 * return the key press status of the specified action
		 * @name me.input#isKeyPressed
		 * @public
		 * @function
		 * @example
		 * if (me.input.isKeyPressed('left'))
		 * {
		 *    //do something
		 * }
		 * else if (me.input.isKeyPressed('right'))
		 * {
		 *    //do something else...
		 * }
		 *
		 */

		obj.isKeyPressed = function(action) {
			if (keyStatus[action]) {
				if (keyLock[action]) {
					keyLocked[action] = true;
					// "eat" the event
					keyStatus[action] = false;
				}
				return true;
			}
			return false;
		};

		/**
		 * return the key status of the specified action
		 * @name me.input#keyStatus
		 * @public
		 * @function
		 * @return {boolean} down (true) or up(false)
		 */

		obj.keyStatus = function(action) {
			return (keyLocked[action] === true) ? true : keyStatus[action];
		};

		/**
		 * associate a user defined action to a keycode
		 * @name me.input#bindKey
		 * @public
		 * @function
		 * @param {me.input#KEY} keycode
		 * @param {String} action user defined corresponding action
		 * @param {boolean} lock cancel the keypress event once read
		 * @example
		 * // enable the keyboard
		 * me.input.bindKey(me.input.KEY.LEFT,  "left");
		 * me.input.bindKey(me.input.KEY.RIGHT, "right");
		 * me.input.bindKey(me.input.KEY.X,     "jump", true);
		 */
		obj.bindKey = function(keycode, action, lock) {
			if (!keyboardInitialized)
				enableKeyboardEvent(true);

			KeyBinding[keycode] = action;

			keyLock[action] = lock ? lock : false;
			keyLocked[action] = false;
			//console.log(this);
		};

		/**
		 * unbind the defined keycode
		 * @name me.input#unbindKey
		 * @public
		 * @function
		 * @example
		 * me.input.unbindKey(me.input.KEY.LEFT);
		 */
		obj.unbindKey = function(keycode) {
			// clear the event status
			keyStatus[KeyBinding[keycode]] = false;
			keyLock[KeyBinding[keycode]] = false;
			// remove the key binding
			KeyBinding[keycode] = null;
		};

		/**
		 * Associate a mouse (button) action to a keycode
		 * Left button – 0
		 * Middle button – 1
		 * Right button – 2
		 * @name me.input#bindMouse
		 * @public
		 * @function
		 * @param {Integer} button (accordingly to W3C values : 0,1,2 for left, middle and right buttons)
		 * @param {me.input#KEY} keyCode
		 * @example
		 * // enable the keyboard
		 * me.input.bindKey(me.input.KEY.X, "shoot");
		 * // map the left button click on the X key
		 * me.input.bindMouse(me.input.mouse.LEFT, me.input.KEY.X);
		 */
		obj.bindMouse = function (button, keyCode)
		{
			// make sure the mouse is initialized
			if (!mouseInitialized)
				obj.enableMouseEvent(true);
			// throw an exception if no action is defined for the specified keycode
			if (!KeyBinding[keyCode])
			  throw "melonJS : no action defined for keycode " + keyCode;
			// map the mouse button to the keycode
			me.input.mouse.bind[button] = keyCode;
		};
		/**
		 * unbind the defined keycode
		 * @name me.input#unbindMouse
		 * @public
		 * @function
		 * @example
		 * me.input.unbindMouse(me.input.mouse.LEFT);
		 */
		obj.unbindMouse = function(button) {
			// clear the event status
			me.input.mouse.bind[button] = null;
		};

		/**
		 * enable mouse event
		 * @name me.input#enableMouseEvent
		 * @public
		 * @function
		 * @deprecated to be rewritten
		 */
		obj.enableMouseEvent = function(enable, callback) {
			if (enable) {
				if (!mouseInitialized) {
					// initialize mouse pos (0,0)
					obj.mouse.pos = new me.Vector2d(0,0);
					// get relative canvas position in the page
					obj.mouse.offset = me.video.getPos();
					// add a listener for the mouse
					me.video.getScreenCanvas().addEventListener('mousemove', onMouseMove, false);
					me.video.getScreenCanvas().addEventListener('mousedown', onMouseEvent, false );
					me.video.getScreenCanvas().addEventListener('mouseup', onMouseEvent, false );
					// set the callback
					mouseEventCB = callback || me.game.mouseEvent.bind(me.game);
				}
			} else {
				me.video.getScreenCanvas().removeEventListener('mousemove', onMouseMove, false);
				me.video.getScreenCanvas().removeEventListener('mousedown', onMouseEvent, false );
				me.video.getScreenCanvas().removeEventListener('mouseup', onMouseEvent, false );
			}
			mouseInitialized = enable;
		};

		/**
		 * enable gyroscopic event (not implemented)
		 * @name me.input#enableGyroscopicEvent
		 * @public
		 * @function
		 */
		obj.enableGyroscopicEvent = function(enable, callback) {
			if ($.sys.gyro) {
				// add a listener for the mouse
				$.ondevicemotion = enable ? onGyroEvent : null;
				// set the callback
				gyroEventCB = enable ? callback : null;
			}

		};

		// return our object
		return obj;

	})();

	/*---------------------------------------------------------*/
	// END END END
	/*---------------------------------------------------------*/
})(window);
<commit_msg>Rewrote mouse event model, allowing to define event handler per object (callback based)<commit_after>/*
 * MelonJS Game Engine
 * Copyright (C) 2012, Olivier BIOT
 * http://www.melonjs.org
 *
 */

(function($, undefined) {

	/**
	 * There is no constructor function for me.input.
	 * @final
	 * @memberOf me
	 * @constructor Should not be called by the user.
	 */
	me.input = (function() {

		// hold public stuff in our singletong
		var obj = {};

		/*---------------------------------------------

			PRIVATE STUFF

		  ---------------------------------------------*/

		// list of binded keys
		var KeyBinding = [];

		// corresponding actions
		var keyStatus = [];

		// lock enable flag for keys
		var keyLock = [];
		// actual lock status of each key
		var keyLocked = [];

		// callback function for mouse & gyro
		var mouseEventCB = null;
		var gyroEventCB = null;

		// some usefull flags
		var keyboardInitialized = false;
		var mouseInitialized = false;

		/**
		 * enable keyboard event
		 * @private
		 */

		function enableKeyboardEvent(enable) {
			if (enable) {
				// Event Management
				if (!keyboardInitialized) {
					$.addEventListener('keydown', keydown, false);
					$.addEventListener('keyup', keyup, false);
				}
			} else {
				// remove the even listeners
				$.removeEventListener('keydown', keydown, false);
				$.removeEventListener('keyup', keyup, false);
			}
			keyboardInitialized = enable;
		};

		/**
		 * enable mouse event
		 * @name me.input#enableMouseEvent
		 * @private
		 * @function
		 */
		function enableMouseEvent(enable/*, callback*/) {
			if (enable) {
				if (!mouseInitialized) {
					// initialize mouse pos (0,0)
					obj.mouse.pos = new me.Vector2d(0,0);
					// get relative canvas position in the page
					obj.mouse.offset = me.video.getPos();
					// add a listener for the mouse
					me.video.getScreenCanvas().addEventListener('mousemove', onMouseMove, false);
					me.video.getScreenCanvas().addEventListener('mousedown', onMouseEvent, false );
					me.video.getScreenCanvas().addEventListener('mouseup', onMouseEvent, false );
					// set the callback
					//mouseEventCB = callback || me.game.mouseEvent.bind(me.game);
				}
			} else {
				me.video.getScreenCanvas().removeEventListener('mousemove', onMouseMove, false);
				me.video.getScreenCanvas().removeEventListener('mousedown', onMouseEvent, false );
				me.video.getScreenCanvas().removeEventListener('mouseup', onMouseEvent, false );
			}
			mouseInitialized = enable;
		};

		/**
		 * prevent event propagation
		 * @private
		 */
		function preventDefault(e) {
			e.stopPropagation();
			if (e.preventDefault)
				e.preventDefault();
			e.returnValue = false;
			// this is apprently needed on some platforms
			e.cancelBubble = true;
		};

		/**
		 * key down event
		 * @private
		 */
		function keydown(e, keyCode) {

			var action = KeyBinding[keyCode || e.keyCode || e.which];

			if (action) {
				//console.log(e, action);

				//console.log(action);
				if (!keyLocked[action]) {
					keyStatus[action] = true;
					// lock the key if requested
					keyLocked[action] = keyLock[action];
				}
				// prevent event propagation
				preventDefault(e);
				return false;
			}
			return true;
		};

		/**
		 * key up event
		 * @private
		 */
		function keyup(e, keyCode) {

			var action = KeyBinding[keyCode || e.keyCode || e.which];

			//console.log(e, action);

			if (action) {

				keyStatus[action] = false;
				keyLocked[action] = false;
				// prevent the event propagation
				preventDefault(e);
				return false;
			}
			return true;

		}

		/**
		 * propagate mouse event to registed object
		 * @private
		 */
		function dispatchMouseEvent(e) {
			var handlers = obj.mouse.handlers[e.type];
			if (handlers) {
				for (var i = handlers.length, handler; i--, handler = handlers[i];) {
					// call the callback
					handler();
				}
			}

		};

		/**
		 * translate Mouse Coordinates
		 * @private
		 */
		function updateMouseCoords(x, y) {
			obj.mouse.pos.set(x,y);
			obj.mouse.pos.sub(obj.mouse.offset);
			return obj.mouse.pos;
		};

		/**
		 * mouse event management (mousemove)
		 * @private
		 */
		function onMouseMove(e) {
			// update mouse position
			updateMouseCoords(e.pageX, e.pageY);
			// dispatch mouse event to registered object
			dispatchMouseEvent(e);
			// prevent default action
			preventDefault(e);
		};

		/**
		 * mouse event management (mousedown, mouseup)
		 * @private
		 */
		function onMouseEvent(e) {
			// dispatch mouse event to registered object
			dispatchMouseEvent(e);
			// check if mouse is mapped to a key
			var keycode = me.input.mouse.bind[event.button];
			if (keycode) {
				if (e.type=="mousedown")
					keydown(e, keycode);
				else // (e.type=="mouseup")
					keyup(e, keycode);
			}
			else {
				// prevent default action
				preventDefault(e);
			}
		};

		/* ---

				 event management (Gyroscopic)

				---										*/
		function onGyroEvent(event) {
			// http://www.mobilexweb.com/samples/ball.html
			// http://www.mobilexweb.com/blog/safari-ios-accelerometer-websockets-html5
			//
			// ax = event.accelerationIncludingGravity.x;
			// ay = event.accelerationIncludingGravity.y;

			// use acceleration instead on iphone4
			// event.accelerationIncludingGravity.x
			// event.accelerationIncludingGravity.y
			// event.accelerationIncludingGravity.z

			// Gyroscope
			// window.ondeviceorientation = function(event) {
			// event.alpha
			// event.beta
			// event.gamma
			//}
		}
		;

		/*---------------------------------------------

			PUBLIC STUFF

		  ---------------------------------------------*/

		obj.mouse = {
			// mouse position
			pos : null,
			// canvas offset
			offset : null,
			// button constants (W3C)
			LEFT : 0,
			MIDDLE: 1,
			RIGHT: 2,
			// bind list for mouse buttons
			bind: [3],
			handlers:{}
		}

		/**
		 * list of mappable keys :
		 * LEFT, UP, RIGHT, DOWN, ENTER, SHIFT, CTRL, ALT, PAUSE, ESC, ESCAPE, [0..9], [A..Z]
		 * @public
		 * @enum {number}
		 * @name me.input#KEY
		 */
		obj.KEY = {
			'LEFT' : 37,
			'UP' : 38,
			'RIGHT' : 39,
			'DOWN' : 40,
			'ENTER' : 13,
			'SHIFT' : 16,
			'CTRL' : 17,
			'ALT' : 18,
			'PAUSE' : 19,
			'ESC' : 27,
			'SPACE' : 32,
			'NUM0' : 48,
			'NUM1' : 49,
			'NUM2' : 50,
			'NUM3' : 51,
			'NUM4' : 52,
			'NUM5' : 53,
			'NUM6' : 54,
			'NUM7' : 55,
			'NUM8' : 56,
			'NUM9' : 57,
			'A' : 65,
			'B' : 66,
			'C' : 67,
			'D' : 68,
			'E' : 69,
			'F' : 70,
			'G' : 71,
			'H' : 72,
			'I' : 73,
			'J' : 74,
			'K' : 75,
			'L' : 76,
			'M' : 77,
			'N' : 78,
			'O' : 79,
			'P' : 80,
			'Q' : 81,
			'R' : 82,
			'S' : 83,
			'T' : 84,
			'U' : 85,
			'V' : 86,
			'W' : 87,
			'X' : 88,
			'Y' : 89,
			'Z' : 90,
		};

		/**
		 * return the key press status of the specified action
		 * @name me.input#isKeyPressed
		 * @public
		 * @function
		 * @example
		 * if (me.input.isKeyPressed('left'))
		 * {
		 *    //do something
		 * }
		 * else if (me.input.isKeyPressed('right'))
		 * {
		 *    //do something else...
		 * }
		 *
		 */

		obj.isKeyPressed = function(action) {
			if (keyStatus[action]) {
				if (keyLock[action]) {
					keyLocked[action] = true;
					// "eat" the event
					keyStatus[action] = false;
				}
				return true;
			}
			return false;
		};

		/**
		 * return the key status of the specified action
		 * @name me.input#keyStatus
		 * @public
		 * @function
		 * @return {boolean} down (true) or up(false)
		 */

		obj.keyStatus = function(action) {
			return (keyLocked[action] === true) ? true : keyStatus[action];
		};

		/**
		 * associate a user defined action to a keycode
		 * @name me.input#bindKey
		 * @public
		 * @function
		 * @param {me.input#KEY} keycode
		 * @param {String} action user defined corresponding action
		 * @param {boolean} lock cancel the keypress event once read
		 * @example
		 * // enable the keyboard
		 * me.input.bindKey(me.input.KEY.LEFT,  "left");
		 * me.input.bindKey(me.input.KEY.RIGHT, "right");
		 * me.input.bindKey(me.input.KEY.X,     "jump", true);
		 */
		obj.bindKey = function(keycode, action, lock) {
			if (!keyboardInitialized)
				enableKeyboardEvent(true);

			KeyBinding[keycode] = action;

			keyLock[action] = lock ? lock : false;
			keyLocked[action] = false;
			//console.log(this);
		};

		/**
		 * unbind the defined keycode
		 * @name me.input#unbindKey
		 * @public
		 * @function
		 * @example
		 * me.input.unbindKey(me.input.KEY.LEFT);
		 */
		obj.unbindKey = function(keycode) {
			// clear the event status
			keyStatus[KeyBinding[keycode]] = false;
			keyLock[KeyBinding[keycode]] = false;
			// remove the key binding
			KeyBinding[keycode] = null;
		};

		/**
		 * Associate a mouse (button) action to a keycode
		 * Left button – 0
		 * Middle button – 1
		 * Right button – 2
		 * @name me.input#bindMouse
		 * @public
		 * @function
		 * @param {Integer} button (accordingly to W3C values : 0,1,2 for left, middle and right buttons)
		 * @param {me.input#KEY} keyCode
		 * @example
		 * // enable the keyboard
		 * me.input.bindKey(me.input.KEY.X, "shoot");
		 * // map the left button click on the X key
		 * me.input.bindMouse(me.input.mouse.LEFT, me.input.KEY.X);
		 */
		obj.bindMouse = function (button, keyCode)
		{
			// make sure the mouse is initialized
			if (!mouseInitialized)
				enableMouseEvent(true);
			// throw an exception if no action is defined for the specified keycode
			if (!KeyBinding[keyCode])
			  throw "melonJS : no action defined for keycode " + keyCode;
			// map the mouse button to the keycode
			me.input.mouse.bind[button] = keyCode;
		};
		/**
		 * unbind the defined keycode
		 * @name me.input#unbindMouse
		 * @public
		 * @function
		 * @param {Integer} button (accordingly to W3C values : 0,1,2 for left, middle and right buttons)
		 * @example
		 * me.input.unbindMouse(me.input.mouse.LEFT);
		 */
		obj.unbindMouse = function(button) {
			// clear the event status
			me.input.mouse.bind[button] = null;
		};

		/**
		 * register on a mouse event
		 * @name me.input#registerMouseEvent
		 * @public
		 * @function
		 * @param {String} eventType ('mousemove','mousedown','mouseup')
		 * @param {Function} callback
		 * @example
		 * // register on the 'mousemove' event
		 * me.input.registerMouseEvent('mousemove',this.mouseMove.bind(this));
		 */
		obj.registerMouseEvent = function(eventType, callback) {
			// make sure the mouse is initialized
			if (!mouseInitialized)
				enableMouseEvent(true);
			// register the mouse handler
			switch (eventType) {
				case 'mousemove':
				case 'mousedown':
				case 'mouseup':
					if (!obj.mouse.handlers[eventType]) {
						obj.mouse.handlers[eventType] = [];
 					}
					obj.mouse.handlers[eventType].push(callback);
					break;
				default :
					throw "melonJS : invalid event type : " + eventType;
			}
		};

		/**
		 * register on a mouse event
		 * @name me.input#registerMouseEvent
		 * @public
		 * @function
		 * @param {String} eventType ('mousemove','mousedown','mouseup')
		 * @param {Function} callback
		 * @example
		 * // register on the 'mousemove' event
		 * me.input.releaseMouseEvent('mousemove',this.mouseMove.bind(this));
		 */
		obj.releaseMouseEvent = function(eventType, callback) {
			switch (eventType) {
				case 'mousemove':
				case 'mousedown':
				case 'mouseup':
					if (obj.mouse.handlers[eventType]) {
						if (obj.mouse.handlers[eventType].length!=0) {
							obj.mouse.handlers[eventType].splice(obj.mouse.handlers[eventType].indexOf(callback), 1);
						}
					}
					break;
				default :
					throw "melonJS : invalid event type : " + eventType;
			}
		};

		/**
		 * enable gyroscopic event (not implemented)
		 * @name me.input#enableGyroscopicEvent
		 * @public
		 * @function
		 */
		obj.enableGyroscopicEvent = function(enable, callback) {
			if ($.sys.gyro) {
				// add a listener for the mouse
				$.ondevicemotion = enable ? onGyroEvent : null;
				// set the callback
				gyroEventCB = enable ? callback : null;
			}

		};

		// return our object
		return obj;

	})();

	/*---------------------------------------------------------*/
	// END END END
	/*---------------------------------------------------------*/
})(window);
<|endoftext|>