Skip to content

Commit

Permalink
Merge branch 'master' into tim/referral-ce
Browse files Browse the repository at this point in the history
# Conflicts:
#	packages/commonwealth/client/scripts/state/api/user/getReferralLink.ts
#	packages/commonwealth/client/scripts/views/modals/InviteLinkModal/InviteLinkModal.tsx
  • Loading branch information
timolegros committed Dec 19, 2024
2 parents ed5b034 + 6da9d1d commit 36d8bd9
Show file tree
Hide file tree
Showing 76 changed files with 694 additions and 881 deletions.
41 changes: 37 additions & 4 deletions libs/model/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const {
ALCHEMY_PUBLIC_APP_KEY,
MEMBERSHIP_REFRESH_BATCH_SIZE,
MEMBERSHIP_REFRESH_TTL_SECONDS,
NEYNAR_BOT_UUID,
NEYNAR_API_KEY,
NEYNAR_CAST_CREATED_WEBHOOK_SECRET,
NEYNAR_REPLY_WEBHOOK_URL,
Expand Down Expand Up @@ -91,6 +92,7 @@ export const config = configure(
: 5,
FLAG_FARCASTER_CONTEST: FLAG_FARCASTER_CONTEST === 'true',
NEYNAR_API_KEY: NEYNAR_API_KEY,
NEYNAR_BOT_UUID: NEYNAR_BOT_UUID,
NEYNAR_CAST_CREATED_WEBHOOK_SECRET: NEYNAR_CAST_CREATED_WEBHOOK_SECRET,
NEYNAR_REPLY_WEBHOOK_URL: NEYNAR_REPLY_WEBHOOK_URL,
FARCASTER_ACTION_URL: FARCASTER_ACTION_URL,
Expand Down Expand Up @@ -195,10 +197,41 @@ export const config = configure(
MIN_USER_ETH: z.number(),
MAX_USER_POSTS_PER_CONTEST: z.number().int(),
FLAG_FARCASTER_CONTEST: z.boolean().nullish(),
NEYNAR_API_KEY: z.string().nullish(),
NEYNAR_CAST_CREATED_WEBHOOK_SECRET: z.string().nullish(),
NEYNAR_REPLY_WEBHOOK_URL: z.string().nullish(),
FARCASTER_ACTION_URL: z.string().nullish(),
NEYNAR_BOT_UUID: z
.string()
.optional()
.refine(
(data) => !(target.APP_ENV === 'production' && !data),
'NEYNAR_BOT_UUID must be set to a non-default value in production.',
),
NEYNAR_API_KEY: z
.string()
.optional()
.refine(
(data) => !(target.APP_ENV === 'production' && !data),
'NEYNAR_API_KEY must be set to a non-default value in production.',
),
NEYNAR_CAST_CREATED_WEBHOOK_SECRET: z
.string()
.optional()
.refine(
(data) => !(target.APP_ENV === 'production' && !data),
'NEYNAR_CAST_CREATED_WEBHOOK_SECRET must be set to a non-default value in production.',
),
NEYNAR_REPLY_WEBHOOK_URL: z
.string()
.optional()
.refine(
(data) => !(target.APP_ENV === 'production' && !data),
'NEYNAR_REPLY_WEBHOOK_URL must be set to a non-default value in production.',
),
FARCASTER_ACTION_URL: z
.string()
.optional()
.refine(
(data) => !(target.APP_ENV === 'production' && !data),
'FARCASTER_ACTION_URL must be set to a non-default value in production.',
),
}),
AUTH: z
.object({
Expand Down
28 changes: 27 additions & 1 deletion libs/model/src/contest/Contests.projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
EvmEventSignatures,
commonProtocol as cp,
} from '@hicommonwealth/evm-protocols';
import { config } from '@hicommonwealth/model';
import { ContestScore, events } from '@hicommonwealth/schemas';
import { buildContestLeaderboardUrl, getBaseUrl } from '@hicommonwealth/shared';
import { QueryTypes } from 'sequelize';
import { z } from 'zod';
import { models } from '../database';
Expand All @@ -17,6 +19,8 @@ import {
decodeThreadContentUrl,
getChainNodeUrl,
getDefaultContestImage,
parseFarcasterContentUrl,
publishCast,
} from '../utils';

const log = logger(import.meta);
Expand Down Expand Up @@ -295,7 +299,9 @@ export function Contests(): Projection<typeof inputs> {
},

ContestContentAdded: async ({ payload }) => {
const { threadId } = decodeThreadContentUrl(payload.content_url);
const { threadId, isFarcaster } = decodeThreadContentUrl(
payload.content_url,
);
await models.ContestAction.create({
...payload,
contest_id: payload.contest_id || 0,
Expand All @@ -306,6 +312,26 @@ export function Contests(): Projection<typeof inputs> {
voting_power: '0',
created_at: new Date(),
});

// post confirmation via FC bot
if (isFarcaster) {
const contestManager = await models.ContestManager.findByPk(
payload.contest_address,
);
const leaderboardUrl = buildContestLeaderboardUrl(
getBaseUrl(config.APP_ENV),
contestManager!.community_id,
contestManager!.contest_address,
);
const { replyCastHash } = parseFarcasterContentUrl(
payload.content_url,
);
await publishCast(
replyCastHash,
({ username }) =>
`Hey @${username}, your entry has been submitted to the contest: ${leaderboardUrl}`,
);
}
},

ContestContentUpvoted: async ({ payload }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { NeynarAPIClient } from '@neynar/nodejs-sdk';
import { config } from '../config';
import { models } from '../database';
import { mustExist } from '../middleware/guards';
import { emitEvent } from '../utils';
import { emitEvent, publishCast } from '../utils';

const log = logger(import.meta);

Expand All @@ -21,13 +21,19 @@ export function FarcasterReplyCastCreatedWebhook(): Command<
mustExist('Farcaster Cast Author FID', payload.data.author?.fid);

const client = new NeynarAPIClient(config.CONTESTS.NEYNAR_API_KEY!);

// get user verified address
const { users } = await client.fetchBulkUsers([payload.data.author.fid]);
const verified_address = users[0].verified_addresses.eth_addresses.at(0);
if (!verified_address) {
log.warn(
'Farcaster verified address not found for reply cast created event- content will be ignored.',
);
await publishCast(
payload.data.hash,
({ username }) =>
`Hey @${username}, you need a verified address to participate in the contest.`,
);
return;
}

Expand Down
17 changes: 4 additions & 13 deletions libs/model/src/contest/FarcasterUpvoteAction.command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { logger, type Command } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import { NeynarAPIClient } from '@neynar/nodejs-sdk';
import { config } from '../config';
import { models } from '../database';
import { mustExist } from '../middleware/guards';
import { buildFarcasterContentUrl, emitEvent } from '../utils';
Expand All @@ -16,23 +14,16 @@ export function FarcasterUpvoteAction(): Command<
...schemas.FarcasterUpvoteAction,
auth: [],
body: async ({ payload }) => {
const client = new NeynarAPIClient(config.CONTESTS.NEYNAR_API_KEY!);
// get user verified address
const { users } = await client.fetchBulkUsers([
payload.untrustedData.fid,
]);
const verified_address = users[0].verified_addresses.eth_addresses.at(0);
const verified_address =
payload.interactor.verified_addresses?.eth_addresses.at(0);

if (!verified_address) {
log.warn(
'Farcaster verified address not found for upvote action- upvote will be ignored.',
);
return;
}

const castsResponse = await client.fetchBulkCasts([
payload.untrustedData.castId.hash,
]);
const { parent_hash, hash } = castsResponse.result.casts.at(0)!;
const { parent_hash, hash } = payload.cast;
const content_url = buildFarcasterContentUrl(parent_hash!, hash);

// find content from farcaster hash
Expand Down
6 changes: 1 addition & 5 deletions libs/model/src/policies/FarcasterWorker.policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,7 @@ export function FarcasterWorker(): Policy<typeof inputs> {
});
},
FarcasterVoteCreated: async ({ payload }) => {
const client = new NeynarAPIClient(config.CONTESTS.NEYNAR_API_KEY!);
const castsResponse = await client.fetchBulkCasts([
payload.untrustedData.castId.hash,
]);
const { parent_hash, hash } = castsResponse.result.casts.at(0)!;
const { parent_hash, hash } = payload.cast;
const content_url = buildFarcasterContentUrl(parent_hash!, hash);

const contestManager = await models.ContestManager.findOne({
Expand Down
3 changes: 1 addition & 2 deletions libs/model/src/services/openai/generateTokenIdea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,8 @@ const generateTokenIdea = async function* ({

// generate image url and send the generated url to the client (to save time on s3 upload)
const imageResponse = await openai.images.generate({
model: 'dall-e-3',
prompt: TOKEN_AI_PROMPTS_CONFIG.image(tokenIdea.name, tokenIdea.symbol),
size: '512x512',
size: '256x256',
n: 1,
response_format: 'url',
});
Expand Down
41 changes: 13 additions & 28 deletions libs/model/src/thread/GetThreads.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,6 @@ export function GetThreads(): Query<typeof schemas.GetThreads> {
pastWinners: ' AND CON.end_time <= NOW()',
all: '',
};
const baseWhereClause = `
community_id = :community_id AND
deleted_at IS NULL AND
archived_at IS ${archived ? 'NOT' : ''} NULL
${topic_id ? ' AND topic_id = :topic_id' : ''}
${stage ? ' AND stage = :stage' : ''}
${from_date ? ' AND T.created_at > :from_date' : ''}
${to_date ? ' AND T.created_at < :to_date' : ''}
${contestAddress ? ' AND id IN (SELECT * FROM "contest_ids")' : ''}
`;
const responseThreadsQuery = models.sequelize.query<
z.infer<typeof schemas.ThreadView>
>(
Expand All @@ -96,10 +86,18 @@ export function GetThreads(): Query<typeof schemas.GetThreads> {
pinned, community_id, T.created_at, updated_at, locked_at as thread_locked, links,
has_poll, last_commented_on, comment_count as "numberOfComments",
marked_as_spam_at, archived_at, topic_id, reaction_weights_sum, canvas_signed_data,
canvas_msg_id, last_edited, address_id, reaction_count
canvas_msg_id, last_edited, address_id, reaction_count,
(COUNT(id) OVER())::INTEGER AS total_num_thread_results
FROM "Threads" T
WHERE ${baseWhereClause}
WHERE
community_id = :community_id AND
deleted_at IS NULL AND
archived_at IS ${archived ? 'NOT' : ''} NULL
${topic_id ? ' AND topic_id = :topic_id' : ''}
${stage ? ' AND stage = :stage' : ''}
${from_date ? ' AND T.created_at > :from_date' : ''}
${to_date ? ' AND T.created_at < :to_date' : ''}
${contestAddress ? ' AND id IN (SELECT * FROM "contest_ids")' : ''}
ORDER BY pinned DESC, ${orderByQueries[order_by ?? 'newest']}
LIMIT :limit OFFSET :offset
), thread_metadata AS (
Expand Down Expand Up @@ -252,37 +250,24 @@ export function GetThreads(): Query<typeof schemas.GetThreads> {
},
);

const countThreadsQuery = models.sequelize.query<{ count: number }>(
`
SELECT COUNT(*) AS count
FROM "Threads" T
WHERE ${baseWhereClause}
`,
{
replacements,
type: QueryTypes.SELECT,
},
);

const numVotingThreadsQuery = models.Thread.count({
where: {
community_id,
stage: 'voting',
},
});

const [threads, numVotingThreads, countResult] = await Promise.all([
const [threads, numVotingThreads] = await Promise.all([
responseThreadsQuery,
numVotingThreadsQuery,
countThreadsQuery,
]);

return {
limit: replacements.limit,
page: replacements.page,
threads,
numVotingThreads,
threadCount: Number(countResult[0]?.count) || 0,
threadCount: threads.at(0)?.total_num_thread_results || 0,
};
},
};
Expand Down
9 changes: 6 additions & 3 deletions libs/model/src/user/GetUserProfile.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ import * as schemas from '@hicommonwealth/schemas';
import { Op } from 'sequelize';
import { z } from 'zod';
import { models } from '../database';
import { mustExist } from '../middleware/guards';

export function GetUserProfile(): Query<typeof schemas.GetUserProfile> {
return {
...schemas.GetUserProfile,
auth: [],
secure: true,
secure: false,
body: async ({ actor, payload }) => {
const user_id = payload.userId ?? actor.user.id;
const user_id = payload.userId ?? actor.user?.id;

const user = await models.User.findOne({
where: { id: user_id },
attributes: ['profile', 'xp_points'],
});

mustExist('User', user);

const addresses = await models.Address.findAll({
where: { user_id },
include: [
Expand Down Expand Up @@ -98,7 +101,7 @@ export function GetUserProfile(): Query<typeof schemas.GetUserProfile> {
commentThreads: commentThreads.map(
(c) => c.toJSON() as z.infer<typeof schemas.ThreadView>,
),
isOwner: actor.user.id === user_id,
isOwner: actor.user?.id === user_id,
// ensure Tag is present in typed response
tags: profileTags.map((t) => ({ id: t.Tag!.id!, name: t.Tag!.name })),
xp_points: user!.xp_points ?? 0,
Expand Down
6 changes: 0 additions & 6 deletions libs/model/src/utils/buildFarcasterContentUrl.ts

This file was deleted.

14 changes: 14 additions & 0 deletions libs/model/src/utils/farcasterUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function buildFarcasterContentUrl(
parentCastHash: string,
replyCashHash: string,
) {
return `/farcaster/${parentCastHash}/${replyCashHash}`;
}

export function parseFarcasterContentUrl(url: string) {
const [, , parentCastHash, replyCastHash] = url.split('/');
return {
parentCastHash,
replyCastHash,
};
}
2 changes: 1 addition & 1 deletion libs/model/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export * from './buildFarcasterContentUrl';
export * from './buildFarcasterWebhookName';
export * from './decodeContent';
export * from './defaultAvatar';
export * from './denormalizedCountUtils';
export * from './farcasterUtils';
export * from './getDefaultContestImage';
export * from './getDelta';
export * from './makeGetBalancesOptions';
Expand Down
Loading

0 comments on commit 36d8bd9

Please sign in to comment.