diff --git a/.env.prod b/.env.prod index fcfd752c..6aed6a39 100644 --- a/.env.prod +++ b/.env.prod @@ -22,18 +22,13 @@ DISCORD_CHANNEL_DEV_WORKROOM_ID=841349002330505266 DISCORD_CHANNEL_WRITERS_ROOM_ID=841332222946312232 # Logger -LOGDNA_APP_NAME=degen-tbd +LOGDNA_APP_NAME=degen LOGDNA_DEFAULT_LEVEL=info # POAP POAP_REQUIRED_PARTICIPATION_DURATION=10 POAP_MAX_EVENT_DURATION_MINUTES=180 -# MISC -DAO_CURRENT_SEASON=2 -DAO_CURRENT_SEASON_END_DATE=2022-01-07T04:00:00.000Z -DAO_GUEST_PASS_EXPIRATION_DAYS=14 - # Twitter TWITTER_API_TOKEN= TWITTER_API_SECRET= diff --git a/.env.qa b/.env.qa index a78ccf28..9deb5eb4 100644 --- a/.env.qa +++ b/.env.qa @@ -46,18 +46,10 @@ DISCORD_CHANNEL_FIRST_QUEST_PROJECT_ID=854401837566001192 LOGDNA_APP_NAME=serendipity-mk1 LOGDNA_DEFAULT_LEVEL=debug -# MISC -DAO_CURRENT_SEASON=2 -DAO_CURRENT_SEASON_END_DATE=2022-01-07T04:00:00.000Z -DAO_GUEST_PASS_EXPIRATION_DAYS=14 - # POAP POAP_REQUIRED_PARTICIPATION_DURATION=10 POAP_MAX_EVENT_DURATION_MINUTES=180 -# URLs -DAO_BOUNTY_BOARD_URL=https://develop--bounty-board-29081e.netlify.app/ - # Twitter TWITTER_API_TOKEN= TWITTER_API_SECRET= diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index d4c07f00..6435a1aa 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -47,3 +47,17 @@ jobs: HD_POAP_CLIENT_ID: ${{secrets.PROD_POAP_CLIENT_ID}} HD_POAP_CLIENT_SECRET: ${{secrets.PROD_POAP_CLIENT_SECRET}} HD_SENTRY_IO_DSN: ${{secrets.PROD_SENTRY_IO_DSN}} + - name: Get current package version + id: package-version + uses: martinbeentjes/npm-get-version-action@v1.1.0 + - name: Sentry Release + uses: getsentry/action-release@v1.1.6 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + sourcemaps: './dist' + with: + environment: 'production' + version: 'degen@${{ steps.package-version.outputs.current-version }}' + sourcemaps: './dist' diff --git a/.github/workflows/deploy-qa.yml b/.github/workflows/deploy-qa.yml index 4151f6db..e4decf5f 100644 --- a/.github/workflows/deploy-qa.yml +++ b/.github/workflows/deploy-qa.yml @@ -44,6 +44,19 @@ jobs: HD_LOGDNA_TOKEN: ${{secrets.QA_LOGDNA_TOKEN}} HD_TWITTER_API_TOKEN: ${{secrets.QA_TWITTER_API_TOKEN}} HD_TWITTER_API_SECRET: ${{secrets.QA_TWITTER_API_SECRET}} - HD_TWITTER_BEARER_TOKEN: ${{secrets.QA_TWITTER_API_BEARER_TOKEN}} - HD_TWITTER_ACCESS_TOKEN_SECRET: ${{secrets.QA_TWITTER_API_ACCESS_TOKEN_SECRET}} - HD_SENTRY_IO_DSN: ${{secrets.QA_SENTRY_IO_DSN}} \ No newline at end of file + HD_TWITTER_BEARER_TOKEN: ${{secrets.QA_TWITTER_BEARER_TOKEN}} + HD_TWITTER_ACCESS_TOKEN_SECRET: ${{secrets.QA_TWITTER_ACCESS_TOKEN_SECRET}} + HD_SENTRY_IO_DSN: ${{secrets.QA_SENTRY_IO_DSN}} + - name: Get current package version + id: package-version + uses: martinbeentjes/npm-get-version-action@v1.1.0 + - name: Sentry Release + uses: getsentry/action-release@v1.1.6 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + with: + environment: 'qa' + version: 'degen@${{ steps.package-version.outputs.current-version }}' + sourcemaps: './dist' \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index adb3a1f0..14151e57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ # Changelog -## 2.5.2-RELEASE +## 2.6.0-RELEASE (2022-01-11) + +1. Stability check + - add sentry github action + - fix poap mint api call + - fix auto end for DM event + - fix gm regex for single line + - refactor poap start/stop tracking event +2. Twitter stability check + - handle timeout for autoend situation + - better error messaging + - stability enhancements + - add forced start script +3. Fix key github action reference for twitter spaces + - remove extra logging + - send twitter auth confirmation only on direct auth flow + +## 2.5.2-RELEASE (2022-01-09) 1. Address sentry.io issues 2022-01-08 - add type guards to messageCreate sentry method diff --git a/package.json b/package.json index d2ab4596..e14d9bd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "degen-tbd", - "version": "2.5.2", + "name": "degen", + "version": "2.6.0", "description": "Administrative and Utilitarian bot for the Bankless Discord Server.", "main": "app.js", "private": true, @@ -9,9 +9,10 @@ "test": "jest", "qa": "node -r dotenv/config --trace-warnings dist/app/app.js dotenv_config_path=.env.qa", "prod": "node -r dotenv/config --trace-warnings dist/app/app.js dotenv_config_path=.env.prod", - "prestart": "yarn install && yarn build", + "prestart": "yarn install && yarn build && yarn lint", "pretest": "yarn install && yarn build", "start": "node --trace-warnings -r dotenv/config dist/app/app.js", + "force-start": "tsc -p tsconfig.json && node -r dotenv/config dist/app/app.js", "lint": "eslint . --ext .ts", "format": "eslint . --ext .ts --fix", "watch": "tsc -p tsconfig.json -w" diff --git a/src/app/api/poap/EventsAPI.ts b/src/app/api/poap/EventsAPI.ts index 13ea23f8..6b89cb99 100644 --- a/src/app/api/poap/EventsAPI.ts +++ b/src/app/api/poap/EventsAPI.ts @@ -54,7 +54,7 @@ const EventsAPI = { start_date: `${request.start_date}`, end_date: `${request.end_date}`, expiry_date: `${request.expiry_date}`, - year: `${request.year}`, + year: request.year, event_url: `${request.event_url}`, virtual_event: `${request.virtual_event}`, secret_code: `${request.secret_code}`, diff --git a/src/app/api/types/poap-events/EventsRequestType.ts b/src/app/api/types/poap-events/EventsRequestType.ts index b99a91d8..ba9eba67 100644 --- a/src/app/api/types/poap-events/EventsRequestType.ts +++ b/src/app/api/types/poap-events/EventsRequestType.ts @@ -8,7 +8,7 @@ export type EventsRequestType = { start_date: string, end_date: string, expiry_date: string, - year: string, + year: number, event_url?: string, virtual_event: boolean, image: AxiosResponse, diff --git a/src/app/app.ts b/src/app/app.ts index 57b32c93..aa82f01d 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -83,7 +83,7 @@ function initializeSentryIO() { dsn: `${apiKeys.sentryDSN}`, tracesSampleRate: 1.0, release: `${constants.APP_NAME}@${constants.APP_VERSION}`, - environment: process.env.SENTRY_ENVIRONMENT, + environment: `${process.env.SENTRY_ENVIRONMENT}`, integrations: [ new RewriteFrames({ root: __dirname, diff --git a/src/app/commands/admin/Account.ts b/src/app/commands/admin/Account.ts index 4f18daa8..304b78e5 100644 --- a/src/app/commands/admin/Account.ts +++ b/src/app/commands/admin/Account.ts @@ -53,7 +53,7 @@ export default class Account extends SlashCommand { const { guildMember } = await ServiceUtils.getGuildAndMember(ctx.guildID, ctx.user.id); try { - await VerifyTwitter(ctx, guildMember).catch(e => { throw e; }); + await VerifyTwitter(ctx, guildMember, true).catch(e => { throw e; }); } catch (e) { if (e instanceof ValidationError) { await ctx.send({ content: `${e.message}`, ephemeral: true }); diff --git a/src/app/events/Ready.ts b/src/app/events/Ready.ts index f47b83af..a64e40aa 100644 --- a/src/app/events/Ready.ts +++ b/src/app/events/Ready.ts @@ -26,8 +26,9 @@ export default class implements DiscordEvent { } const db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); await updateActiveDiscordServers(client, db); - await POAPService.runAutoEndSetup(client, constants.PLATFORM_TYPE_DISCORD).catch(Log.error); - await POAPService.runAutoEndSetup(client, constants.PLATFORM_TYPE_TWITTER).catch(Log.error); + // 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(); Log.info(`${constants.APP_NAME} is ready!`); diff --git a/src/app/events/VoiceStateUpdate.ts b/src/app/events/VoiceStateUpdate.ts index 2bae8342..ddc33ee8 100644 --- a/src/app/events/VoiceStateUpdate.ts +++ b/src/app/events/VoiceStateUpdate.ts @@ -1,7 +1,7 @@ import { VoiceState } from 'discord.js'; -import addUserForEvent from './poap/AddUserForEvent'; import { DiscordEvent } from '../types/discord/DiscordEvent'; import { LogUtils } from '../utils/Log'; +import HandleParticipantDuringEvent from './poap/HandleParticipantDuringEvent'; /** * voiceStateUpdate @@ -18,7 +18,7 @@ export default class implements DiscordEvent { */ async execute(oldState: VoiceState, newState: VoiceState): Promise { try { - await addUserForEvent(oldState, newState).catch(e => LogUtils.logError('failed to add user for POAP event', e, oldState.guild.id)); + await HandleParticipantDuringEvent(oldState, newState).catch(e => LogUtils.logError('failed to handle user in POAP event', e, oldState.guild.id)); } catch (e) { LogUtils.logError('failed to process event voiceStateUpdate', e); } diff --git a/src/app/events/chat/HandlePOAPGM.ts b/src/app/events/chat/HandlePOAPGM.ts index cdfdc0b9..1ce1b89f 100644 --- a/src/app/events/chat/HandlePOAPGM.ts +++ b/src/app/events/chat/HandlePOAPGM.ts @@ -15,9 +15,10 @@ const HandlePOAPGM = async (message: Message): Promise => { return; } - if (!content.match(/gm/gi)) { + if (!content.match(/^gm$/gi)) { return; } + Log.debug(`found gm from ${message.author.tag}`); const dmChannel: DMChannel = message.channel as DMChannel; diff --git a/src/app/events/poap/AddUserForEvent.ts b/src/app/events/poap/AddUserForEvent.ts deleted file mode 100644 index b9bac62a..00000000 --- a/src/app/events/poap/AddUserForEvent.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Guild, GuildChannel, GuildMember, VoiceState } from 'discord.js'; -import { Collection, Cursor, Db, InsertOneWriteOpResult, MongoError } from 'mongodb'; -import constants from '../../service/constants/constants'; -import { POAPSettings } from '../../types/poap/POAPSettings'; -import { POAPParticipant } from '../../types/poap/POAPParticipant'; -import Log, { LogUtils } from '../../utils/Log'; -import dayjs, { Dayjs } from 'dayjs'; -import EndPOAP from '../../service/poap/end/EndPOAP'; -import MongoDbUtils from '../../utils/MongoDbUtils'; - -export default async (oldState: VoiceState, newState: VoiceState): Promise => { - if (oldState.channelId === newState.channelId && (oldState.deaf == newState.deaf)) { - // user did not change channels - return; - } - - const guild: Guild = (oldState.guild != null) ? oldState.guild : newState.guild; - const member: GuildMember | null = (oldState.guild != null) ? oldState.member : newState.member; - - if (member == null) { - // could not find member - return; - } - - const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); - db.collection(constants.DB_COLLECTION_POAP_SETTINGS); - - const poapSettingsDB: Collection = db.collection(constants.DB_COLLECTION_POAP_SETTINGS); - const activeChannelsCursor: Cursor = await poapSettingsDB.find({ - isActive: true, - discordServerId: `${guild.id}`, - }); - for await (const poapSetting of activeChannelsCursor) { - const currentDate: Dayjs = dayjs(); - try { - const endDate: Dayjs = (poapSetting.endTime == null) ? currentDate : dayjs(poapSetting.endTime); - if (currentDate.isBefore(endDate)) { - const voiceChannel: GuildChannel | null = await guild.channels.fetch(poapSetting.voiceChannelId); - if (voiceChannel == null) { - Log.warn('voice channel might have been deleted.'); - return; - } - await addUserToDb(oldState, newState, db, voiceChannel, member); - } else { - Log.debug(`current date is after or equal to event end date, currentDate: ${currentDate}, endDate: ${endDate}`); - const poapOrganizerGuildMember: GuildMember = await guild.members.fetch(poapSetting.discordUserId); - await EndPOAP(poapOrganizerGuildMember, constants.PLATFORM_TYPE_DISCORD); - } - } catch (e) { - LogUtils.logError(`failed to add ${member.user.tag} to db`, e); - } - } -}; - -export const addUserToDb = async ( - oldState: VoiceState, newState: VoiceState, db: Db, channel: GuildChannel, member: GuildMember, -): Promise => { - if (!(newState.channelId === channel.id || oldState.channelId === channel.id)) { - // event change is not related to event parameter - return; - } - if (newState.deaf) { - await updateUserForPOAP(member, db, channel, false, true).catch(e => LogUtils.logError('failed to capture user joined for poap', e)); - return; - } - const hasJoined: boolean = (newState.channelId === channel.id); - await updateUserForPOAP(member, db, channel, hasJoined).catch(e => LogUtils.logError(`failed to capture user change for POAP hasJoined: ${hasJoined}`, e)); - return; -}; - -export const updateUserForPOAP = async ( - member: GuildMember, db: Db, channel: GuildChannel, hasJoined?: boolean, hasDeafened?: boolean, -): Promise => { - const poapParticipantsDb: Collection = db.collection(constants.DB_COLLECTION_POAP_PARTICIPANTS); - const poapParticipant: POAPParticipant = await poapParticipantsDb.findOne({ - discordServerId: `${channel.guild.id}`, - voiceChannelId: `${channel.id}`, - discordUserId: `${member.user.id}`, - }); - - if (hasDeafened) { - Log.debug(`${member.user.tag} | deafened themselves ${channel.name} in ${channel.guild.name}`); - await poapParticipantsDb.deleteOne(poapParticipant).catch(Log.error); - return; - } - const currentDate: Dayjs = dayjs(); - if (!hasJoined) { - Log.debug(`${member.user.tag} | left ${channel.name} in ${channel.guild.name}`); - const startTimeDate: Dayjs = dayjs(poapParticipant.startTime); - let durationInMinutes: number = poapParticipant.durationInMinutes; - if ((currentDate.unix() - startTimeDate.unix() > 0)) { - durationInMinutes += ((currentDate.unix() - startTimeDate.unix()) / 60); - } - await poapParticipantsDb.updateOne(poapParticipant, { - $set: { - endTime: (new Date).toISOString(), - durationInMinutes: durationInMinutes, - }, - }).catch(Log.error); - return; - } - if (poapParticipant !== null && poapParticipant.discordUserId != null && poapParticipant.discordUserId === member.user.id) { - Log.debug(`${member.user.tag} | rejoined ${channel.name} in ${channel.guild.name}`); - await poapParticipantsDb.updateOne(poapParticipant, { - $set: { - startTime: currentDate.toISOString(), - }, - $unset: { - endTime: null, - }, - }).catch(Log.error); - return; - } - - const currentDateStr = (new Date()).toISOString(); - const result: InsertOneWriteOpResult | void = await poapParticipantsDb.insertOne({ - discordUserId: `${member.user.id}`, - discordUserTag: `${member.user.tag}`, - startTime: currentDateStr, - voiceChannelId: `${channel.id}`, - discordServerId: `${channel.guild.id}`, - durationInMinutes: 0, - }).catch(Log.error); - if (result == null || result.insertedCount !== 1) { - throw new MongoError('failed to insert poapParticipant'); - } - Log.debug(`${member.user.tag} | joined ${channel.name} in ${channel.guild.name}`); -}; diff --git a/src/app/events/poap/HandleParticipantDuringEvent.ts b/src/app/events/poap/HandleParticipantDuringEvent.ts new file mode 100644 index 00000000..12b34571 --- /dev/null +++ b/src/app/events/poap/HandleParticipantDuringEvent.ts @@ -0,0 +1,213 @@ +import { + VoiceState, +} from 'discord.js'; +import { + Collection, + Db, + DeleteWriteOpResultObject, + InsertOneWriteOpResult, + MongoError, + UpdateWriteOpResult, +} from 'mongodb'; +import constants from '../../service/constants/constants'; +import { POAPParticipant } from '../../types/poap/POAPParticipant'; +import Log from '../../utils/Log'; +import dayjs, { Dayjs } from 'dayjs'; +import MongoDbUtils from '../../utils/MongoDbUtils'; +import { POAPSettings } from '../../types/poap/POAPSettings'; + +type BasicUser = { + id: string; + tag: string | undefined; +} + +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)) { + await removeDeafenedUser(oldState.channelId, oldState.guild.id, oldState.id); + } + if (newState.channelId != oldState.channelId && await isChannelActivePOAPEvent(newState.channelId, newState.guild.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)) { + await startTrackingUserParticipation({ id: newState.id, tag: newState.member?.user.tag }, newState.guild.id, newState.channelId); + } + } + + if (isUserDeaf(newState)) { + Log.log(`user is deaf, userId: ${newState.id}`); + return; + } + + if (hasUserChangedChannels(oldState, newState)) { + if (await isChannelActivePOAPEvent(oldState.channelId, oldState.guild.id)) { + await stopTrackingUserParticipation({ id: oldState.id, tag: oldState.member?.user.tag }, oldState.guild.id, oldState.channelId, null); + } + if (await isChannelActivePOAPEvent(newState.channelId, newState.guild.id)) { + await startTrackingUserParticipation({ id: newState.id, tag: newState.member?.user.tag }, newState.guild.id, newState.channelId); + } + } + +}; + +const hasUserBeenDeafened = (oldState: VoiceState, newState: VoiceState): boolean => { + return newState.deaf != null && newState.deaf && newState.deaf != oldState.deaf; +}; + +const hasUserBeenUnDeafened = (oldState: VoiceState, newState: VoiceState): boolean => { + return newState.deaf != null && !newState.deaf && newState.deaf != oldState.deaf; +}; + +const isUserDeaf = (newState: VoiceState): boolean => { + return newState.deaf !== null && newState.deaf; +}; + +const hasUserChangedChannels = (oldState: VoiceState, newState: VoiceState): boolean => { + return newState.channelId != oldState.channelId; +}; + +const isChannelActivePOAPEvent = async ( + channelId: string | null, guildId: string | null, +): Promise => { + if (channelId == null || guildId == null) { + return false; + } + + const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); + const poapSettingsDB: Collection = db.collection(constants.DB_COLLECTION_POAP_SETTINGS); + const activeEvent: POAPSettings | null = await poapSettingsDB.findOne({ + isActive: true, + voiceChannelId: channelId, + discordServerId: guildId, + }); + + if (activeEvent != null) { + // Log.debug(`channel is active, channelId: ${channelId}, guildId: ${guildId}`); + return true; + } + + // Log.debug('channel not active'); + return false; +}; + +const removeDeafenedUser = async (channelId: string | null, guildId: string | null, userId: string | null) => { + if (channelId == null || guildId == null || userId == null) { + return; + } + + const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); + const poapParticipantsCol: Collection = db.collection(constants.DB_COLLECTION_POAP_PARTICIPANTS); + + const result: DeleteWriteOpResultObject | void = await poapParticipantsCol.deleteOne({ + voiceChannelId: channelId, + discordServerId: guildId, + discordUserId: userId, + }).catch(Log.warn); + if (result != null && result.deletedCount == 1) { + Log.debug(`user deafened themselves and removed from db, userId: ${userId}, channelId: ${channelId}, discordServerId: ${guildId}`); + return; + } + Log.debug('deafened user not removed/found in any active channels'); +}; + +export const startTrackingUserParticipation = async (user: BasicUser, guildId: string, channelId: string | null): Promise => { + const participant = await retrieveActiveParticipant(user, channelId, guildId); + + channelId = channelId as string; + guildId = guildId as string; + + const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); + const poapParticipantsDb: Collection = db.collection(constants.DB_COLLECTION_POAP_PARTICIPANTS); + if (participant == null) { + const userTag: string = user.tag ? user.tag : ''; + const resultInsert: InsertOneWriteOpResult | void = await poapParticipantsDb.insertOne({ + discordUserId: user.id, + discordUserTag: userTag, + voiceChannelId: channelId, + startTime: dayjs().toISOString(), + discordServerId: guildId, + durationInMinutes: 0, + } as POAPParticipant).catch(Log.error); + if (resultInsert == null || resultInsert.insertedCount !== 1) { + throw new MongoError('failed to insert poapParticipant'); + } + Log.debug(`${user.tag} | joined, channelId: ${channelId}, guildId: ${guildId}, userId: ${user.id}`); + return; + } + if (participant.endTime != null) { + const updateResult: UpdateWriteOpResult | void = await poapParticipantsDb.updateOne(participant, { + $set: { + startTime: dayjs().toISOString(), + }, + $unset: { + endTime: '', + }, + }).catch(Log.error); + + if (updateResult == null || updateResult.result.ok != 1) { + Log.error('failed to update rejoined participant in db'); + } + Log.debug(`${user.tag} | rejoined, channelId: ${channelId}, guildId: ${guildId}, userId: ${user.id}`); + } +}; + +export const stopTrackingUserParticipation = async (user: BasicUser, guildId: string, channelId: string | null, participant: POAPParticipant | null): Promise => { + if (!participant) { + participant = await retrieveActiveParticipant(user, channelId, guildId); + } + + if (participant == null) { + throw new MongoError('could not find participant in db when trying to stop tracking'); + } + + channelId = channelId as string; + guildId = guildId as string; + + const durationInMinutes: number = calculateDuration(participant.startTime, participant.durationInMinutes); + const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); + const poapParticipantsDb: Collection = db.collection(constants.DB_COLLECTION_POAP_PARTICIPANTS); + const result: UpdateWriteOpResult | void = await poapParticipantsDb.updateOne(participant, { + $set: { + endTime: dayjs().toISOString(), + durationInMinutes: durationInMinutes, + }, + }).catch(Log.error); + + if (result == null || result.result.ok != 1) { + throw new MongoError('failed to update present participant in db'); + } + Log.debug(`${user.tag} | left, channelId: ${channelId}, guildId: ${guildId}, userId: ${user.id}`); +}; + +const calculateDuration = (startTime: string, currentDuration: number): number => { + const currentDate: Dayjs = dayjs(); + const startTimeDate: Dayjs = dayjs(startTime); + let durationInMinutes: number = currentDuration; + if ((currentDate.unix() - startTimeDate.unix() > 0)) { + durationInMinutes += ((currentDate.unix() - startTimeDate.unix()) / 60); + } + return durationInMinutes; +}; + +const retrieveActiveParticipant = async ( + user: BasicUser, channelId: string | null, guildId: string, +): Promise => { + if (user.id == null || channelId == null || guildId == null) { + return null; + } + const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); + const poapParticipantsDb: Collection = db.collection(constants.DB_COLLECTION_POAP_PARTICIPANTS); + return await poapParticipantsDb.findOne({ + discordUserId: user.id, + voiceChannelId: channelId, + discordServerId: guildId, + }); +}; + +export default HandleParticipantDuringEvent; \ No newline at end of file diff --git a/src/app/service/account/VerifyTwitter.ts b/src/app/service/account/VerifyTwitter.ts index 52acf6c2..a3c1ef11 100644 --- a/src/app/service/account/VerifyTwitter.ts +++ b/src/app/service/account/VerifyTwitter.ts @@ -21,10 +21,10 @@ export type VerifiedTwitter = { twitterClientV1: TwitterApi }; -const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember): Promise => { +const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember, sendConfirmationMsg: boolean): Promise => { Log.debug('starting to verify twitter account link'); - const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Hi! Let me check your twitter info'); + const isDmOn: boolean = (sendConfirmationMsg) ? await ServiceUtils.tryDMUser(guildMember, 'Hi! Let me check your twitter info') : false; if (isDmOn) { await ctx.send({ content: 'DM sent!', ephemeral: true }); @@ -97,10 +97,13 @@ const VerifyTwitter = async (ctx: CommandContext, guildMember: GuildMember): Pro { name: 'URL', value: `https://twitter.com/${userCall.screen_name}` }, ], }; - if (isDmOn) { - await guildMember.send({ embeds: [verifiedEmbeds] }); - } else { - await ctx.send({ embeds: [verifiedEmbeds], ephemeral: true }); + + if (sendConfirmationMsg) { + if (isDmOn) { + await guildMember.send({ embeds: [verifiedEmbeds] }); + } else { + await ctx.send({ embeds: [verifiedEmbeds], ephemeral: true }); + } } Log.debug('done verifying twitter account'); diff --git a/src/app/service/poap/ClaimPOAP.ts b/src/app/service/poap/ClaimPOAP.ts index 1df79f12..cc65e797 100644 --- a/src/app/service/poap/ClaimPOAP.ts +++ b/src/app/service/poap/ClaimPOAP.ts @@ -96,7 +96,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 | undefined = await VerifyTwitter(ctx, guildMember); + const verifiedTwitter: VerifiedTwitter | undefined = await VerifyTwitter(ctx, guildMember, false); if (verifiedTwitter == null) { return; } diff --git a/src/app/service/poap/OptInPOAP.ts b/src/app/service/poap/OptInPOAP.ts index 1426db3f..076d7a4b 100644 --- a/src/app/service/poap/OptInPOAP.ts +++ b/src/app/service/poap/OptInPOAP.ts @@ -92,6 +92,7 @@ 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!' }); } }; diff --git a/src/app/service/poap/POAPService.ts b/src/app/service/poap/POAPService.ts index f97feee5..084907b9 100644 --- a/src/app/service/poap/POAPService.ts +++ b/src/app/service/poap/POAPService.ts @@ -46,9 +46,14 @@ const POAPService = { for (const expiredEvent of expiredEventsList) { const poapGuild: Guild = await client.guilds.fetch(expiredEvent.discordServerId); const poapOrganizer: GuildMember = await poapGuild.members.fetch(expiredEvent.discordUserId); - EndPOAP(poapOrganizer, platform).catch(Log.error); + EndPOAP(poapOrganizer, platform).catch((e) => { + if (e instanceof ValidationError) { + poapOrganizer.send({ content: `${e?.message}` }).catch(Log.error); + } + Log.error(e); + }); } - Log.debug(`all expired events ended for ${platform}`); + Log.debug(`all expired events ended for ${platform} and possibly pending user input, now checking for active events`); const poapSettingsActiveEventsCursor: Cursor = await poapSettingsDB.find({ isActive: true, endTime: { @@ -62,6 +67,7 @@ const POAPService = { }); Log.debug(`found ${activeEventsList.length} active events for ${platform}`); + // Skip twitter active event check (since it uses a participant check in system) for (const activeEvent of activeEventsList) { if (platform == constants.PLATFORM_TYPE_DISCORD) { try { @@ -70,7 +76,7 @@ const POAPService = { if (!channelChoice) { throw new ValidationError('Missing channel'); } - await storePresentMembers(db, channelChoice).catch(); + storePresentMembers(db, channelChoice).catch(Log.error); } catch (e) { LogUtils.logError('failed trying to store present members for active poap event', e); } diff --git a/src/app/service/poap/SchedulePOAP.ts b/src/app/service/poap/SchedulePOAP.ts index 5e407196..3e166ad5 100644 --- a/src/app/service/poap/SchedulePOAP.ts +++ b/src/app/service/poap/SchedulePOAP.ts @@ -23,7 +23,7 @@ import { MessageOptions as MessageOptionsSlash } from 'slash-create/lib/structur const SchedulePOAP = async (ctx: CommandContext, guildMember: GuildMember, numberToMint: number): Promise => { if (ctx.guildID == undefined) { - await ctx.send('Please try schedule within discord channel'); + await ctx.send('Please try poap mint within discord channel'); return; } const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Minting POAPs is always super exciting!'); @@ -51,7 +51,7 @@ const SchedulePOAP = async (ctx: CommandContext, guildMember: GuildMember, numbe if (isDmOn) { await guildMember.send(msg1); - await ctx.sendFollowUp('I just sent you a DM!'); + await ctx.send({ content: 'I just sent you a DM!', ephemeral: true }); } else if (ctx) { await ctx.sendFollowUp(msg1); } diff --git a/src/app/service/poap/end/EndPOAP.ts b/src/app/service/poap/end/EndPOAP.ts index b6f832b5..cfeb1125 100644 --- a/src/app/service/poap/end/EndPOAP.ts +++ b/src/app/service/poap/end/EndPOAP.ts @@ -5,17 +5,29 @@ import { MessageOptions, TextChannel, } from 'discord.js'; -import { Collection, Db, UpdateWriteOpResult } from 'mongodb'; +import { + Collection as MongoCollection, + Collection, + Cursor, + Db, + UpdateWriteOpResult, +} from 'mongodb'; import constants from '../../constants/constants'; import { POAPSettings } from '../../../types/poap/POAPSettings'; import POAPUtils, { POAPFileParticipant } from '../../../utils/POAPUtils'; -import { CommandContext, MessageOptions as MessageOptionsSlash } from 'slash-create'; +import { + CommandContext, + MessageOptions as MessageOptionsSlash, +} from 'slash-create'; import Log from '../../../utils/Log'; import dayjs from 'dayjs'; import MongoDbUtils from '../../../utils/MongoDbUtils'; import ServiceUtils from '../../../utils/ServiceUtils'; import EndTwitterFlow from './EndTwitterFlow'; import { POAPDistributionResults } from '../../../types/poap/POAPDistributionResults'; +import channelIds from '../../constants/channelIds'; +import { POAPParticipant } from '../../../types/poap/POAPParticipant'; +import { stopTrackingUserParticipation } from '../../../events/poap/HandleParticipantDuringEvent'; export default async (guildMember: GuildMember, platform: string, ctx?: CommandContext): Promise => { Log.debug('attempting to end poap event'); @@ -31,11 +43,11 @@ export default async (guildMember: GuildMember, platform: string, ctx?: CommandC } const poapSettingsDB: Collection = db.collection(constants.DB_COLLECTION_POAP_SETTINGS); - const poapSettingsDoc: POAPSettings | null = await poapSettingsDB.findOne({ + const poapSettingsDoc: POAPSettings | null | void = await poapSettingsDB.findOne({ discordUserId: guildMember.user.id, discordServerId: guildMember.guild.id, isActive: true, - }); + }).catch(Log.error); if (poapSettingsDoc == null) { Log.debug('poap event not found'); @@ -47,14 +59,14 @@ export default async (guildMember: GuildMember, platform: string, ctx?: CommandC Log.debug('active poap event found'); - const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Over already? Can\'t wait for the next one'); + const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Hello! I found a poap event, let me try ending it.'); let channelExecution: TextChannel | null = null; if (!isDmOn && ctx) { 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 the POAP links! ⚠', ephemeral: true }); } else if (ctx) { await ctx.send({ content: 'Please check your DMs!', ephemeral: true }); - } else { + } else if (poapSettingsDoc.channelExecutionId != channelIds.DM) { if (poapSettingsDoc.channelExecutionId == null || poapSettingsDoc.channelExecutionId == '') { Log.debug(`channelExecutionId missing for ${guildMember.user.tag}, ${guildMember.user.id}, skipping poap end for expired event`); return; @@ -79,7 +91,7 @@ export default async (guildMember: GuildMember, platform: string, ctx?: CommandC Log.debug(`poap event ended for ${guildMember.user.tag} and updated in db`, { indexMeta: true, meta: { - discordId: poapSettingsDoc.discordServerId, + guildId: poapSettingsDoc.discordServerId, voiceChannelId: poapSettingsDoc.voiceChannelId, event: poapSettingsDoc.event, }, @@ -87,16 +99,16 @@ export default async (guildMember: GuildMember, platform: string, ctx?: CommandC const channel: GuildChannel | null = await guildMember.guild.channels.fetch(poapSettingsDoc.voiceChannelId); if (channel == null) { - Log.warn('channel not found'); - return; + Log.warn('channel not found, might have been deleted, oh well'); } - const listOfParticipants: POAPFileParticipant[] = await POAPUtils.getListOfParticipants(db, channel); + await handleEventEndForPresentParticipants(poapSettingsDoc); + const listOfParticipants: POAPFileParticipant[] = await POAPUtils.getListOfParticipants(poapSettingsDoc); const numberOfParticipants: number = listOfParticipants.length; if (numberOfParticipants <= 0) { Log.debug('no eligible attendees found during event'); - const eventEndMsg = `POAP event ended. No participants found for \`${channel.name}\` in \`${channel.guild.name}\`.`; + const eventEndMsg = `POAP event ended. No participants found for \`${channel?.name}\` in \`${channel?.guild.name}\`.`; if (isDmOn) { await guildMember.send({ content: eventEndMsg }); } else if (ctx) { @@ -117,8 +129,8 @@ export default async (guildMember: GuildMember, platform: string, ctx?: CommandC fields: [ { name: 'Date', value: `${currentDate} UTC`, inline: true }, { name: 'Event', value: `${poapSettingsDoc.event}`, inline: true }, - { name: 'Discord Server', value: channel.guild.name, inline: true }, - { name: 'Location', value: channel.name, inline: true }, + { name: 'Discord Server', value: `${channel?.guild.name} `, inline: true }, + { name: 'Location', value: `${channel?.name} `, inline: true }, { name: 'Total Participants', value: `${numberOfParticipants}`, inline: true }, ], }, @@ -152,3 +164,25 @@ export default async (guildMember: GuildMember, platform: string, ctx?: CommandC await POAPUtils.handleDistributionResults(isDmOn, guildMember, distributionResults, channelExecution, ctx); Log.debug('POAP end complete'); }; + +const handleEventEndForPresentParticipants = async ( + poapSettingsDoc: POAPSettings, +): Promise => { + Log.debug('starting to handle present members for end of poap event'); + const participantsCursor: Cursor = await getPoapParticipantsFromDB(poapSettingsDoc.voiceChannelId, poapSettingsDoc.discordServerId); + for await (const participant of participantsCursor) { + if (participant.endTime == null || participant.endTime == '') { + await stopTrackingUserParticipation({ id: participant.discordUserId, tag: participant.discordUserTag }, participant.discordServerId, participant.voiceChannelId, participant); + } + } + Log.debug('finished setting endDate for present participants in db'); +}; + +export const getPoapParticipantsFromDB = async (channelId: string, guildId: string): Promise> => { + const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); + const poapParticipants: MongoCollection = db.collection(constants.DB_COLLECTION_POAP_PARTICIPANTS); + return poapParticipants.find({ + voiceChannelId: channelId, + discordServerId: guildId, + }); +}; diff --git a/src/app/service/poap/end/EndTwitterFlow.ts b/src/app/service/poap/end/EndTwitterFlow.ts index e1fe8c94..506358fb 100644 --- a/src/app/service/poap/end/EndTwitterFlow.ts +++ b/src/app/service/poap/end/EndTwitterFlow.ts @@ -18,6 +18,7 @@ import dayjs, { Dayjs } from 'dayjs'; import POAPUtils, { TwitterPOAPFileParticipant } from '../../../utils/POAPUtils'; import { Buffer } from 'buffer'; import { POAPDistributionResults } from '../../../types/poap/POAPDistributionResults'; +import channelIds from '../../constants/channelIds'; const EndTwitterFlow = async (guildMember: GuildMember, db: Db, ctx?: CommandContext): Promise => { Log.debug('starting twitter poap end flow...'); @@ -35,14 +36,14 @@ const EndTwitterFlow = async (guildMember: GuildMember, db: Db, ctx?: CommandCon } Log.debug('active twitter poap event found'); - const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Over already? Can\'t wait for the next one'); + const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Hello! I found a poap event, let me try ending it.'); let channelExecution: TextChannel | null = null; if (!isDmOn && ctx) { 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 the POAP links! ⚠', ephemeral: true }); } else if (ctx) { await ctx.send({ content: 'Please check your DMs!', ephemeral: true }); - } else { + } else if (activeTwitterSettings.channelExecutionId != channelIds.DM) { if (activeTwitterSettings.channelExecutionId == null || activeTwitterSettings.channelExecutionId == '') { Log.debug(`channelExecutionId missing for ${guildMember.user.tag}, ${guildMember.user.id}, skipping poap end for expired event`); return; @@ -102,7 +103,9 @@ const EndTwitterFlow = async (guildMember: GuildMember, db: Db, ctx?: CommandCon const poapLinksFile: MessageAttachment = await POAPUtils.askForPOAPLinks(guildMember, isDmOn, numberOfParticipants, ctx); const listOfPOAPLinks: string[] = await POAPUtils.getListOfPoapLinks(poapLinksFile); const distributionResults: POAPDistributionResults = await POAPUtils.sendOutTwitterPoapLinks(listOfParticipants, activeTwitterSettings.event, listOfPOAPLinks); - await POAPUtils.setupFailedAttendeesDelivery(guildMember, distributionResults, activeTwitterSettings.event, constants.PLATFORM_TYPE_TWITTER); + if (distributionResults.didNotSendList.length > 0) { + await POAPUtils.setupFailedAttendeesDelivery(guildMember, distributionResults, activeTwitterSettings.event, constants.PLATFORM_TYPE_TWITTER); + } await POAPUtils.handleDistributionResults(isDmOn, guildMember, distributionResults, channelExecution, ctx); Log.debug('POAP twitter end complete'); }; diff --git a/src/app/service/poap/start/StartPOAP.ts b/src/app/service/poap/start/StartPOAP.ts index 8ad61322..e2d72d4e 100644 --- a/src/app/service/poap/start/StartPOAP.ts +++ b/src/app/service/poap/start/StartPOAP.ts @@ -16,14 +16,12 @@ import { } from 'slash-create'; import { Collection, - Cursor, Db, FindAndModifyWriteOpResultObject, } from 'mongodb'; import constants from '../../constants/constants'; import { POAPSettings } from '../../../types/poap/POAPSettings'; import ValidationError from '../../../errors/ValidationError'; -import { updateUserForPOAP } from '../../../events/poap/AddUserForEvent'; import ServiceUtils from '../../../utils/ServiceUtils'; import EarlyTermination from '../../../errors/EarlyTermination'; import POAPUtils from '../../../utils/POAPUtils'; @@ -35,6 +33,10 @@ import MongoDbUtils from '../../../utils/MongoDbUtils'; import StartTwitterFlow from './StartTwitterFlow'; import StartChannelFlow from './StartChannelFlow'; import channelIds from '../../constants/channelIds'; +import { + startTrackingUserParticipation, +} from '../../../events/poap/HandleParticipantDuringEvent'; +import { POAPParticipant } from '../../../types/poap/POAPParticipant'; export default async (ctx: CommandContext, guildMember: GuildMember, platform: string, event: string, duration?: number): Promise => { if (ctx.guildID == undefined) { @@ -55,19 +57,19 @@ export default async (ctx: CommandContext, guildMember: GuildMember, platform: s return; } - const poapSettingsDB: Collection = db.collection(constants.DB_COLLECTION_POAP_SETTINGS); - const activeSettingsCursor: Cursor = await poapSettingsDB.find({ + const poapSettingsDB: Collection = db.collection(constants.DB_COLLECTION_POAP_SETTINGS); + const activeSettings: POAPSettings | null | void = await poapSettingsDB.findOne({ discordUserId: guildMember.id, discordServerId: guildMember.guild.id, isActive: true, - }); - const activeSettings: POAPSettings | null = await activeSettingsCursor.next(); + }).catch(Log.error); + if (activeSettings != null) { Log.debug('unable to start due to active event'); throw new ValidationError(`Please end \`${activeSettings.voiceChannelName}\` event before starting a new event.`); } - const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Hello! For which voice channel should the POAP event occur?'); + const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Hello! In which voice channel should the POAP event start?'); const voiceChannels: DiscordCollection = ServiceUtils.getAllVoiceChannels(guildMember); if (!isDmOn) { @@ -83,12 +85,12 @@ export default async (ctx: CommandContext, guildMember: GuildMember, platform: s throw new ValidationError('Missing channel'); } - const poapSettingsDoc: POAPSettings = await poapSettingsDB.findOne({ + const poapSettingsDoc: POAPSettings | null | void = await poapSettingsDB.findOne({ discordServerId: channelChoice.guild.id, voiceChannelId: channelChoice.id, - }); + }).catch(Log.error); - if (poapSettingsDoc !== null && poapSettingsDoc.isActive) { + if (poapSettingsDoc != null && poapSettingsDoc.isActive) { Log.info('unable to start due to active event'); await guildMember.send({ content: 'Event is already active.' }); throw new ValidationError(`\`${channelChoice.name}\` is already active. Please reach out to <@${poapSettingsDoc.discordUserId}> to end event.`); @@ -110,13 +112,13 @@ export default async (ctx: CommandContext, guildMember: GuildMember, platform: s ], }, ], - }); - await guildMember.send({ content: 'Everything is set, catch you later!' }); + }).catch(Log.error); + await guildMember.send({ content: 'Everything is set, catch you later!' }).catch(Log.error); }; export const clearPOAPParticipants = async (db: Db, guildChannel: GuildChannel): Promise => { Log.debug(`attempting to delete all previous participants for ${guildChannel.guild.name} on channel: ${guildChannel.name}`); - const poapParticipantsDB: Collection = db.collection(constants.DB_COLLECTION_POAP_PARTICIPANTS); + const poapParticipantsDB: Collection = db.collection(constants.DB_COLLECTION_POAP_PARTICIPANTS); await poapParticipantsDB.deleteMany({ voiceChannelId: guildChannel.id, discordServerId: guildChannel.guild.id, @@ -133,7 +135,9 @@ export const clearPOAPParticipants = async (db: Db, guildChannel: GuildChannel): export const storePresentMembers = async (db: Db, channel: GuildChannel): Promise => { try { channel.members.forEach((member: GuildMember) => { - updateUserForPOAP(member, db, channel, true); + if (!member.voice.deaf) { + startTrackingUserParticipation({ id: member.id, tag: member.user.tag }, channel.guildId, channel.id).catch(Log.error); + } }); } catch (e) { LogUtils.logError('failed to store present members', e); diff --git a/src/app/service/poap/start/StartTwitterFlow.ts b/src/app/service/poap/start/StartTwitterFlow.ts index 6be47559..f36bef87 100644 --- a/src/app/service/poap/start/StartTwitterFlow.ts +++ b/src/app/service/poap/start/StartTwitterFlow.ts @@ -12,20 +12,23 @@ import ValidationError from '../../../errors/ValidationError'; import dayjs, { Dayjs } from 'dayjs'; import POAPService from '../POAPService'; import { POAPTwitterParticipants } from '../../../types/poap/POAPTwitterParticipants'; +import channelIds from '../../constants/channelIds'; const StartTwitterFlow = async (ctx: CommandContext, guildMember: GuildMember, db: Db, event: string, duration: number): Promise => { Log.debug('starting twitter poap flow...'); - const verifiedTwitter: VerifiedTwitter | undefined = await VerifyTwitter(ctx, guildMember); + const verifiedTwitter: VerifiedTwitter | undefined = await VerifyTwitter(ctx, guildMember, false); if (verifiedTwitter == null) { return; } const twitterClientV2: TwitterApi = new TwitterApi(apiKeys.twitterBearerToken as string); - const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Oh yea, time for a POAP event!...'); + const isDmOn: boolean = await ServiceUtils.tryDMUser(guildMember, 'Hello! I can help start a POAP event!'); let twitterSpaceResult: SpaceV2LookupResult | null = null; try { + Log.debug(`twitterId: ${verifiedTwitter.twitterUser.id_str}`); twitterSpaceResult = await twitterClientV2.v2.spacesByCreators(verifiedTwitter.twitterUser.id_str); + Log.debug(twitterSpaceResult.data); } catch (e) { LogUtils.logError('failed trying to get twitter spaces', e); } @@ -65,6 +68,7 @@ const StartTwitterFlow = async (ctx: CommandContext, guildMember: GuildMember, d Log.debug('setting up active twitter event in db'); const currentDate: Dayjs = dayjs(); const endTimeISO: string = currentDate.add(duration, 'minute').toISOString(); + const channelExecutionId: string = isDmOn ? channelIds.DM : ctx.channelID; const twitterSettingsResult: FindAndModifyWriteOpResultObject = await poapTwitterSettings.findOneAndReplace({ discordUserId: guildMember.id, discordServerId: guildMember.guild.id, @@ -78,7 +82,8 @@ const StartTwitterFlow = async (ctx: CommandContext, guildMember: GuildMember, d discordServerId: guildMember.guild.id, twitterUserId: verifiedTwitter.twitterUser.id_str, twitterSpaceId: twitterSpaceId, - }, { + channelExecutionId: channelExecutionId, + } as POAPTwitterSettings, { upsert: true, returnDocument: 'after', }); diff --git a/src/app/types/poap/POAPParticipant.ts b/src/app/types/poap/POAPParticipant.ts index fa2f2bd5..9820f146 100644 --- a/src/app/types/poap/POAPParticipant.ts +++ b/src/app/types/poap/POAPParticipant.ts @@ -6,7 +6,7 @@ export interface POAPParticipant extends Collection { discordUserId: string, discordUserTag: string, startTime: string, - endTime: string, + endTime: string | null, voiceChannelId: string, discordServerId: string, durationInMinutes: number, diff --git a/src/app/utils/POAPUtils.ts b/src/app/utils/POAPUtils.ts index 717b0532..fb317d85 100644 --- a/src/app/utils/POAPUtils.ts +++ b/src/app/utils/POAPUtils.ts @@ -1,7 +1,6 @@ import { AwaitMessagesOptions, DMChannel, - GuildChannel, GuildMember, Message, MessageActionRow, @@ -32,6 +31,9 @@ import { POAPDistributionResults } from '../types/poap/POAPDistributionResults'; import ApiKeys from '../service/constants/apiKeys'; import buttonIds from '../service/constants/buttonIds'; import { DiscordUserCollection } from '../types/discord/DiscordUserCollection'; +import { POAPSettings } from '../types/poap/POAPSettings'; +import { getPoapParticipantsFromDB } from '../service/poap/end/EndPOAP'; +import { POAPTwitterUnclaimedParticipants } from '../types/poap/POAPTwitterUnclaimedParticipants'; export type POAPFileParticipant = { discordUserId: string, @@ -49,22 +51,17 @@ export type TwitterPOAPFileParticipant = { const POAPUtils = { - async getListOfParticipants(db: Db, voiceChannel: GuildChannel): Promise { - const poapParticipants: MongoCollection = db.collection(constants.DB_COLLECTION_POAP_PARTICIPANTS); - const resultCursor: Cursor = await poapParticipants.find({ - voiceChannelId: voiceChannel.id, - discordServerId: voiceChannel.guild.id, - }); - - if ((await resultCursor.count()) === 0) { - Log.debug(`no participants found for ${voiceChannel.name} in ${voiceChannel.guild.name}`); + async getListOfParticipants(poapSettingsDoc: POAPSettings): Promise { + Log.debug('checking for participants in db cursor'); + const participantsCursor: Cursor = await getPoapParticipantsFromDB(poapSettingsDoc.voiceChannelId, poapSettingsDoc.discordServerId); + if ((await participantsCursor.count()) === 0) { + Log.debug('no participants found'); return []; } - - await POAPUtils.setEndDateForPresentParticipants(poapParticipants, resultCursor); + Log.debug('found participants from cursor'); const participants: POAPFileParticipant[] = []; - await resultCursor.forEach((participant: POAPParticipant) => { + await participantsCursor.forEach((participant: POAPParticipant) => { if (participant.durationInMinutes >= constants.POAP_REQUIRED_PARTICIPATION_DURATION) { participants.push({ discordUserId: participant.discordUserId, @@ -73,6 +70,7 @@ const POAPUtils = { }); } }); + Log.debug('finished preparing participants array'); return participants; }, @@ -98,38 +96,6 @@ const POAPUtils = { return participants; }, - async setEndDateForPresentParticipants(poapParticipantsCollection: MongoCollection, poapParticipantsCursor: Cursor): Promise { - Log.debug('starting to set endDate for present participants in db'); - const currentDateStr = dayjs().toISOString(); - for await (const participant of poapParticipantsCursor) { - if (participant.endTime != null) { - // skip setting endDate for present endTime; - continue; - } - let result: UpdateWriteOpResult; - try { - const currentDate: Dayjs = dayjs(); - const startTimeDate: Dayjs = dayjs(participant.startTime); - let durationInMinutes: number = participant.durationInMinutes; - if ((currentDate.unix() - startTimeDate.unix() > 0)) { - durationInMinutes += ((currentDate.unix() - startTimeDate.unix()) / 60); - } - result = await poapParticipantsCollection.updateOne(participant, { - $set: { - endTime: currentDateStr, - durationInMinutes: durationInMinutes, - }, - }); - if (result == null) { - throw new Error('Mongodb operation failed'); - } - } catch (e) { - LogUtils.logError('failed to update poap participants with endTime', e); - } - } - Log.debug('finished setting endDate for present participants in db'); - }, - async askForPOAPLinks( guildMember: GuildMember, isDmOn: boolean, numberOfParticipants: number, ctx?: CommandContext, adminChannel?: TextChannel | null, @@ -147,24 +113,30 @@ const POAPUtils = { if (isDmOn) { await guildMember.send({ content: uploadLinksMsg }); const dmChannel: DMChannel = await guildMember.createDM(); - message = (await dmChannel.awaitMessages(replyOptions)).first(); + 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); const guildChannel: TextChannel = await guildMember.guild.channels.fetch(ctx.channelID) as TextChannel; - message = (await guildChannel.awaitMessages(replyOptions)).first(); + 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); - message = (await adminChannel.awaitMessages(replyOptions)).first(); + message = (await adminChannel.awaitMessages(replyOptions).catch(() => { + throw new ValidationError('Invalid attachment. Session ended, please try the command again.'); + })).first(); } if (message == null) { - throw new ValidationError('Invalid attachment. Session ended. Please try the command again.'); + throw new ValidationError('Invalid attachment. Session ended, please try the command again.'); } const poapLinksFile: MessageAttachment | undefined = message.attachments.first(); if (poapLinksFile == null) { - throw new ValidationError('Invalid attachment. Session ended. Please try the command again.'); + throw new ValidationError('Invalid attachment. Session ended, please try the command again.'); } Log.debug(`obtained poap links attachment in discord: ${poapLinksFile.url}`); @@ -183,7 +155,7 @@ const POAPUtils = { } catch (e) { listOfPOAPLinks = []; } - Log.debug(`DEGEN given ${listOfPOAPLinks.length} poap links`); + Log.debug(`${constants.APP_NAME} given ${listOfPOAPLinks.length} poap links`); return listOfPOAPLinks; } catch (e) { LogUtils.logError('failed to process links.txt file', e); @@ -469,7 +441,12 @@ const POAPUtils = { guildMember: GuildMember, distributionResults: POAPDistributionResults, event: string, platform: string, ): Promise { - Log.debug(`${distributionResults.didNotSendList} poaps were not sent`); + if (distributionResults.didNotSendList.length <= 0) { + Log.warn('failed delivery participants not found'); + return; + } + + Log.debug(`${distributionResults.didNotSendList.length} poaps were not sent`); const db: Db = await MongoDbUtils.connect(constants.DB_NAME_DEGEN); if (platform == constants.PLATFORM_TYPE_DISCORD) { @@ -500,10 +477,13 @@ const POAPUtils = { expiresAt: expirationISO, twitterUserId: failedAttendee.twitterUserId, twitterSpaceId: failedAttendee.twitterSpaceId, - }; + } as POAPTwitterUnclaimedParticipants; }); Log.debug('attempting to store failed attendees into db'); - await unclaimedCollection.insertMany(unclaimedPOAPsList); + await unclaimedCollection.insertMany(unclaimedPOAPsList).catch(e => { + Log.error(e); + throw new ValidationError('failed trying to store unclaimed participants, please try distribution command'); + }); distributionResults.claimSetUp = unclaimedPOAPsList.length; } else { Log.warn('missing platform type when trying to setup failed attendees'); @@ -654,8 +634,8 @@ const POAPUtils = { } }, - getEventYear(startDateObj: Dayjs): string { - return startDateObj.year().toString(); + getEventYear(startDateObj: Dayjs): number { + return startDateObj.year(); }, }; diff --git a/src/app/utils/ServiceUtils.ts b/src/app/utils/ServiceUtils.ts index 3c992981..5177401a 100644 --- a/src/app/utils/ServiceUtils.ts +++ b/src/app/utils/ServiceUtils.ts @@ -129,7 +129,6 @@ const ServiceUtils = { async tryDMUser(guildMember: GuildMember, message: string): Promise { try { await guildMember.send({ content: message }); - Log.debug(`DM is turned off for ${guildMember.user.tag}`); return true; } catch (e) { LogUtils.logError(`DM is turned off for ${guildMember.user.tag}`, e);