-
Notifications
You must be signed in to change notification settings - Fork 730
feat: integrate mailing list service (CM-1318) #4346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
themarolt
wants to merge
16
commits into
main
Choose a base branch
from
feat/mailing-list-integration-CM-1318
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
449f02b
chore: scaffold mailing_list_integration service (CM-1318)
themarolt 1c9c16c
feat: add mailinglist schema tables (CM-1318)
themarolt 1194e9c
feat: add mailing list database layer (CM-1318)
themarolt 2cbb25f
feat: port noteren email parser into service (CM-1318)
themarolt 93e5793
feat: add public-inbox mirror service (CM-1318)
themarolt 3da5d5c
feat: add kafka queue producer (CM-1318)
themarolt f250f68
feat: add mailing list poll worker and server (CM-1318)
themarolt 9fd947f
build: add mailing_list_integration dockerfile and wiring (CM-1318)
themarolt 7e086b0
chore: add mailing list dev seed and docs (CM-1318)
themarolt bfc56b6
fix: ruff CI-breaking lint errors and wrong activity type (CM-1318)
themarolt 212875a
chore: remove dead settings from mailing list integration (CM-1318)
themarolt f6b291c
fix: regenerate manifest after initial lore list clone (CM-1318)
themarolt 04136f0
fix: correct base image and container paths for mailing-list-integrat…
themarolt 1b9e96a
feat: add mailing list onboarding endpoint (CM-1318)
themarolt 037a969
fix: harden mailing list bug fixes and add activity type wiring (CM-1…
themarolt cf4735c
Merge branch 'main' into feat/mailing-list-integration-CM-1318
themarolt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
backend/src/api/integration/helpers/mailingListAuthenticate.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { z } from 'zod' | ||
|
|
||
| import Permissions from '../../../security/permissions' | ||
| import IntegrationService from '../../../services/integrationService' | ||
| import PermissionChecker from '../../../services/user/permissionChecker' | ||
| import { validateOrThrow } from '../../../utils/validation' | ||
|
|
||
| const bodySchema = z.object({ | ||
| lists: z | ||
| .array( | ||
| z.object({ | ||
| name: z.string().trim().min(1), | ||
|
|
||
| sourceUrl: z.string().trim().min(1), | ||
| }), | ||
| ) | ||
| .default([]), | ||
| }) | ||
|
|
||
| export default async (req, res) => { | ||
| new PermissionChecker(req).validateHas(Permissions.values.tenantEdit) | ||
| const integrationData = validateOrThrow(bodySchema, req.body) | ||
|
|
||
| const payload = await new IntegrationService(req).mailingListConnectOrUpdate(integrationData) | ||
| await req.responseHandler.success(req, res, payload) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
backend/src/bin/scripts/create-mailing-list-integration.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| /* eslint-disable no-console */ | ||
|
|
||
| /* eslint-disable import/no-extraneous-dependencies */ | ||
| import commandLineArgs from 'command-line-args' | ||
| import commandLineUsage from 'command-line-usage' | ||
| import * as fs from 'fs' | ||
|
|
||
| import { DEFAULT_TENANT_ID, generateUUIDv1 } from '@crowd/common' | ||
|
|
||
| import SegmentRepository from '@/database/repositories/segmentRepository' | ||
| import SequelizeRepository from '@/database/repositories/sequelizeRepository' | ||
| import IntegrationService from '@/services/integrationService' | ||
|
|
||
| const options = [ | ||
| { | ||
| name: 'help', | ||
| alias: 'h', | ||
| type: Boolean, | ||
| description: 'Print this usage guide.', | ||
| }, | ||
| { | ||
| name: 'file', | ||
| alias: 'f', | ||
| type: String, | ||
| description: | ||
| 'Path to a JSON file with the lists array, each entry shaped as name + sourceUrl ' + | ||
| '(e.g. name "linux-serial", sourceUrl https://lore.kernel.org/linux-serial).', | ||
| }, | ||
| { | ||
| name: 'segment', | ||
| alias: 's', | ||
| type: String, | ||
| description: "Segment id. Optional — defaults to the tenant's first subproject.", | ||
| }, | ||
| ] | ||
|
|
||
| const sections = [ | ||
| { | ||
| header: 'Create mailing list integration', | ||
| content: | ||
| 'Calls IntegrationService.mailingListConnectOrUpdate() directly to create/update the ' + | ||
| 'mailinglist integration and onboard its lists for processing, until the frontend connect ' + | ||
| 'UI ships (CM-1318).', | ||
| }, | ||
| { | ||
| header: 'Options', | ||
| optionList: options, | ||
| }, | ||
| ] | ||
|
|
||
| const usage = commandLineUsage(sections) | ||
| // pnpm forwards a literal `--` separator when args are passed via `pnpm run ... -- <args>`; | ||
| // strip it so command-line-args doesn't choke on it. | ||
| const argv = process.argv.slice(2).filter((arg) => arg !== '--') | ||
| const parameters = commandLineArgs(options, { argv }) | ||
|
|
||
| if (parameters.help || !parameters.file) { | ||
| console.log(usage) | ||
| process.exit(parameters.help ? 0 : 1) | ||
| } else { | ||
| setImmediate(async () => { | ||
| let fileContents: string | ||
| try { | ||
| fileContents = fs.readFileSync(parameters.file, 'utf-8') | ||
| } catch (err) { | ||
| console.error(`Could not read file at ${parameters.file}: ${(err as Error).message}`) | ||
| process.exit(1) | ||
| } | ||
|
|
||
| let lists | ||
| try { | ||
| lists = JSON.parse(fileContents) | ||
| } catch (err) { | ||
| console.error(`File at ${parameters.file} is not valid JSON: ${(err as Error).message}`) | ||
| process.exit(1) | ||
| } | ||
|
|
||
| const repoOptions = await SequelizeRepository.getDefaultIRepositoryOptions() | ||
| repoOptions.currentTenant = { id: DEFAULT_TENANT_ID } | ||
| ;(repoOptions as unknown as { requestId: string }).requestId = generateUUIDv1() | ||
|
|
||
| const adminUser = await repoOptions.database.user.findOne({ where: {} }) | ||
| if (!adminUser) { | ||
| console.error('No user found — run script:onboard-default-tenant first.') | ||
| process.exit(1) | ||
| } | ||
| repoOptions.currentUser = adminUser | ||
|
|
||
| const segmentRepository = new SegmentRepository(repoOptions) | ||
| repoOptions.currentSegments = parameters.segment | ||
| ? await segmentRepository.findInIds([parameters.segment]) | ||
| : (await segmentRepository.querySubprojects({ limit: 1, offset: 0 })).rows | ||
|
|
||
| if (repoOptions.currentSegments.length === 0) { | ||
| console.error('No segment found/resolved — pass --segment explicitly.') | ||
| process.exit(1) | ||
| } | ||
|
|
||
| console.log(`Segment: ${repoOptions.currentSegments[0].id} (${repoOptions.currentSegments[0].name})`) | ||
| console.log('Lists:', JSON.stringify(lists, null, 2)) | ||
|
|
||
| const integration = await new IntegrationService(repoOptions).mailingListConnectOrUpdate( | ||
| { lists }, | ||
| repoOptions, | ||
| ) | ||
|
|
||
| console.log('Integration:', JSON.stringify(integration, null, 2)) | ||
| process.exit(0) | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /* eslint-disable no-console */ | ||
|
|
||
| /* eslint-disable import/no-extraneous-dependencies */ | ||
| import commandLineArgs from 'command-line-args' | ||
| import commandLineUsage from 'command-line-usage' | ||
|
|
||
| import SequelizeRepository from '@/database/repositories/sequelizeRepository' | ||
| import AuthService from '@/services/auth/authService' | ||
|
|
||
| const options = [ | ||
| { | ||
| name: 'help', | ||
| alias: 'h', | ||
| type: Boolean, | ||
| description: 'Print this usage guide.', | ||
| }, | ||
| { | ||
| name: 'email', | ||
| alias: 'e', | ||
| type: String, | ||
| defaultValue: 'local-dev@example.com', | ||
| description: 'Email for the dev user driving the onboarding (default: local-dev@example.com)', | ||
| }, | ||
| ] | ||
|
|
||
| const sections = [ | ||
| { | ||
| header: 'Onboard default tenant', | ||
| content: | ||
| 'Runs the same onboarding logic as a real Auth0 signup (AuthService.signinFromSSO -> ' + | ||
| 'handleOnboard -> TenantService.createOrJoinDefault), without needing a real Auth0 token. ' + | ||
| 'Creates the default tenant, default segment, settings, and an admin user/tenantUser row. ' + | ||
| 'Safe to re-run — createOrJoinDefault joins the existing tenant instead of duplicating it.', | ||
| }, | ||
| { | ||
| header: 'Options', | ||
| optionList: options, | ||
| }, | ||
| ] | ||
|
|
||
| const usage = commandLineUsage(sections) | ||
| const parameters = commandLineArgs(options) | ||
|
|
||
| if (parameters.help) { | ||
| console.log(usage) | ||
| } else { | ||
| setImmediate(async () => { | ||
| const repoOptions = await SequelizeRepository.getDefaultIRepositoryOptions() | ||
|
|
||
| await AuthService.signinFromSSO( | ||
| 'auth0', | ||
| `dev|${parameters.email}`, | ||
| parameters.email, | ||
| true, | ||
| 'Local', | ||
| 'Dev', | ||
| 'Local Dev', | ||
| null, | ||
| null, | ||
| null, | ||
| repoOptions, | ||
| ) | ||
|
|
||
| const tenant = await repoOptions.database.tenant.findOne({ where: {} }) | ||
| const segment = await repoOptions.database.segment.findOne({ | ||
| where: { tenantId: tenant.id }, | ||
| }) | ||
|
|
||
| console.log(`Tenant: ${tenant.id} (${tenant.name})`) | ||
| console.log(`Segment: ${segment.id} (${segment.name})`) | ||
|
|
||
| process.exit(0) | ||
| }) | ||
| } |
50 changes: 50 additions & 0 deletions
50
backend/src/database/migrations/V1784048135__mailinglist-schema.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| -- Create the mailinglist schema | ||
| CREATE SCHEMA IF NOT EXISTS mailinglist; | ||
|
|
||
| -- Mailing lists (lore/public-inbox lists onboarded into CDP) | ||
| CREATE TABLE mailinglist.lists ( | ||
| id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), | ||
| "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
| "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
| "deletedAt" TIMESTAMP WITH TIME ZONE, | ||
|
|
||
| name TEXT NOT NULL, | ||
| "sourceUrl" TEXT NOT NULL, | ||
|
|
||
| "segmentId" UUID NOT NULL REFERENCES segments (id), | ||
| "integrationId" UUID NOT NULL REFERENCES public."integrations" (id), | ||
|
|
||
| UNIQUE ("sourceUrl") | ||
| ); | ||
|
|
||
| -- Per-list processing state (public-inbox shard head tracking) | ||
| CREATE TABLE mailinglist."listProcessing" ( | ||
| "listId" UUID PRIMARY KEY NOT NULL REFERENCES mailinglist.lists (id) ON DELETE CASCADE, | ||
|
|
||
| "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
| "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
|
|
||
| state VARCHAR(50) NOT NULL, | ||
| priority INTEGER NOT NULL DEFAULT 2, -- 0=urgent, 1=high, 2=normal | ||
|
|
||
| "lockedAt" TIMESTAMP WITH TIME ZONE, | ||
| "lastProcessedAt" TIMESTAMP WITH TIME ZONE, | ||
| "lastProcessedHeads" JSONB NOT NULL DEFAULT '{}' | ||
| ); | ||
|
|
||
| -- Indexes for optimal query performance | ||
|
|
||
| CREATE INDEX "ix_mailinglist_lists_segmentId" ON mailinglist.lists ("segmentId"); | ||
| CREATE INDEX "ix_mailinglist_lists_integrationId" ON mailinglist.lists ("integrationId"); | ||
|
|
||
| CREATE INDEX "ix_mailinglist_listProcessing_state" ON mailinglist."listProcessing" (state); | ||
| CREATE INDEX "ix_mailinglist_listProcessing_state_priority" ON mailinglist."listProcessing" (state, priority); | ||
|
|
||
| -- Comments for documentation | ||
| COMMENT ON SCHEMA mailinglist IS 'Schema for mailing list integration system that manages mailing list processing state'; | ||
| COMMENT ON TABLE mailinglist.lists IS 'Stores onboarded mailing lists (public-inbox/lore) and their segment/integration associations'; | ||
| COMMENT ON TABLE mailinglist."listProcessing" IS 'Per-list processing state including public-inbox shard head tracking'; | ||
|
|
||
| COMMENT ON COLUMN mailinglist."listProcessing".priority IS 'Processing priority: 0=urgent, 1=high, 2=normal'; | ||
| COMMENT ON COLUMN mailinglist."listProcessing".state IS 'Current processing state of the list'; | ||
| COMMENT ON COLUMN mailinglist."listProcessing"."lastProcessedHeads" IS 'JSONB map of shard index to last processed commit SHA, e.g. {"0": sha, "1": sha}'; |
3 changes: 3 additions & 0 deletions
3
backend/src/database/migrations/V1784291394__add_mailinglist_activity_type.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| -- Mailing list (Collaboration) | ||
| INSERT INTO "activityTypes" ("activityType", platform, "isCodeContribution", "isCollaboration", description) VALUES | ||
| ('message', 'mailinglist', false, true, 'Sent a message to a mailing list'); | ||
|
Comment on lines
+2
to
+3
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Git integration CI drops format check
Low Severity
The
lint-python-git-integrationjob’s lint step now runs onlyruff check. The previousruff format --checkfor that job was removed when the mailing-list lint job was added, so unformatted Python undergit_integrationcan pass CI.Reviewed by Cursor Bugbot for commit cf4735c. Configure here.