Skip to content
Open
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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ KV_REST_API_READ_ONLY_TOKEN=
INNGEST_EVENT_KEY=
INNGEST_SIGNING_KEY=

# Email
RESEND_API_KEY=
EMAIL_FROM=onboarding@resend.dev

# Groq (LLM)
GROQ_API_KEY=
GROQ_BUDGET_PER_DAY=400000
Expand Down
29 changes: 27 additions & 2 deletions src/inngest/functions/help-dispatch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { inngest } from '../client';
import { getServiceSupabase } from '@/lib/supabase/service';
import { rankReviewers, type ReviewerCandidate } from '@/lib/help/dispatch';
import { sendHelpDispatchEmail } from '@/lib/email';

/**
* Help dispatch: when a user fires a help request, fan out notifications to
Expand All @@ -25,7 +26,7 @@ export const helpDispatch = inngest.createFunction(

const { data: mentee } = await sb
.from('profiles')
.select('level, primary_language')
.select('level, primary_language, github_handle')
.eq('id', userId)
.maybeSingle();
const menteeLevel = mentee?.level ?? 0;
Expand All @@ -42,7 +43,7 @@ export const helpDispatch = inngest.createFunction(
// Pool: all L2+ profiles. In production we'd narrow by recent activity etc.
const { data: pool } = await sb
.from('profiles')
.select('id, level, primary_language')
.select('id, level, primary_language, github_handle, email')
.gte('level', 2)
.neq('id', userId);

Expand All @@ -68,6 +69,12 @@ export const helpDispatch = inngest.createFunction(

if (targets.length === 0) return { notified: 0 };

const { data: helpRequest } = await sb
.from('help_requests')
.select('reason, pr_url')
.eq('id', helpRequestId)
.maybeSingle();

// Write a notification row per target for the help-inbox to pick up.
const rows = targets.map((t) => ({
user_id: t.userId,
Expand All @@ -76,6 +83,24 @@ export const helpDispatch = inngest.createFunction(
}));
await sb.from('activity_log').insert(rows);

for (const target of targets) {
const mentor = pool?.find((p) => p.id === target.userId);

if (!mentor?.email) continue;

try {
await sendHelpDispatchEmail({
to: mentor.email,
mentorHandle: mentor.github_handle ?? 'mentor',
menteeHandle: mentee?.github_handle ?? 'contributor',
prUrl: helpRequest?.pr_url ?? '',
helpReason: helpRequest?.reason ?? null,
});
} catch (error) {
console.error('failed to send help dispatch email', error);
}
}

return { notified: targets.length };
});

Expand Down
15 changes: 15 additions & 0 deletions src/lib/email.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest';
import { sendHelpDispatchEmail } from './email';

describe('sendHelpDispatchEmail', () => {
it('skips sending when resend is not configured', async () => {
const result = await sendHelpDispatchEmail({
to: 'test@example.com',
mentorHandle: 'mentor',
menteeHandle: 'mentee',
prUrl: 'https://github.com/test/pr/1',
});

expect(result).toEqual({ skipped: true });
});
});
59 changes: 59 additions & 0 deletions src/lib/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Resend } from 'resend';

type SendHelpDispatchEmailArgs = {
to: string;
mentorHandle: string;
menteeHandle: string;
prUrl: string;
helpReason?: string | null;
};

const resendApiKey = process.env.RESEND_API_KEY;

const resend = resendApiKey ? new Resend(resendApiKey) : null;

export async function sendHelpDispatchEmail({
to,
mentorHandle,
menteeHandle,
prUrl,
helpReason,
}: SendHelpDispatchEmailArgs) {
if (!resend) {
console.warn('RESEND_API_KEY missing, skipping email send');
return { skipped: true };
}

return resend.emails.send({
from: process.env.EMAIL_FROM || 'onboarding@resend.dev',
to,
subject: '[MergeShip] Someone needs your help on a PR',
html: `
<h2>Someone needs your help on a PR</h2>

<p>Hello ${mentorHandle},</p>

<p>${menteeHandle} has requested help on a pull request.</p>

<p>
<strong>Pull Request:</strong><br />
<a href="${prUrl}">${prUrl}</a>
</p>

${
helpReason
? `
<p>
<strong>Help Request:</strong><br />
${helpReason}
</p>
`
: ''
}

<p>
Visit the Help Inbox to respond and assist the contributor.
</p>
`,
});
}
Loading