Skip to content

Commit

Permalink
Merge pull request #456 from uwcsc/issue#-307
Browse files Browse the repository at this point in the history
Issue# 307 Add Receiver Confirmation to Coin Transfer
  • Loading branch information
Fan-Yang-284 authored Mar 6, 2023
2 parents 26062b4 + 2e87926 commit 92edc44
Show file tree
Hide file tree
Showing 5 changed files with 324 additions and 43 deletions.
56 changes: 21 additions & 35 deletions src/commandDetails/coin/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,10 @@ import {
SapphireMessageExecuteType,
getUserFromMessage,
SapphireMessageResponseWithMetadata,
SapphireAfterReplyType,
} from '../../codeyCommand';
import {
adjustCoinBalanceByUserId,
getCoinBalanceByUserId,
UserCoinEvent,
} from '../../components/coin';
import { getCoinBalanceByUserId, transferTracker } from '../../components/coin';
import { getCoinEmoji } from '../../components/emojis';
import { pluralize } from '../../utils/pluralize';

const coinTransferExecuteCommand: SapphireMessageExecuteType = async (
client,
Expand All @@ -36,7 +32,7 @@ const coinTransferExecuteCommand: SapphireMessageExecuteType = async (
}

// Optional argument is reason
const reason = args['reason'];
const reason = <string>args['reason'] ?? '';

// Retrieve sending user
const sendingUser = getUserFromMessage(messageFromUser);
Expand All @@ -53,41 +49,30 @@ const coinTransferExecuteCommand: SapphireMessageExecuteType = async (
`You don't have enough ${getCoinEmoji()} to transfer that amount.`,
{},
);
} else if (amount <= 0) {
} else if (amount < 1) {
return new SapphireMessageResponseWithMetadata(`You can't transfer less than 1 coin.`, {});
}

// Adjust the receiver balance with coins transferred
await adjustCoinBalanceByUserId(
receivingUser.id,
const transfer = await transferTracker.startTransfer(
sendingUser,
receivingUser,
amount,
UserCoinEvent.AdminCoinAdjust,
<string>(reason ?? ''),
client.user?.id,
reason,
client,
messageFromUser.channelId,
);

// Get new receiver balance
const newReceiverBalance = await getCoinBalanceByUserId(receivingUser.id);

// Adjust the sender balance with coins transferred
await adjustCoinBalanceByUserId(
sendingUser.id,
<number>(-1 * amount),
UserCoinEvent.AdminCoinAdjust,
<string>(reason ?? ''),
client.user?.id,
);

// Get new sender balance
const newSenderBalance = await getCoinBalanceByUserId(sendingUser.id);
return new SapphireMessageResponseWithMetadata(await transfer.getTransferResponse(), {
transferId: transfer.transferId,
});
};

return `${receivingUser.username} now has ${newReceiverBalance} Codey ${pluralize(
'coin',
newReceiverBalance,
)} ${getCoinEmoji()}. ${sendingUser.username} now has ${newSenderBalance} Codey ${pluralize(
'coin',
newReceiverBalance,
)} ${getCoinEmoji()}.`;
const transferAfterMessageReply: SapphireAfterReplyType = async (result, sentMessage) => {
if (typeof result.metadata.transferId === 'undefined') return;
// Store the message which the game takes place in the game object
transferTracker.runFuncOnTransfer(<string>result.metadata.transferId, (transfer) => {
transfer.transferMessage = sentMessage;
});
};

export const coinTransferCommandDetails: CodeyCommandDetails = {
Expand All @@ -100,6 +85,7 @@ export const coinTransferCommandDetails: CodeyCommandDetails = {

isCommandResponseEphemeral: false,
messageWhenExecutingCommand: 'Transferring coins...',
afterMessageReply: transferAfterMessageReply,
executeCommand: coinTransferExecuteCommand,
options: [
{
Expand Down
197 changes: 196 additions & 1 deletion src/components/coin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import _ from 'lodash';
import _, { uniqueId } from 'lodash';
import { openDB } from './db';
import { SapphireClient } from '@sapphire/framework';
import { ColorResolvable, MessageActionRow, MessageButton, MessageEmbed, User } from 'discord.js';
import { SapphireMessageResponse, SapphireSentMessageType } from '../codeyCommand';
import { pluralize } from '../utils/pluralize';
import { getCoinEmoji, getEmojiByName } from './emojis';

export enum BonusType {
Daily = 0,
Expand All @@ -17,6 +22,8 @@ export enum UserCoinEvent {
RpsLoss,
RpsDrawAgainstCodey,
RpsWin,
CoinTransferReceiver,
CoinTransferSender,
}

export type Bonus = {
Expand Down Expand Up @@ -269,3 +276,191 @@ export const applyBonusByUserId = async (userId: string): Promise<boolean> => {
}
return false;
};

export enum TransferSign {
Pending = 0,
Accept = 1,
Decline = 2,
}

export const getEmojiFromSign = (sign: TransferSign): string => {
switch (sign) {
case TransferSign.Pending:
return '❓';
case TransferSign.Accept:
return '✅';
case TransferSign.Decline:
return '❌';
}
};

export enum TransferResult {
Pending,
Rejected,
Confirmed,
}

type TransferState = {
sender: User;
receiver: User;
result: TransferResult;
amount: number;
reason?: string;
};

class TransferTracker {
transfers: Map<string, Transfer>; // id, transfer

constructor() {
this.transfers = new Map<string, Transfer>();
}
getTransferFromId(id: string): Transfer | undefined {
return this.transfers.get(id);
}

runFuncOnTransfer(transferId: string, func: (transfer: Transfer) => void): void {
func(this.getTransferFromId(transferId)!);
}

async startTransfer(
sender: User,
receiver: User,
amount: number,
channelId: string,
client: SapphireClient<boolean>,
reason?: string,
): Promise<Transfer> {
const transferId = uniqueId();
const transferState: TransferState = {
sender: sender,
receiver: receiver,
amount: amount,
reason: reason ?? '',
result: TransferResult.Pending,
};
const transfer = new Transfer(channelId, client, transferId, transferState);
this.transfers.set(transferId, transfer);
return transfer;
}

async endTransfer(transferId: string): Promise<void> {
const transfer = this.transfers.get(transferId);
if (!transfer) {
throw new Error(`No transfer with transfer ID ${transferId} found`);
}

if (transfer.state.result === TransferResult.Pending) return;
await transfer.handleTransaction();
}
}

export const transferTracker = new TransferTracker();

export class Transfer {
channelId: string;
client: SapphireClient<boolean>;
state: TransferState;
transferId: string;
transferMessage!: SapphireSentMessageType;

constructor(
channelId: string,
client: SapphireClient<boolean>,
transferId: string,
transferState: TransferState,
) {
this.channelId = channelId;
this.state = transferState;
this.client = client;
this.transferId = transferId;
}

// called if state is (believed to be) no longer pending. Transfers coins and updates balances if transfer is confirmed
async handleTransaction(): Promise<void> {
if (this.state.result === TransferResult.Confirmed) {
// Adjust the receiver balance with coins transferred
await adjustCoinBalanceByUserId(
this.state.receiver.id,
this.state.amount,
UserCoinEvent.CoinTransferReceiver,
<string>(this.state.reason ?? ''),
this.client.user?.id,
);

// Adjust the sender balance with coins transferred
await adjustCoinBalanceByUserId(
this.state.sender.id,
<number>(-1 * this.state.amount),
UserCoinEvent.CoinTransferSender,
<string>(this.state.reason ?? ''),
this.client.user?.id,
);
}
}

public getEmbedColor(): ColorResolvable {
switch (this.state.result) {
case TransferResult.Confirmed:
return 'GREEN';
case TransferResult.Rejected:
return 'RED';
default:
return 'YELLOW';
}
}

public async getStatusAsString(): Promise<string> {
switch (this.state.result) {
case TransferResult.Confirmed:
const newReceiverBalance = await getCoinBalanceByUserId(this.state.receiver.id);
const newSenderBalance = await getCoinBalanceByUserId(this.state.sender.id);
return `${this.state.receiver.username} accepted the transfer. ${
this.state.receiver.username
} now has ${newReceiverBalance} Codey ${pluralize(
'coin',
newReceiverBalance,
)} ${getCoinEmoji()}. ${
this.state.sender.username
} now has ${newSenderBalance} Codey ${pluralize(
'coin',
newReceiverBalance,
)} ${getCoinEmoji()}.`;
case TransferResult.Rejected:
return `This transfer was rejected by ${this.state.receiver.username}.`;
case TransferResult.Pending:
return 'Please choose whether you would like to accept this transfer.';
default:
return `Something went wrong! ${getEmojiByName('codey_sad')}`;
}
}

public async getTransferResponse(): Promise<SapphireMessageResponse> {
const embed = new MessageEmbed()
.setColor(this.getEmbedColor())
.setTitle('Coin Transfer')
.setDescription(
`
Amount: ${this.state.amount} ${getCoinEmoji()}
Sender: ${this.state.sender.username}
${await this.getStatusAsString()}
`,
);
// Buttons
const row = new MessageActionRow().addComponents(
new MessageButton()
.setCustomId(`transfer-check-${this.transferId}`)
.setLabel('Accept')
.setStyle('SUCCESS'),
new MessageButton()
.setCustomId(`transfer-x-${this.transferId}`)
.setLabel('Reject')
.setStyle('DANGER'),
);

return {
embeds: [embed],
components: this.state.result === TransferResult.Pending ? [row] : [],
};
}
}
9 changes: 3 additions & 6 deletions src/interaction-handlers/games/rps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
Maybe,
PieceContext,
} from '@sapphire/framework';
import { ButtonInteraction, CommandInteraction, Message, MessagePayload } from 'discord.js';
import { ButtonInteraction } from 'discord.js';
import { getEmojiByName } from '../../components/emojis';
import { getCodeyRpsSign, RpsGameSign, rpsGameTracker } from '../../components/games/rps';
import { updateMessageEmbed } from '../../utils/embeds';

export class RpsHandler extends InteractionHandler {
public constructor(ctx: PieceContext, options: InteractionHandler.Options) {
Expand Down Expand Up @@ -64,11 +65,7 @@ export class RpsHandler extends InteractionHandler {
game.state.player2Sign = getCodeyRpsSign();
game.setStatus(undefined);
}
if (game.gameMessage instanceof Message) {
game.gameMessage.edit(<MessagePayload>game.getGameResponse());
} else if (game.gameMessage instanceof CommandInteraction) {
game.gameMessage.editReply(<MessagePayload>game.getGameResponse());
}
updateMessageEmbed(game.gameMessage, game.getGameResponse());
});
rpsGameTracker.endGame(result.gameId);
}
Expand Down
Loading

0 comments on commit 92edc44

Please sign in to comment.