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
46 changes: 39 additions & 7 deletions src/lib/telegram/chat/__tests__/connectTelegramToAccount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,25 @@ describe('connectTelegramToAccount', () => {
);
});

it('replies with a not-found message when no wallet matches the email', async () => {
it('sends a verification code with a null artistId when no wallet matches the email', async () => {
vi.mocked(getPrivyWalletAddressesByEmail).mockResolvedValue([]);
const thread = makeThread();

await connectTelegramToAccount(thread as never, 'user@example.com');

expect(selectWallets).not.toHaveBeenCalled();
expect(sendCodeHandler).not.toHaveBeenCalled();
expect(sendCodeHandler).toHaveBeenCalledWith('user@example.com');
expect(setPendingCode).toHaveBeenCalledWith(thread, {
email: 'user@example.com',
artistId: null,
username: null,
});
expect(thread.post).toHaveBeenCalledWith(
expect.stringContaining("couldn't find")
expect.stringContaining('verification code')
);
});

it('replies with a not-found message when the wallet has no linked artist', async () => {
it('sends a verification code with a null artistId when the wallet has no linked artist', async () => {
vi.mocked(getPrivyWalletAddressesByEmail).mockResolvedValue([
WALLET_ADDRESS,
]);
Expand All @@ -67,13 +72,18 @@ describe('connectTelegramToAccount', () => {

await connectTelegramToAccount(thread as never, 'user@example.com');

expect(sendCodeHandler).not.toHaveBeenCalled();
expect(sendCodeHandler).toHaveBeenCalledWith('user@example.com');
expect(setPendingCode).toHaveBeenCalledWith(thread, {
email: 'user@example.com',
artistId: null,
username: null,
});
expect(thread.post).toHaveBeenCalledWith(
expect.stringContaining("couldn't find")
expect.stringContaining('verification code')
);
});

it('sends a verification code and does not link the artist yet when a match is found', async () => {
it('sends a verification code with the matched artistId when a match is found', async () => {
vi.mocked(getPrivyWalletAddressesByEmail).mockResolvedValue([
WALLET_ADDRESS,
]);
Expand All @@ -95,4 +105,26 @@ describe('connectTelegramToAccount', () => {
expect.stringContaining('verification code')
);
});

it('replies with the same verification-code message regardless of whether a match was found', async () => {
vi.mocked(getPrivyWalletAddressesByEmail).mockResolvedValue([]);
const unmatchedThread = makeThread();
await connectTelegramToAccount(
unmatchedThread as never,
'user@example.com'
);

vi.mocked(getPrivyWalletAddressesByEmail).mockResolvedValue([
WALLET_ADDRESS,
]);
vi.mocked(selectWallets).mockResolvedValue({
data: [{ artist: ARTIST }],
} as never);
const matchedThread = makeThread();
await connectTelegramToAccount(matchedThread as never, 'user@example.com');

expect(unmatchedThread.post.mock.calls[0]).toEqual(
matchedThread.post.mock.calls[0]
);
});
});
44 changes: 44 additions & 0 deletions src/lib/telegram/chat/__tests__/verifyTelegramCode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('@/lib/privy/authenticatePrivyPasswordless', () => ({
default: vi.fn(),
}));
vi.mock('@/lib/privy/ensurePrivySocialWallet', () => ({ default: vi.fn() }));
vi.mock('@/lib/artists/getOrCreateArtist', () => ({ default: vi.fn() }));
vi.mock('@/lib/supabase/in_process_artists/upsertArtists', () => ({
upsertArtists: vi.fn(),
}));
vi.mock('../clearPendingCode', () => ({ default: vi.fn() }));

import authenticatePrivyPasswordless from '@/lib/privy/authenticatePrivyPasswordless';
import ensurePrivySocialWallet from '@/lib/privy/ensurePrivySocialWallet';
import getOrCreateArtist from '@/lib/artists/getOrCreateArtist';
import { upsertArtists } from '@/lib/supabase/in_process_artists/upsertArtists';
import clearPendingCode from '../clearPendingCode';
import verifyTelegramCode from '../verifyTelegramCode';
Expand All @@ -19,6 +23,8 @@ const PENDING = {
artistId: 'uuid-artist-1234',
username: 'alice',
};
const NEW_WALLET_ADDRESS = '0xNewWallet';
const NEW_ARTIST_ID = 'uuid-new-artist';

const makeThread = () => ({
post: vi.fn().mockResolvedValue(undefined),
Expand All @@ -28,6 +34,12 @@ beforeEach(() => {
vi.clearAllMocks();
vi.mocked(upsertArtists).mockResolvedValue([{ id: PENDING.artistId }]);
vi.mocked(clearPendingCode).mockResolvedValue(undefined);
vi.mocked(ensurePrivySocialWallet).mockResolvedValue(NEW_WALLET_ADDRESS);
vi.mocked(getOrCreateArtist).mockResolvedValue({
artistId: NEW_ARTIST_ID,
primaryWallet: NEW_WALLET_ADDRESS,
wallets: [],
} as never);
});

describe('verifyTelegramCode', () => {
Expand Down Expand Up @@ -73,6 +85,38 @@ describe('verifyTelegramCode', () => {
telegram: TG_USERNAME,
});
expect(clearPendingCode).toHaveBeenCalledWith(thread);
expect(ensurePrivySocialWallet).not.toHaveBeenCalled();
expect(getOrCreateArtist).not.toHaveBeenCalled();
expect(thread.post).toHaveBeenCalledWith(expect.stringContaining('alice'));
});

it('creates a wallet and artist, then links telegram, when no artist was matched', async () => {
const authResult = { user: { id: 'privy-user-1' } };
vi.mocked(authenticatePrivyPasswordless).mockResolvedValue(
authResult as never
);
const thread = makeThread();
const unmatchedPending = { ...PENDING, artistId: null, username: null };

await verifyTelegramCode(
thread as never,
'123456',
TG_USERNAME,
unmatchedPending
);

expect(ensurePrivySocialWallet).toHaveBeenCalledWith(authResult);
expect(getOrCreateArtist).toHaveBeenCalledWith({
address: NEW_WALLET_ADDRESS,
type: 'privy',
});
expect(upsertArtists).toHaveBeenCalledWith({
id: NEW_ARTIST_ID,
telegram: TG_USERNAME,
});
expect(clearPendingCode).toHaveBeenCalledWith(thread);
expect(thread.post).toHaveBeenCalledWith(
expect.stringContaining('Welcome to In Process')
);
});
});
18 changes: 6 additions & 12 deletions src/lib/telegram/chat/connectTelegramToAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import setPendingCode from './setPendingCode';

const INVALID_EMAIL_MESSAGE =
"That doesn't look like a valid email address. Please try again.";
const NO_ACCOUNT_FOUND_MESSAGE =
"We couldn't find an In Process account for that email yet. Please sign up at https://inprocess.world, then reply here with your email again to connect your Telegram.";
const CODE_SENT_MESSAGE =
"We've sent a 6-digit verification code to that email. Please reply with the code to confirm it's yours.";

Expand All @@ -32,21 +30,17 @@ async function connectTelegramToAccount(
)?.artist ?? null)
: null;

if (!artist) {
// TODO: no In Process account exists for this email/wallet yet — support
// creating one directly from this Telegram flow instead of requiring web signup first.
await thread.post(NO_ACCOUNT_FOUND_MESSAGE);
return;
}

// Require proof the sender owns this email before linking their Telegram
// account to it — otherwise anyone could hijack an artist's account by
// typing in an email address they don't own.
// typing in an email address they don't own. Send the code and reply
// identically whether or not an artist was found: revealing that up front
// would let anyone probe arbitrary emails to learn which ones have an
// In Process account.
await sendCodeHandler(email);
await setPendingCode(thread, {
email,
artistId: artist.id,
username: artist.username,
artistId: artist?.id ?? null,
username: artist?.username ?? null,
});
await clearPendingEmail(thread);
await thread.post(CODE_SENT_MESSAGE);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/telegram/chat/getPendingCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import getStateAdapter, { TELEGRAM_PENDING_CODE_KEY } from './stateAdapter';

export interface PendingCode {
email: string;
artistId: string;
artistId: string | null;
username: string | null;
}

Expand Down
25 changes: 23 additions & 2 deletions src/lib/telegram/chat/verifyTelegramCode.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { Address } from 'viem';
import type { Thread } from 'chat';
import type { TelegramThreadState } from './telegramThreadState';
import telegramCodeSchema from '@/lib/schema/telegramCodeSchema';
import authenticatePrivyPasswordless from '@/lib/privy/authenticatePrivyPasswordless';
import ensurePrivySocialWallet from '@/lib/privy/ensurePrivySocialWallet';
import getOrCreateArtist from '@/lib/artists/getOrCreateArtist';
import { upsertArtists } from '@/lib/supabase/in_process_artists/upsertArtists';
import clearPendingCode from './clearPendingCode';
import type { PendingCode } from './getPendingCode';

const INVALID_CODE_MESSAGE =
"That code doesn't look right. Please check your email and try again.";
const WELCOME_MESSAGE =
"Welcome to In Process! We've created your account and connected your Telegram. You can now send photos and videos to publish moments.";

async function verifyTelegramCode(
thread: Thread<TelegramThreadState>,
Expand All @@ -21,15 +26,31 @@ async function verifyTelegramCode(
return;
}

let authResult;
try {
await authenticatePrivyPasswordless(pending.email, result.data);
authResult = await authenticatePrivyPasswordless(
pending.email,
result.data
);
} catch {
await thread.post(INVALID_CODE_MESSAGE);
return;
}

await upsertArtists({ id: pending.artistId, telegram: telegramUsername });
await clearPendingCode(thread);

if (!pending.artistId) {
const walletAddress = await ensurePrivySocialWallet(authResult);
const artist = await getOrCreateArtist({
address: walletAddress as Address,
type: 'privy',
});
await upsertArtists({ id: artist.artistId, telegram: telegramUsername });
await thread.post(WELCOME_MESSAGE);
return;
}

await upsertArtists({ id: pending.artistId, telegram: telegramUsername });
await thread.post(
`You're all set! Your Telegram is now connected to your In Process account${pending.username ? ` (${pending.username})` : ''}. You can now send photos and videos to publish moments.`
);
Expand Down
Loading