Skip to content

Groups: import a Meetup group (events, past events, members) - #2203

Open
bor0 wants to merge 10 commits into
productionfrom
groups/meetup-import
Open

bor0 wants to merge 10 commits into
productionfrom
groups/meetup-import

Conversation

@bor0

@bor0 bor0 commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Part of #1788. Adds Network Admin > Groups > Import from Meetup.

Flow

  1. Pick a Meetup group and a group site, click Preview. A cron run fetches the group and shows what would happen. Nothing is written.
  2. Click Import. Cron writes it in batches: group details, organizers, members (200/run), events (10/run). Reload to follow progress; Cancel anytime.
  3. Upcoming events land as drafts. Optional: past events (published as history) and matching people to w.org accounts by email (organizer roles, members, hosts, RSVPs). Nobody gets a new account, and nothing sends email.

Reruns are safe: published events and anything the organizer changed are left alone. Uses Central's existing Meetup connection.

Full flow, step by step

Before the first import

  1. Deploy. The Groups network uses Central's Meetup connection (meetups@wordcamp.org); nothing to configure. Only reconnect from the Groups Meetup settings page while logged into meetup.com as meetups@wordcamp.org, since both networks share the token.
  2. Create the group site with Add Group Site. The lead organizer is its administrator; the import never grants administrator.

Preview
3. Open Groups > Import from Meetup (network admins only), enter the Meetup group URL, pick the site, optionally tick past events and matching people, click Preview.
4. The job is queued (fetching); another import in progress is refused.
5. Cron run 1 fetches the group, events and (if matching) organizers and members, and works out the plan: group details set / kept / missing, people who'd join or be promoted, each event created / published / refreshed / skipped. The job becomes ready. Nothing is written. If Meetup fails, the job becomes failed with the reason.
6. Reload to see the preview, with Import and Cancel.

Import
7. Click Import. The job becomes importing; each cron run takes the lock, writes one batch, saves progress, and queues the next:

  • Group details, only where the site has none: location, About draft (only while untouched), site icon, source group and founding date.
  • Organizers (matching): organizers/co-organizers become Organizers, assistant/event organizers Event Organizers. Only promotes, never administrator, skips the meetups@wordcamp.org account. Event emails are turned off before anyone is added.
  • Members (matching), 200 per run: matches join as Members with emails off; members who later leave aren't re-added.
  • Events, 10 per run: published or trashed ones are skipped; new ones are created as drafts, existing drafts refreshed, with date/time, venue, online flag, featured photo (empty slot only) and RSVP counts. With matching: hosts are credited (if none yet) and RSVPs become real RSVPs, "attended" checked in. Past events are published last, already marked as notified.
  1. Reload to follow progress ("Importing events: 120 of 412"). When done, the job keeps only its results and the form shows again.

Emails: none. Upcoming events stay drafts, past events are pre-marked as notified, RSVPs are saved by the admin onto drafts, and everyone added has emails off. Publishing a draft later sends the usual new-event email.

When something goes wrong

  • A run times out or crashes: the lock is released, and the next run (or a reload) redoes that batch without duplicating anything.
  • Two runs at once: the second leaves the job alone and makes sure a run is queued.
  • A lost cron event: loading the import screen queues it again.
  • Cancel: deletes the job; what's written stays, and a later import picks up from there.

After
9. The organizer adds meeting links, checks the About page, and publishes.
10. Rerun anytime before cutover: new events are created, drafts refreshed, new matches added; anything the organizer changed stays.

What it brings

  • Group (only where the site has none yet): location, description into the untouched About draft, group photo as site icon, founding date.
  • Upcoming events as drafts: title, description, date/time, venue, online flag, featured photo.
  • Past events (optional): published, with Meetup's RSVP/attendance counts.
  • People (optional, only those matching a w.org account by email): organizer roles, members (emails off), event hosts, RSVPs ("attended" checked in).

What it doesn't bring

  • People without a matching w.org account or shared email (only counts survive).
  • Cancelled and Meetup-draft events, online meeting links, attendance limits.
  • Comments, discussions, photo albums, RSVP answers, ratings/reviews, member profiles/join dates, sponsors.

Never overwrites anything the site already has or the organizer changed.

Testing

  1. Local: preview and import a 25-event fixture into a group site. Progress goes 0 → 10 → 25, events are drafts, no duplicates on rerun, no mail sent.
  2. As a non network admin, the page returns "not allowed".
  3. phpunit --group groups: 935/935.

Not tested against the live Meetup API yet: next step is a dry run on a sandbox.

🤖 Generated with Claude Code

bor0 and others added 2 commits October 16, 2026 16:36
First step of the #1788 pilot. A network admin picks a Meetup group and a
group site, previews, then imports upcoming events and venues as draft
GatherPress events for the organizer to review.

Reuses the existing Meetup_Client and the Network Settings > Meetup OAuth
connection, so there are no new credentials. Events are written through the
same helpers as the frontend's draft save, so they behave like hand-made
drafts. Events and venues are keyed on their Meetup IDs: reruns refresh
drafts and leave published or trashed events to the organizer. Nothing is
published and no emails are sent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Covers more of #1788's acceptance criteria in the same tool.

Past events (opt-in) are published as the group's history, with Meetup's
RSVP and attendance counts kept as meta. Their first-publish notification
is marked as handled, so publishing them emails nobody.

Members (opt-in) are only matched to existing WordPress.org accounts by
email. A match joins as a Member with this group's event emails off, since
GatherPress opts users in by default. Nobody else gets an account. Meetup
member IDs are remembered per group, so someone who leaves isn't re-added.

Imports take a network-wide lock: events are looked up and then inserted,
so two overlapping imports could otherwise create duplicates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bor0 bor0 changed the title Groups: import a Meetup group's upcoming events as drafts Groups: import a Meetup group (events, past events, members) Oct 16, 2026
bor0 and others added 3 commits October 16, 2026 17:08
…ports

Featured images: each event's Meetup photo becomes its featured image. Only
HTTPS URLs on Meetup's image host are downloaded, a shared cover photo is
downloaded once per group site, and a draft refresh never replaces an image
the organizer picked.

Shared connection: the Groups network can't hold its own Meetup token,
because the OAuth redirect URI points at WordCamp Central. On Groups, the
token option now reads from and writes to Central's, so the importer uses
the Meetup Pro account Central is already connected as, and both networks
refresh one token instead of invalidating each other's copies.

Resumable imports: a run that dies partway (e.g. a timeout) can be retried
safely. Meetup IDs are saved with the event and venue posts themselves; past
events are published only once complete, via a pending marker the next run
finishes; members are opted out of emails before they join; and the import
lock is released at shutdown, and only by the request that holds it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Group details fill whatever the site doesn't have yet: the location (the
same site meta the group settings tab writes), the About draft seeded by Add
Group Site while it's untouched, and the site icon from Meetup's group
photo. The source group and its founding date are recorded too.

With "match people by email" ticked, Meetup organizers and co-organizers get
the Organizer role and assistant/event organizers the Event Organizer role.
Imports only ever promote and never grant administrator, and the Meetup
account the connection belongs to is skipped, since Pro networks list it as
an organizer of every group. Event hosts are added to the group if needed
and credited on their events. RSVPs become real GatherPress RSVPs, with
"attended" checked in, without joining anyone to the group or sending
confirmations.

Meetup's waiting list is imported as attending and left to GatherPress to
place: saving waiting_list directly on an event with room trips a bug in
GatherPress 0.35's waiting-list promotion.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every import now runs as a job through cron, whatever the group's size, so
no import is bounded by a single request. Preview queues a job; one cron run
fetches the group from Meetup and works out the preview, which waits for
Import. Later runs then write it a batch at a time: group details,
organizers, members 200 per run, and events 10 per run, saving the cursor
and results after each batch and queueing the next.

One job exists at a time, network-wide. Each run takes the import lock, so
two runs never work the same job at once; a run that dies is redone by the
next, which the importer's idempotency already makes safe. The screen shows
the job's preview, progress, or results, re-queues a stalled job when it
loads (as the network export does), and has a Cancel button. A batch that
was running when the job was cancelled can't write it back.

The form now posts to admin-post.php and redirects, like the network export.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bor0
bor0 marked this pull request as ready for review October 16, 2026 15:50
@bor0
bor0 requested a balanced review from Copilot October 16, 2026 15:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved defects affect imported content, organizer edits, departed members, past-event classification, and cancellation safety.

5 open findings
What changed in this PR

Adds a network-admin Meetup migration tool for Groups, supporting the migration scope in #1788.

Changes:

  • Adds preview and batched imports for group details, events, and optional people matching.
  • Shares Central’s Meetup connection with Groups.
  • Adds import and shared-connection tests.
File Description
public_html/​wp-content/​mu-plugins/​groups/​tests/​test-meetup-shared-connection.php Tests shared token handling.
public_html/​wp-content/​mu-plugins/​groups/​tests/​test-meetup-import.php Tests import behavior and job lifecycle.
public_html/​wp-content/​mu-plugins/​groups/​tests/​bootstrap.php Loads import modules for tests.
public_html/​wp-content/​mu-plugins/​groups/​meetup-shared-connection.php Routes Groups token operations to Central.
public_html/​wp-content/​mu-plugins/​groups/​meetup-import.php Implements preview, matching, and batched imports.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread public_html/wp-content/mu-plugins/groups/meetup-import.php Outdated
Comment thread public_html/wp-content/mu-plugins/groups/meetup-import.php
Comment thread public_html/wp-content/mu-plugins/groups/meetup-import.php
Comment thread public_html/wp-content/mu-plugins/groups/meetup-import.php
Comment thread public_html/wp-content/mu-plugins/groups/meetup-import.php
- Past events: `Meetup_Client::get_group_events()` rewrites past statuses
  to "upcoming", so past events came in as drafts. Ask for Meetup's raw
  status under an alias and use that.
- Descriptions: store them as paragraph blocks. The event editor only
  loads block content, so plain HTML opened as an empty description.
- Edited drafts: a draft the organizer has saved since the import now
  belongs to them, and reruns leave it alone. Refreshing an untouched
  draft keeps a meeting link the organizer added.
- Departures: one record of everyone an import put in the group, checked
  by the organizer and host steps too, so nobody who left is re-added.
- Job saves: a single conditional UPDATE keyed on the job ID, so a batch
  can't restore a cancelled job or overwrite a newer one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@wppoland

Copy link
Copy Markdown
Contributor

Took a look into the implementation and spotted two edge cases, addressed with unit tests in #2205:

  1. Localized Meetup URLs:
    Meetup serves localized URLs in non-English regions (such as https://www.meetup.com/pl-PL/group-name/ or https://www.meetup.com/de-DE/group-name/events/...). parse_group_urlname() previously treated the locale prefix (pl-PL) as the urlname. In Groups: support localized Meetup URLs and rich Gutenberg block descriptions #2205, locale prefixes are detected and the actual group slug is extracted.

  2. Preserving Rich Blocks in Event/About Descriptions:
    Defaults\extract_description_blocks() drops freeform HTML that lacks block comments (blockName === null). Meetup event descriptions often include lists (<ul>/<ol> for agendas), headings (<h2>-<h6>), quotes, or code. If only <p> is wrapped in block comments, all other elements remain raw HTML and get stripped when loading the draft in the editor. In Groups: support localized Meetup URLs and rich Gutenberg block descriptions #2205, to_paragraph_blocks() wraps headings, lists, quotes, and code into their corresponding core blocks, ensuring rich formatting survives the editor round trip without content loss.

@gedex gedex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested locally with a 25-event fixture. Preview, import and rerun work, no mail goes out, and a non-network admin is refused.

Please fix the two inline blockers before merge. A rerun overwrites the organizer's About page and promotes demoted organizers back.

Smaller things:

  • Past events get the import time as post_date, so feeds show them as new.
  • No test fails if you remove the nonce checks.
  • No test covers ensure_member() keeping an existing admin's or editor's role when they host an event.

Couldn't test the real Meetup API, so skipping meetups@wordcamp.org, cancelled events and pagination are still unchecked.

Comment thread public_html/wp-content/mu-plugins/groups/meetup-import.php Outdated
Comment thread public_html/wp-content/mu-plugins/groups/meetup-import.php
Comment thread public_html/wp-content/mu-plugins/groups/meetup-import.php
Comment thread public_html/wp-content/mu-plugins/groups/meetup-import.php
Comment thread public_html/wp-content/mu-plugins/groups/tests/test-meetup-import.php Outdated
- About page: an untouched seed is one with no revisions. Saving an
  unpublished draft resets its post_date, so comparing dates let a rerun
  overwrite the organizer's text.
- Organizers: each is handled by an import once. A group that demotes one
  doesn't see them promoted back by the next import.
- Authors: cron runs as user 0, so imported events had no author and no
  Event Organizer could edit them. They now belong to their first matched
  host, else the site's lead organizer.
- Retries: a job that fails 3 times at the same point is given up on, rather
  than re-running (and re-fetching from Meetup) forever. A preview waiting
  for confirmation is never counted against.
- Past events are dated when they happened, not when they were imported.
- Tests: the handler tests now pass a valid nonce, so they fail if the
  capability check is removed; separate tests cover the nonce checks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bor0

bor0 commented Oct 20, 2026 •

Copy link
Copy Markdown
Member Author

Thanks @gedex. Fixed the review-summary points in cdfe116 too: past events are dated when they happened, nonce checks have their own tests, and test_admin_host_keeps_role covers ensure_member() keeping an admin's role. Real-API checks (skipping meetups@, cancelled events, pagination) are still for the sandbox dry run.

Thanks @wppoland for #2205, reviewed there.

@gedex

gedex commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Thanks @bor0. All good from my side.

@gedex
gedex self-requested a review October 21, 2026 00:02

@gedex gedex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes verified in cdfe116.

wppoland and others added 3 commits October 22, 2026 12:35
…ptions (#2205)

* Groups: support localized Meetup URLs and rich Gutenberg block descriptions

* Groups: Adjust list and quote assertions in test_rich_description_blocks_load_in_editor

* Groups: Match editor block markup and preserve two-letter group names

* Groups: Support nested lists and separator blocks in Meetup descriptions

- Convert lists using DOMDocument and DOMXPath to recursively generate valid Gutenberg block markup for nested lists of arbitrary depth.
- Convert <hr> tags into core/separator blocks with alpha channel opacity class.
- Avoid wrapping block-level elements (table, div, section, etc.) in paragraph tags.
- Update unit tests with nested list, separator, and container preservation assertions.

* Groups: Format separator tag with self-closing space
GatherPress 0.36 removes `Event::$rsvp`, so the importer got null back and
saved no RSVPs. Build `Rsvp` directly, as `rest.php` now does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants