From 36c5d03633d2acde09fabda99f66b73b1bb6c579 Mon Sep 17 00:00:00 2001 From: Brian Date: Fri, 14 Jan 2022 13:52:24 -0500 Subject: [PATCH 01/19] handle poap claim exceptions --- CHANGELOG.md | 5 ++ src/app/commands/admin/Account.ts | 48 +++++++++++++++++-- .../poap/HandleParticipantDuringEvent.ts | 5 +- src/app/service/account/AccountStatus.ts | 0 src/app/service/account/UnlinkAccount.ts | 10 ++++ src/app/service/poap/ClaimPOAP.ts | 21 ++++++-- src/app/service/poap/OptInPOAP.ts | 15 +++++- 7 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 src/app/service/account/AccountStatus.ts create mode 100644 src/app/service/account/UnlinkAccount.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f7d568d..b3a31b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2.7.0-RELEASE (2022-01-18) + +1. Add more stability to opt-in messages +2. Add account unlink/status commands + ## 2.6.2-RELEASE (2022-01-13) 1. Handle twitter spaces exceptions diff --git a/src/app/commands/admin/Account.ts b/src/app/commands/admin/Account.ts index 304b78e5..69db3e31 100644 --- a/src/app/commands/admin/Account.ts +++ b/src/app/commands/admin/Account.ts @@ -1,5 +1,5 @@ import { CommandContext, CommandOptionType, SlashCommand, SlashCreator } from 'slash-create'; -import { LogUtils } from '../../utils/Log'; +import Log, { LogUtils } from '../../utils/Log'; import VerifyTwitter from '../../service/account/VerifyTwitter'; import ServiceUtils from '../../utils/ServiceUtils'; import discordServerIds from '../../service/constants/discordServerIds'; @@ -22,6 +22,25 @@ export default class Account extends SlashCommand { name: 'verify', type: CommandOptionType.SUB_COMMAND, description: 'Link DEGEN to your account or wallet.', + options: [ + { + name: 'platform', + type: CommandOptionType.STRING, + description: 'Type of account or wallet to unlink from discord.', + required: true, + choices: [ + { + name: 'Twitter', + value: 'TWITTER_ACCOUNT', + }, + ], + }, + ], + }, + { + name: 'unlink', + type: CommandOptionType.SUB_COMMAND, + description: 'Link DEGEN to your account or wallet.', options: [ { name: 'platform', @@ -37,6 +56,12 @@ export default class Account extends SlashCommand { }, ], }, + { + name: 'status', + type: CommandOptionType.SUB_COMMAND, + description: 'Check linked accounts', + options: [], + }, ], }); } @@ -51,14 +76,29 @@ export default class Account extends SlashCommand { return; } - const { guildMember } = await ServiceUtils.getGuildAndMember(ctx.guildID, ctx.user.id); + const subCommand: string = ctx.subcommands[0]; + try { - await VerifyTwitter(ctx, guildMember, true).catch(e => { throw e; }); + const { guildMember } = await ServiceUtils.getGuildAndMember(ctx.guildID, ctx.user.id); + + switch (subCommand) { + case 'verify': + await VerifyTwitter(ctx, guildMember, true).catch(e => { throw e; }); + break; + case 'unlink': + break; + case 'status': + break; + default: + await ctx.send({ content: 'Please try again' }).catch(Log.error); + break; + } + } catch (e) { if (e instanceof ValidationError) { await ctx.send({ content: `${e.message}`, ephemeral: true }); } else { - LogUtils.logError('failed to verify user', e, guildMember.guild.id); + LogUtils.logError('failed to verify user', e); await ServiceUtils.sendOutErrorMessage(ctx); } } diff --git a/src/app/events/poap/HandleParticipantDuringEvent.ts b/src/app/events/poap/HandleParticipantDuringEvent.ts index da4660bd..bfb7714e 100644 --- a/src/app/events/poap/HandleParticipantDuringEvent.ts +++ b/src/app/events/poap/HandleParticipantDuringEvent.ts @@ -23,19 +23,20 @@ type BasicUser = { const HandleParticipantDuringEvent = async (oldState: VoiceState, newState: VoiceState): Promise => { if (hasUserBeenDeafened(oldState, newState)) { - Log.log(`user has deafened, userId: ${newState.id}`); if (await isChannelActivePOAPEvent(oldState.channelId, oldState.guild.id)) { + Log.log(`user has deafened for previous channel, userId: ${newState.id}`); await removeDeafenedUser(oldState.channelId, oldState.guild.id, oldState.id); } if (newState.channelId != oldState.channelId && await isChannelActivePOAPEvent(newState.channelId, newState.guild.id)) { + Log.log(`user has deafened for new channel, userId: ${newState.id}`); await removeDeafenedUser(newState.channelId, newState.guild.id, newState.id); } return; } if (hasUserBeenUnDeafened(oldState, newState)) { - Log.log(`user has undeafened, userId: ${newState.id}`); if (await isChannelActivePOAPEvent(newState.channelId, newState.guild.id)) { + Log.log(`user has undeafened for new channel, userId: ${newState.id}`); await startTrackingUserParticipation({ id: newState.id, tag: newState.member?.user.tag }, newState.guild.id, newState.channelId); } } diff --git a/src/app/service/account/AccountStatus.ts b/src/app/service/account/AccountStatus.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/service/account/UnlinkAccount.ts b/src/app/service/account/UnlinkAccount.ts new file mode 100644 index 00000000..7518922e --- /dev/null +++ b/src/app/service/account/UnlinkAccount.ts @@ -0,0 +1,10 @@ +import { CommandContext } from 'slash-create'; +import Log from '../../utils/Log'; + +const UnlinkAccount = async (ctx: CommandContext, platform: string): Promise => { + Log.debug(`starting to unlink account ${platform}`); + + +}; + +export default UnlinkAccount; \ No newline at end of file diff --git a/src/app/service/poap/ClaimPOAP.ts b/src/app/service/poap/ClaimPOAP.ts index cc65e797..a83f612a 100644 --- a/src/app/service/poap/ClaimPOAP.ts +++ b/src/app/service/poap/ClaimPOAP.ts @@ -3,21 +3,24 @@ import { DMChannel, GuildMember, MessageEmbedOptions, + Message as Message, } from 'discord.js'; import { Collection, Cursor, Db } from 'mongodb'; import MongoDbUtils from '../../utils/MongoDbUtils'; import constants from '../constants/constants'; import { POAPUnclaimedParticipants } from '../../types/poap/POAPUnclaimedParticipants'; -import Log from '../../utils/Log'; +import Log, { LogUtils } from '../../utils/Log'; import { POAPTwitterUnclaimedParticipants } from '../../types/poap/POAPTwitterUnclaimedParticipants'; import VerifyTwitter, { VerifiedTwitter } from '../account/VerifyTwitter'; import dayjs from 'dayjs'; import { EmbedField as EmbedFieldSlash, + Message as MessageSlash, MessageEmbedOptions as MessageEmbedOptionsSlash, } from 'slash-create/lib/structures/message'; import POAPUtils from '../../utils/POAPUtils'; import apiKeys from '../constants/apiKeys'; +import ValidationError from '../../errors/ValidationError'; const ClaimPOAP = async (ctx: CommandContext, platform: string, guildMember?: GuildMember): Promise => { Log.debug(`starting claim for ${ctx.user.username}, with ID: ${ctx.user.id}`); @@ -64,22 +67,32 @@ export const claimForDiscord = async (userId: string, ctx?: CommandContext | nul unclaimedParticipants = await unclaimedParticipantsCollection.find({ discordUserId: userId, }); - + let result: Message | MessageSlash | boolean | void; if (ctx) { Log.debug('sending message in channel'); await ctx.send({ content: `POAP claimed! Consider sending \`gm\` to <@${apiKeys.DISCORD_BOT_ID}>` }); const embeds: MessageEmbedOptionsSlash[] = await generatePOAPClaimEmbedMessages(numberOfPOAPs, unclaimedParticipants) as MessageEmbedOptionsSlash[]; - await ctx.send({ + result = await ctx.send({ embeds: embeds, ephemeral: true, + }).catch(e => { + LogUtils.logError('failed to provide poap links to user', e); + throw new ValidationError('try the command in a valid channel'); }); } else if (dmChannel) { Log.debug('sending DM to user'); const embeds: MessageEmbedOptions[] = await generatePOAPClaimEmbedMessages(numberOfPOAPs, unclaimedParticipants) as MessageEmbedOptions[]; - await dmChannel.send({ + result = await dmChannel.send({ embeds: embeds, + }).catch(e => { + LogUtils.logError('failed to send POAP DMs to user', e); + throw new ValidationError('try turning on DMs'); }); } + if (result == null) { + Log.warn('failed to send poaps'); + return; + } Log.debug('message sent to user!'); diff --git a/src/app/service/poap/OptInPOAP.ts b/src/app/service/poap/OptInPOAP.ts index 91e7b6c7..4892d90f 100644 --- a/src/app/service/poap/OptInPOAP.ts +++ b/src/app/service/poap/OptInPOAP.ts @@ -56,10 +56,19 @@ const OptInPOAP = async (user: User, dmChannel: DMChannel): Promise => { .setStyle('SECONDARY'), ); Log.debug('user has DMs option turned off, now asking user for opt-in to get DM POAPs'); - const message: Message = await dmChannel.send({ + const message: Message | void = await dmChannel.send({ content: 'I can send you POAPs directly to you. Would you like me to do that going forward?', components: [row], + }).catch(e => { + LogUtils.logError('failed to ask for opt-in', e); + return; }); + + if (message == null) { + Log.debug('did not sent opt-in message'); + return; + } + // 5 minute timeout try { await message.awaitMessageComponent({ @@ -102,7 +111,9 @@ const OptInPOAP = async (user: User, dmChannel: DMChannel): Promise => { Log.debug('user settings update skipped'); } else { Log.debug(`user is opted in to dms, userId: ${user.id}`); - await dmChannel.send({ content: 'I will send you POAPs as soon as I get them!' }); + await dmChannel.send({ content: 'I will send you POAPs as soon as I get them!' }).catch(e => { + LogUtils.logError('failed to send opt-in confirmation', e); + }); } }; From cbc954f215e704f8ee8ddcf8569985f0ea3387e8 Mon Sep 17 00:00:00 2001 From: Brian Date: Fri, 14 Jan 2022 17:45:28 -0500 Subject: [PATCH 02/19] upgrade deps and other changes --- CHANGELOG.md | 2 + package.json | 4 +- src/app/commands/admin/Account.ts | 4 +- src/app/commands/help/Help.ts | 4 + src/app/commands/poap/POAP.ts | 12 +- src/app/events/poap/IntroductionMessage.ts | 0 src/app/service/constants/allowedServers.ts | 127 ++++++++++++++++++++ src/app/service/help/HowToAccount.ts | 35 ++++++ src/app/utils/POAPUtils.ts | 2 +- src/app/utils/ServiceUtils.ts | 2 +- yarn.lock | 22 ++-- 11 files changed, 189 insertions(+), 25 deletions(-) create mode 100644 src/app/events/poap/IntroductionMessage.ts create mode 100644 src/app/service/constants/allowedServers.ts create mode 100644 src/app/service/help/HowToAccount.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a31b8d..6351a8d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ 1. Add more stability to opt-in messages 2. Add account unlink/status commands +3. Restrict DEGEN to certain discord servers +4. Upgrade discord.js -> 13.6.0, upgrade slash-create -> 5.0.3 ## 2.6.2-RELEASE (2022-01-13) diff --git a/package.json b/package.json index 37de750f..77228697 100644 --- a/package.json +++ b/package.json @@ -44,14 +44,14 @@ "csv-parse": "^5.0.3", "csv-stringify": "^6.0.3", "dayjs": "^1.10.7", - "discord.js": "^13.5.1", + "discord.js": "^13.6.0", "dotenv": "^10.0.0", "form-data": "^4.0.0", "lodash.clonedeep": "^4.5.0", "lodash.isequal": "^4.5.0", "mongodb": "^3.6.9", "p-queue": "^6.6.2", - "slash-create": "^5.0.2", + "slash-create": "^5.0.3", "twitter-api-v2": "^1.6.5", "uuid": "^8.3.2" }, diff --git a/src/app/commands/admin/Account.ts b/src/app/commands/admin/Account.ts index 69db3e31..43002507 100644 --- a/src/app/commands/admin/Account.ts +++ b/src/app/commands/admin/Account.ts @@ -2,9 +2,9 @@ import { CommandContext, CommandOptionType, SlashCommand, SlashCreator } from 's import Log, { LogUtils } from '../../utils/Log'; import VerifyTwitter from '../../service/account/VerifyTwitter'; import ServiceUtils from '../../utils/ServiceUtils'; -import discordServerIds from '../../service/constants/discordServerIds'; import ValidationError from '../../errors/ValidationError'; import { command } from '../../utils/SentryUtils'; +import allowedServers from '../../service/constants/allowedServers'; export default class Account extends SlashCommand { constructor(creator: SlashCreator) { @@ -15,7 +15,7 @@ export default class Account extends SlashCommand { usages: 1, duration: 2, }, - guildIDs: [discordServerIds.banklessDAO, discordServerIds.discordBotGarage], + guildIDs: allowedServers, defaultPermission: true, options: [ { diff --git a/src/app/commands/help/Help.ts b/src/app/commands/help/Help.ts index 952f6c91..172ba8f3 100644 --- a/src/app/commands/help/Help.ts +++ b/src/app/commands/help/Help.ts @@ -7,6 +7,7 @@ import { import HowToPOAP from '../../service/help/HowToPOAP'; import { LogUtils } from '../../utils/Log'; import { command } from '../../utils/SentryUtils'; +import HowToAccount from '../../service/help/HowToAccount'; export default class Help extends SlashCommand { constructor(creator: SlashCreator) { @@ -38,6 +39,9 @@ export default class Help extends SlashCommand { case 'poap': messageOptions = HowToPOAP(); break; + case 'account': + messageOptions = HowToAccount(); + break; default: messageOptions = { content: 'Invalid command selected' }; break; diff --git a/src/app/commands/poap/POAP.ts b/src/app/commands/poap/POAP.ts index 5cf49fc4..f042ada0 100644 --- a/src/app/commands/poap/POAP.ts +++ b/src/app/commands/poap/POAP.ts @@ -18,12 +18,19 @@ import { GuildMember } from 'discord.js'; import ModifyPOAP from '../../service/poap/config/ModifyPOAP'; import StatusPOAP from '../../service/poap/config/StatusPOAP'; import { command } from '../../utils/SentryUtils'; +import allowedServers from '../../service/constants/allowedServers'; export default class POAP extends SlashCommand { constructor(creator: SlashCreator) { super(creator, { name: 'poap', description: 'Receive a list of all attendees in the specified voice channel and optionally send out POAP links.', + throttling: { + usages: 10, + duration: 1, + }, + defaultPermission: true, + guildIDs: allowedServers, options: [ { name: 'config', @@ -205,11 +212,6 @@ export default class POAP extends SlashCommand { ], }, ], - throttling: { - usages: 10, - duration: 1, - }, - defaultPermission: true, }); } diff --git a/src/app/events/poap/IntroductionMessage.ts b/src/app/events/poap/IntroductionMessage.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/service/constants/allowedServers.ts b/src/app/service/constants/allowedServers.ts new file mode 100644 index 00000000..2c821956 --- /dev/null +++ b/src/app/service/constants/allowedServers.ts @@ -0,0 +1,127 @@ +const allowedServers: string[] = [ + '850840267082563596', + '748031363935895552', + '851552281249972254', + '895442799971942410', + '93153043494739968', + '553741558869131266', + '815892525407535104', + '834499078434979890', + '879721699875102720', + '890276404187516948', + '893893180351713370', + '641757613889159209', + '819669388370247732', + '820637983488475186', + '845400066532704256', + '847908414981275648', + '860356969521217536', + '877380330997305396', + '887426921892315137', + '894694737922113546', + '904913380131880970', + '909933079181799524', + '913643651903676476', + '923790332863328316', + '721358849034158104', + '732297947387002990', + '827890916726931497', + '864154905958154290', + '877883581903564860', + '808358975287722045', + '864926382954250242', + '866378471868727316', + '907267970630307844', + '917822256657350678', + '875424808194691113', + '883478451850473483', + '902943676685230100', + '917372465699782686', + '629836507280048129', + '924706388444319794', + '554694662431178782', + '647279669388771329', + '872937934511300708', + '793031479394566184', + '896032839362035742', + '928298100911656980', + '929164522772631572', + '644739124741406738', + '927038967847739452', + '721572630301507666', + '913320567061491742', + '916511749581193256', + '900768309635592252', + '879735806422552647', + '811225628346286122', + '889589694390763601', + '910420392454279188', + '748111918698463312', + '722409280497516566', + '930343100667277372', + '853685089187397662', + '562828676480237578', + '905746313247879209', + '898114293143310366', + '890955091589345363', + '919675957147762698', + '897951616215429130', + '931328872828522576', + '815793624888901673', + '908655401220866068', + '888468681435271209', + '909326148331274251', + '816454912015073281', + '894214126405029908', + '921101482017828874', + '892217445304131615', + '835066439891157012', + '868548871549222972', + '715804406842392586', + '756135419359395930', + '779364937503604777', + '808292126818304041', + '845267378912493628', + '411959613370400778', + '762061559744299010', + '851528533783609394', + '731786972946759710', + '822104143439069276', + '864186991683305472', + '793638148139253810', + '890249060999635066', + '894764180668833833', + '415935345075421194', + '821411516142059541', + '880868035185999913', + '902620728602538016', + '709210493549674598', + '884858269091369010', + '843461452023070720', + '851148409218007050', + '888297613789646849', + '900125460606881854', + '397872799483428865', + '879489926092169287', + '819687801343311923', + '890753139869880330', + '622859637309571072', + '852273007370960937', + '718590743446290492', + '832216920387878922', + '880376030446112768', + '899795875981848657', + '881636640320274463', + '744257429453275256', + '694822223575384095', + '810180621930070088', + '678414857510453309', + '819514788710973540', + '828728824534532136', + '887023633564831784', + '829529316382867468', + '544761450724458498', + '850513622194192395', +]; + +export default allowedServers; \ No newline at end of file diff --git a/src/app/service/help/HowToAccount.ts b/src/app/service/help/HowToAccount.ts new file mode 100644 index 00000000..98735bbf --- /dev/null +++ b/src/app/service/help/HowToAccount.ts @@ -0,0 +1,35 @@ +import { MessageOptions } from 'slash-create'; + +export default (): MessageOptions => { + return { + embeds: [{ + title: 'Account Information', + description: 'These set of commands allows linking, unlinking, and viewing external accounts.\n\n', + fields: [ + { + name: '-> /account verify', + value: 'Link external account to your discord account.', + inline: false, + }, + { + name: '-> /account status', + value: 'Display the currently linked accounts to your discord account.', + inline: false, + }, + { + name: '-> /account unlink', + value: 'Remove the link between your discord account and external account.', + inline: false, + }, + { + name: '-> Useful Links', + value: '[BanklessDAO Product Support Center invite](https://discord.gg/85Kb6Qv6gd)\n' + + '[Commands Wiki](https://www.notion.so/bankless/The-POAP-Distribution-Commands-and-Workflow-96cac11447b44d27885c160dc9af85fe)\n' + + '[Feature Request Feedback](https://degen.canny.io/feature-requests)\n' + + '[POAP Website](https://poap.xyz/)', + inline: false, + }, + ], + }], + }; +}; \ No newline at end of file diff --git a/src/app/utils/POAPUtils.ts b/src/app/utils/POAPUtils.ts index fb317d85..0a9af053 100644 --- a/src/app/utils/POAPUtils.ts +++ b/src/app/utils/POAPUtils.ts @@ -101,7 +101,7 @@ const POAPUtils = { adminChannel?: TextChannel | null, ): Promise { Log.debug('asking poap organizer for poap links attachment'); - const uploadLinksMsg = `Please upload the POAP links.txt file. This file should have a least ${numberOfParticipants} link(s). Each link should be on a new line.`; + const uploadLinksMsg = `Please upload the POAP links.txt file. This file should have a least ${numberOfParticipants} link(s). Each link should be on a new line. This file can be obtained from \`/poap mint\` command`; const replyOptions: AwaitMessagesOptions = { max: 1, time: 900000, diff --git a/src/app/utils/ServiceUtils.ts b/src/app/utils/ServiceUtils.ts index 5177401a..2a165fe9 100644 --- a/src/app/utils/ServiceUtils.ts +++ b/src/app/utils/ServiceUtils.ts @@ -263,7 +263,7 @@ const ServiceUtils = { addActiveDiscordServer: async (guild: Guild): Promise => { const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); const discordServerCollection = await db.collection(constants.DB_COLLECTION_DISCORD_SERVERS); - Log.info(`DEGEN active for: ${guild.id}, ${guild.name}`); + Log.info(`${constants.APP_NAME} active for: ${guild.id}, ${guild.name}`); await discordServerCollection.updateOne({ serverId: guild.id.toString(), }, { diff --git a/yarn.lock b/yarn.lock index fd6a64e6..87a51583 100644 --- a/yarn.lock +++ b/yarn.lock @@ -320,11 +320,6 @@ tslib "^2.3.1" zod "^3.11.6" -"@discordjs/collection@0.2.4": - version "0.2.4" - resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.2.4.tgz#c8ff2250430dcec7324dd4aafd1ccbcbdfa9ac14" - integrity sha512-PVrEJH+V6Ob0OwfagYQ/57kwt/HNEJxt5jqY4P+S3st9y29t9iokdnGMQoJXG5VEMAQIPbzu9Snw1F6yE8PdLA== - "@discordjs/collection@^0.4.0": version "0.4.0" resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.4.0.tgz#b6488286a1cc7b41b644d7e6086f25a1c1e6f837" @@ -1682,10 +1677,10 @@ discord-api-types@^0.26.0: resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.26.1.tgz#726f766ddc37d60da95740991d22cb6ef2ed787b" integrity sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ== -discord.js@^13.5.1: - version "13.5.1" - resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-13.5.1.tgz#c0af11c7bfdcf6ac3f6bf28c7d96855c0c6a8997" - integrity sha512-ejEG5MXzB0eda9Nt+VzqgdvDWVO5U/GynGzq6DRPLaCH1yyn2YRU9J+vCMl77pWA1rzYGX+b/9RI31x0wt3qXA== +discord.js@^13.6.0: + version "13.6.0" + resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-13.6.0.tgz#d8a8a591dbf25cbcf9c783d5ddf22c4694860475" + integrity sha512-tXNR8zgsEPxPBvGk3AQjJ9ljIIC6/LOPjzKwpwz8Y1Q2X66Vi3ZqFgRHYwnHKC0jC0F+l4LzxlhmOJsBZDNg9g== dependencies: "@discordjs/builders" "^0.11.0" "@discordjs/collection" "^0.4.0" @@ -3610,12 +3605,11 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -slash-create@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/slash-create/-/slash-create-5.0.2.tgz#999cc80fe558a34da8bb996da5666390256ab5fa" - integrity sha512-qsfkoDcybkpQQr9qVG6On/HH7HMNFdD0uHjg2IPy4IcmLCcRPgHhhbl0z3Ju3MAYdIVAk5EI6neL2XCfBrJRVA== +slash-create@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/slash-create/-/slash-create-5.0.3.tgz#c1288dff43876c74c24d25e7b5bb01f28c1f7ff8" + integrity sha512-ge0HDaiAe1kLyxKRtizeKJZY0mMPApDWrDYE2dBWfbeh/An/Adhfrt49T1GB1IHT67WwVQi7tFf9+WvhOuEmBQ== dependencies: - "@discordjs/collection" "0.2.4" eventemitter3 "^4.0.7" lodash.isequal "^4.5.0" tweetnacl "^1.0.3" From 9f7c0f6066fdba1609fe1cddb640c17bae3c03fa Mon Sep 17 00:00:00 2001 From: Brian Patino Date: Sun, 16 Jan 2022 19:15:10 -0500 Subject: [PATCH 03/19] update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 77228697..17619e04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "degen", - "version": "2.6.2", + "version": "2.7.0", "description": "Administrative and Utilitarian bot for the Bankless Discord Server.", "main": "app.js", "private": true, From 5253b11702684874bcdb8d9137496bf5e7d7c90c Mon Sep 17 00:00:00 2001 From: Brian Patino Date: Sun, 16 Jan 2022 19:43:53 -0500 Subject: [PATCH 04/19] update text --- src/app/commands/help/Help.ts | 2 +- src/app/utils/POAPUtils.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/commands/help/Help.ts b/src/app/commands/help/Help.ts index 172ba8f3..28bd9e77 100644 --- a/src/app/commands/help/Help.ts +++ b/src/app/commands/help/Help.ts @@ -18,7 +18,7 @@ export default class Help extends SlashCommand { { name: 'poap', type: CommandOptionType.SUB_COMMAND, - description: 'Information on how to start, stop, and optionally send out POAP links', + description: 'Information on how to claim, start, stop, and send out POAP links.', }, ], throttling: { diff --git a/src/app/utils/POAPUtils.ts b/src/app/utils/POAPUtils.ts index 0a9af053..5183aadf 100644 --- a/src/app/utils/POAPUtils.ts +++ b/src/app/utils/POAPUtils.ts @@ -309,6 +309,7 @@ const POAPUtils = { i++; } results.didNotSendList = failedPOAPsList; + Log.info(results); Log.info(`Links sent to ${results.successfullySent} participants.`); return results; }, From 11597d228f799892b658250f0c76d1f816f30931 Mon Sep 17 00:00:00 2001 From: Brian Patino Date: Sun, 16 Jan 2022 20:53:01 -0500 Subject: [PATCH 05/19] introduce /claim command --- CHANGELOG.md | 1 + src/app/commands/admin/Account.ts | 3 +- src/app/commands/poap/Claim.ts | 63 ++++++++++++++++++++++ src/app/commands/poap/POAP.ts | 4 +- src/app/events/poap/IntroductionMessage.ts | 0 src/app/service/poap/ClaimPOAP.ts | 15 +++++- src/app/service/poap/OptInPOAP.ts | 8 +-- src/app/utils/ServiceUtils.ts | 4 +- 8 files changed, 86 insertions(+), 12 deletions(-) create mode 100644 src/app/commands/poap/Claim.ts delete mode 100644 src/app/events/poap/IntroductionMessage.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6351a8d8..3c0e3d71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ 2. Add account unlink/status commands 3. Restrict DEGEN to certain discord servers 4. Upgrade discord.js -> 13.6.0, upgrade slash-create -> 5.0.3 +5. Introduce basic `/claim` command and prompt user for opt-in on slash command ## 2.6.2-RELEASE (2022-01-13) diff --git a/src/app/commands/admin/Account.ts b/src/app/commands/admin/Account.ts index 43002507..34df98ca 100644 --- a/src/app/commands/admin/Account.ts +++ b/src/app/commands/admin/Account.ts @@ -4,7 +4,6 @@ import VerifyTwitter from '../../service/account/VerifyTwitter'; import ServiceUtils from '../../utils/ServiceUtils'; import ValidationError from '../../errors/ValidationError'; import { command } from '../../utils/SentryUtils'; -import allowedServers from '../../service/constants/allowedServers'; export default class Account extends SlashCommand { constructor(creator: SlashCreator) { @@ -15,7 +14,7 @@ export default class Account extends SlashCommand { usages: 1, duration: 2, }, - guildIDs: allowedServers, + // guildIDs: allowedServers, defaultPermission: true, options: [ { diff --git a/src/app/commands/poap/Claim.ts b/src/app/commands/poap/Claim.ts new file mode 100644 index 00000000..76ff9423 --- /dev/null +++ b/src/app/commands/poap/Claim.ts @@ -0,0 +1,63 @@ +import { + CommandContext, + SlashCommand, + SlashCreator, +} from 'slash-create'; +import ServiceUtils from '../../utils/ServiceUtils'; +import ValidationError from '../../errors/ValidationError'; +import EarlyTermination from '../../errors/EarlyTermination'; +import Log, { LogUtils } from '../../utils/Log'; +import ClaimPOAP from '../../service/poap/ClaimPOAP'; +import constants from '../../service/constants/constants'; +import { GuildMember } from 'discord.js'; +import { command } from '../../utils/SentryUtils'; + +export default class POAP extends SlashCommand { + constructor(creator: SlashCreator) { + super(creator, { + name: 'claim', + description: 'Claim your POAPs.', + throttling: { + usages: 20, + duration: 1, + }, + defaultPermission: true, + }); + } + + @command + async run(ctx: CommandContext): Promise { + LogUtils.logCommandStart(ctx); + if (ctx.user.bot) return; + + let guildMember: GuildMember | undefined; + let commandPromise: Promise | null = null; + let platform: string; + + try { + if (ctx.guildID) { + guildMember = (await ServiceUtils.getGuildAndMember(ctx.guildID, ctx.user.id)).guildMember; + } + + platform = ctx.options.platform != null && ctx.options.platform != '' ? ctx.options.platform : constants.PLATFORM_TYPE_DISCORD; + Log.debug(`platform: ${platform}`); + commandPromise = ClaimPOAP(ctx, platform, guildMember); + if (commandPromise == null) { + ServiceUtils.sendOutErrorMessage(ctx).catch(Log.error); + return; + } + } catch (e) { + LogUtils.logError('failed to process POAP command', e); + if (e instanceof ValidationError) { + await ServiceUtils.sendOutErrorMessage(ctx, `${e?.message}`); + return; + } else if (e instanceof EarlyTermination) { + await ctx.sendFollowUp({ content: `${e?.message}`, ephemeral: true }).catch(Log.error); + return; + } else { + LogUtils.logError('failed to handle poap command', e); + await ServiceUtils.sendOutErrorMessage(ctx); + } + } + } +} \ No newline at end of file diff --git a/src/app/commands/poap/POAP.ts b/src/app/commands/poap/POAP.ts index f042ada0..56c80b5b 100644 --- a/src/app/commands/poap/POAP.ts +++ b/src/app/commands/poap/POAP.ts @@ -18,7 +18,6 @@ import { GuildMember } from 'discord.js'; import ModifyPOAP from '../../service/poap/config/ModifyPOAP'; import StatusPOAP from '../../service/poap/config/StatusPOAP'; import { command } from '../../utils/SentryUtils'; -import allowedServers from '../../service/constants/allowedServers'; export default class POAP extends SlashCommand { constructor(creator: SlashCreator) { @@ -30,7 +29,6 @@ export default class POAP extends SlashCommand { duration: 1, }, defaultPermission: true, - guildIDs: allowedServers, options: [ { name: 'config', @@ -191,7 +189,7 @@ export default class POAP extends SlashCommand { { name: 'claim', type: CommandOptionType.SUB_COMMAND, - description: 'Claim POAPs for all the events DEGEN failed to deliver.', + description: 'Claim your POAPs.', options: [ { name: 'platform', diff --git a/src/app/events/poap/IntroductionMessage.ts b/src/app/events/poap/IntroductionMessage.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/app/service/poap/ClaimPOAP.ts b/src/app/service/poap/ClaimPOAP.ts index a83f612a..42dbd940 100644 --- a/src/app/service/poap/ClaimPOAP.ts +++ b/src/app/service/poap/ClaimPOAP.ts @@ -21,6 +21,8 @@ import { import POAPUtils from '../../utils/POAPUtils'; import apiKeys from '../constants/apiKeys'; import ValidationError from '../../errors/ValidationError'; +import OptInPOAP from './OptInPOAP'; +import ServiceUtils from '../../utils/ServiceUtils'; const ClaimPOAP = async (ctx: CommandContext, platform: string, guildMember?: GuildMember): Promise => { Log.debug(`starting claim for ${ctx.user.username}, with ID: ${ctx.user.id}`); @@ -34,6 +36,17 @@ const ClaimPOAP = async (ctx: CommandContext, platform: string, guildMember?: Gu return; } await claimForDiscord(ctx.user.id, ctx); + if (guildMember && ctx) { + try { + const dmChannel: DMChannel = await guildMember.createDM(); + await OptInPOAP(guildMember.user, await dmChannel).catch(e => { + Log.error(e); + ServiceUtils.sendOutErrorMessageForDM(dmChannel).catch(Log.error); + }); + } catch (e) { + LogUtils.logError('failed to ask for opt-in', e); + } + } }; export const claimForDiscord = async (userId: string, ctx?: CommandContext | null, dmChannel?: DMChannel | null): Promise => { @@ -70,7 +83,7 @@ export const claimForDiscord = async (userId: string, ctx?: CommandContext | nul let result: Message | MessageSlash | boolean | void; if (ctx) { Log.debug('sending message in channel'); - await ctx.send({ content: `POAP claimed! Consider sending \`gm\` to <@${apiKeys.DISCORD_BOT_ID}>` }); + await ctx.send({ content: `POAP claimed! Consider sending \`gm\` to <@${apiKeys.DISCORD_BOT_ID}> to get POAPs directly in your DMs.` }); const embeds: MessageEmbedOptionsSlash[] = await generatePOAPClaimEmbedMessages(numberOfPOAPs, unclaimedParticipants) as MessageEmbedOptionsSlash[]; result = await ctx.send({ embeds: embeds, diff --git a/src/app/service/poap/OptInPOAP.ts b/src/app/service/poap/OptInPOAP.ts index 4892d90f..db37bbf9 100644 --- a/src/app/service/poap/OptInPOAP.ts +++ b/src/app/service/poap/OptInPOAP.ts @@ -55,9 +55,9 @@ const OptInPOAP = async (user: User, dmChannel: DMChannel): Promise => { .setLabel('No') .setStyle('SECONDARY'), ); - Log.debug('user has DMs option turned off, now asking user for opt-in to get DM POAPs'); + Log.debug('user has not opted in to DMs, now asking user for opt-in to get DM POAPs'); const message: Message | void = await dmChannel.send({ - content: 'I can send you POAPs directly to you. Would you like me to do that going forward?', + content: 'Would you like me to send you POAPs directly to you going forward?', components: [row], }).catch(e => { LogUtils.logError('failed to ask for opt-in', e); @@ -65,7 +65,7 @@ const OptInPOAP = async (user: User, dmChannel: DMChannel): Promise => { }); if (message == null) { - Log.debug('did not sent opt-in message'); + Log.debug('did not send opt-in message'); return; } @@ -105,7 +105,7 @@ const OptInPOAP = async (user: User, dmChannel: DMChannel): Promise => { isDMEnabled: true, }, }); - await message.edit({ content: 'Direct messages enabled! You will now receive POAPs, thank you!', components: [] }); + await message.edit({ content: 'Direct messages enabled! I will send you POAPs as soon as I get them, thank you!', components: [] }); Log.debug('user settings updated'); } Log.debug('user settings update skipped'); diff --git a/src/app/utils/ServiceUtils.ts b/src/app/utils/ServiceUtils.ts index 2a165fe9..1bda29b9 100644 --- a/src/app/utils/ServiceUtils.ts +++ b/src/app/utils/ServiceUtils.ts @@ -21,7 +21,7 @@ import { VoiceChannel, } from 'discord.js'; import client from '../app'; -import Log, { LogUtils } from './Log'; +import Log from './Log'; import { stringify } from 'csv-stringify/sync'; import { parse } from 'csv-parse/sync'; import { POAPFileParticipant, @@ -131,7 +131,7 @@ const ServiceUtils = { await guildMember.send({ content: message }); return true; } catch (e) { - LogUtils.logError(`DM is turned off for ${guildMember.user.tag}`, e); + Log.warn(`DM is turned off for ${guildMember.user.tag}`); return false; } }, From e53bd057ae0d43f73055f45f763871fd4e186b67 Mon Sep 17 00:00:00 2001 From: Brian Patino Date: Sun, 16 Jan 2022 21:49:40 -0500 Subject: [PATCH 06/19] unlink wip --- src/app/commands/admin/Account.ts | 9 +++-- src/app/service/account/UnlinkAccount.ts | 47 +++++++++++++++++++++++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/app/commands/admin/Account.ts b/src/app/commands/admin/Account.ts index 34df98ca..03b9c9bb 100644 --- a/src/app/commands/admin/Account.ts +++ b/src/app/commands/admin/Account.ts @@ -4,6 +4,9 @@ import VerifyTwitter from '../../service/account/VerifyTwitter'; import ServiceUtils from '../../utils/ServiceUtils'; import ValidationError from '../../errors/ValidationError'; import { command } from '../../utils/SentryUtils'; +import UnlinkAccount from '../../service/account/UnlinkAccount'; +import { platform } from 'os'; +import constants from '../../service/constants/constants'; export default class Account extends SlashCommand { constructor(creator: SlashCreator) { @@ -14,7 +17,6 @@ export default class Account extends SlashCommand { usages: 1, duration: 2, }, - // guildIDs: allowedServers, defaultPermission: true, options: [ { @@ -30,7 +32,7 @@ export default class Account extends SlashCommand { choices: [ { name: 'Twitter', - value: 'TWITTER_ACCOUNT', + value: constants.PLATFORM_TYPE_TWITTER, }, ], }, @@ -49,7 +51,7 @@ export default class Account extends SlashCommand { choices: [ { name: 'Twitter', - value: 'TWITTER_ACCOUNT', + value: constants.PLATFORM_TYPE_TWITTER, }, ], }, @@ -85,6 +87,7 @@ export default class Account extends SlashCommand { await VerifyTwitter(ctx, guildMember, true).catch(e => { throw e; }); break; case 'unlink': + await UnlinkAccount(ctx, guildMember, ctx.options.unlink.platform).catch(e => { throw e; }); break; case 'status': break; diff --git a/src/app/service/account/UnlinkAccount.ts b/src/app/service/account/UnlinkAccount.ts index 7518922e..ab2e928a 100644 --- a/src/app/service/account/UnlinkAccount.ts +++ b/src/app/service/account/UnlinkAccount.ts @@ -1,10 +1,53 @@ import { CommandContext } from 'slash-create'; -import Log from '../../utils/Log'; +import Log, { LogUtils } from '../../utils/Log'; +import ServiceUtils from '../../utils/ServiceUtils'; +import { GuildMember } from 'discord.js'; +import VerifyTwitter, { VerifiedTwitter } from './VerifyTwitter'; +import constants from '../constants/constants'; +import { Collection, Db, DeleteWriteOpResultObject } from 'mongodb'; +import MongoDbUtils from '../../utils/MongoDbUtils'; +import { NextAuthAccountCollection } from '../../types/nextauth/NextAuthAccountCollection'; -const UnlinkAccount = async (ctx: CommandContext, platform: string): Promise => { +const UnlinkAccount = async (ctx: CommandContext, guildMember: GuildMember, platform: string): Promise => { Log.debug(`starting to unlink account ${platform}`); + + const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, `Attempting to unlink account \`${platform}\``); + + if (isDmOn) { + await ctx.send({ content: 'DM sent!', ephemeral: true }); + } + + try { + if (platform == constants.PLATFORM_TYPE_TWITTER) { + await unlinkTwitter(ctx, guildMember).catch(Log.error); + await ctx.send({ content: 'Twitter account is unlinked.', ephemeral: true }).catch(Log.error); + } else { + Log.error('could not find platform'); + } + } catch (e) { + LogUtils.logError('failed to unlink twitter account', e); + await ServiceUtils.sendOutErrorMessage(ctx).catch(Log.error); + } +}; + +const unlinkTwitter = async (ctx: CommandContext, guildMember: GuildMember): Promise => { + const twitterUser: VerifiedTwitter | undefined = await VerifyTwitter(ctx, guildMember, true); + if (twitterUser == null) { + return; + } + + const db: Db = await MongoDbUtils.connect(constants.DB_NAME_NEXTAUTH); + const accountsCollection: Collection = db.collection(constants.DB_COLLECTION_NEXT_AUTH_ACCOUNTS); + const result: DeleteWriteOpResultObject = await accountsCollection.deleteMany({ + providerId: 'twitter', + providerAccountId: twitterUser.twitterUser.id_str, + }); + if (result.result.ok != 1) { + Log.warn('failed to remove twitter account'); + throw new Error('failed to unlink twitter account'); + } }; export default UnlinkAccount; \ No newline at end of file From 7ad6d9041a3070a24539e83d083fa09ea044035d Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 17 Jan 2022 12:29:28 -0500 Subject: [PATCH 07/19] handle in-channel flow for account unlinking --- src/app/commands/admin/Account.ts | 13 +- src/app/service/account/UnlinkAccount.ts | 138 ++++++++++++++++-- src/app/service/account/VerifyTwitter.ts | 2 +- src/app/service/constants/buttonIds.ts | 2 + src/app/service/poap/ClaimPOAP.ts | 2 +- src/app/service/poap/SchedulePOAP.ts | 2 +- src/app/service/poap/config/ModifyPOAP.ts | 2 +- .../service/poap/start/StartChannelFlow.ts | 2 +- .../service/poap/start/StartTwitterFlow.ts | 2 +- src/app/utils/POAPUtils.ts | 2 +- src/app/utils/ServiceUtils.ts | 2 +- 11 files changed, 144 insertions(+), 25 deletions(-) diff --git a/src/app/commands/admin/Account.ts b/src/app/commands/admin/Account.ts index 03b9c9bb..e162848f 100644 --- a/src/app/commands/admin/Account.ts +++ b/src/app/commands/admin/Account.ts @@ -5,8 +5,8 @@ import ServiceUtils from '../../utils/ServiceUtils'; import ValidationError from '../../errors/ValidationError'; import { command } from '../../utils/SentryUtils'; import UnlinkAccount from '../../service/account/UnlinkAccount'; -import { platform } from 'os'; import constants from '../../service/constants/constants'; +import discordServerIds from '../../service/constants/discordServerIds'; export default class Account extends SlashCommand { constructor(creator: SlashCreator) { @@ -18,9 +18,10 @@ export default class Account extends SlashCommand { duration: 2, }, defaultPermission: true, + guildIDs: [discordServerIds.discordBotGarage], options: [ { - name: 'verify', + name: 'link', type: CommandOptionType.SUB_COMMAND, description: 'Link DEGEN to your account or wallet.', options: [ @@ -58,9 +59,9 @@ export default class Account extends SlashCommand { ], }, { - name: 'status', + name: 'list', type: CommandOptionType.SUB_COMMAND, - description: 'Check linked accounts', + description: 'Display all linked accounts.', options: [], }, ], @@ -83,13 +84,13 @@ export default class Account extends SlashCommand { const { guildMember } = await ServiceUtils.getGuildAndMember(ctx.guildID, ctx.user.id); switch (subCommand) { - case 'verify': + case 'link': await VerifyTwitter(ctx, guildMember, true).catch(e => { throw e; }); break; case 'unlink': await UnlinkAccount(ctx, guildMember, ctx.options.unlink.platform).catch(e => { throw e; }); break; - case 'status': + case 'list': break; default: await ctx.send({ content: 'Please try again' }).catch(Log.error); diff --git a/src/app/service/account/UnlinkAccount.ts b/src/app/service/account/UnlinkAccount.ts index ab2e928a..28e548b0 100644 --- a/src/app/service/account/UnlinkAccount.ts +++ b/src/app/service/account/UnlinkAccount.ts @@ -1,12 +1,29 @@ -import { CommandContext } from 'slash-create'; +import { + ButtonStyle, + CommandContext, + ComponentContext, + ComponentType, +} from 'slash-create'; import Log, { LogUtils } from '../../utils/Log'; import ServiceUtils from '../../utils/ServiceUtils'; -import { GuildMember } from 'discord.js'; +import { + GuildMember, + Message, + MessageActionRow, + MessageButton, + MessageOptions, +} from 'discord.js'; import VerifyTwitter, { VerifiedTwitter } from './VerifyTwitter'; import constants from '../constants/constants'; import { Collection, Db, DeleteWriteOpResultObject } from 'mongodb'; import MongoDbUtils from '../../utils/MongoDbUtils'; import { NextAuthAccountCollection } from '../../types/nextauth/NextAuthAccountCollection'; +import { + Message as MessageSlash, + MessageOptions as MessageOptionsSlash, +} from 'slash-create'; +import buttonIds from '../constants/buttonIds'; +import ValidationError from '../../errors/ValidationError'; const UnlinkAccount = async (ctx: CommandContext, guildMember: GuildMember, platform: string): Promise => { Log.debug(`starting to unlink account ${platform}`); @@ -19,8 +36,16 @@ const UnlinkAccount = async (ctx: CommandContext, guildMember: GuildMember, plat try { if (platform == constants.PLATFORM_TYPE_TWITTER) { - await unlinkTwitter(ctx, guildMember).catch(Log.error); - await ctx.send({ content: 'Twitter account is unlinked.', ephemeral: true }).catch(Log.error); + const twitterUser: VerifiedTwitter | undefined = await VerifyTwitter(ctx, guildMember, false); + if (twitterUser != null) { + const shouldUnlink: boolean = await promptToUnlink(ctx, guildMember, isDmOn, twitterUser); + if (shouldUnlink) { + await unlinkTwitter(ctx, guildMember, twitterUser.twitterUser.id_str).catch(Log.error); + await ctx.send({ content: 'Twitter account removed. To relink account try `/account link`.', ephemeral: true }).catch(Log.error); + return; + } + await ctx.send({ content: 'Account not removed. To see list of accounts try `/account list`.', ephemeral: true }).catch(Log.error); + } } else { Log.error('could not find platform'); } @@ -30,19 +55,110 @@ const UnlinkAccount = async (ctx: CommandContext, guildMember: GuildMember, plat } }; -const unlinkTwitter = async (ctx: CommandContext, guildMember: GuildMember): Promise => { - const twitterUser: VerifiedTwitter | undefined = await VerifyTwitter(ctx, guildMember, true); - if (twitterUser == null) { - return; - } - +const promptToUnlink = async (ctx: CommandContext, guildMember: GuildMember, isDMOn: boolean, twitterUser: VerifiedTwitter): Promise => { + Log.debug('attempting to ask user for confirmation on unlinking'); + let shouldUnlinkPromise: Promise; + let shouldUnlinkMsg: MessageOptions | MessageOptionsSlash = { + embeds: [ + { + title: 'Unlink Confirmation', + description: 'Are you sure you want to remove the twitter account from your discord? You can relink later it with `/account link`.', + fields: [ + { name: 'UserId', value: `${twitterUser.twitterUser.id_str}`, inline: false }, + { name: 'Name', value: `${twitterUser.twitterUser.screen_name}`, inline: false }, + { name: 'Description', value: `${twitterUser.twitterUser.description}`, inline: false }, + { name: 'Profile', value: `https://twitter.com/${twitterUser.twitterUser.screen_name}`, inline: false }, + ], + }, + ], + }; + let messageResponse: Message; + const expiration = 600_000; + if (isDMOn) { + shouldUnlinkMsg = shouldUnlinkMsg as MessageOptions; + shouldUnlinkMsg.components = [ + new MessageActionRow().addComponents( + new MessageButton() + .setCustomId(buttonIds.ACCOUNT_UNLINK_APPROVE) + .setLabel('Yes') + .setStyle('SUCCESS'), + new MessageButton() + .setCustomId(buttonIds.ACCOUNT_UNLINK_REJECT) + .setLabel('No') + .setStyle('DANGER'), + ), + ]; + messageResponse = await guildMember.send(shouldUnlinkMsg); + Log.debug('dm message confirmation on unlink sent'); + shouldUnlinkPromise = new Promise((resolve, reject) => { + messageResponse.awaitMessageComponent({ + time: expiration, + filter: args => (args.customId == buttonIds.ACCOUNT_UNLINK_APPROVE) && args.user.id == guildMember.id.toString(), + }).then((interaction) => { + resolve(true); + Log.log(interaction); + }).catch(error => { + Log.error(error); + reject(new ValidationError('Timeout reached, please try command again to start a new session.')); + }); + }); + } else { + shouldUnlinkMsg = shouldUnlinkMsg as MessageOptionsSlash; + shouldUnlinkMsg.ephemeral = true; + shouldUnlinkMsg.components = [ + { + type: ComponentType.ACTION_ROW, + components: [{ + type: ComponentType.BUTTON, + style: ButtonStyle.SUCCESS, + label: 'Yes', + custom_id: buttonIds.ACCOUNT_UNLINK_APPROVE, + }, { + type: ComponentType.BUTTON, + style: ButtonStyle.DESTRUCTIVE, + label: 'No', + custom_id: buttonIds.ACCOUNT_UNLINK_REJECT, + }], + }, + ]; + Log.debug('attempting to send msg to user'); + Log.debug(shouldUnlinkMsg); + await ctx.defer(true); + const msgSlashResponse: MessageSlash = await ctx.send(shouldUnlinkMsg) as MessageSlash; + Log.debug('ctx message on user confirmation sent'); + shouldUnlinkPromise = new Promise((resolve, reject) => { + ctx.registerComponentFrom(msgSlashResponse.id, buttonIds.ACCOUNT_UNLINK_APPROVE, (compCtx: ComponentContext) => { + if (compCtx.user.id == guildMember.id) { + compCtx.editParent({ components: [] }); + resolve(true); + } + }, expiration, () => { + ctx.send({ content: 'Message expired, please try command again.', ephemeral: true }); + reject(false); + }); + + ctx.registerComponentFrom(msgSlashResponse.id, buttonIds.ACCOUNT_UNLINK_REJECT, (compCtx: ComponentContext) => { + if (compCtx.user.id == guildMember.id) { + compCtx.editParent({ components: [] }); + resolve(false); + } + }, expiration, () => { + ctx.send({ content: 'Message expired, please try command again.', ephemeral: true }); + reject(false); + }); + }); + Log.debug('ctx response message registered'); + } + return await shouldUnlinkPromise; +}; +const unlinkTwitter = async (ctx: CommandContext, guildMember: GuildMember, twitterUserId: string): Promise => { const db: Db = await MongoDbUtils.connect(constants.DB_NAME_NEXTAUTH); const accountsCollection: Collection = db.collection(constants.DB_COLLECTION_NEXT_AUTH_ACCOUNTS); const result: DeleteWriteOpResultObject = await accountsCollection.deleteMany({ providerId: 'twitter', - providerAccountId: twitterUser.twitterUser.id_str, + providerAccountId: twitterUserId, }); if (result.result.ok != 1) { Log.warn('failed to remove twitter account'); diff --git a/src/app/service/account/VerifyTwitter.ts b/src/app/service/account/VerifyTwitter.ts index a3c1ef11..154168d8 100644 --- a/src/app/service/account/VerifyTwitter.ts +++ b/src/app/service/account/VerifyTwitter.ts @@ -94,7 +94,7 @@ const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, send fields: [ { name: 'Display Name', value: `${userCall.screen_name}` }, { name: 'Description', value: `${ServiceUtils.prepEmbedField(userCall.description)}` }, - { name: 'URL', value: `https://twitter.com/${userCall.screen_name}` }, + { name: 'Profile', value: `https://twitter.com/${userCall.screen_name}` }, ], }; diff --git a/src/app/service/constants/buttonIds.ts b/src/app/service/constants/buttonIds.ts index 8e2bd126..e49a2016 100644 --- a/src/app/service/constants/buttonIds.ts +++ b/src/app/service/constants/buttonIds.ts @@ -4,6 +4,8 @@ const buttonIds = Object.freeze({ POAP_REPORT_SPAM: 'REPORT_SPAM', POAP_CONFIG_APPROVE: 'APPROVE_CONFIG', POAP_CONFIG_REMOVE: 'REMOVE_CONFIG', + ACCOUNT_UNLINK_APPROVE: 'UNLINK_ACCOUNT_YES', + ACCOUNT_UNLINK_REJECT: 'UNLINK_ACCOUNT_NO', }); export default buttonIds; \ No newline at end of file diff --git a/src/app/service/poap/ClaimPOAP.ts b/src/app/service/poap/ClaimPOAP.ts index 42dbd940..7e64822d 100644 --- a/src/app/service/poap/ClaimPOAP.ts +++ b/src/app/service/poap/ClaimPOAP.ts @@ -17,7 +17,7 @@ import { EmbedField as EmbedFieldSlash, Message as MessageSlash, MessageEmbedOptions as MessageEmbedOptionsSlash, -} from 'slash-create/lib/structures/message'; +} from 'slash-create'; import POAPUtils from '../../utils/POAPUtils'; import apiKeys from '../constants/apiKeys'; import ValidationError from '../../errors/ValidationError'; diff --git a/src/app/service/poap/SchedulePOAP.ts b/src/app/service/poap/SchedulePOAP.ts index 3e166ad5..b51f9c76 100644 --- a/src/app/service/poap/SchedulePOAP.ts +++ b/src/app/service/poap/SchedulePOAP.ts @@ -19,7 +19,7 @@ import ServiceUtils from '../../utils/ServiceUtils'; import Log, { LogUtils } from '../../utils/Log'; import DateUtils from '../../utils/DateUtils'; import MongoDbUtils from '../../utils/MongoDbUtils'; -import { MessageOptions as MessageOptionsSlash } from 'slash-create/lib/structures/interfaces/messageInteraction'; +import { MessageOptions as MessageOptionsSlash } from 'slash-create'; const SchedulePOAP = async (ctx: CommandContext, guildMember: GuildMember, numberToMint: number): Promise => { if (ctx.guildID == undefined) { diff --git a/src/app/service/poap/config/ModifyPOAP.ts b/src/app/service/poap/config/ModifyPOAP.ts index 7bb6f4d5..a4ea31b9 100644 --- a/src/app/service/poap/config/ModifyPOAP.ts +++ b/src/app/service/poap/config/ModifyPOAP.ts @@ -29,7 +29,7 @@ import { } from 'slash-create'; import Log, { LogUtils } from '../../../utils/Log'; import MongoDbUtils from '../../../utils/MongoDbUtils'; -import { MessageOptions as MessageOptionsSlash } from 'slash-create/lib/structures/interfaces/messageInteraction'; +import { MessageOptions as MessageOptionsSlash } from 'slash-create'; import dayjs from 'dayjs'; import buttonIds from '../../constants/buttonIds'; diff --git a/src/app/service/poap/start/StartChannelFlow.ts b/src/app/service/poap/start/StartChannelFlow.ts index 7e6725aa..72893ca5 100644 --- a/src/app/service/poap/start/StartChannelFlow.ts +++ b/src/app/service/poap/start/StartChannelFlow.ts @@ -18,7 +18,7 @@ import { import { Collection as CollectionMongo, } from 'mongodb'; -import { MessageEmbedOptions as MessageEmbedOptionsSlash } from 'slash-create/lib/structures/message'; +import { MessageEmbedOptions as MessageEmbedOptionsSlash } from 'slash-create'; import ValidationError from '../../../errors/ValidationError'; const StartChannelFlow = async ( ctx: CommandContext, guildMember: GuildMember, db: Db, event: string, duration: number, diff --git a/src/app/service/poap/start/StartTwitterFlow.ts b/src/app/service/poap/start/StartTwitterFlow.ts index e88a6cb0..da171175 100644 --- a/src/app/service/poap/start/StartTwitterFlow.ts +++ b/src/app/service/poap/start/StartTwitterFlow.ts @@ -16,7 +16,7 @@ import dayjs, { Dayjs } from 'dayjs'; import POAPService from '../POAPService'; import { POAPTwitterParticipants } from '../../../types/poap/POAPTwitterParticipants'; import channelIds from '../../constants/channelIds'; -import { MessageOptions as MessageOptionsSlash } from 'slash-create/lib/structures/interfaces/messageInteraction'; +import { MessageOptions as MessageOptionsSlash } from 'slash-create'; const StartTwitterFlow = async (ctx: CommandContext, guildMember: GuildMember, db: Db, event: string, duration: number): Promise => { Log.debug('starting twitter poap flow...'); diff --git a/src/app/utils/POAPUtils.ts b/src/app/utils/POAPUtils.ts index 5183aadf..2b305585 100644 --- a/src/app/utils/POAPUtils.ts +++ b/src/app/utils/POAPUtils.ts @@ -25,7 +25,7 @@ import TwitterApi, { DirectMessageCreateV1Result } from 'twitter-api-v2'; import apiKeys from '../service/constants/apiKeys'; import { Buffer } from 'buffer'; import ServiceUtils from './ServiceUtils'; -import { MessageOptions as MessageOptionsSlash } from 'slash-create/lib/structures/interfaces/messageInteraction'; +import { MessageOptions as MessageOptionsSlash } from 'slash-create'; import { TwitterApiTokens } from 'twitter-api-v2/dist/types'; import { POAPDistributionResults } from '../types/poap/POAPDistributionResults'; import ApiKeys from '../service/constants/apiKeys'; diff --git a/src/app/utils/ServiceUtils.ts b/src/app/utils/ServiceUtils.ts index 1bda29b9..d818356f 100644 --- a/src/app/utils/ServiceUtils.ts +++ b/src/app/utils/ServiceUtils.ts @@ -34,7 +34,7 @@ import { ButtonStyle, EmbedField as EmbedFieldSlash, MessageEmbedOptions as MessageEmbedOptionsSlash, } from 'slash-create'; -import { ComponentActionRow } from 'slash-create/lib/constants'; +import { ComponentActionRow } from 'slash-create'; import ValidationError from '../errors/ValidationError'; import { Db, From c6cc74df26a877cad647b1c07b5f7003c8121881 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 17 Jan 2022 16:15:43 -0500 Subject: [PATCH 08/19] unlink via DMs or in channel --- src/app/service/account/UnlinkAccount.ts | 68 ++++++++++++----- src/app/service/account/VerifyTwitter.ts | 95 ++++++++++++------------ src/app/service/poap/ClaimPOAP.ts | 9 ++- src/app/service/poap/DistributePOAP.ts | 2 +- 4 files changed, 105 insertions(+), 69 deletions(-) diff --git a/src/app/service/account/UnlinkAccount.ts b/src/app/service/account/UnlinkAccount.ts index 28e548b0..a88f2f63 100644 --- a/src/app/service/account/UnlinkAccount.ts +++ b/src/app/service/account/UnlinkAccount.ts @@ -13,9 +13,17 @@ import { MessageButton, MessageOptions, } from 'discord.js'; -import VerifyTwitter, { VerifiedTwitter } from './VerifyTwitter'; +import { + retrieveVerifiedTwitter, + VerifiedTwitter, +} from './VerifyTwitter'; import constants from '../constants/constants'; -import { Collection, Db, DeleteWriteOpResultObject } from 'mongodb'; +import { + Collection, + Db, + DeleteWriteOpResultObject, + ObjectID, +} from 'mongodb'; import MongoDbUtils from '../../utils/MongoDbUtils'; import { NextAuthAccountCollection } from '../../types/nextauth/NextAuthAccountCollection'; import { @@ -23,7 +31,6 @@ import { MessageOptions as MessageOptionsSlash, } from 'slash-create'; import buttonIds from '../constants/buttonIds'; -import ValidationError from '../../errors/ValidationError'; const UnlinkAccount = async (ctx: CommandContext, guildMember: GuildMember, platform: string): Promise => { Log.debug(`starting to unlink account ${platform}`); @@ -36,16 +43,18 @@ const UnlinkAccount = async (ctx: CommandContext, guildMember: GuildMember, plat try { if (platform == constants.PLATFORM_TYPE_TWITTER) { - const twitterUser: VerifiedTwitter | undefined = await VerifyTwitter(ctx, guildMember, false); + const twitterUser: VerifiedTwitter | null = await retrieveVerifiedTwitter(ctx, guildMember); if (twitterUser != null) { const shouldUnlink: boolean = await promptToUnlink(ctx, guildMember, isDmOn, twitterUser); if (shouldUnlink) { - await unlinkTwitter(ctx, guildMember, twitterUser.twitterUser.id_str).catch(Log.error); - await ctx.send({ content: 'Twitter account removed. To relink account try `/account link`.', ephemeral: true }).catch(Log.error); + await unlinkTwitterAccount(guildMember).catch(e => { throw e; }); + await ServiceUtils.sendContextMessage(isDmOn, guildMember, ctx, { content: 'Twitter account removed. To relink account try `/account link`.', ephemeral: true }).catch(Log.error); return; } - await ctx.send({ content: 'Account not removed. To see list of accounts try `/account list`.', ephemeral: true }).catch(Log.error); + await ServiceUtils.sendContextMessage(isDmOn, guildMember, ctx, { content: 'Account not removed. To see list of accounts try `/account list`.', ephemeral: true }).catch(Log.error); + return; } + await ServiceUtils.sendContextMessage(isDmOn, guildMember, ctx, { content: 'Twitter account not found!', ephemeral: true }); } else { Log.error('could not find platform'); } @@ -57,7 +66,7 @@ const UnlinkAccount = async (ctx: CommandContext, guildMember: GuildMember, plat const promptToUnlink = async (ctx: CommandContext, guildMember: GuildMember, isDMOn: boolean, twitterUser: VerifiedTwitter): Promise => { Log.debug('attempting to ask user for confirmation on unlinking'); - let shouldUnlinkPromise: Promise; + let shouldUnlinkPromise: Promise; let shouldUnlinkMsg: MessageOptions | MessageOptionsSlash = { embeds: [ { @@ -89,18 +98,26 @@ const promptToUnlink = async (ctx: CommandContext, guildMember: GuildMember, isD .setStyle('DANGER'), ), ]; + Log.debug('attempting to send dm to user'); + Log.debug(shouldUnlinkMsg); messageResponse = await guildMember.send(shouldUnlinkMsg); Log.debug('dm message confirmation on unlink sent'); - shouldUnlinkPromise = new Promise((resolve, reject) => { + shouldUnlinkPromise = new Promise((resolve, _) => { messageResponse.awaitMessageComponent({ time: expiration, - filter: args => (args.customId == buttonIds.ACCOUNT_UNLINK_APPROVE) && args.user.id == guildMember.id.toString(), + filter: args => (args.customId == buttonIds.ACCOUNT_UNLINK_APPROVE || args.customId == buttonIds.ACCOUNT_UNLINK_REJECT) + && args.user.id == guildMember.id.toString(), }).then((interaction) => { - resolve(true); - Log.log(interaction); + if (interaction.customId == buttonIds.ACCOUNT_UNLINK_APPROVE) { + messageResponse.edit({ components: [] }).catch(Log.error); + resolve(true); + } else if (interaction.customId == buttonIds.ACCOUNT_UNLINK_REJECT) { + messageResponse.edit({ components: [] }).catch(Log.error); + resolve(false); + } }).catch(error => { Log.error(error); - reject(new ValidationError('Timeout reached, please try command again to start a new session.')); + resolve(false); }); }); } else { @@ -127,7 +144,7 @@ const promptToUnlink = async (ctx: CommandContext, guildMember: GuildMember, isD await ctx.defer(true); const msgSlashResponse: MessageSlash = await ctx.send(shouldUnlinkMsg) as MessageSlash; Log.debug('ctx message on user confirmation sent'); - shouldUnlinkPromise = new Promise((resolve, reject) => { + shouldUnlinkPromise = new Promise((resolve, _) => { ctx.registerComponentFrom(msgSlashResponse.id, buttonIds.ACCOUNT_UNLINK_APPROVE, (compCtx: ComponentContext) => { if (compCtx.user.id == guildMember.id) { compCtx.editParent({ components: [] }); @@ -135,7 +152,7 @@ const promptToUnlink = async (ctx: CommandContext, guildMember: GuildMember, isD } }, expiration, () => { ctx.send({ content: 'Message expired, please try command again.', ephemeral: true }); - reject(false); + resolve(false); }); ctx.registerComponentFrom(msgSlashResponse.id, buttonIds.ACCOUNT_UNLINK_REJECT, (compCtx: ComponentContext) => { @@ -145,7 +162,7 @@ const promptToUnlink = async (ctx: CommandContext, guildMember: GuildMember, isD } }, expiration, () => { ctx.send({ content: 'Message expired, please try command again.', ephemeral: true }); - reject(false); + resolve(false); }); }); Log.debug('ctx response message registered'); @@ -153,17 +170,32 @@ const promptToUnlink = async (ctx: CommandContext, guildMember: GuildMember, isD return await shouldUnlinkPromise; }; -const unlinkTwitter = async (ctx: CommandContext, guildMember: GuildMember, twitterUserId: string): Promise => { +export const unlinkTwitterAccount = async (guildMember: GuildMember): Promise => { + Log.debug('removing twitter account link from db'); const db: Db = await MongoDbUtils.connect(constants.DB_NAME_NEXTAUTH); const accountsCollection: Collection = db.collection(constants.DB_COLLECTION_NEXT_AUTH_ACCOUNTS); + + const nextAuthAccount: NextAuthAccountCollection | null = await accountsCollection.findOne({ + providerId: 'discord', + providerAccountId: guildMember.user.id.toString(), + }); + + if (nextAuthAccount == null || nextAuthAccount.userId == null) { + Log.debug('next auth account not found'); + return; + } + const result: DeleteWriteOpResultObject = await accountsCollection.deleteMany({ providerId: 'twitter', - providerAccountId: twitterUserId, + userId: new ObjectID(nextAuthAccount.userId), }); + if (result.result.ok != 1) { Log.warn('failed to remove twitter account'); throw new Error('failed to unlink twitter account'); } + Log.debug('twitter account unlinked and removed from db'); + return; }; export default UnlinkAccount; \ No newline at end of file diff --git a/src/app/service/account/VerifyTwitter.ts b/src/app/service/account/VerifyTwitter.ts index 154168d8..a5b71e14 100644 --- a/src/app/service/account/VerifyTwitter.ts +++ b/src/app/service/account/VerifyTwitter.ts @@ -5,7 +5,10 @@ import { import apiKeys from '../constants/apiKeys'; import MongoDbUtils from '../../utils/MongoDbUtils'; import constants from '../constants/constants'; -import { Collection, Db, DeleteWriteOpResultObject } from 'mongodb'; +import { + Collection, + Db, +} from 'mongodb'; import { NextAuthAccountCollection } from '../../types/nextauth/NextAuthAccountCollection'; import Log from '../../utils/Log'; import { TwitterApi, UserV1 } from 'twitter-api-v2'; @@ -15,6 +18,7 @@ import { MessageEmbedOptions as MessageEmbedSlash, } from 'slash-create'; import { TwitterApiTokens } from 'twitter-api-v2/dist/types/client.types'; +import { unlinkTwitterAccount } from './UnlinkAccount'; export type VerifiedTwitter = { twitterUser: UserV1, @@ -30,6 +34,39 @@ const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, send await ctx.send({ content: 'DM sent!', ephemeral: true }); } + const verifiedTwitter: VerifiedTwitter | null = await retrieveVerifiedTwitter(ctx, guildMember); + + if (verifiedTwitter == null) { + await sendTwitterAuthenticationMessage(guildMember, ctx, isDmOn); + return; + } + + Log.debug(`${guildMember.user.tag} has linked their twitter account, twitterId: ${verifiedTwitter.twitterUser.id_str}, sending message`); + const verifiedEmbeds = { + title: 'Twitter Authentication', + description: 'Twitter account linked 👍', + fields: [ + { name: 'Display Name', value: `${verifiedTwitter.twitterUser.screen_name}` }, + { name: 'Description', value: `${ServiceUtils.prepEmbedField(verifiedTwitter.twitterUser.description)}` }, + { name: 'Profile', value: `https://twitter.com/${verifiedTwitter.twitterUser.screen_name}` }, + ], + }; + + if (sendConfirmationMsg) { + if (isDmOn) { + await guildMember.send({ embeds: [verifiedEmbeds] }); + } else { + await ctx.send({ embeds: [verifiedEmbeds], ephemeral: true }); + } + } + + Log.debug('done verifying twitter account'); + return verifiedTwitter; +}; + +export const retrieveVerifiedTwitter = async (ctx: CommandContext, guildMember: GuildMember): Promise => { + Log.debug('starting to link twitter account link'); + const db: Db = await MongoDbUtils.connect(constants.DB_NAME_NEXTAUTH); const accountsCollection: Collection = db.collection(constants.DB_COLLECTION_NEXT_AUTH_ACCOUNTS); @@ -41,8 +78,7 @@ const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, send if (nextAuthAccount == null || nextAuthAccount.userId == null) { Log.debug('next auth account not found'); - await sendTwitterAuthenticationMessage(guildMember, ctx, isDmOn); - return; + return null; } Log.debug('found next auth discord from db, now looking for twitter account'); @@ -53,8 +89,7 @@ const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, send if (twitterCollection == null || twitterCollection.accessToken == null) { Log.debug('twitter account not linked'); - await sendTwitterAuthenticationMessage(guildMember, ctx, isDmOn); - return; + return null; } const twitterAccessToken = twitterCollection.accessToken; @@ -71,64 +106,28 @@ const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, send accessToken: twitterAccessToken, accessSecret: twitterAccessSecret, } as TwitterApiTokens); - + Log.debug('testing validity of twitter account'); userCall = await userClient.currentUser(true); } catch (e) { Log.warn('invalid twitter auth found in db, verifyCredentials failed. Now removing from db...'); - await removeTwitterAccountLink(nextAuthAccount); - await sendTwitterAuthenticationMessage(guildMember, ctx, isDmOn); - return; + await unlinkTwitterAccount(guildMember); + return null; } if (twitterId != userCall.id_str) { - Log.debug('invalid twitter account, sending auth message'); - await sendTwitterAuthenticationMessage(guildMember, ctx, isDmOn); - return; + Log.debug('invalid twitter account Id'); + await unlinkTwitterAccount(guildMember); + return null; } - Log.debug(`${guildMember.user.tag} has linked their twitter account, twitterId: ${twitterId}, sending message`); - const verifiedEmbeds = { - title: 'Twitter Authentication', - description: 'Twitter account linked 👍', - fields: [ - { name: 'Display Name', value: `${userCall.screen_name}` }, - { name: 'Description', value: `${ServiceUtils.prepEmbedField(userCall.description)}` }, - { name: 'Profile', value: `https://twitter.com/${userCall.screen_name}` }, - ], - }; - - if (sendConfirmationMsg) { - if (isDmOn) { - await guildMember.send({ embeds: [verifiedEmbeds] }); - } else { - await ctx.send({ embeds: [verifiedEmbeds], ephemeral: true }); - } - } - - Log.debug('done verifying twitter account'); + Log.debug('done linking twitter account'); return { twitterUser: userCall, twitterClientV1: userClient, }; }; -const removeTwitterAccountLink = async (nextAuthAccount: NextAuthAccountCollection): Promise => { - Log.debug('removing twitter account link from db'); - const db: Db = await MongoDbUtils.connect(constants.DB_NAME_NEXTAUTH); - const accountsCollection: Collection = db.collection(constants.DB_COLLECTION_NEXT_AUTH_ACCOUNTS); - const result: DeleteWriteOpResultObject = await accountsCollection.deleteOne({ - providerId: 'twitter', - userId: nextAuthAccount.userId, - }); - if (result.result.ok != 1) { - Log.warn('failed to remove twitter account'); - throw new Error('failed to unlink twitter account'); - } - Log.debug('twitter account unlinked and removed from db'); - return; -}; - const sendTwitterAuthenticationMessage = async (guildMember: GuildMember, ctx: CommandContext, isDmOn: boolean): Promise => { Log.info(`${guildMember.user.tag} is not twitter authorized, sending request to link`); const embedsMsg: MessageEmbedOptions | MessageEmbedSlash = { diff --git a/src/app/service/poap/ClaimPOAP.ts b/src/app/service/poap/ClaimPOAP.ts index 7e64822d..7d693c9f 100644 --- a/src/app/service/poap/ClaimPOAP.ts +++ b/src/app/service/poap/ClaimPOAP.ts @@ -11,7 +11,10 @@ import constants from '../constants/constants'; import { POAPUnclaimedParticipants } from '../../types/poap/POAPUnclaimedParticipants'; import Log, { LogUtils } from '../../utils/Log'; import { POAPTwitterUnclaimedParticipants } from '../../types/poap/POAPTwitterUnclaimedParticipants'; -import VerifyTwitter, { VerifiedTwitter } from '../account/VerifyTwitter'; +import { + retrieveVerifiedTwitter, + VerifiedTwitter, +} from '../account/VerifyTwitter'; import dayjs from 'dayjs'; import { EmbedField as EmbedFieldSlash, @@ -122,8 +125,10 @@ export const claimForDiscord = async (userId: string, ctx?: CommandContext | nul const claimPOAPForTwitter = async (ctx: CommandContext, guildMember: GuildMember) => { Log.debug('claiming POAP for Twitter'); - const verifiedTwitter: VerifiedTwitter | undefined = await VerifyTwitter(ctx, guildMember, false); + const verifiedTwitter: VerifiedTwitter | null = await retrieveVerifiedTwitter(ctx, guildMember); + if (verifiedTwitter == null) { + return; } diff --git a/src/app/service/poap/DistributePOAP.ts b/src/app/service/poap/DistributePOAP.ts index 16b4a0b3..c7ad9010 100644 --- a/src/app/service/poap/DistributePOAP.ts +++ b/src/app/service/poap/DistributePOAP.ts @@ -147,7 +147,7 @@ const distributeTwitterFlow = async (ctx: CommandContext, guildMember: GuildMemb let distributionResults: POAPDistributionResults; if (!participantsList[0].poapLink) { - const poapLinksFile: MessageAttachment = await POAPUtils.askForPOAPLinks(guildMember, false, numberOfParticipants, ctx); + const poapLinksFile: MessageAttachment = await POAPUtils.askForPOAPLinks(guildMember, isDmOn, numberOfParticipants, ctx); const listOfPOAPLinks: string[] = await POAPUtils.getListOfPoapLinks(poapLinksFile); distributionResults = await POAPUtils.sendOutTwitterPoapLinks(participantsList, event, listOfPOAPLinks); } else { From 7e22fde76e14d89555f05d856ae883b9444ddade Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 17 Jan 2022 16:59:54 -0500 Subject: [PATCH 09/19] defer in channel private messages --- src/app/service/account/UnlinkAccount.ts | 5 ++- src/app/service/account/VerifyTwitter.ts | 57 +++++++++++++++--------- src/app/service/poap/ClaimPOAP.ts | 2 +- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/app/service/account/UnlinkAccount.ts b/src/app/service/account/UnlinkAccount.ts index a88f2f63..c3e2cc78 100644 --- a/src/app/service/account/UnlinkAccount.ts +++ b/src/app/service/account/UnlinkAccount.ts @@ -37,13 +37,16 @@ const UnlinkAccount = async (ctx: CommandContext, guildMember: GuildMember, plat const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, `Attempting to unlink account \`${platform}\``); + // important + await ctx.defer(true); + if (isDmOn) { await ctx.send({ content: 'DM sent!', ephemeral: true }); } try { if (platform == constants.PLATFORM_TYPE_TWITTER) { - const twitterUser: VerifiedTwitter | null = await retrieveVerifiedTwitter(ctx, guildMember); + const twitterUser: VerifiedTwitter | null = await retrieveVerifiedTwitter(guildMember); if (twitterUser != null) { const shouldUnlink: boolean = await promptToUnlink(ctx, guildMember, isDmOn, twitterUser); if (shouldUnlink) { diff --git a/src/app/service/account/VerifyTwitter.ts b/src/app/service/account/VerifyTwitter.ts index a5b71e14..31a6dc1e 100644 --- a/src/app/service/account/VerifyTwitter.ts +++ b/src/app/service/account/VerifyTwitter.ts @@ -1,6 +1,6 @@ import { GuildMember, - MessageEmbedOptions, + MessageOptions, } from 'discord.js'; import apiKeys from '../constants/apiKeys'; import MongoDbUtils from '../../utils/MongoDbUtils'; @@ -15,7 +15,7 @@ import { TwitterApi, UserV1 } from 'twitter-api-v2'; import ServiceUtils from '../../utils/ServiceUtils'; import { CommandContext, - MessageEmbedOptions as MessageEmbedSlash, + MessageOptions as MessageOptionsSlash, } from 'slash-create'; import { TwitterApiTokens } from 'twitter-api-v2/dist/types/client.types'; import { unlinkTwitterAccount } from './UnlinkAccount'; @@ -30,11 +30,14 @@ const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, send const isDmOn: boolean = (sendConfirmationMsg) ? await ServiceUtils.tryDMUser(guildMember, 'Hi! Let me check your twitter info') : false; + // important + await ctx.defer(true); + if (isDmOn) { await ctx.send({ content: 'DM sent!', ephemeral: true }); } - const verifiedTwitter: VerifiedTwitter | null = await retrieveVerifiedTwitter(ctx, guildMember); + const verifiedTwitter: VerifiedTwitter | null = await retrieveVerifiedTwitter(guildMember); if (verifiedTwitter == null) { await sendTwitterAuthenticationMessage(guildMember, ctx, isDmOn); @@ -42,21 +45,28 @@ const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, send } Log.debug(`${guildMember.user.tag} has linked their twitter account, twitterId: ${verifiedTwitter.twitterUser.id_str}, sending message`); - const verifiedEmbeds = { - title: 'Twitter Authentication', - description: 'Twitter account linked 👍', - fields: [ - { name: 'Display Name', value: `${verifiedTwitter.twitterUser.screen_name}` }, - { name: 'Description', value: `${ServiceUtils.prepEmbedField(verifiedTwitter.twitterUser.description)}` }, - { name: 'Profile', value: `https://twitter.com/${verifiedTwitter.twitterUser.screen_name}` }, + let authenticatedMsg: MessageOptions | MessageOptionsSlash = { + embeds: [ + { + title: 'Twitter Authentication', + description: 'Twitter account linked 👍', + fields: [ + { name: 'Display Name', value: `${verifiedTwitter.twitterUser.screen_name}` }, + { name: 'Description', value: `${ServiceUtils.prepEmbedField(verifiedTwitter.twitterUser.description)}` }, + { name: 'Profile', value: `https://twitter.com/${verifiedTwitter.twitterUser.screen_name}` }, + ], + }, ], }; if (sendConfirmationMsg) { if (isDmOn) { - await guildMember.send({ embeds: [verifiedEmbeds] }); + authenticatedMsg = authenticatedMsg as MessageOptions; + await guildMember.send(authenticatedMsg); } else { - await ctx.send({ embeds: [verifiedEmbeds], ephemeral: true }); + authenticatedMsg = authenticatedMsg as MessageOptionsSlash; + authenticatedMsg.ephemeral = true; + await ctx.send(authenticatedMsg); } } @@ -64,7 +74,7 @@ const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, send return verifiedTwitter; }; -export const retrieveVerifiedTwitter = async (ctx: CommandContext, guildMember: GuildMember): Promise => { +export const retrieveVerifiedTwitter = async (guildMember: GuildMember): Promise => { Log.debug('starting to link twitter account link'); const db: Db = await MongoDbUtils.connect(constants.DB_NAME_NEXTAUTH); @@ -130,17 +140,24 @@ export const retrieveVerifiedTwitter = async (ctx: CommandContext, guildMember: const sendTwitterAuthenticationMessage = async (guildMember: GuildMember, ctx: CommandContext, isDmOn: boolean): Promise => { Log.info(`${guildMember.user.tag} is not twitter authorized, sending request to link`); - const embedsMsg: MessageEmbedOptions | MessageEmbedSlash = { - title: 'Twitter Authentication', - description: 'Please verify your twitter account by following the link below.', - fields: [ - { name: 'URL', value: `${apiKeys.twitterVerificationUrl}` }, + let msg: MessageOptions | MessageOptionsSlash = { + embeds: [ + { + title: 'Twitter Authentication', + description: 'Please verify your twitter account by following the link below.', + fields: [ + { name: 'URL', value: `${apiKeys.twitterVerificationUrl}` }, + ], + }, ], }; if (isDmOn) { - await guildMember.send({ embeds: [ embedsMsg as MessageEmbedOptions ] }).catch(Log.error); + msg = msg as MessageOptions; + await guildMember.send(msg).catch(Log.error); } else { - await ctx.send({ embeds: [embedsMsg as MessageEmbedSlash], ephemeral: true }).catch(Log.error); + msg = msg as MessageOptionsSlash; + msg.ephemeral = true; + await ctx.send(msg).catch(Log.error); } }; diff --git a/src/app/service/poap/ClaimPOAP.ts b/src/app/service/poap/ClaimPOAP.ts index 7d693c9f..dd85b782 100644 --- a/src/app/service/poap/ClaimPOAP.ts +++ b/src/app/service/poap/ClaimPOAP.ts @@ -125,7 +125,7 @@ export const claimForDiscord = async (userId: string, ctx?: CommandContext | nul const claimPOAPForTwitter = async (ctx: CommandContext, guildMember: GuildMember) => { Log.debug('claiming POAP for Twitter'); - const verifiedTwitter: VerifiedTwitter | null = await retrieveVerifiedTwitter(ctx, guildMember); + const verifiedTwitter: VerifiedTwitter | null = await retrieveVerifiedTwitter(guildMember); if (verifiedTwitter == null) { From f5d05fbe2c309cf8865bad3a66dc58457e83f330 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 17 Jan 2022 17:45:49 -0500 Subject: [PATCH 10/19] handle in-channel flow for account list --- src/app/commands/admin/Account.ts | 4 +- src/app/service/account/AccountStatus.ts | 0 src/app/service/account/ListAccounts.ts | 58 +++++++++++++++++++++++ src/app/service/account/UnlinkAccount.ts | 3 +- src/app/service/account/VerifyTwitter.ts | 27 ++++------- src/app/service/poap/SchedulePOAP.ts | 2 + src/app/service/poap/config/ModifyPOAP.ts | 5 +- src/app/utils/ServiceUtils.ts | 6 ++- 8 files changed, 81 insertions(+), 24 deletions(-) delete mode 100644 src/app/service/account/AccountStatus.ts create mode 100644 src/app/service/account/ListAccounts.ts diff --git a/src/app/commands/admin/Account.ts b/src/app/commands/admin/Account.ts index e162848f..4c15e651 100644 --- a/src/app/commands/admin/Account.ts +++ b/src/app/commands/admin/Account.ts @@ -7,6 +7,7 @@ import { command } from '../../utils/SentryUtils'; import UnlinkAccount from '../../service/account/UnlinkAccount'; import constants from '../../service/constants/constants'; import discordServerIds from '../../service/constants/discordServerIds'; +import ListAccounts from '../../service/account/ListAccounts'; export default class Account extends SlashCommand { constructor(creator: SlashCreator) { @@ -74,7 +75,7 @@ export default class Account extends SlashCommand { if (ctx.user.bot) return; if (ctx.guildID == null) { - await ctx.send({ content: 'Please try this command within a discord server.' }); + await ctx.send({ content: 'Please try command within a discord server.' }); return; } @@ -91,6 +92,7 @@ export default class Account extends SlashCommand { await UnlinkAccount(ctx, guildMember, ctx.options.unlink.platform).catch(e => { throw e; }); break; case 'list': + await ListAccounts(ctx, guildMember).catch(e => { throw e; }); break; default: await ctx.send({ content: 'Please try again' }).catch(Log.error); diff --git a/src/app/service/account/AccountStatus.ts b/src/app/service/account/AccountStatus.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/app/service/account/ListAccounts.ts b/src/app/service/account/ListAccounts.ts new file mode 100644 index 00000000..33848ed2 --- /dev/null +++ b/src/app/service/account/ListAccounts.ts @@ -0,0 +1,58 @@ +import { CommandContext } from 'slash-create'; +import { + GuildMember, + MessageEmbedOptions, +} from 'discord.js'; +import Log from '../../utils/Log'; +import ServiceUtils from '../../utils/ServiceUtils'; +import { + retrieveVerifiedTwitter, + VerifiedTwitter, +} from './VerifyTwitter'; +import { + MessageEmbedOptions as MessageEmbedOptionsSlash, +} from 'slash-create'; + +const ListAccounts = async (ctx: CommandContext, guildMember: GuildMember): Promise => { + Log.debug('starting to list external accounts'); + + const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Attempting to list external accounts.'); + + // important + await ctx.defer(true); + + if (isDmOn) { + await ctx.send({ content: 'DM sent!', ephemeral: true }); + } + + const twitterUser: VerifiedTwitter | null = await retrieveVerifiedTwitter(guildMember); + + if (twitterUser == null) { + await ServiceUtils.sendContextMessage(isDmOn, guildMember, ctx, { content: 'No external accounts found!', ephemeral: true }); + return; + } + + if (isDmOn) { + const embedMsg: MessageEmbedOptions = generateTwitterEmbedItem(twitterUser) as MessageEmbedOptions; + await guildMember.send({ embeds: [embedMsg] }); + } else { + const embedMsg: MessageEmbedOptionsSlash = generateTwitterEmbedItem(twitterUser) as MessageEmbedOptionsSlash; + await ctx.send({ embeds: [embedMsg], ephemeral: true }); + } + + Log.debug('finished listing external accounts'); +}; + +export const generateTwitterEmbedItem = (verifiedTwitter: VerifiedTwitter): MessageEmbedOptions | MessageEmbedOptionsSlash => { + return { + title: 'Twitter Authentication', + description: 'Twitter account linked 👍', + fields: [ + { name: 'Display Name', value: `${verifiedTwitter.twitterUser.screen_name}` }, + { name: 'Description', value: `${ServiceUtils.prepEmbedField(verifiedTwitter.twitterUser.description)}` }, + { name: 'Profile', value: `https://twitter.com/${verifiedTwitter.twitterUser.screen_name}` }, + ], + }; +}; + +export default ListAccounts; \ No newline at end of file diff --git a/src/app/service/account/UnlinkAccount.ts b/src/app/service/account/UnlinkAccount.ts index c3e2cc78..701bcbdd 100644 --- a/src/app/service/account/UnlinkAccount.ts +++ b/src/app/service/account/UnlinkAccount.ts @@ -35,7 +35,7 @@ import buttonIds from '../constants/buttonIds'; const UnlinkAccount = async (ctx: CommandContext, guildMember: GuildMember, platform: string): Promise => { Log.debug(`starting to unlink account ${platform}`); - const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, `Attempting to unlink account \`${platform}\``); + const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, `Attempting to unlink account \`${platform}\`.`); // important await ctx.defer(true); @@ -65,6 +65,7 @@ const UnlinkAccount = async (ctx: CommandContext, guildMember: GuildMember, plat LogUtils.logError('failed to unlink twitter account', e); await ServiceUtils.sendOutErrorMessage(ctx).catch(Log.error); } + Log.debug('finished linking account'); }; const promptToUnlink = async (ctx: CommandContext, guildMember: GuildMember, isDMOn: boolean, twitterUser: VerifiedTwitter): Promise => { diff --git a/src/app/service/account/VerifyTwitter.ts b/src/app/service/account/VerifyTwitter.ts index 31a6dc1e..985d520f 100644 --- a/src/app/service/account/VerifyTwitter.ts +++ b/src/app/service/account/VerifyTwitter.ts @@ -1,5 +1,6 @@ import { GuildMember, + MessageEmbedOptions, MessageOptions, } from 'discord.js'; import apiKeys from '../constants/apiKeys'; @@ -19,6 +20,8 @@ import { } from 'slash-create'; import { TwitterApiTokens } from 'twitter-api-v2/dist/types/client.types'; import { unlinkTwitterAccount } from './UnlinkAccount'; +import { generateTwitterEmbedItem } from './ListAccounts'; +import { MessageEmbedOptions as MessageEmbedOptionsSlash } from 'slash-create/lib/structures/message'; export type VerifiedTwitter = { twitterUser: UserV1, @@ -28,7 +31,7 @@ export type VerifiedTwitter = { const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, sendConfirmationMsg: boolean): Promise => { Log.debug('starting to verify twitter account link'); - const isDmOn: boolean = (sendConfirmationMsg) ? await ServiceUtils.tryDMUser(guildMember, 'Hi! Let me check your twitter info') : false; + const isDmOn: boolean = (sendConfirmationMsg) ? await ServiceUtils.tryDMUser(guildMember, 'Hi! Let me check your twitter info.') : false; // important await ctx.defer(true); @@ -45,28 +48,14 @@ const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, send } Log.debug(`${guildMember.user.tag} has linked their twitter account, twitterId: ${verifiedTwitter.twitterUser.id_str}, sending message`); - let authenticatedMsg: MessageOptions | MessageOptionsSlash = { - embeds: [ - { - title: 'Twitter Authentication', - description: 'Twitter account linked 👍', - fields: [ - { name: 'Display Name', value: `${verifiedTwitter.twitterUser.screen_name}` }, - { name: 'Description', value: `${ServiceUtils.prepEmbedField(verifiedTwitter.twitterUser.description)}` }, - { name: 'Profile', value: `https://twitter.com/${verifiedTwitter.twitterUser.screen_name}` }, - ], - }, - ], - }; if (sendConfirmationMsg) { if (isDmOn) { - authenticatedMsg = authenticatedMsg as MessageOptions; - await guildMember.send(authenticatedMsg); + const embedMsg: MessageEmbedOptions = generateTwitterEmbedItem(verifiedTwitter) as MessageEmbedOptions; + await guildMember.send({ embeds: [embedMsg] }); } else { - authenticatedMsg = authenticatedMsg as MessageOptionsSlash; - authenticatedMsg.ephemeral = true; - await ctx.send(authenticatedMsg); + const embedMsg: MessageEmbedOptionsSlash = generateTwitterEmbedItem(verifiedTwitter) as MessageEmbedOptionsSlash; + await ctx.send({ embeds: [embedMsg], ephemeral: true }); } } diff --git a/src/app/service/poap/SchedulePOAP.ts b/src/app/service/poap/SchedulePOAP.ts index b51f9c76..498aff08 100644 --- a/src/app/service/poap/SchedulePOAP.ts +++ b/src/app/service/poap/SchedulePOAP.ts @@ -28,6 +28,8 @@ const SchedulePOAP = async (ctx: CommandContext, guildMember: GuildMember, numbe } const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Minting POAPs is always super exciting!'); + await ctx.defer(true); + const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); await POAPUtils.validateUserAccess(guildMember, db); diff --git a/src/app/service/poap/config/ModifyPOAP.ts b/src/app/service/poap/config/ModifyPOAP.ts index a4ea31b9..86b0b49f 100644 --- a/src/app/service/poap/config/ModifyPOAP.ts +++ b/src/app/service/poap/config/ModifyPOAP.ts @@ -44,6 +44,8 @@ export default async (ctx: CommandContext, guildMember: GuildMember, roles: stri } const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'I can help you configure authorized users for the POAP commands!'); + await ctx.defer(true); + if (isDmOn) { await ctx.send({ content: 'I just sent you a DM!', ephemeral: true }); } else { @@ -165,11 +167,12 @@ export const askForGrantOrRemoval = async ( } as MessageOptionsSlash; } - let message: Message | MessageSlash = await ServiceUtils.sendContextMessage(isDmOn, guildMember, ctx, msg1); + let message = await ServiceUtils.sendContextMessage(isDmOn, guildMember, ctx, msg1); if (isDmOn) { message = message as Message; } else { + message = message as MessageSlash; const textChannel: TextChannel = await guildMember.guild.channels.fetch(ctx.channelID) as TextChannel; message = await textChannel.messages.fetch(message.id) as Message; } diff --git a/src/app/utils/ServiceUtils.ts b/src/app/utils/ServiceUtils.ts index d818356f..32ee4e37 100644 --- a/src/app/utils/ServiceUtils.ts +++ b/src/app/utils/ServiceUtils.ts @@ -206,11 +206,13 @@ const ServiceUtils = { guildMember: GuildMember, ctx: CommandContext, msg: MessageOptions | MessageOptionsSlash, - ): Promise => { + ): Promise => { if (isDmOn) { return await guildMember.send(msg as MessageOptions); } else { - return await ctx.send(msg as MessageOptionsSlash) as MessageSlash; + msg = msg as MessageOptionsSlash; + msg.ephemeral = true; + return await ctx.send(msg); } }, From 415c7d12a87df1a2ac58ddc7bbf7bb71f6153bb2 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 17 Jan 2022 17:50:36 -0500 Subject: [PATCH 11/19] PR prep --- CHANGELOG.md | 6 +++++- src/app/commands/admin/Account.ts | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c0e3d71..83b65749 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,14 @@ ## 2.7.0-RELEASE (2022-01-18) 1. Add more stability to opt-in messages -2. Add account unlink/status commands +2. Add account commands + - /account link (renamed from /account verify) + - /account list + - /account unlink 3. Restrict DEGEN to certain discord servers 4. Upgrade discord.js -> 13.6.0, upgrade slash-create -> 5.0.3 5. Introduce basic `/claim` command and prompt user for opt-in on slash command +6. ## 2.6.2-RELEASE (2022-01-13) diff --git a/src/app/commands/admin/Account.ts b/src/app/commands/admin/Account.ts index 4c15e651..e4f2d444 100644 --- a/src/app/commands/admin/Account.ts +++ b/src/app/commands/admin/Account.ts @@ -6,7 +6,6 @@ import ValidationError from '../../errors/ValidationError'; import { command } from '../../utils/SentryUtils'; import UnlinkAccount from '../../service/account/UnlinkAccount'; import constants from '../../service/constants/constants'; -import discordServerIds from '../../service/constants/discordServerIds'; import ListAccounts from '../../service/account/ListAccounts'; export default class Account extends SlashCommand { @@ -19,7 +18,6 @@ export default class Account extends SlashCommand { duration: 2, }, defaultPermission: true, - guildIDs: [discordServerIds.discordBotGarage], options: [ { name: 'link', From c4cafcf0ab4b279b5536446669c5e9bcfde4c96d Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 17 Jan 2022 17:51:40 -0500 Subject: [PATCH 12/19] update help command --- src/app/commands/admin/Account.ts | 2 +- src/app/service/help/HowToAccount.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/commands/admin/Account.ts b/src/app/commands/admin/Account.ts index e4f2d444..ed5edad2 100644 --- a/src/app/commands/admin/Account.ts +++ b/src/app/commands/admin/Account.ts @@ -12,7 +12,7 @@ export default class Account extends SlashCommand { constructor(creator: SlashCreator) { super(creator, { name: 'account', - description: 'Manage your account\'s integration.', + description: 'Manage external account integration.', throttling: { usages: 1, duration: 2, diff --git a/src/app/service/help/HowToAccount.ts b/src/app/service/help/HowToAccount.ts index 98735bbf..18d25502 100644 --- a/src/app/service/help/HowToAccount.ts +++ b/src/app/service/help/HowToAccount.ts @@ -7,13 +7,13 @@ export default (): MessageOptions => { description: 'These set of commands allows linking, unlinking, and viewing external accounts.\n\n', fields: [ { - name: '-> /account verify', + name: '-> /account link', value: 'Link external account to your discord account.', inline: false, }, { - name: '-> /account status', - value: 'Display the currently linked accounts to your discord account.', + name: '-> /account list', + value: 'Display the currently linked external accounts to your discord account.', inline: false, }, { From 638043b32d6b331c7275e5ab5ec5887f31a1f45f Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 17 Jan 2022 17:56:49 -0500 Subject: [PATCH 13/19] increase poap max duration to 720 minutes --- .env.prod | 2 +- .env.qa | 2 +- CHANGELOG.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.prod b/.env.prod index 6aed6a39..4628d249 100644 --- a/.env.prod +++ b/.env.prod @@ -27,7 +27,7 @@ LOGDNA_DEFAULT_LEVEL=info # POAP POAP_REQUIRED_PARTICIPATION_DURATION=10 -POAP_MAX_EVENT_DURATION_MINUTES=180 +POAP_MAX_EVENT_DURATION_MINUTES=720 # Twitter TWITTER_API_TOKEN= diff --git a/.env.qa b/.env.qa index 9deb5eb4..4a64572e 100644 --- a/.env.qa +++ b/.env.qa @@ -48,7 +48,7 @@ LOGDNA_DEFAULT_LEVEL=debug # POAP POAP_REQUIRED_PARTICIPATION_DURATION=10 -POAP_MAX_EVENT_DURATION_MINUTES=180 +POAP_MAX_EVENT_DURATION_MINUTES=720 # Twitter TWITTER_API_TOKEN= diff --git a/CHANGELOG.md b/CHANGELOG.md index 83b65749..c435cd14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ 3. Restrict DEGEN to certain discord servers 4. Upgrade discord.js -> 13.6.0, upgrade slash-create -> 5.0.3 5. Introduce basic `/claim` command and prompt user for opt-in on slash command -6. +6. Increase poap max time to 12 hours ## 2.6.2-RELEASE (2022-01-13) From a24a55ae542b54d3e2f948d09b12b351b3886d0d Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 17 Jan 2022 19:01:06 -0500 Subject: [PATCH 14/19] setup cron job for expired poaps --- CHANGELOG.md | 1 + package.json | 2 ++ src/app/events/Ready.ts | 3 ++- src/app/schema/discordUsers.json | 2 +- src/app/service/poap/POAPService.ts | 16 ++++++++++++++-- yarn.lock | 24 ++++++++++++++++++++++++ 6 files changed, 44 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c435cd14..c12e82d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ 4. Upgrade discord.js -> 13.6.0, upgrade slash-create -> 5.0.3 5. Introduce basic `/claim` command and prompt user for opt-in on slash command 6. Increase poap max time to 12 hours +7. Add poap expiration cron job ## 2.6.2-RELEASE (2022-01-13) diff --git a/package.json b/package.json index 17619e04..70221d77 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@sentry/node": "^6.16.1", "@sentry/tracing": "^6.16.1", "@types/node": "^16.7.1", + "@types/node-cron": "^3.0.1", "axios": "^0.21.4", "csv-parse": "^5.0.3", "csv-stringify": "^6.0.3", @@ -50,6 +51,7 @@ "lodash.clonedeep": "^4.5.0", "lodash.isequal": "^4.5.0", "mongodb": "^3.6.9", + "node-cron": "^3.0.0", "p-queue": "^6.6.2", "slash-create": "^5.0.3", "twitter-api-v2": "^1.6.5", diff --git a/src/app/events/Ready.ts b/src/app/events/Ready.ts index a64e40aa..3629736c 100644 --- a/src/app/events/Ready.ts +++ b/src/app/events/Ready.ts @@ -29,7 +29,8 @@ export default class implements DiscordEvent { // should not wait POAPService.runAutoEndSetup(client, constants.PLATFORM_TYPE_DISCORD).catch(Log.error); POAPService.runAutoEndSetup(client, constants.PLATFORM_TYPE_TWITTER).catch(Log.error); - await POAPService.clearExpiredPOAPs(); + await POAPService.clearExpiredPOAPs().catch(Log.error); + POAPService.setupPOAPCleanupCronJob(); Log.info(`${constants.APP_NAME} is ready!`); } catch (e) { diff --git a/src/app/schema/discordUsers.json b/src/app/schema/discordUsers.json index 10230ca0..4ba85070 100644 --- a/src/app/schema/discordUsers.json +++ b/src/app/schema/discordUsers.json @@ -4,7 +4,7 @@ "required": [ "userId", "tag", - "isDMEnabled", + "isDMEnabled" ], "properties": { "userId": { diff --git a/src/app/service/poap/POAPService.ts b/src/app/service/poap/POAPService.ts index 084907b9..2e6467ea 100644 --- a/src/app/service/poap/POAPService.ts +++ b/src/app/service/poap/POAPService.ts @@ -11,6 +11,7 @@ import { storePresentMembers } from './start/StartPOAP'; import { POAPUnclaimedParticipants } from '../../types/poap/POAPUnclaimedParticipants'; import { POAPTwitterUnclaimedParticipants } from '../../types/poap/POAPTwitterUnclaimedParticipants'; import ValidationError from '../../errors/ValidationError'; +import cron, { ScheduledTask } from 'node-cron'; const POAPService = { runAutoEndSetup: async (client: DiscordClient, platform: string): Promise => { @@ -111,6 +112,7 @@ const POAPService = { }, clearExpiredPOAPs: async (): Promise => { + Log.debug('starting cleanup of expired POAPs'); try { const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); const unclaimedPOAPsCollection: Collection = await db.collection(constants.DB_COLLECTION_POAP_UNCLAIMED_PARTICIPANTS); @@ -127,11 +129,21 @@ const POAPService = { }, }).catch(Log.error); - Log.debug('deleted all expired poaps'); + Log.debug('deleted all expired POAPs'); } catch (e) { - LogUtils.logError('failed to delete expired poaps', e); + LogUtils.logError('failed to delete expired POAPs', e); } }, + + setupPOAPCleanupCronJob: (): void => { + Log.debug('setting up cron job for checking expired POAPs'); + // run cron job every 23 hours + const task: ScheduledTask = cron.schedule('* */23 * * *', () => { + POAPService.clearExpiredPOAPs().catch(Log.error); + }); + task.start(); + Log.debug('cron job setup for checking expired POAPs'); + }, }; export default POAPService; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index cc692f47..a0986740 100644 --- a/yarn.lock +++ b/yarn.lock @@ -813,6 +813,11 @@ "@types/bson" "*" "@types/node" "*" +"@types/node-cron@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.1.tgz#e01a874d4c2aa1a02ebc64cfd1cd8ebdbad7a996" + integrity sha512-BkMHHonDT8NJUE/pQ3kr5v2GLDKm5or9btLBoBx4F2MB2cuqYC748LYMDC55VlrLI5qZZv+Qgc3m4P3dBPcmeg== + "@types/node-fetch@^2.5.10", "@types/node-fetch@^2.5.12": version "2.5.12" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66" @@ -3095,6 +3100,18 @@ mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +moment-timezone@^0.5.31: + version "0.5.34" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" + integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + mongodb-memory-server-core@7.3.6: version "7.3.6" resolved "https://registry.yarnpkg.com/mongodb-memory-server-core/-/mongodb-memory-server-core-7.3.6.tgz#9c16cf120e498011a8702e7735271cbe878dd6e5" @@ -3167,6 +3184,13 @@ new-find-package-json@^1.1.0: debug "^4.3.2" tslib "^2.3.0" +node-cron@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.0.tgz#b33252803e430f9cd8590cf85738efa1497a9522" + integrity sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA== + dependencies: + moment-timezone "^0.5.31" + node-fetch@^2.6.1: version "2.6.6" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" From 26616646f7e9e1ecd0dcee6668e3ce3af42d9fd1 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 17 Jan 2022 23:11:17 -0500 Subject: [PATCH 15/19] add defer msg to twitter start --- src/app/service/poap/POAPService.ts | 2 +- src/app/service/poap/start/StartTwitterFlow.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/service/poap/POAPService.ts b/src/app/service/poap/POAPService.ts index 2e6467ea..e1043faf 100644 --- a/src/app/service/poap/POAPService.ts +++ b/src/app/service/poap/POAPService.ts @@ -138,7 +138,7 @@ const POAPService = { setupPOAPCleanupCronJob: (): void => { Log.debug('setting up cron job for checking expired POAPs'); // run cron job every 23 hours - const task: ScheduledTask = cron.schedule('* */23 * * *', () => { + const task: ScheduledTask = cron.schedule('* 23 * * *', () => { POAPService.clearExpiredPOAPs().catch(Log.error); }); task.start(); diff --git a/src/app/service/poap/start/StartTwitterFlow.ts b/src/app/service/poap/start/StartTwitterFlow.ts index da171175..5b1a5417 100644 --- a/src/app/service/poap/start/StartTwitterFlow.ts +++ b/src/app/service/poap/start/StartTwitterFlow.ts @@ -28,6 +28,12 @@ const StartTwitterFlow = async (ctx: CommandContext, guildMember: GuildMember, d const twitterClientV2: TwitterApi = new TwitterApi(apiKeys.twitterBearerToken as string); const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Hello! I can help start a POAP event!'); + await ctx.defer(true); + + if (isDmOn) { + await ctx.send({ content: 'DM sent!', ephemeral: true }); + } + let twitterSpaceResult: SpaceV2LookupResult | null = null; try { Log.debug(`twitterId: ${verifiedTwitter.twitterUser.id_str}`); From d1afefb0755630800c21f219350793c731e59fb9 Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 18 Jan 2022 11:35:22 -0500 Subject: [PATCH 16/19] enhance poap distribution --- CHANGELOG.md | 1 + src/app/events/VoiceStateUpdate.ts | 2 +- src/app/service/poap/DistributePOAP.ts | 19 ++- src/app/utils/Log.ts | 172 +++++++++++++++++-------- src/app/utils/POAPUtils.ts | 36 ++++-- src/app/utils/ServiceUtils.ts | 6 +- 6 files changed, 159 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c12e82d7..1c47bb0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ 5. Introduce basic `/claim` command and prompt user for opt-in on slash command 6. Increase poap max time to 12 hours 7. Add poap expiration cron job +8. Enhance poap distribution to work on public channels ## 2.6.2-RELEASE (2022-01-13) diff --git a/src/app/events/VoiceStateUpdate.ts b/src/app/events/VoiceStateUpdate.ts index ddc33ee8..c0494501 100644 --- a/src/app/events/VoiceStateUpdate.ts +++ b/src/app/events/VoiceStateUpdate.ts @@ -18,7 +18,7 @@ export default class implements DiscordEvent { */ async execute(oldState: VoiceState, newState: VoiceState): Promise { try { - await HandleParticipantDuringEvent(oldState, newState).catch(e => LogUtils.logError('failed to handle user in POAP event', e, oldState.guild.id)); + await HandleParticipantDuringEvent(oldState, newState).catch(e => LogUtils.logError('failed to handle user in POAP event', e)); } catch (e) { LogUtils.logError('failed to process event voiceStateUpdate', e); } diff --git a/src/app/service/poap/DistributePOAP.ts b/src/app/service/poap/DistributePOAP.ts index c7ad9010..8311ffee 100644 --- a/src/app/service/poap/DistributePOAP.ts +++ b/src/app/service/poap/DistributePOAP.ts @@ -31,10 +31,12 @@ export default async (ctx: CommandContext, guildMember: GuildMember, event: stri const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Hello! I can help you distribute POAPS.'); - if (!isDmOn) { - await ctx.sendFollowUp({ content: '⚠ Please make sure this is a private channel. I can help you distribute POAPs but anyone who has access to this channel can see private information! ⚠' }); - } else if (ctx) { + await ctx.defer(true); + + if (isDmOn) { await ctx.send({ content: 'Please check your DMs!', ephemeral: true }); + } else { + await ctx.send({ content: '⚠ Please make sure this is a private channel. I can help you distribute POAPs but anyone who has access to this channel can see private information! ⚠', ephemeral: true }); } let participantsList: POAPFileParticipant[] | TwitterPOAPFileParticipant[] = await askForParticipantsList(guildMember, platform, isDmOn, ctx); @@ -56,7 +58,7 @@ export default async (ctx: CommandContext, guildMember: GuildMember, event: stri if (isDmOn) { await guildMember.send({ content: msg }).catch(Log.error); } else { - await ctx.send({ content: msg }); + await ctx.send({ content: msg, ephemeral: true }); } throw Error('failed to parse'); } @@ -91,7 +93,7 @@ export const askForParticipantsList = async (guildMember: GuildMember, platform: if (isDmOn) { await guildMember.send({ content: csvPrompt }); } else { - await ctx.sendFollowUp({ content: csvPrompt }); + await ctx.send({ content: csvPrompt, ephemeral: true }); } Log.debug(`message: '${csvPrompt}' send to user`); @@ -104,8 +106,9 @@ export const askForParticipantsList = async (guildMember: GuildMember, platform: try { const message: Message | undefined = (await contextChannel.awaitMessages({ max: 1, - time: 180000, + time: 180_000, errors: ['time'], + filter: m => m.author.id == guildMember.id && m.attachments.size >= 1, })).first(); if (message == null) { throw new ValidationError('Invalid message'); @@ -120,6 +123,10 @@ export const askForParticipantsList = async (guildMember: GuildMember, platform: const fileResponse = await axios.get(participantAttachment.url); participantsList = ServiceUtils.parseCSVFile(fileResponse.data); + if (!isDmOn) { + await message.delete(); + } + if ((participantsList as POAPFileParticipant[])[0].discordUserId == null) { if ((participantsList as TwitterPOAPFileParticipant[])[0].twitterUserId == null) { throw new Error('missing ID'); diff --git a/src/app/utils/Log.ts b/src/app/utils/Log.ts index 93b69378..771728af 100644 --- a/src/app/utils/Log.ts +++ b/src/app/utils/Log.ts @@ -20,121 +20,176 @@ try { // eslint-disable-next-line no-console console.log('Please setup LogDNA token.'); // eslint-disable-next-line no-console - console.log(e); - throw new Error(); + console.error(e); } const Log = { info(statement: string | any, options?: Omit): void { - if (process.env.NODE_ENV != 'production' || !logger.info) { + try { + if (process.env.NODE_ENV != 'production' || !logger.info) { + // eslint-disable-next-line no-console + console.log(statement); + } else { + logger.info(statement, options); + } + } catch (e) { // eslint-disable-next-line no-console - console.log(statement); - } else { - logger.info(statement, options); + console.error(e); } }, warn(statement: string | any, options?: Omit): void { - if (process.env.NODE_ENV != 'production' || !logger.warn) { + try { + if (process.env.NODE_ENV != 'production' || !logger.warn) { + // eslint-disable-next-line no-console + console.log(statement); + } else { + logger.warn(statement, options); + } + } catch (e) { // eslint-disable-next-line no-console - console.log(statement); - } else { - logger.warn(statement, options); + console.error(e); } }, debug(statement: string | any, options?: Omit): void { - if (process.env.NODE_ENV != 'production' || !logger.debug) { + try { + if (process.env.NODE_ENV != 'production' || !logger.debug) { + // eslint-disable-next-line no-console + console.debug(statement); + } else { + logger.debug(statement, options); + } + } catch (e) { // eslint-disable-next-line no-console - console.debug(statement); - } else { - logger.debug(statement, options); + console.error(e); } }, error(statement: string | any, options?: Omit): void { - if (process.env.NODE_ENV != 'production' || !logger.error) { + try { + if (process.env.NODE_ENV != 'production' || !logger.error) { + // eslint-disable-next-line no-console + console.error(statement); + } else { + logger.error(statement, options); + } + } catch (e) { // eslint-disable-next-line no-console - console.error(statement); - } else { - logger.error(statement, options); + console.error(e); } }, fatal(statement: string | any, options?: Omit): void { - if (process.env.NODE_ENV != 'production' || !logger.fatal) { + try { + if (process.env.NODE_ENV != 'production' || !logger.fatal) { + // eslint-disable-next-line no-console + console.error(statement); + } else { + logger.fatal(statement, options); + } + } catch (e) { // eslint-disable-next-line no-console - console.error(statement); - } else { - logger.fatal(statement, options); + console.error(e); } }, trace(statement: string | any, options?: Omit): void { - if (process.env.NODE_ENV != 'production' || !logger.trace) { + try { + if (process.env.NODE_ENV != 'production' || !logger.trace) { + // eslint-disable-next-line no-console + console.log(statement); + } else { + logger.trace(statement, options); + } + } catch (e) { // eslint-disable-next-line no-console - console.log(statement); - } else { - logger.trace(statement, options); + console.error(e); } }, log(statement: string | any, options?: Omit): void { - if (process.env.NODE_ENV != 'production') { + try { + if (process.env.NODE_ENV != 'production') { + // eslint-disable-next-line no-console + console.log(statement); + } + logger.log(statement, options); + } catch (e) { // eslint-disable-next-line no-console - console.log(statement); + console.error(e); } - logger.log(statement, options); }, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types addMetaProperty(key: string, value: any): void { - logger.addMetaProperty(key, value); + try { + logger.addMetaProperty(key, value); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } }, removeMetaProperty(key: string): void { - logger.removeMetaProperty(key); + try { + logger.removeMetaProperty(key); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } }, flush(): void { - logger.flush(); + try { + logger.flush(); + } catch(e) { + // eslint-disable-next-line no-console + console.error(e); + } }, }; export const LogUtils = { logCommandStart(ctx: CommandContext): void { - Log.info(`/${ctx.commandName} ran ${ctx.user.username}#${ctx.user.discriminator}`, { - indexMeta: true, - meta: { - guildId: ctx.guildID, - userTag: `${ctx.user.username}#${ctx.user.discriminator}`, - userId: ctx.user.id, - params: ctx.options, - }, - }); + try { + Log.info(`/${ctx.commandName} ran ${ctx.user.username}#${ctx.user.discriminator}`, { + indexMeta: true, + meta: { + guildId: ctx.guildID, + userTag: `${ctx.user.username}#${ctx.user.discriminator}`, + userId: ctx.user.id, + params: ctx.options, + }, + }); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } }, logCommandEnd(ctx: CommandContext): void { - Log.info(`/${ctx.commandName} ended ${ctx.user.username}#${ctx.user.discriminator}`, { - indexMeta: true, - meta: { - guildId: ctx.guildID, - userTag: `${ctx.user.username}#${ctx.user.discriminator}`, - userId: ctx.user.id, - params: ctx.options, - }, - }); + try { + Log.info(`/${ctx.commandName} ended ${ctx.user.username}#${ctx.user.discriminator}`, { + indexMeta: true, + meta: { + guildId: ctx.guildID, + userTag: `${ctx.user.username}#${ctx.user.discriminator}`, + userId: ctx.user.id, + params: ctx.options, + }, + }); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } }, logError(message: string, error: Error | any, guildId?: string): void { try { if (error != null && error instanceof Error) { - Sentry.captureException(error, { - tags: { - guildId: guildId, - }, - }); + Sentry.captureException(error); Log.error(message, { indexMeta: true, meta: { @@ -144,11 +199,16 @@ export const LogUtils = { guildId: guildId, }, }); + if (process.env.SENTRY_ENVIRONMENT == 'local') { + // eslint-disable-next-line no-console + console.error(error); + } } else { Log.error(message); } } catch (e) { - Log.warn(message); + // eslint-disable-next-line no-console + console.error(e); } }, }; diff --git a/src/app/utils/POAPUtils.ts b/src/app/utils/POAPUtils.ts index 2b305585..2790a83c 100644 --- a/src/app/utils/POAPUtils.ts +++ b/src/app/utils/POAPUtils.ts @@ -101,29 +101,29 @@ const POAPUtils = { adminChannel?: TextChannel | null, ): Promise { Log.debug('asking poap organizer for poap links attachment'); - const uploadLinksMsg = `Please upload the POAP links.txt file. This file should have a least ${numberOfParticipants} link(s). Each link should be on a new line. This file can be obtained from \`/poap mint\` command`; + const uploadLinksMsg = `Please upload the \`links.txt\` file. This file should have a least ${numberOfParticipants} link(s). Each link should be on a new line. This file can be obtained from \`/poap mint\` command.`; const replyOptions: AwaitMessagesOptions = { max: 1, - time: 900000, + time: 900_000, errors: ['time'], - filter: m => m.author.id == guildMember.user.id, + filter: m => m.author.id == guildMember.user.id && m.attachments.size >= 1, }; let message: Message | undefined; if (isDmOn) { - await guildMember.send({ content: uploadLinksMsg }); - const dmChannel: DMChannel = await guildMember.createDM(); + const promptMsg: Message = await guildMember.send({ content: uploadLinksMsg }); + const dmChannel: DMChannel = await promptMsg.channel.fetch() as DMChannel; message = (await dmChannel.awaitMessages(replyOptions).catch(() => { throw new ValidationError('Invalid attachment. Session ended, please try the command again.'); })).first(); } else if (ctx) { - await ctx.sendFollowUp(uploadLinksMsg); + await ctx.send({ content: uploadLinksMsg, ephemeral: true }); const guildChannel: TextChannel = await guildMember.guild.channels.fetch(ctx.channelID) as TextChannel; message = (await guildChannel.awaitMessages(replyOptions).catch(() => { throw new ValidationError('Invalid attachment. Session ended, please try the command again.'); })).first(); } else if (adminChannel != null) { - await adminChannel.send(uploadLinksMsg); + await adminChannel.send({ content: uploadLinksMsg }); message = (await adminChannel.awaitMessages(replyOptions).catch(() => { throw new ValidationError('Invalid attachment. Session ended, please try the command again.'); })).first(); @@ -139,6 +139,10 @@ const POAPUtils = { throw new ValidationError('Invalid attachment. Session ended, please try the command again.'); } + if (!isDmOn) { + await message.delete(); + } + Log.debug(`obtained poap links attachment in discord: ${poapLinksFile.url}`); return poapLinksFile; }, @@ -195,6 +199,9 @@ const POAPUtils = { const poapHostRegex = /^http[s]?:\/\/poap\.xyz\/.*$/gis; + const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); + const dbUsersCollection: MongoCollection = await db.collection(constants.DB_COLLECTION_DISCORD_USERS); + while (i < length) { const participant: POAPFileParticipant | undefined = listOfParticipants.pop(); if (participant == null) { @@ -203,6 +210,8 @@ const POAPUtils = { continue; } + Log.debug(participant); + let poapLink: string | undefined = ''; if (listOfPOAPLinks) { poapLink = listOfPOAPLinks.pop(); @@ -223,6 +232,7 @@ const POAPUtils = { if (participant.discordUserId.length < 17) { throw new ValidationError('There appears to be a parsing error. Please check that the discordUserID is greater than 16 digits.'); } + try { if (!poapLink.match(poapHostRegex)) { Log.warn('invalid POAP link provided', { @@ -239,11 +249,12 @@ const POAPUtils = { discordUserTag: participant.discordUserTag, poapLink: 'Invalid POAP link', }); + i++; continue; } const participantMember: GuildMember = await guildMember.guild.members.fetch(participant.discordUserId); - if (!(await ServiceUtils.isDMEnabledForUser(participantMember))) { + if (!(await ServiceUtils.isDMEnabledForUser(participantMember, dbUsersCollection))) { Log.debug('user has not opted in to DMs'); results.hasDMOff++; failedPOAPsList.push({ @@ -251,6 +262,7 @@ const POAPUtils = { discordUserTag: participant.discordUserTag, poapLink: poapLink, }); + i++; continue; } @@ -295,7 +307,10 @@ const POAPUtils = { }).then((_) => { message.edit({ content: 'Report received, thank you!', components: [] }); POAPUtils.reportPOAPOrganizer(guildMember).catch(Log.error); + }).catch(e => { + LogUtils.logError('failed to handle user poap link response', e); }); + results.successfullySent++; } catch (e) { LogUtils.logError('user might have been banned or has DMs off', e); @@ -518,6 +533,7 @@ const POAPUtils = { await guildMember.send(distributionEmbedMsg).catch(Log.error); } else if (ctx) { distributionEmbedMsg = distributionEmbedMsg as MessageOptionsSlash; + distributionEmbedMsg.ephemeral = true; distributionEmbedMsg.file = [{ name: 'failed_to_send_poaps.csv', file: failedPOAPsBuffer }]; await ctx.sendFollowUp(distributionEmbedMsg); } else if (channelExecution) { @@ -539,14 +555,14 @@ const POAPUtils = { if (isDmOn) { await guildMember.send({ content: deliveryMsg }).catch(Log.error); } else if (ctx) { - await ctx.sendFollowUp(deliveryMsg); + await ctx.send({ content: deliveryMsg, ephemeral: true }); } } else { const failedDeliveryMsg = `Looks like some degens have DMs off or they haven't oped in for delivery. They can claim their POAPs by sending \`gm\` to <@${ApiKeys.DISCORD_BOT_ID}> or executing slash command \`/poap claim\``; if (isDmOn) { await guildMember.send({ content: failedDeliveryMsg }); } else if (ctx) { - await ctx.sendFollowUp({ content: failedDeliveryMsg, ephemeral: true }); + await ctx.send({ content: failedDeliveryMsg, ephemeral: true }); } } }, diff --git a/src/app/utils/ServiceUtils.ts b/src/app/utils/ServiceUtils.ts index 32ee4e37..51f0928f 100644 --- a/src/app/utils/ServiceUtils.ts +++ b/src/app/utils/ServiceUtils.ts @@ -247,11 +247,9 @@ const ServiceUtils = { } as MessageOptionsSlash; }, - isDMEnabledForUser: async (member: GuildMember): Promise => { - const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); - const dbUsers: MongoCollection = await db.collection(constants.DB_COLLECTION_DISCORD_USERS); + isDMEnabledForUser: async (member: GuildMember, dbUsersCollection: MongoCollection): Promise => { await member.fetch(); - const result: DiscordUserCollection | null = await dbUsers.findOne({ + const result: DiscordUserCollection | null = await dbUsersCollection.findOne({ userId: member.id.toString(), }); From 644374aef9c38c44c8a25ad19ca24805d72f079c Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 18 Jan 2022 12:03:22 -0500 Subject: [PATCH 17/19] handle gm opt-in from in-channel --- CHANGELOG.md | 7 ++++++- src/app/service/account/UnlinkAccount.ts | 3 +-- src/app/service/account/VerifyTwitter.ts | 6 +++--- src/app/service/poap/ClaimPOAP.ts | 13 ++++++++----- src/app/service/poap/SchedulePOAP.ts | 4 ++-- src/app/utils/ServiceUtils.ts | 2 +- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c47bb0d..839d0aac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,12 @@ 5. Introduce basic `/claim` command and prompt user for opt-in on slash command 6. Increase poap max time to 12 hours 7. Add poap expiration cron job -8. Enhance poap distribution to work on public channels +8. Enhance poap distribution to with ephemeral + - fix timeout reply after poap distribution + - enhance poap distribution loop + - enhance poap end +9. Parse blank strings for msg embed display +10. Prompt users to DM delivery is /claim is executed from channel ## 2.6.2-RELEASE (2022-01-13) diff --git a/src/app/service/account/UnlinkAccount.ts b/src/app/service/account/UnlinkAccount.ts index 701bcbdd..2e0dbbe7 100644 --- a/src/app/service/account/UnlinkAccount.ts +++ b/src/app/service/account/UnlinkAccount.ts @@ -79,7 +79,7 @@ const promptToUnlink = async (ctx: CommandContext, guildMember: GuildMember, isD fields: [ { name: 'UserId', value: `${twitterUser.twitterUser.id_str}`, inline: false }, { name: 'Name', value: `${twitterUser.twitterUser.screen_name}`, inline: false }, - { name: 'Description', value: `${twitterUser.twitterUser.description}`, inline: false }, + { name: 'Description', value: `${ServiceUtils.prepEmbedField(twitterUser.twitterUser.description)}`, inline: false }, { name: 'Profile', value: `https://twitter.com/${twitterUser.twitterUser.screen_name}`, inline: false }, ], }, @@ -145,7 +145,6 @@ const promptToUnlink = async (ctx: CommandContext, guildMember: GuildMember, isD ]; Log.debug('attempting to send msg to user'); Log.debug(shouldUnlinkMsg); - await ctx.defer(true); const msgSlashResponse: MessageSlash = await ctx.send(shouldUnlinkMsg) as MessageSlash; Log.debug('ctx message on user confirmation sent'); shouldUnlinkPromise = new Promise((resolve, _) => { diff --git a/src/app/service/account/VerifyTwitter.ts b/src/app/service/account/VerifyTwitter.ts index 985d520f..d01d025b 100644 --- a/src/app/service/account/VerifyTwitter.ts +++ b/src/app/service/account/VerifyTwitter.ts @@ -64,7 +64,7 @@ const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, send }; export const retrieveVerifiedTwitter = async (guildMember: GuildMember): Promise => { - Log.debug('starting to link twitter account link'); + Log.debug('starting to retrieve twitter account'); const db: Db = await MongoDbUtils.connect(constants.DB_NAME_NEXTAUTH); const accountsCollection: Collection = db.collection(constants.DB_COLLECTION_NEXT_AUTH_ACCOUNTS); @@ -87,7 +87,7 @@ export const retrieveVerifiedTwitter = async (guildMember: GuildMember): Promise }); if (twitterCollection == null || twitterCollection.accessToken == null) { - Log.debug('twitter account not linked'); + Log.debug('twitter account not found'); return null; } @@ -120,7 +120,7 @@ export const retrieveVerifiedTwitter = async (guildMember: GuildMember): Promise return null; } - Log.debug('done linking twitter account'); + Log.debug('found twitter account'); return { twitterUser: userCall, twitterClientV1: userClient, diff --git a/src/app/service/poap/ClaimPOAP.ts b/src/app/service/poap/ClaimPOAP.ts index dd85b782..a670af47 100644 --- a/src/app/service/poap/ClaimPOAP.ts +++ b/src/app/service/poap/ClaimPOAP.ts @@ -41,11 +41,14 @@ const ClaimPOAP = async (ctx: CommandContext, platform: string, guildMember?: Gu await claimForDiscord(ctx.user.id, ctx); if (guildMember && ctx) { try { - const dmChannel: DMChannel = await guildMember.createDM(); - await OptInPOAP(guildMember.user, await dmChannel).catch(e => { - Log.error(e); - ServiceUtils.sendOutErrorMessageForDM(dmChannel).catch(Log.error); - }); + const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'gm'); + if (isDmOn) { + const dmChannel: DMChannel = await guildMember.createDM(); + await OptInPOAP(guildMember.user, dmChannel).catch(e => { + Log.error(e); + ServiceUtils.sendOutErrorMessageForDM(dmChannel).catch(Log.error); + }); + } } catch (e) { LogUtils.logError('failed to ask for opt-in', e); } diff --git a/src/app/service/poap/SchedulePOAP.ts b/src/app/service/poap/SchedulePOAP.ts index 498aff08..15daa4ce 100644 --- a/src/app/service/poap/SchedulePOAP.ts +++ b/src/app/service/poap/SchedulePOAP.ts @@ -223,8 +223,8 @@ const SchedulePOAP = async (ctx: CommandContext, guildMember: GuildMember, numbe { name: 'Event Title', value: request.name }, { name: 'Event Description', value: request.description }, { name: 'Virtual Event', value: (request.virtual_event ? 'yes' : 'no'), inline: true }, - { name: 'City', value: `${request.city} `, inline: true }, - { name: 'Country', value: `${request.country} `, inline: true }, + { name: 'City', value: `${ServiceUtils.prepEmbedField(request.city)}`, inline: true }, + { name: 'Country', value: `${ServiceUtils.prepEmbedField(request.country)}`, inline: true }, { name: 'Event Start', value: request.start_date, inline: true }, { name: 'Event End', value: request.end_date, inline: true }, { name: 'Event URL', value: `${request.event_url} `, inline: true }, diff --git a/src/app/utils/ServiceUtils.ts b/src/app/utils/ServiceUtils.ts index 51f0928f..2a3eb9ae 100644 --- a/src/app/utils/ServiceUtils.ts +++ b/src/app/utils/ServiceUtils.ts @@ -136,7 +136,7 @@ const ServiceUtils = { } }, - prepEmbedField: (field: string | null): string => { + prepEmbedField: (field: string | null | undefined): string => { return (field) ? field : '-'; }, From aec8a0b3da1f97dca30765a6e0c1483fdf5bc4f2 Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 18 Jan 2022 12:36:58 -0500 Subject: [PATCH 18/19] enhance messaging --- CHANGELOG.md | 1 + src/app/service/poap/DistributePOAP.ts | 4 ++-- src/app/service/poap/start/StartChannelFlow.ts | 8 ++++---- src/app/service/poap/start/StartPOAP.ts | 2 ++ src/app/service/poap/start/StartTwitterFlow.ts | 8 ++------ src/app/utils/POAPUtils.ts | 2 +- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 839d0aac..d4496471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - enhance poap end 9. Parse blank strings for msg embed display 10. Prompt users to DM delivery is /claim is executed from channel +11. Message enhancements to twitter flow ## 2.6.2-RELEASE (2022-01-13) diff --git a/src/app/service/poap/DistributePOAP.ts b/src/app/service/poap/DistributePOAP.ts index 8311ffee..803f2a11 100644 --- a/src/app/service/poap/DistributePOAP.ts +++ b/src/app/service/poap/DistributePOAP.ts @@ -85,9 +85,9 @@ export const askForParticipantsList = async (guildMember: GuildMember, platform: Log.debug('preparing to ask for participants list csv file'); let csvPrompt = ''; if (platform == constants.PLATFORM_TYPE_DISCORD) { - csvPrompt = 'Please upload participants.csv file with header containing discordUserId. POAPs will be distributed to these degens.'; + csvPrompt = 'Please upload distribution file with header containing discordUserId. POAPs will be distributed to these degens.'; } else if (platform == constants.PLATFORM_TYPE_TWITTER) { - csvPrompt = 'Please upload participants.csv file with header containing twitterUserId. POAPs will be distributed to these degens.'; + csvPrompt = 'Please upload distribution file with header containing twitterUserId. POAPs will be distributed to these degens.'; } if (isDmOn) { diff --git a/src/app/service/poap/start/StartChannelFlow.ts b/src/app/service/poap/start/StartChannelFlow.ts index 72893ca5..da68b5f1 100644 --- a/src/app/service/poap/start/StartChannelFlow.ts +++ b/src/app/service/poap/start/StartChannelFlow.ts @@ -27,7 +27,6 @@ const StartChannelFlow = async ( Log.debug('starting channel flow for poap start'); const voiceChannels: Collection = ServiceUtils.getAllVoiceChannels(guildMember); - await ctx.sendFollowUp({ content: '⚠ **Please make sure this is a private channel.** I can help you setup the poap event! ⚠' }); const embedsVoiceChannels = generateVoiceChannelEmbedMessage(voiceChannels) as MessageEmbedOptionsSlash[]; const message = await ctx.sendFollowUp({ embeds: embedsVoiceChannels }); @@ -45,13 +44,13 @@ const StartChannelFlow = async ( if (poapSettingsDoc !== null && poapSettingsDoc.isActive) { Log.warn('unable to start due to active event'); - await ctx.sendFollowUp(`\`${channelChoice.name}\` is already active. Please reach out to <@${poapSettingsDoc.discordUserId}> to end event.`); + await ctx.send({ content: `\`${channelChoice.name}\` is already active. Please reach out to <@${poapSettingsDoc.discordUserId}> to end event.`, ephemeral: true }); return; } await setActiveEventInDb(guildMember, db, channelChoice, event, duration, ctx.channelID); - await ctx.sendFollowUp({ + await ctx.send({ embeds: [ { title: 'Event Started', @@ -65,9 +64,10 @@ const StartChannelFlow = async ( ], }, ], + ephemeral: true, }); - await ctx.sendFollowUp(({ content: 'Everything is set, catch you later!' })); + await ctx.sendFollowUp(({ content: 'Everything is set, catch you later!', ephemeral: true })); }; export default StartChannelFlow; \ No newline at end of file diff --git a/src/app/service/poap/start/StartPOAP.ts b/src/app/service/poap/start/StartPOAP.ts index e2d72d4e..52f812ab 100644 --- a/src/app/service/poap/start/StartPOAP.ts +++ b/src/app/service/poap/start/StartPOAP.ts @@ -52,6 +52,8 @@ export default async (ctx: CommandContext, guildMember: GuildMember, platform: s Log.debug('poap start validated'); + await ctx.defer(); + if (platform == constants.PLATFORM_TYPE_TWITTER) { await StartTwitterFlow(ctx, guildMember, db, event, duration); return; diff --git a/src/app/service/poap/start/StartTwitterFlow.ts b/src/app/service/poap/start/StartTwitterFlow.ts index 5b1a5417..d80559d1 100644 --- a/src/app/service/poap/start/StartTwitterFlow.ts +++ b/src/app/service/poap/start/StartTwitterFlow.ts @@ -54,14 +54,10 @@ const StartTwitterFlow = async (ctx: CommandContext, guildMember: GuildMember, d return; } - if (!isDmOn) { - await ctx.send({ content: '⚠ **Please make sure this is a private channel.** I can help you setup the poap event! ⚠', ephemeral: true }); - } - const twitterSpaceId: string = twitterSpaceResult.data[0]['id']; Log.debug(`twitter spaces event active: ${twitterSpaceId}`); - await ctx.send({ content: `Something really special is starting...:bird: https://twitter.com/i/spaces/${twitterSpaceId}` }); + await ctx.send({ content: `Twitter Spaces :bird: is live at https://twitter.com/i/spaces/${twitterSpaceId}` }); const poapTwitterSettings: Collection = db.collection(constants.DB_COLLECTION_POAP_TWITTER_SETTINGS); const activeSettings: POAPTwitterSettings | null = await poapTwitterSettings.findOne({ @@ -74,7 +70,7 @@ const StartTwitterFlow = async (ctx: CommandContext, guildMember: GuildMember, d Log.debug('unable to start twitter event due to active event'); const msg = 'Looks like you have an active twitter spaces event!'; if (isDmOn) { - await ctx.send({ content: msg }); + await ctx.send({ content: msg, ephemeral: true }); } throw new ValidationError(msg); } diff --git a/src/app/utils/POAPUtils.ts b/src/app/utils/POAPUtils.ts index 2790a83c..16375362 100644 --- a/src/app/utils/POAPUtils.ts +++ b/src/app/utils/POAPUtils.ts @@ -558,7 +558,7 @@ const POAPUtils = { await ctx.send({ content: deliveryMsg, ephemeral: true }); } } else { - const failedDeliveryMsg = `Looks like some degens have DMs off or they haven't oped in for delivery. They can claim their POAPs by sending \`gm\` to <@${ApiKeys.DISCORD_BOT_ID}> or executing slash command \`/poap claim\``; + const failedDeliveryMsg = `Looks like some degens have DMs off or they haven't opted in for delivery. They can claim their POAPs by sending \`gm\` to <@${ApiKeys.DISCORD_BOT_ID}> or executing slash command \`/poap claim\``; if (isDmOn) { await guildMember.send({ content: failedDeliveryMsg }); } else if (ctx) { From c00e0da7bd77fb73f5e5a16667ecf57dc9fbc5c5 Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 18 Jan 2022 19:13:09 -0500 Subject: [PATCH 19/19] fix poap end file --- src/app/service/poap/end/EndTwitterFlow.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/app/service/poap/end/EndTwitterFlow.ts b/src/app/service/poap/end/EndTwitterFlow.ts index cf37c675..9649baee 100644 --- a/src/app/service/poap/end/EndTwitterFlow.ts +++ b/src/app/service/poap/end/EndTwitterFlow.ts @@ -1,6 +1,7 @@ import { GuildMember, MessageAttachment, + MessageOptions, TextChannel, } from 'discord.js'; import { @@ -19,6 +20,7 @@ import POAPUtils, { TwitterPOAPFileParticipant } from '../../../utils/POAPUtils' import { Buffer } from 'buffer'; import { POAPDistributionResults } from '../../../types/poap/POAPDistributionResults'; import channelIds from '../../constants/channelIds'; +import { MessageOptions as MessageOptionsSlash } from 'slash-create/lib/structures/interfaces/messageInteraction'; const EndTwitterFlow = async (guildMember: GuildMember, db: Db, ctx?: CommandContext): Promise => { Log.debug('starting twitter poap end flow...'); @@ -81,7 +83,8 @@ const EndTwitterFlow = async (guildMember: GuildMember, db: Db, ctx?: CommandCon } const bufferFile: Buffer = ServiceUtils.generateCSVStringBuffer(listOfParticipants); - const embedTwitterEnd = { + const fileName = `twitter_participants_${numberOfParticipants}.csv`; + let embedTwitterEnd: MessageOptionsSlash | MessageOptions = { embeds: [ { title: 'Twitter Event Ended', @@ -92,11 +95,14 @@ const EndTwitterFlow = async (guildMember: GuildMember, db: Db, ctx?: CommandCon ], }, ], - files: [{ name: `twitter_participants_${numberOfParticipants}.csv`, attachment: bufferFile }], }; if (isDmOn) { + embedTwitterEnd = embedTwitterEnd as MessageOptions; + embedTwitterEnd.files = [{ name: fileName, attachment: bufferFile }]; await guildMember.send(embedTwitterEnd); } else if (ctx) { + embedTwitterEnd = embedTwitterEnd as MessageOptionsSlash; + embedTwitterEnd.file = [{ name: fileName, file: bufferFile }]; await ctx.send(embedTwitterEnd); }