Making WordPress.org

Changeset 14847


Ignore:
Timestamp:
04/28/2026 06:41:41 AM (6 months ago)
Author:
dd32
Message:

Slack: Subgroups: Allow for #foo-team to manage #foo-* subgroups, as #foo is often the public channel.

Adds a channel rotate feature to archive-and-start-fresh on channels as often needed.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/api.wordpress.org/public_html/dotorg/slack/subgroup.php

    r14836 r14847  
    341341                        return;
    342342
     343                case 'rotate_subgroup':
     344                        ack_and_finish();
     345
     346                        $rotate_id   = $action['value'] ?? '';
     347                        $parent_id   = $meta['parent'] ?? '';
     348                        $parent_name = $meta['parent_name'] ?? '';
     349                        $root_view   = $meta['root_view'] ?? '';
     350
     351                        if ( $rotate_id && $parent_id && $parent_name ) {
     352                                rotate_subgroup( $rotate_id, $parent_id, $parent_name, $user_id, $view_id, $root_view );
     353                        }
     354                        return;
     355
    343356                case 'open_create_modal':
    344357                        $parent_id   = $meta['parent'] ?? '';
    … …  
    386399
    387400        $new_name = trim( $values['name']['value']['value'] ?? '' );
    388         $prefix   = $parent_name . '-';
     401        $prefix   = derive_prefix_name( $parent_name ) . '-';
    389402
    390403        if ( strpos( $new_name, $prefix ) !== 0 || strlen( $new_name ) <= strlen( $prefix ) ) {
    … …  
    459472        $selected_users = $values['users']['value']['selected_users'] ?? [];
    460473
    461         $prefix = $parent_name . '-';
     474        $prefix = derive_prefix_name( $parent_name ) . '-';
    462475        if ( strpos( $name, $prefix ) !== 0 || strlen( $name ) <= strlen( $prefix ) ) {
    463476                header( 'Content-Type: application/json' );
    … …  
    559572        $new_archived = trim( $values['new_name_for_archived']['value']['value'] ?? '' );
    560573
    561         $prefix = $parent_name . '-';
     574        $prefix = derive_prefix_name( $parent_name ) . '-';
    562575
    563576        if ( $action === 'rename' ) {
    … …  
    623636                ] );
    624637
    625                 finalize_create( $new_id, $original_name, $creator, $parent_id, $user_id, $purpose, $topic, $strategy, $selected_users, $root_view, false );
     638                finalize_create( $new_id, $original_name, $creator, $parent_id, $parent_name, $user_id, $purpose, $topic, $strategy, $selected_users, $root_view, false );
    626639                return;
    627640        }
    … …  
    704717        if ( $root_view && $parent_name ) {
    705718                refresh_home_view( $root_view, [ 'id' => $parent_id, 'name' => $parent_name ], $user_id );
     719        }
     720}
     721
     722/*
     723 * Rotate a subgroup: rename the existing channel with a year suffix, archive
     724 * it, then create a fresh channel with the same name (purpose/topic carried
     725 * over) seeded with just the triggering user. Useful when a long-running
     726 * channel accumulates context that's no longer relevant — e.g. annual events.
     727 */
     728function rotate_subgroup( $channel_id, $parent_id, $parent_name, $user_id, $manage_view_id, $root_view ) {
     729        $info = api_call( 'conversations.info', [ 'channel' => $channel_id ] );
     730        $orig = $info['channel'] ?? null;
     731        if ( ! $orig ) {
     732                return;
     733        }
     734
     735        $orig_name = $orig['name'];
     736        $purpose   = $orig['purpose']['value'] ?? '';
     737        $topic     = $orig['topic']['value'] ?? '';
     738        $year      = pick_archive_year( $orig );
     739
     740        // Try `name-YEAR`, then `name-YEAR-2`, etc. on collision. Slack caps
     741        // channel names at 80 chars; trim the base if needed.
     742        $rename = null;
     743        for ( $i = 1; $i <= 9; $i++ ) {
     744                $candidate = $orig_name . '-' . $year . ( $i > 1 ? "-{$i}" : '' );
     745                if ( strlen( $candidate ) > 80 ) {
     746                        $tail      = '-' . $year . ( $i > 1 ? "-{$i}" : '' );
     747                        $candidate = substr( $orig_name, 0, 80 - strlen( $tail ) ) . $tail;
     748                }
     749                $rename = api_call( 'conversations.rename', [
     750                        'channel' => $channel_id,
     751                        'name'    => $candidate,
     752                ], SUBGROUP_USER_TOKEN );
     753                if ( ! empty( $rename['ok'] ) ) {
     754                        $archived_name = $candidate;
     755                        break;
     756                }
     757                if ( ( $rename['error'] ?? '' ) !== 'name_taken' ) {
     758                        break;
     759                }
     760        }
     761
     762        if ( empty( $rename['ok'] ) ) {
     763                api_call( 'chat.postEphemeral', [
     764                        'channel' => $parent_id,
     765                        'user'    => $user_id,
     766                        'text'    => sprintf( 'Could not rotate `%s`: %s', $orig_name, $rename['error'] ?? 'unknown' ),
     767                ] );
     768                return;
     769        }
     770
     771        $arch = api_call( 'conversations.archive', [
     772                'channel' => $channel_id,
     773        ], SUBGROUP_USER_TOKEN );
     774        if ( empty( $arch['ok'] ) ) {
     775                // Roll back the rename so the channel is left as we found it.
     776                api_call( 'conversations.rename', [
     777                        'channel' => $channel_id,
     778                        'name'    => $orig_name,
     779                ], SUBGROUP_USER_TOKEN );
     780                api_call( 'chat.postEphemeral', [
     781                        'channel' => $parent_id,
     782                        'user'    => $user_id,
     783                        'text'    => sprintf( 'Could not archive `%s` during rotate: %s', $archived_name, $arch['error'] ?? 'unknown' ),
     784                ] );
     785                return;
     786        }
     787
     788        $new = api_call( 'conversations.create', [
     789                'name'       => $orig_name,
     790                'is_private' => true,
     791        ], SUBGROUP_USER_TOKEN );
     792        if ( empty( $new['ok'] ) ) {
     793                api_call( 'chat.postEphemeral', [
     794                        'channel' => $parent_id,
     795                        'user'    => $user_id,
     796                        'text'    => sprintf( 'Rotated `%s` → `%s` and archived, but creating the replacement failed: %s', $orig_name, $archived_name, $new['error'] ?? 'unknown' ),
     797                ] );
     798                invalidate_channel_lists();
     799                return;
     800        }
     801
     802        $new_id  = $new['channel']['id'];
     803        $creator = $new['channel']['creator'] ?? '';
     804
     805        if ( $purpose ) {
     806                api_call( 'conversations.setPurpose', [ 'channel' => $new_id, 'purpose' => $purpose ], SUBGROUP_USER_TOKEN );
     807        }
     808        // Roll the topic year forward — "WCUS 2024 design" becomes "WCUS 2025
     809        // design" on the rotated channel. First match only, digit-bounded so we
     810        // don't mangle dates like 20240501. No-op if the topic has no year.
     811        $new_topic = $topic
     812                ? preg_replace( '/(?<!\d)' . preg_quote( $year, '/' ) . '(?!\d)/', (string) ( (int) $year + 1 ), $topic, 1 )
     813                : '';
     814        if ( $new_topic ) {
     815                api_call( 'conversations.setTopic', [ 'channel' => $new_id, 'topic' => $new_topic ], SUBGROUP_USER_TOKEN );
     816        }
     817
     818        $invitees = array_values( array_diff( array_unique( [ $user_id, SUBGROUP_BOT_USER_ID ] ), [ $creator ] ) );
     819        if ( $invitees ) {
     820                api_call( 'conversations.invite', [
     821                        'channel' => $new_id,
     822                        'users'   => implode( ',', $invitees ),
     823                ], SUBGROUP_USER_TOKEN );
     824        }
     825
     826        invalidate_channel_lists();
     827        invalidate_members( $new_id );
     828        invalidate_members( $channel_id );
     829
     830        // Don't link the archived channel — non-members see a strikethrough
     831        // dead link. New channel is fine to link.
     832        api_call( 'chat.postMessage', [
     833                'channel' => $parent_id,
     834                'text'    => sprintf( '<@%s> rotated `%s`: archived as `%s`, fresh <#%s> created.', $user_id, $orig_name, $archived_name, $new_id ),
     835        ] );
     836
     837        $parent = [ 'id' => $parent_id, 'name' => $parent_name ];
     838        api_call( 'views.update', [
     839                'view_id' => $manage_view_id,
     840                'view'    => json_encode( build_manage_view( $parent, $user_id, $root_view ) ),
     841        ] );
     842        if ( $root_view ) {
     843                refresh_home_view( $root_view, $parent, $user_id );
    706844        }
    707845}
    … …  
    830968}
    831969
     970/*
     971 * Channels ending in -team are private workspaces for a (often public) base
     972 * channel — `#wcus-team` for `#wcus`. Subgroups should follow the base
     973 * channel's name (`wcus-foo`), not the team's (`wcus-team-foo`), so users in
     974 * the public base channel discover them naturally. Notifications still go to
     975 * the channel where /subgroup was invoked.
     976 */
     977function derive_prefix_name( $channel_name ) {
     978        if ( substr( $channel_name, -5 ) === '-team' ) {
     979                return substr( $channel_name, 0, -5 );
     980        }
     981        return $channel_name;
     982}
     983
    832984function get_subgroups( $parent_name, $include_archived = false ) {
    833         $prefix = $parent_name . '-';
     985        $prefix = derive_prefix_name( $parent_name ) . '-';
    834986        $out    = [];
    835987        foreach ( list_all_private_channels( $include_archived ) as $g ) {
    … …  
    9821134                                        [
    9831135                                                'type'      => 'button',
     1136                                                'text'      => [ 'type' => 'plain_text', 'text' => 'Rotate' ],
     1137                                                'action_id' => 'rotate_subgroup',
     1138                                                'value'     => $g['id'],
     1139                                                'confirm'   => [
     1140                                                        'title'   => [ 'type' => 'plain_text', 'text' => 'Rotate channel?' ],
     1141                                                        'text'    => [ 'type' => 'mrkdwn', 'text' => "Archive <#{$g['id']}> with a year suffix and create a fresh `{$g['name']}` with only you as a member." ],
     1142                                                        'confirm' => [ 'type' => 'plain_text', 'text' => 'Rotate' ],
     1143                                                        'deny'    => [ 'type' => 'plain_text', 'text' => 'Cancel' ],
     1144                                                ],
     1145                                        ],
     1146                                        [
     1147                                                'type'      => 'button',
    9841148                                                'text'      => [ 'type' => 'plain_text', 'text' => 'Archive' ],
    9851149                                                'style'     => 'danger',
    … …  
    10601224
    10611225function build_rename_view( $parent, $channel_id, $current_name, $user_id, $manage_view_id, $root_view_id ) {
    1062         $prefix = $parent['name'] . '-';
     1226        $prefix = derive_prefix_name( $parent['name'] ) . '-';
    10631227
    10641228        return [
    … …  
    11021266
    11031267function build_create_view( $parent, $user_id, $root_view_id ) {
    1104         $prefix = $parent['name'] . '-';
     1268        $prefix = derive_prefix_name( $parent['name'] ) . '-';
    11051269
    11061270        return [
    … …  
    11971361}
    11981362
     1363function pick_archive_year( $channel ) {
     1364        // Prefer a year named in the topic/purpose ("WordCamp US 2024") so the
     1365        // archived name reflects what the channel was *for*, not when it was
     1366        // archived. Fall back to creation year, then current year.
     1367        $text = ( $channel['topic']['value'] ?? '' ) . ' ' . ( $channel['purpose']['value'] ?? '' );
     1368        if ( preg_match( '/(?<!\d)(20\d{2})(?!\d)/', $text, $m ) ) {
     1369                return $m[1];
     1370        }
     1371        if ( ! empty( $channel['created'] ) ) {
     1372                return date( 'Y', (int) $channel['created'] );
     1373        }
     1374        return date( 'Y' );
     1375}
     1376
    11991377function build_conflict_view( $archived, $ctx ) {
    1200         $prefix    = $ctx['parent_name'] . '-';
    1201         $suffix    = '-archived-' . date( 'Ymd' );
     1378        $prefix    = derive_prefix_name( $ctx['parent_name'] ) . '-';
     1379        $suffix    = '-' . pick_archive_year( $archived );
    12021380        $suggested = $ctx['original_name'] . $suffix;
    12031381        if ( strlen( $suggested ) > 80 ) {
Note: See TracChangeset for help on using the changeset viewer.