Making WordPress.org

Changeset 11783


Ignore:
Timestamp:
04/22/2022 07:55:13 PM (2 years ago)
Author:
iandunn
Message:

Props: Add #props messages to w.org profiles.

This helps to give more recogntion for non-code contributions, which we don't track as much as we should.

This won't be enabled until dotorg/props.php calls the handler (next week).

See https://github.com/WordPress/five-for-the-future/issues/169

Location:
sites/trunk/common/includes/slack/props
Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/common/includes/slack/props/lib.php

    r11761 r11783  
    33namespace Dotorg\Slack\Props;
    44use Dotorg\Slack\Send;
     5use function Dotorg\Profiles\{ post as profiles_post };
    56
    67function show_error( $user ) {
     
    1011/**
    1112 * Receive `/props` request and send to `#props`.
     13 *
     14 * This is being deprecated in favor of `handle_props_message()`.
    1215 *
    1316 * @param array $data
     
    5760    return sprintf( "Your props to @%s have been sent.\n", $receiver );
    5861}
     62
     63/**
     64 * Adds props in Slack to w.org profiles.
     65 *
     66 * Receives webhook notifications for all new messages in `#props`,
     67 */
     68function handle_props_message( object $request ) : string {
     69    if ( ! is_valid_props( $request->event ) ) {
     70        return 'Invalid props';
     71    }
     72
     73    $giver_user          = map_slack_users_to_wporg( array( $request->event->user ) )[0];
     74    $recipient_slack_ids = get_recipient_slack_ids( $request->event->blocks );
     75
     76    if ( empty( $recipient_slack_ids ) ) {
     77        return 'Nobody was mentioned';
     78    }
     79
     80    $recipient_users = map_slack_users_to_wporg( $recipient_slack_ids );
     81    $recipient_ids   = array_column( $recipient_users, 'id' );
     82
     83    $url = sprintf(
     84        'https://wordpress.slack.com/archives/%s/p%s',
     85        $request->event->channel,
     86        $request->event->ts
     87    );
     88    $url = filter_var( $url, FILTER_SANITIZE_URL );
     89
     90    if ( empty( $recipient_ids ) ) {
     91        return 'No recipients';
     92    }
     93
     94    // This is Slack's unintuitive way of giving messages a unique ID :|
     95    // https://api.slack.com/messaging/retrieving#individual_messages
     96    $message_id = sprintf( '%s-%s', $request->event->channel, $request->event->ts );
     97    $message    = prepare_message( $request->event->text, $recipient_users );
     98
     99    add_activity_to_profile( compact( 'giver_user', 'recipient_ids', 'url', 'message_id', 'message' ) );
     100
     101    // The request was successful from Slack's perspective as long as we received and validated it. Any errors
     102    // that occurred when pushing to Profiles are only significant to us.
     103    return 'Success';
     104}
     105
     106/**
     107 * Determine if this is an event that we should handle.
     108 */
     109function is_valid_props( object $event ) : bool {
     110    $valid_channels = array(
     111        'C0FRG66LR'  #props
     112    );
     113
     114    if ( defined( 'WPORG_SANDBOXED' ) && WPORG_SANDBOXED ) {
     115        $valid_channels[] = 'C03AKLN7P9U'; #iandunn-testing
     116    }
     117
     118    $has_required_params = isset( $event->channel, $event->blocks, $event->type ) && is_array( $event->blocks );
     119    $in_valid_channel    = in_array( $event->channel, $valid_channels, true );
     120
     121    $is_correct_type =
     122        'message' === $event->type &&
     123        empty( $event->subtype ) && // e.g., `message.deleted`, `message.changed`.
     124        empty( $event->hidden ) &&
     125        empty( $event->thread_ts );
     126
     127    if ( $is_correct_type && $has_required_params && $in_valid_channel ) {
     128        return true;
     129    }
     130
     131    return false;
     132}
     133
     134/**
     135 * Parse the mentioned Slack user IDs from a message event.
     136 *
     137 * This assumes that the app is configured to escape usernames.
     138 */
     139function get_recipient_slack_ids( array $blocks ) : array {
     140    $ids = array();
     141
     142    foreach ( $blocks as $block ) {
     143        foreach ( $block->elements as $element ) {
     144            foreach ( $element->elements as $inner_element ) {
     145                if ( 'user' !== $inner_element->type ) {
     146                    continue;
     147                }
     148
     149                $ids[] = $inner_element->user_id;
     150            }
     151        }
     152    }
     153
     154    return $ids;
     155}
     156
     157/**
     158 * Find the w.org users associated with the given slack accounts.
     159 */
     160function map_slack_users_to_wporg( array $slack_ids ) : array {
     161    global $wpdb;
     162
     163    if ( empty( $slack_ids ) ) {
     164        return array();
     165    }
     166
     167    $wporg_users     = array();
     168    $id_placeholders = implode( ', ', array_fill( 0, count( $slack_ids ), '%s' ) );
     169
     170    $query = $wpdb->prepare( "
     171        SELECT
     172            su.slack_id, su.user_id AS wporg_id,
     173            mu.user_login
     174        FROM `slack_users` su
     175            JOIN `minibb_users` mu ON su.user_id = mu.ID
     176        WHERE `slack_id` IN( $id_placeholders )",
     177        $slack_ids
     178    );
     179
     180    $results = $wpdb->get_results( $query, ARRAY_A );
     181
     182    foreach ( $results as $user ) {
     183        $wporg_users[ $user['slack_id'] ] = array(
     184            'id'         => (int) $user['wporg_id'],
     185            'user_login' => $user['user_login'],
     186        );
     187    }
     188
     189    return $wporg_users;
     190}
     191
     192/**
     193 * Replace Slack IDs with w.org usernames, to better fit w.org profiles.
     194 */
     195function prepare_message( string $original, array $user_map ) : string {
     196    $search  = array();
     197    $replace = array();
     198
     199    foreach ( $user_map as $slack_id => $wporg_user ) {
     200        $search[]  = sprintf( '<@%s>', $slack_id );
     201        $replace[] = '@' . $wporg_user['user_login'];
     202    }
     203
     204    return str_replace( $search, $replace, $original );
     205}
     206
     207/**
     208 * Send a request to Profiles to add the activity.
     209 *
     210 * See `handle_slack_activity()` in `buddypress.org/.../wporg-profiles-activity-handler.php` for the needed args.
     211 */
     212function add_activity_to_profile( array $request_args ) : bool {
     213    require_once dirname( __DIR__, 2 ) . '/profiles/profiles.php';
     214
     215    $request_args = array_merge(
     216        $request_args,
     217        array(
     218            'action'   => 'wporg_handle_activity',
     219            'source'   => 'slack',
     220            'activity' => "props_given",
     221        )
     222    );
     223
     224    $response_body = profiles_post( $request_args );
     225
     226    if ( is_numeric( $response_body ) && (int) $response_body > 0 ) {
     227        $success = true;
     228
     229    } else {
     230        $success = false;
     231
     232        trigger_error( 'Adding activity failed with error: ' . $response_body, E_USER_WARNING );
     233    }
     234
     235    return $success;
     236}
  • sites/trunk/common/includes/slack/props/tests/test-lib.php

    r11763 r11783  
    22
    33namespace Dotorg\Slack\Props\Tests;
     4use wpdbStub;
    45use PHPUnit\Framework\TestCase;
    5 use function Dotorg\Slack\Props\{ run };
     6use function Dotorg\Slack\Props\{ run, is_valid_props, get_recipient_slack_ids, map_slack_users_to_wporg, prepare_message };
    67
    78/**
     
    910 * @group props
    1011 */
    11 class Test_Props extends TestCase {
     12class Test_Props_Lib extends TestCase {
    1213    public static function setUpBeforeClass() : void {
    1314        require_once dirname( __DIR__ ) . '/lib.php';
     15    }
     16
     17    protected static function get_valid_request() {
     18        $json = file_get_contents( __DIR__ . '/valid-request.json' );
     19
     20        return json_decode( $json );
    1421    }
    1522
     
    9097        return $cases;
    9198    }
     99
     100    /**
     101     * @covers ::is_valid_props
     102     * @dataProvider data_is_valid_props
     103     * @group unit
     104     */
     105    public function test_is_valid_props( object $event, bool $expected ) : void {
     106        $actual = is_valid_props( $event );
     107
     108        $this->assertSame( $expected, $actual );
     109    }
     110
     111    public function data_is_valid_props() : array {
     112        $valid_request = self::get_valid_request();
     113
     114        $wrong_channel_event = json_decode( json_encode( $valid_request->event ) );
     115        $wrong_channel_event->channel = 'C01234567';
     116
     117        $reaction_event = json_decode( json_encode( $valid_request->event ) );
     118        $reaction_event->type = 'reaction_added';
     119
     120        $deleted_event = json_decode( json_encode( $valid_request->event ) );
     121        $deleted_event->subtype = 'message_deleted';
     122
     123        $hidden_event = json_decode( json_encode( $valid_request->event ) );
     124        $hidden_event->hidden = true;
     125
     126        $thread_event = json_decode( json_encode( $valid_request->event ) );
     127        $thread_event->thread_ts = $valid_request->event->ts;
     128
     129        $cases = array(
     130            'missing critical properties' => array(
     131                'request'  => (object) array( 'foo' => 'bar' ),
     132                'expected' => false,
     133            ),
     134
     135            'wrong channel' => array(
     136                'request'  => $wrong_channel_event,
     137                'expected' => false,
     138            ),
     139
     140            'wrong type' => array(
     141                'request'  => $reaction_event,
     142                'expected' => false,
     143            ),
     144
     145            'wrong subtype' => array(
     146                'request'  => $deleted_event,
     147                'expected' => false,
     148            ),
     149
     150            'hidden' => array(
     151                'request'  => $hidden_event,
     152                'expected' => false,
     153            ),
     154
     155            'reply in thread' => array(
     156                'request'  => $thread_event,
     157                'expected' => false,
     158            ),
     159
     160            'valid event' => array(
     161                'request'  => $valid_request->event,
     162                'expected' => true,
     163            ),
     164        );
     165
     166        return $cases;
     167    }
     168
     169    /**
     170     * @covers ::get_recipient_slack_ids
     171     * @dataProvider data_get_recipient_slack_ids
     172     * @group unit
     173     */
     174    public function test_get_recipient_slack_ids( array $blocks, array $expected ) : void {
     175        $actual = get_recipient_slack_ids( $blocks );
     176
     177        $this->assertSame( $expected, $actual );
     178    }
     179
     180    public function data_get_recipient_slack_ids() : array {
     181        $valid_request = self::get_valid_request();
     182
     183        $cases = array(
     184            'empty' => array(
     185                'blocks'   => array(),
     186                'expected' => array(),
     187            ),
     188
     189            'valid' => array(
     190                'blocks'   => $valid_request->event->blocks,
     191                'expected' => array(
     192                    'U02RR6SGY',
     193                    'U02RQHNND',
     194                    'U3KJ0TK4L',
     195                    'U4L99HZB6',
     196                    'U024MFP4L',
     197                    'U6R2E3Y9Y',
     198                    'U023GFZJ07L',
     199                    'U1E5RLU1L',
     200                ),
     201            ),
     202        );
     203
     204        return $cases;
     205    }
     206
     207    /**
     208     * @covers ::map_slack_users_to_wporg
     209     * @dataProvider data_map_slack_users_to_wporg
     210     * @group unit
     211     */
     212    public function test_map_slack_users_to_wporg( array $slack_ids, array $db_results, array $expected ) : void {
     213        global $wpdb;
     214
     215        $wpdb = $this->createStub( wpdbStub::class );
     216        $wpdb->method( 'get_results' )->willReturn( $db_results );
     217
     218        $actual = map_slack_users_to_wporg( $slack_ids );
     219
     220        $this->assertSame( $expected, $actual );
     221    }
     222
     223    public function data_map_slack_users_to_wporg() : array {
     224        $cases = array(
     225            'empty' => array(
     226                'slack_ids'  => array(),
     227                'db_results' => array(),
     228                'expected'   => array(),
     229            ),
     230
     231            'valid giver' => array(
     232                'slack_ids' => array( 'U02QCF502' ),
     233
     234                'db_results' => array(
     235                    array(
     236                        'slack_id'   => 'U02QCF502',
     237                        'wporg_id'   => '33690',
     238                        'user_login' => 'iandunn',
     239                    ),
     240                ),
     241
     242                'expected' => array(
     243                    'U02QCF502' => array(
     244                        'id'         => 33690,
     245                        'user_login' => 'iandunn',
     246                    ),
     247                ),
     248            ),
     249
     250            'valid receivers' => array(
     251                'slack_ids' => array( 'U02RQHNND', 'U02RR6SGY', 'U3KJ0TK4L', 'U4L99HZB6' ),
     252
     253                'db_results' => array(
     254                    array(
     255                        'slack_id'   => 'U02RQHNND',
     256                        'wporg_id'   => '297445',
     257                        'user_login' => 'SergeyBiryukov',
     258                    ),
     259
     260                    array(
     261                        'slack_id'   => 'U02RR6SGY',
     262                        'wporg_id'   => '2255796',
     263                        'user_login' => 'Mamaduka',
     264                    ),
     265
     266
     267                    array(
     268                        'slack_id'   => 'U3KJ0TK4L',
     269                        'wporg_id'   => '15049054',
     270                        'user_login' => 'davidbaumwald',
     271                    ),
     272
     273                    array(
     274                        'slack_id'   => 'U4L99HZB6',
     275                        'wporg_id'   => '8976791',
     276                        'user_login' => 'pbiron',
     277                    ),
     278                ),
     279
     280                'expected' => array(
     281                    'U02RQHNND' => array(
     282                        'id'         => 297445,
     283                        'user_login' => 'SergeyBiryukov',
     284                    ),
     285                    'U02RR6SGY' => array(
     286                        'id'         => 2255796,
     287                        'user_login' => 'Mamaduka',
     288                    ),
     289                    'U3KJ0TK4L' => array(
     290                        'id'         => 15049054,
     291                        'user_login' => 'davidbaumwald',
     292                    ),
     293
     294                    'U4L99HZB6' => array(
     295                        'id'         => 8976791,
     296                        'user_login' => 'pbiron',
     297                    ),
     298                ),
     299            ),
     300        );
     301
     302        return $cases;
     303    }
     304
     305    /**
     306     * @covers ::prepare_message
     307     * @dataProvider data_prepare_message
     308     * @group unit
     309     */
     310    public function test_prepare_message( string $text, array $user_map, string $expected ) : void {
     311        $actual = prepare_message( $text, $user_map );
     312
     313        $this->assertSame( $expected, $actual );
     314    }
     315
     316    public function data_prepare_message() : array {
     317        $valid_request = self::get_valid_request();
     318
     319        $cases = array(
     320            'empty' => array(
     321                'text'     => '',
     322                'user_map' => array(),
     323                'expected' => '',
     324            ),
     325
     326            'valid' => array(
     327                'text' => $valid_request->event->text,
     328                'user_map' => array(
     329                    'U023GFZJ07L' => array(
     330                        'id' => 18752239,
     331                        'user_login' => 'costdev',
     332                    ),
     333                    'U024MFP4L' => array(
     334                        'id' => 2545,
     335                        'user_login' => 'markjaquith',
     336                    ),
     337
     338                    'U02RQHNND' => array(
     339                        'id' => 297445,
     340                        'user_login' => 'SergeyBiryukov',
     341                    ),
     342
     343                    'U02RR6SGY' => array(
     344                        'id' => 2255796,
     345                        'user_login' => 'Mamaduka',
     346                    ),
     347
     348                    'U1E5RLU1L' => array(
     349                        'id' => 15152479,
     350                        'user_login' => 'jeroenrotty',
     351                    ),
     352
     353                    'U3KJ0TK4L' => array(
     354                        'id' => 15049054,
     355                        'user_login' => 'davidbaumwald',
     356                    ),
     357
     358                    'U4L99HZB6' => array(
     359                        'id' => 8976791,
     360                        'user_login' => 'pbiron',
     361                    ),
     362
     363                    'U6R2E3Y9Y' => array(
     364                        'id' => 15524609,
     365                        'user_login' => 'webcommsat',
     366                    ),
     367                ),
     368                'expected' => 'props to @Mamaduka for co-leading 5.9.3 RC 1, to @SergeyBiryukov for running mission control and to @davidbaumwald @pbiron @markjaquith @webcommsat @costdev @jeroenrotty for their help testing the release package :community: :wordpress:',
     369            ),
     370        );
     371
     372        return $cases;
     373    }
    92374}
Note: See TracChangeset for help on using the changeset viewer.