Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ Follow the Single Responsibility Principle:
- **Thin route files** - Route handlers should only call validators and handlers
- Separate validation and business logic into `@/lib/<domain>/` files

#### KISS: Decide context values as close to the core function as possible

When a wrapper function always operates in a fixed context (e.g., `createMomentFromMedia` is always SMS, `createMomentFromTelegramAttachment` is always Telegram), hardcode that context value directly at the `createMoment` call site — do **not** thread it as a parameter up the call stack. Adding a parameter only makes sense when the caller genuinely controls the value.

#### Schema Organization

- **All Zod schemas live in `@/lib/schema/`** — never define schemas inline in route files or handler files
Expand Down
4 changes: 3 additions & 1 deletion src/app/api/moment/create/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextRequest } from 'next/server';
import { createMomentSchema } from '@/lib/schema/createMomentSchema';
import { createMoment } from '@/lib/moment/createMoment';
import { validate } from '@/lib/schema/validate';
import getChannelFromReqHeader from '@/lib/moment/getChannelFromReqHeader';

export async function POST(req: NextRequest) {
try {
Expand All @@ -11,7 +12,8 @@ export async function POST(req: NextRequest) {
return validationResult.response;
}
const data = validationResult.data;
const result = await createMoment(data);
const channel = data.channel ?? getChannelFromReqHeader(req);
const result = await createMoment({ ...data, channel });
return Response.json(result);
} catch (e: any) {
console.log(e);
Expand Down
77 changes: 77 additions & 0 deletions src/lib/messages/__tests__/getMomentFromMessage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, it, expect } from 'vitest';
import getMomentFromMessage from '@/lib/messages/getMomentFromMessage';

const CONTRACT = '0xAbCd1234567890AbCd1234567890AbCd12345678';

const makeMessage = (text: string) =>
({ parts: [{ type: 'text', text }] }) as any;

describe('getMomentFromMessage', () => {
it('matches a /sms/ URL on base chain', () => {
const result = getMomentFromMessage(
makeMessage(
`✅ Moment created! https://inprocess.world/sms/base:${CONTRACT}/42`
)
);
expect(result).toEqual({
collectionAddress: CONTRACT.toLowerCase(),
tokenId: '42',
});
});

it('matches a /collect/ URL on base chain', () => {
const result = getMomentFromMessage(
makeMessage(
`✅ Moment created! https://inprocess.world/collect/base:${CONTRACT}/7`
)
);
expect(result).toEqual({
collectionAddress: CONTRACT.toLowerCase(),
tokenId: '7',
});
});

it('matches a /sms/ URL on bsep (testnet)', () => {
const result = getMomentFromMessage(
makeMessage(
`✅ Moment created! https://inprocess.world/sms/bsep:${CONTRACT}/1`
)
);
expect(result).toEqual({
collectionAddress: CONTRACT.toLowerCase(),
tokenId: '1',
});
});

it('matches a /collect/ URL on bsep (testnet)', () => {
const result = getMomentFromMessage(
makeMessage(
`✅ Moment created! https://inprocess.world/collect/bsep:${CONTRACT}/3`
)
);
expect(result).toEqual({
collectionAddress: CONTRACT.toLowerCase(),
tokenId: '3',
});
});

it('returns null for an unrecognized path', () => {
const result = getMomentFromMessage(
makeMessage(`https://inprocess.world/unknown/base:${CONTRACT}/1`)
);
expect(result).toBeNull();
});

it('returns null when parts is null', () => {
expect(getMomentFromMessage({ parts: null } as any)).toBeNull();
});

it('returns null for a non-text part', () => {
const result = getMomentFromMessage({
parts: [
{ type: 'file', url: `https://inprocess.world/sms/base:${CONTRACT}/1` },
],
} as any);
expect(result).toBeNull();
});
});
46 changes: 46 additions & 0 deletions src/lib/messages/__tests__/logMessage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,50 @@ describe('logMessage', () => {
artist_address: undefined,
});
});

it('passes "web" client to insertMessageMetadata', async () => {
vi.mocked(insertMessageMetadata).mockResolvedValue({
data: { id: 'metadata-123' },
error: null,
} as any);
vi.mocked(insertMessage).mockResolvedValue({
data: { id: 'message-456' },
error: null,
} as any);

await logMessage(
[{ type: 'text', text: 'Hello' }],
'assistant',
'0x123',
'web'
);

expect(insertMessageMetadata).toHaveBeenCalledWith({
client: 'web',
artist_address: '0x123',
});
});

it('passes "api" client to insertMessageMetadata', async () => {
vi.mocked(insertMessageMetadata).mockResolvedValue({
data: { id: 'metadata-123' },
error: null,
} as any);
vi.mocked(insertMessage).mockResolvedValue({
data: { id: 'message-456' },
error: null,
} as any);

await logMessage(
[{ type: 'text', text: 'Hello' }],
'assistant',
'0x123',
'api'
);

expect(insertMessageMetadata).toHaveBeenCalledWith({
client: 'api',
artist_address: '0x123',
});
});
});
87 changes: 0 additions & 87 deletions src/lib/messages/__tests__/processMomentMessage.test.ts

This file was deleted.

10 changes: 4 additions & 6 deletions src/lib/messages/getMomentFromMessage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { FormattedMessage } from './formatMessages';
import { SITE_ORIGINAL_URL } from '@/lib/consts';

const MOMENT_URL_REGEX =
/\/(?:sms|collect)\/(?:base|bsep):(0x[a-fA-F0-9]+)\/(\d+)/;

const getMomentFromMessage = (message: FormattedMessage) => {
if (!message.parts) return null;
const escapedBase = SITE_ORIGINAL_URL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const MOMENT_URL_REGEX = new RegExp(
`${escapedBase}/sms/(?:base|bsep):(0x[a-fA-F0-9]+)/(\\d+)`
);

for (const part of message.parts as unknown as {
type: string;
Expand All @@ -16,7 +14,7 @@ const getMomentFromMessage = (message: FormattedMessage) => {
const match = part.text.match(MOMENT_URL_REGEX);
if (match) {
return {
collectionAddress: match[1] as `0x${string}`,
collectionAddress: match[1].toLowerCase() as `0x${string}`,
tokenId: match[2],
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/messages/logMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function logMessage(
parts: MessagePart[],
role: 'user' | 'assistant',
artistAddress?: string,
client: 'sms' | 'telegram' = 'sms'
client: 'sms' | 'telegram' | 'web' | 'api' = 'sms'
) {
const { data: metadata } = await insertMessageMetadata({
client,
Expand Down
29 changes: 0 additions & 29 deletions src/lib/messages/processMomentMessage.ts

This file was deleted.

20 changes: 19 additions & 1 deletion src/lib/moment/__tests__/createMoment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ vi.mock('@/lib/trigger.dev/triggerMuxMigration', () => ({
default: vi.fn(),
}));

vi.mock('@/lib/moment/processMessageMoment', () => ({
default: vi.fn(),
}));

vi.mock('@/lib/protocolSdk/create/factory-addresses', () => ({
getFactoryAddress: vi.fn(),
}));
Expand All @@ -51,6 +55,7 @@ import { sendUserOperation } from '@/lib/coinbase/sendUserOperation';
import parseMomentTransaction from '@/lib/moment/parseMomentTransaction';
import triggerMuxMigration from '@/lib/trigger.dev/triggerMuxMigration';
import { getFactoryAddress } from '@/lib/protocolSdk/create/factory-addresses';
import processMessageMoment from '@/lib/moment/processMessageMoment';
import {
createMoment,
type CreateMomentContractInput,
Expand All @@ -70,7 +75,7 @@ const baseInput: CreateMomentContractInput = {
createReferral: '0x0000000000000000000000000000000000000000',
salesConfig: {
type: 'fixedPrice',
pricePerToken: '0',
pricePerToken: 0n,
saleStart: 0n,
saleEnd: 9999999999n,
},
Expand Down Expand Up @@ -111,6 +116,7 @@ describe('createMoment', () => {
} as any);

vi.mocked(triggerMuxMigration).mockResolvedValue(undefined);
vi.mocked(processMessageMoment).mockResolvedValue(undefined);
});

it('returns contractAddress, tokenId, hash, and chainId', async () => {
Expand Down Expand Up @@ -164,4 +170,16 @@ describe('createMoment', () => {
'Trigger.dev unavailable'
);
});

it('calls processMessageMoment with contractAddress, tokenId, account, and channel', async () => {
const input = { ...baseInput, channel: 'web' };
await createMoment(input);

expect(processMessageMoment).toHaveBeenCalledWith({
contractAddress: RESULT_CONTRACT,
tokenId: '7',
artistAddress: ARTIST,
channel: 'web',
});
});
});
31 changes: 31 additions & 0 deletions src/lib/moment/__tests__/getChannelFromReqHeader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, it, expect } from 'vitest';
import getChannelFromReqHeader from '@/lib/moment/getChannelFromReqHeader';

const makeReq = (origin?: string) =>
({
headers: {
get: (key: string) => (key === 'origin' ? (origin ?? null) : null),
},
}) as any;

describe('getChannelFromReqHeader', () => {
it('returns "web" for inprocess.world origin', () => {
expect(getChannelFromReqHeader(makeReq('https://inprocess.world'))).toBe(
'web'
);
});

it('returns "web" for stayinprocess.vercel.app origin', () => {
expect(
getChannelFromReqHeader(makeReq('https://stayinprocess.vercel.app'))
).toBe('web');
});

it('returns "api" for an unknown origin', () => {
expect(getChannelFromReqHeader(makeReq('https://other.com'))).toBe('api');
});

it('returns "api" when origin header is absent', () => {
expect(getChannelFromReqHeader(makeReq())).toBe('api');
});
});
Loading
Loading