Permalink
Browse files

Enable gang managers to purchase gang chat encryption

  • Loading branch information...
RussellLVP committed Oct 23, 2016
1 parent 6d7c2e6 commit d4cd11b98170b8ef4bb6495cddaa7cda96784ab4
View
@@ -89,6 +89,8 @@
"GANG_LEAVE_PROMO_CONFIRMATION": "Are you sure that you want to leave the %s gang? %s (now a %s) will be promoted to its leader.",
"GANG_MEMBERS_HEADER": "{B1FC17}Members of the [%s] %s gang:",
"GANG_NOT_IN_GANG": "@error You need to be part of a gang in order to be able to use this command.",
"GANG_SETTINGS_ENC_TIME_BOUGHT": "You have purchased %s of chat encryption time for %$!",
"GANG_SETTINGS_ENC_TIME_MONEY": "This encryption package costs %$, but you only have %$!",
"GANG_SETTINGS_MEMBER_KICKED": "%s has been kicked from your gang.",
"GANG_SETTINGS_NAME_TAKEN": "Sorry, the name you chose is being used by another gang.",
"GANG_SETTINGS_NEW_COLOR": "Your gang's color has been changed to \"%s\".",
View
@@ -24,6 +24,45 @@ class Clock {
return highResolutionTime();
}
// Formats the given |date|, in seconds, relative to the current date.
formatRelativeTime(date, { allowFutureTimes = true } = {}) {
const seconds = Math.floor(this.currentTime() / 1000) - date;
const suffix = seconds < 0 ? ' from now'
: ' ago';
// Handle clock skew by the server, which isn't likely, but may happen.
if (seconds < 0 && !allowFutureTimes)
return 'In the future!';
// Handle visits that have occurred less than two minutes ago, which we consider to be now.
if (seconds < 60 && !allowFutureTimes)
return "Just now!";
// Otherwise, create separate buckets for minutes, hours, days, weeks, months and years.
const minutes = Math.floor(Math.abs(seconds) / 60);
if (minutes < 60)
return minutes + ' minute' + (minutes == 1 ? '' : 's') + suffix;
const hours = Math.floor(minutes / 60);
if (hours < 24)
return hours + ' hour' + (hours == 1 ? '' : 's') + suffix;
const days = Math.floor(hours / 24);
if (days < 7)
return days + ' day' + (days == 1 ? '' : 's') + suffix;
const weeks = Math.floor(days / 7);
if (weeks <= 4 && days < 30.25)
return weeks + ' week' + (weeks == 1 ? '' : 's') + suffix;
const months = Math.floor(days / 30.25);
if (months < 12)
return months + ' month' + (months == 1 ? '' : 's') + suffix;
const years = Math.floor(months / 12);
return years + ' year' + (years == 1 ? '' : 's') + suffix;
}
// Disposes of the instance.
dispose() {}
}
@@ -2,6 +2,7 @@
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.
const Clock = require('base/clock.js');
const PriorityQueue = require('base/priority_queue.js');
// Private variable to ensure that only a single MockClock exists at any given time. This is
@@ -93,4 +94,7 @@ class MockClock {
}
}
// Carry-over the formatRelativeTime() implementation from the real Clock.
MockClock.prototype.formatRelativeTime = Clock.prototype.formatRelativeTime;
exports = MockClock;
@@ -12,6 +12,8 @@ class Gang {
this.goal_ = info.goal;
this.color_ = info.color;
this.chatEncryptionExpiry_ = info.chatEncryptionExpiry;
this.members_ = new Map();
}
@@ -33,6 +35,11 @@ class Gang {
// Gets the color of members of this gang.
get color() { return this.color_; }
// Gets or sets the expiry time, in seconds since the UNIX epoch, at which the chat encryption
// for this gang expires. Should only be updated by the GangManager.
get chatEncryptionExpiry() { return this.chatEncryptionExpiry_; }
set chatEncryptionExpiry(value) { this.chatEncryptionExpiry_ = value; }
// Gets an iterable of the members in this gang. Must be used with `for of`.
get members() { return this.members_.keys(); }
@@ -2,12 +2,15 @@
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.
const alert = require('components/dialogs/alert.js');
const ColorPicker = require('components/dialogs/color_picker.js');
const CommandBuilder = require('components/command_manager/command_builder.js');
const Dialog = require('components/dialogs/dialog.js');
const Gang = require('features/gangs/gang.js');
const GangDatabase = require('features/gangs/gang_database.js');
const Menu = require('components/menu/menu.js');
const PlayerMoneyBridge = require('features/gangs/util/player_money_bridge.js');
const Question = require('components/dialogs/question.js');
const QuestionSequence = require('components/dialogs/question_sequence.js');
@@ -469,11 +472,14 @@ class GangCommands {
return;
}
const isLeader = gang.getPlayerRole(player) === Gang.ROLE_LEADER;
const isManager = isLeader || gang.getPlayerRole(player) === Gang.ROLE_MANAGER;
// Create a "gang has been created" promise that tests can use to observe progress.
this.settingsPromiseForTesting_ = new Promise(resolve => resolveForTests = resolve);
let menu = new Menu('Which setting do you want to change?', ['Option', 'Current value']);
if (gang.getPlayerRole(player) === Gang.ROLE_LEADER) {
if (isLeader) {
menu.addItem('Member settings', '-', () => {
this.manager_.getFullMemberList(gang).then(members => {
let memberMenu =
@@ -521,7 +527,61 @@ class GangCommands {
}).then(() => resolveForTests());
});
}
if (isManager) {
let encryptionLabel = '';
if (!gang.chatEncryptionExpiry) {
encryptionLabel = '{FF0000}No encryption';
} else {
const timeDiff = server.clock.formatRelativeTime(gang.chatEncryptionExpiry);
encryptionLabel = timeDiff.includes('from now') ? '{00FF00}Secure until ' + timeDiff
: '{FF0000}Expired ' + timeDiff;
}
menu.addItem('Gang chat encryption', encryptionLabel, async() => {
const purchaseMenu = new Menu('How much encryption time to buy?', ['Time', 'Price'])
const prices = [
[ '1 hour', 25000, 3600 ],
[ '1 day', 300000, 86400 ],
[ '1 week', 1750000, 604800 ],
[ '1 month', 5500000, 2613600 ]
];
const currentBalance = await PlayerMoneyBridge.getBalanceForPlayer(player);
for (const [label, price, seconds] of prices) {
const pricePrefix = (price < currentBalance ? '{00FF00}' : '{FF0000}');
const priceLabel = pricePrefix + Message.formatPrice(price);
purchaseMenu.addItem(label, priceLabel, async(player) => {
const balance = await PlayerMoneyBridge.getBalanceForPlayer(player);
if (balance < price) {
return await alert(player, {
title: 'Unable to purchase the additional time',
message: Message.format(Message.GANG_SETTINGS_ENC_TIME_MONEY,
price, balance)
});
}
await PlayerMoneyBridge.setBalanceForPlayer(player, balance - price);
await this.manager_.updateChatEncryption(gang, player, seconds);
await alert(player, {
title: 'The encryption package has been purchased',
message: Message.format(Message.GANG_SETTINGS_ENC_TIME_BOUGHT,
label, price)
});
});
}
await purchaseMenu.displayForPlayer(player);
resolveForTests();
});
}
if (isLeader) {
menu.addItem('Gang name', gang.name, () => {
Question.ask(player, NAME_QUESTION).then(answer => {
if (!answer)
@@ -7,6 +7,7 @@ const Gang = require('features/gangs/gang.js');
const GangCommands = require('features/gangs/gang_commands.js');
const GangManager = require('features/gangs/gang_manager.js');
const MockGangDatabase = require('features/gangs/test/mock_gang_database.js');
const PlayerMoneyBridge = require('features/gangs/util/player_money_bridge.js');
describe('GangCommands', (it, beforeEach, afterEach) => {
let player = null;
@@ -40,7 +41,8 @@ describe('GangCommands', (it, beforeEach, afterEach) => {
tag: tag,
name: name,
goal: goal || 'Testing gang',
color: color
color: color,
chatEncryptionExpiry: 0
});
return gangManager.gangs_[gangId];
@@ -596,7 +598,7 @@ describe('GangCommands', (it, beforeEach, afterEach) => {
assert.isTrue(player.issueCommand('/gang settings'));
assert.equal(player.messages.length, 0);
player.respondToDialog({ listitem: 1 /* Gang color */ }).then(() =>
player.respondToDialog({ listitem: 1 /* Member color */ }).then(() =>
player.respondToDialog({ response: 0 /* Ok */}));
return gangCommands.settingsPromiseForTesting_.then(() => {
@@ -608,6 +610,61 @@ describe('GangCommands', (it, beforeEach, afterEach) => {
});
});
it('should enable managers to purchase gang chat encryption time', async(assert) => {
const gang = createGang();
player.identify({ userId: 1337 });
addPlayerToGang(player, gang, Gang.ROLE_MANAGER);
PlayerMoneyBridge.setMockedBalanceForTests(25000000); // give the |player| 25M
assert.equal(gang.chatEncryptionExpiry, 0);
for (let days = 1; days < 3; ++days) {
player.respondToDialog({ listitem: 0 /* Gang Chat Encryption */ }).then(() =>
player.respondToDialog({ response: 1, listitem: 1 /* one day */ })).then(() =>
player.respondToDialog({ response: 1 /* Yeah I got it */}));
assert.isTrue(player.issueCommand('/gang settings'));
assert.equal(player.messages.length, 0);
await gangCommands.settingsPromiseForTesting_;
// Verify that the |chatEncryptionExpiry| property has been updated with a day.
assert.closeTo(
gang.chatEncryptionExpiry, (server.clock.currentTime() / 1000) + 86400 * days, 5);
}
PlayerMoneyBridge.setMockedBalanceForTests(null);
});
it('should do balance checks when purchasing gang chat encryption time', async(assert) => {
const gang = createGang();
player.identify({ userId: 1337 });
addPlayerToGang(player, gang, Gang.ROLE_MANAGER);
PlayerMoneyBridge.setMockedBalanceForTests(12500); // make sure the |player| has few monies
assert.equal(gang.chatEncryptionExpiry, 0);
player.respondToDialog({ listitem: 0 /* Gang Chat Encryption */ }).then(() =>
player.respondToDialog({ response: 1, listitem: 1 /* one day */ })).then(() =>
player.respondToDialog({ response: 1 /* Yeah I got it */}));
assert.isTrue(player.issueCommand('/gang settings'));
assert.equal(player.messages.length, 0);
await gangCommands.settingsPromiseForTesting_;
assert.equal(gang.chatEncryptionExpiry, 0);
assert.equal(
player.lastDialog, Message.format(Message.GANG_SETTINGS_ENC_TIME_MONEY, 300000, 12500));
PlayerMoneyBridge.setMockedBalanceForTests(null);
});
it('should not enable leaders to change the name to an existing one', assert => {
const gang = createGang({ name: 'Candy Crush' });
@@ -621,7 +678,7 @@ describe('GangCommands', (it, beforeEach, afterEach) => {
assert.isTrue(player.issueCommand('/gang settings'));
assert.equal(player.messages.length, 0);
player.respondToDialog({ listitem: 2 /* Gang name */ }).then(() =>
player.respondToDialog({ listitem: 3 /* Gang name */ }).then(() =>
player.respondToDialog({ inputtext: 'Hello Kitty Online' })).then(() =>
player.respondToDialog({ response: 0 /* Ok */}));
@@ -645,7 +702,7 @@ describe('GangCommands', (it, beforeEach, afterEach) => {
assert.isTrue(player.issueCommand('/gang settings'));
assert.equal(player.messages.length, 0);
player.respondToDialog({ listitem: 2 /* Gang name */ }).then(() =>
player.respondToDialog({ listitem: 3 /* Gang name */ }).then(() =>
player.respondToDialog({ inputtext: 'Thundering Offline Kittens' })).then(() =>
player.respondToDialog({ response: 0 /* Ok */}));
@@ -670,7 +727,7 @@ describe('GangCommands', (it, beforeEach, afterEach) => {
assert.isTrue(player.issueCommand('/gang settings'));
assert.equal(player.messages.length, 0);
player.respondToDialog({ listitem: 3 /* Gang tag */ }).then(() =>
player.respondToDialog({ listitem: 4 /* Gang tag */ }).then(() =>
player.respondToDialog({ inputtext: 'HKO' })).then(() =>
player.respondToDialog({ response: 0 /* Ok */}));
@@ -694,7 +751,7 @@ describe('GangCommands', (it, beforeEach, afterEach) => {
assert.isTrue(player.issueCommand('/gang settings'));
assert.equal(player.messages.length, 0);
player.respondToDialog({ listitem: 3 /* Gang tag */ }).then(() =>
player.respondToDialog({ listitem: 4 /* Gang tag */ }).then(() =>
player.respondToDialog({ inputtext: 'GG' })).then(() =>
player.respondToDialog({ response: 0 /* Ok */}));
@@ -718,7 +775,7 @@ describe('GangCommands', (it, beforeEach, afterEach) => {
assert.isTrue(player.issueCommand('/gang settings'));
assert.equal(player.messages.length, 0);
player.respondToDialog({ listitem: 4 /* Gang goal */ }).then(() =>
player.respondToDialog({ listitem: 5 /* Gang goal */ }).then(() =>
player.respondToDialog({ inputtext: 'We rule more!' })).then(() =>
player.respondToDialog({ response: 0 /* Ok */}));
@@ -9,11 +9,23 @@ const LOAD_GANG_FOR_PLAYER_QUERY = `
SELECT
users_gangs.user_role,
users_gangs.user_use_gang_color,
gangs.*
gangs.*,
UNIX_TIMESTAMP(gang_chat_encryption.encryption_expire) AS encryption_expire
FROM
users_gangs
LEFT JOIN
gangs ON gangs.gang_id = users_gangs.gang_id
LEFT JOIN
(
SELECT
gang_chat_encryption.gang_id,
MAX(gang_chat_encryption.encryption_expire) AS encryption_expire
FROM
gang_chat_encryption
GROUP BY
gang_chat_encryption.gang_id
) AS gang_chat_encryption ON
gang_chat_encryption.gang_id = users_gangs.gang_id
WHERE
users_gangs.user_id = ? AND
users_gangs.gang_id = ? AND
@@ -138,6 +150,14 @@ const GANG_UPDATE_ROLE_QUERY = `
users_gangs.gang_id = ? AND
users_gangs.left_gang IS NULL`;
// Query to purchase additional encryption time for the gang's communications.
const PURCHASE_CHAT_ENCRYPTION_QUERY = `
INSERT INTO
gang_chat_encryption
(gang_id, user_id, purchase_date, purchase_amount, encryption_expire)
VALUES
(?, ?, NOW(), ?, ?)`;
// Query to update the color of a gang.
const GANG_UPDATE_COLOR_QUERY = `
UPDATE
@@ -208,7 +228,8 @@ class GangDatabase {
tag: info.gang_tag,
name: info.gang_name,
goal: info.gang_goal,
color: info.gang_color ? Color.fromNumberRGBA(info.gang_color) : null
color: info.gang_color ? Color.fromNumberRGBA(info.gang_color) : null,
chatEncryptionExpiry: info.encryption_expire || 0
}
};
});
@@ -270,7 +291,8 @@ class GangDatabase {
tag: tag,
name: name,
goal: goal,
color: null
color: null,
chatEncryptionExpiry: 0
};
});
}
@@ -338,6 +360,14 @@ class GangDatabase {
userId, gang.id);
}
// Asynchronously creates an entry in the database where the |player| member of the |gang| has
// purchased an additional |encryptionTime| seconds of gang chat encryption.
async purchaseChatEncryption(gang, player, encryptionTime) {
await this.database_.query(
PURCHASE_CHAT_ENCRYPTION_QUERY, gang.id, player.id, encryptionTime,
gang.chatEncryptionExpiry);
}
// Updates the color of the |gang| to |color|. Returns a promise that will be resolved when the
// database has been updated with the new information.
updateColor(gang, color) {
Oops, something went wrong.

0 comments on commit d4cd11b

Please sign in to comment.