diff --git a/prisma/migrations/20241106125304_/migration.sql b/prisma/migrations/20241106125304_/migration.sql new file mode 100644 index 0000000..6891b67 --- /dev/null +++ b/prisma/migrations/20241106125304_/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "FarcasterConnection" ADD COLUMN "thankYouCastSent" BOOLEAN DEFAULT FALSE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f39c7a1..547cd73 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -180,6 +180,7 @@ model FarcasterConnection { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id]) + thankYouCastSent Boolean @default(false) } model WorldIdConnection { diff --git a/src/cronJobs.ts b/src/cronJobs.ts index 2d10456..8039c1e 100644 --- a/src/cronJobs.ts +++ b/src/cronJobs.ts @@ -1,5 +1,8 @@ import { schedule } from 'node-cron'; -import { sendDailyCasts } from './neynar/utils'; +import { + sendDailyDelegationCasts, + sendDailyThankYouCast, +} from './neynar/utils'; const sendCastsCronJobTime = '21 1 22 * * *'; // at 22:01 Tehran time every day @@ -12,7 +15,8 @@ const sendCastsCronJob = () => { sendCastsCronJobTime, async () => { try { - await sendDailyCasts(); + await sendDailyDelegationCasts(); + await sendDailyThankYouCast(); } catch (e) { console.error('sendCastsCronJob error', e); } diff --git a/src/flow/flow.controller.ts b/src/flow/flow.controller.ts index 845d2ce..13d2548 100644 --- a/src/flow/flow.controller.ts +++ b/src/flow/flow.controller.ts @@ -172,6 +172,21 @@ export class FlowController { return 'Success'; } + @UseGuards(AuthGuard) + @ApiOperation({ + summary: 'Used to unmark a project as Conflict of Interest', + }) + @ApiBody({ + type: SetCoIDto, + description: 'Project id', + }) + @UseGuards(AuthGuard) + @Post('/unmark-coI') + async unmarkCoI(@Req() { userId }: AuthedReq, @Body() { pid }: SetCoIDto) { + await this.flowService.unsetCoi(userId, pid); + return 'Success'; + } + @UseGuards(AuthGuard) @ApiOperation({ summary: 'Used for a pairwise vote between two projects', diff --git a/src/flow/flow.service.ts b/src/flow/flow.service.ts index 413dab6..e112e55 100644 --- a/src/flow/flow.service.ts +++ b/src/flow/flow.service.ts @@ -260,6 +260,30 @@ export class FlowService { }); }; + unsetCoi = async (userId: number, projectId: number) => { + await this.prismaService.projectCoI.delete({ + where: { + userId_projectId: { + projectId, + userId, + }, + }, + }); + }; + + isCoi = async (userId: number, projectId: number) => { + const res = await this.prismaService.projectCoI.findUnique({ + where: { + userId_projectId: { + projectId, + userId, + }, + }, + }); + + return !!res; + }; + setRating = async ( projectId: number, userId: number, @@ -568,17 +592,20 @@ export class FlowService { include: { project: true }, }); - const withStars = await Promise.all( + const withMoreFields = await Promise.all( ranking.map(async (el) => ({ ...el, stars: await this.getProjectStars(el.projectId, userId), + coi: await this.isCoi(userId, el.projectId), })), ); - return withStars.sort((a, b) => b.share - a.share); + return withMoreFields.sort((a, b) => b.share - a.share); }; undo = async (userId: number, parentCollection: number | null) => { + // TODO: Check if a colleciton is still wip and not finished + const lastVote = await this.prismaService.vote.findFirst({ where: { userId, @@ -758,6 +785,7 @@ export class FlowService { projectStars, allProjects, ); + // console.log('real progress:', realProgress); const progress = Math.min(1, realProgress * 3); @@ -808,7 +836,7 @@ export class FlowService { new Set(allProjects.map((item) => item.implicitCategory)), ).map((cat, index) => ({ name: cat, priority: index * 2 })); - console.log(shuffleArraySeeded(implicitCategoryPriorities, userId)); + // console.log(shuffleArraySeeded(implicitCategoryPriorities, userId)); const getImplicitCatScore = (cat: string) => shuffleArraySeeded(implicitCategoryPriorities, userId).find( diff --git a/src/neynar/utils.ts b/src/neynar/utils.ts index 4e9653e..540765e 100644 --- a/src/neynar/utils.ts +++ b/src/neynar/utils.ts @@ -2,6 +2,141 @@ import { PrismaClient } from '@prisma/client'; import { FarcasterMetadata } from 'src/flow/types'; import neynarClient from './neynarClient'; +const findMaxiUsers = async () => { + const prisma = new PrismaClient({ + datasources: { + db: { + url: process.env.POSTGRES_PRISMA_URL, + }, + }, + }); + + await prisma.$connect(); + + const collectionCategories = await prisma.project.findMany({ + where: { + type: 'collection', + }, + select: { + id: true, + }, + }); + const collectionCategoryIds = collectionCategories.map( + (category) => category.id, + ); + + const farcasterConnections = await prisma.farcasterConnection.findMany({ + where: { + thankYouCastSent: false, + }, + select: { + userId: true, + }, + }); + + const userIdsWithFarcaster = farcasterConnections.map( + (connection) => connection.userId, + ); + + // Find users who have delegated or attested to all categories (Collection + Budget) + const maxiUsers = await prisma.user.findMany({ + where: { + id: { in: userIdsWithFarcaster }, + // Check that the user has attested or delegated to the Budget category + OR: [ + { + budgetAttestations: { + some: {}, + }, + }, + { + budgetDelegation: { + isNot: null, + }, + }, + ], + // Ensure the user has attested or delegated to all Collection categories + AND: [ + { + AND: collectionCategoryIds.map((categoryId) => ({ + OR: [ + { + attestations: { + some: { + collectionId: categoryId, + }, + }, + }, + { + delegations: { + some: { + collectionId: categoryId, + }, + }, + }, + ], + })), + }, + ], + }, + select: { + farcasterConnection: { + select: { + userId: true, + metadata: true, + }, + }, + }, + }); + return maxiUsers.map((user) => user.farcasterConnection); +}; + +const sendThankYouCast = async (username: string) => { + if (!farcasterSignerUUID) { + throw new Error( + 'Make sure you set FARCASTER_SIGNER_UUID in your .env file', + ); + } + await neynarClient.publishCast( + farcasterSignerUUID, + `@${username}! Thank you for playing with Pairwise! + +YOU ROCK!`, + ); + console.log(`The Thanks you Cast successfully sent to @${username}`); +}; + +const updateThankYouCastSent = async (userId?: number) => { + if (!userId) return; + const prisma = new PrismaClient({ + datasources: { + db: { + url: process.env.POSTGRES_PRISMA_URL, + }, + }, + }); + await prisma.farcasterConnection.update({ + where: { userId }, + data: { thankYouCastSent: true }, + }); + await prisma.$disconnect(); +}; + +export const sendDailyThankYouCast = async () => { + const maxiUsers = await findMaxiUsers(); + if (!maxiUsers || maxiUsers.length === 0) return; + for (const user of maxiUsers) { + try { + await sendThankYouCast( + (user?.metadata?.valueOf() as FarcasterMetadata)['username'], + ); + await updateThankYouCastSent(user?.userId); + } catch (e) { + console.error('Error sending Thank you cast for user ID: ', user?.userId); + } + } +}; + /** * Returns an array of `{fid, username, totalDelegates}` mapping in which `totalDelegates` is * the number of times that another user has delegated to this specific username/fid @@ -77,7 +212,7 @@ export const getDelegations = async (start: number, end?: number) => { return result; }; -export const sendDailyCasts = async () => { +export const sendDailyDelegationCasts = async () => { const endTimestamp = new Date(); endTimestamp.setMinutes(0, 0, 0); // set to xx:00:00 const delegations = await getDelegations(