<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[11393] sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events: Official WordPress Events: Upgrade the Meetup.com client to query via a new GraphQL API, as the REST API is being deprecated by meetup.com.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/11393">11393</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/11393","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>dd32</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2021-12-22 02:08:56 +0000 (Wed, 22 Dec 2021)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Official WordPress Events: Upgrade the Meetup.com client to query via a new GraphQL API, as the REST API is being deprecated by meetup.com.

The API return values have changed significantly, but also mostly the same. This only queries for data required, and should be much more efficient in terms of querying for events and groups.

See https://github.com/WordPress/wordcamp.org/issues/697</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsofficialwordpresseventsmeetupclassmeetupclientphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/meetup/class-meetup-client.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsofficialwordpresseventsofficialwordpresseventsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsofficialwordpresseventsmeetupclassmeetupclientphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/meetup/class-meetup-client.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/meetup/class-meetup-client.php 2021-12-21 01:50:11 UTC (rev 11392)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/meetup/class-meetup-client.php   2021-12-22 02:08:56 UTC (rev 11393)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1,6 +1,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> <?php
</span><span class="cx" style="display: block; padding: 0 10px"> namespace WordCamp\Utilities;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+use DateTime, DateTimeZone;
</ins><span class="cx" style="display: block; padding: 0 10px"> use WP_Error;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> defined( 'WPINC' ) || die();
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -19,12 +20,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * - wporg: wp-content/plugins/official-wordpress-events/meetup
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> class Meetup_Client extends API_Client {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
</ins><span class="cx" style="display: block; padding: 0 10px">         /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @var string The base URL for the API endpoints.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @var int The Venue ID for online events.
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected $api_base = 'https://api.meetup.com/';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ const ONLINE_VENUE_ID = 26906060;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @var string The URL for the API endpoints.
+        */
+       protected $api_url = 'https://api.meetup.com/gql';
+
+       /**
+        * @var string The GraphQL field that must be present for pagination to work.
+        */
+       public $pageInfo = 'pageInfo { hasNextPage endCursor }';
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * @var Meetup_OAuth2_Client|null
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        protected $oauth_client = null;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -58,10 +70,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                         * a `400` response, but then get a `200` response if that exact same request is retried.
</span><span class="cx" style="display: block; padding: 0 10px">                         */
</span><span class="cx" style="display: block; padding: 0 10px">                        'breaking_response_codes' => array(
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                // TODO: NOTE: These headers are not returned from the GraphQL API, every request is 200 even if throttled.
</ins><span class="cx" style="display: block; padding: 0 10px">                                 401, // Unauthorized (invalid key).
</span><span class="cx" style="display: block; padding: 0 10px">                                429, // Too many requests (rate-limited).
</span><span class="cx" style="display: block; padding: 0 10px">                                404, // Unable to find group
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+                               503, // Timeout between API cache & GraphQL Server.
</ins><span class="cx" style="display: block; padding: 0 10px">                         ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        // NOTE: GraphQL does not expose the Quota Headers.
</ins><span class="cx" style="display: block; padding: 0 10px">                         'throttle_callback'       => array( __CLASS__, 'throttle' ),
</span><span class="cx" style="display: block; padding: 0 10px">                ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -122,101 +138,119 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Send a paginated request to the Meetup API and return the aggregated response.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * This automatically paginates requests and will repeat requests to ensure all results are retrieved.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * It also tries to account for API request limits and throttles to avoid getting a limit error.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * For pagination to work, $this->pageInfo must be present within the string, and a 'cursor' variable defined.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $request_url The API endpoint URL to send the request to.
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @param array  $variables   The Query variables used in the query.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array|WP_Error The results of the request.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected function send_paginated_request( $request_url ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ public function send_paginated_request( $query, $variables = null ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 $data = array();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $request_url = add_query_arg( array(
-                       'page' => 200,
-               ), $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $has_next_page        = false;
+               $is_paginated_request = ! empty( $variables ) &&
+                       array_key_exists( 'cursor', $variables ) &&
+                       false !== stripos( $query, $this->pageInfo );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                while ( $request_url ) {
-                       $response = $this->tenacious_remote_get( $request_url, $this->get_request_args() );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         do {
+                       $request_args = $this->get_request_args( $query, $variables );
+                       $response     = $this->tenacious_remote_post( $this->api_url, $request_args );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
-                               $body = json_decode( wp_remote_retrieve_body( $response ), true );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+                               $this->handle_error_response( $response, $this->api_url, $request_args );
+                               break;
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( isset( $body['results'] ) ) {
-                                       $new_data = $body['results'];
-                               } else {
-                                       $new_data = $body;
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $new_data = json_decode( wp_remote_retrieve_body( $response ), true );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                if ( is_array( $new_data ) ) {
-                                       $data = array_merge( $data, $new_data );
-                               } else {
-                                       $this->error->add(
-                                               'unexpected_response_data',
-                                               'The API response did not provide the expected data format.',
-                                               $response
-                                       );
-                                       break;
-                               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( ! empty( $new_data['error'] ) ) {
+                               $this->handle_error_response( $response, $this->api_url, $request_args );
+                               break;
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $request_url = $this->get_next_url( $response );
-                       } else {
-                               $this->handle_error_response( $response, $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( ! is_array( $new_data ) || ! isset( $new_data['data'] ) ) {
+                               $this->error->add(
+                                       'unexpected_response_data',
+                                       'The API response did not provide the expected data format.',
+                                       $response
+                               );
</ins><span class="cx" style="display: block; padding: 0 10px">                                 break;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( $request_url && $this->debug ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 // Merge the data, overwriting scalar values (they should be the same), and merging arrays.
+                       $data = ! $data ? $new_data : $this->array_merge_recursive_numeric_arrays(
+                               $data,
+                               $new_data
+                       );
+
+                       // Pagination - Find the values inside the 'pageInfo' key.
+                       if ( $is_paginated_request ) {
+                               $has_next_page = false;
+                               $end_cursor    = null;
+
+                               // Flatten the data array to a set of [ $key => $value ] pairs for LEAF nodes,
+                               // $value will never be an array, and $key will never be set to 'pageInfo' where
+                               // the targetted values are living.
+                               array_walk_recursive(
+                                       $new_data,
+                                       function( $value, $key ) use( &$has_next_page, &$end_cursor ) {
+                                               // NOTE: This will be truthful and present on the final page causing paged
+                                               // requests to always make an additional request to a final empty page.
+                                               if ( $key === 'hasNextPage' ) {
+                                                       $has_next_page = $value;
+                                               } elseif ( 'endCursor' === $key ) {
+                                                       $end_cursor = $value;
+                                               }
+                                       }
+                               );
+
+                               // Do not iterate if the cursor was what we just made the request with.
+                               // This should never happen, but protects against an infinite loop otherwise.
+                               if ( ! $end_cursor || $end_cursor === $variables['cursor'] ) {
+                                       $has_next_page = false;
+                                       $end_cursor    = false;
+                               }
+
+                               $variables['cursor'] = $end_cursor;
+                       }
+
+                       if ( $has_next_page && $this->debug ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                                 if ( 'cli' === php_sapi_name() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        echo "\nDebug mode: Skipping future paginated requests to $request_url";
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 echo "\nDebug mode: Skipping future paginated requests";
</ins><span class="cx" style="display: block; padding: 0 10px">                                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                                break;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         } while ( $has_next_page );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! empty( $this->error->get_error_messages() ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return $this->error;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $data;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return $data['data'];
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Send a single request to the Meetup API and return the total number of results available.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Similar to array_merge_recursive(), but only merges numeric arrays with one another, overwriting associative elements.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param string $request_url The API endpoint URL to send the request to.
-        *
-        * @return int|WP_Error
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * Based on https://www.php.net/manual/en/function.array-merge-recursive.php#92195
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected function send_total_count_request( $request_url ) {
-               $count = 0;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ private function array_merge_recursive_numeric_arrays( array &$array1, array &$array2 ) {
+               $merged = $array1;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $request_url = add_query_arg( array(
-                       // We're only interested in the headers, so we don't need to receive more than one result.
-                       'page' => 1,
-               ), $request_url );
-
-               $response = $this->tenacious_remote_get( $request_url, $this->get_request_args() );
-
-               if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
-                       $count_header = wp_remote_retrieve_header( $response, 'X-Total-Count' );
-
-                       if ( $count_header ) {
-                               $count = absint( $count_header );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         foreach ( $array2 as $key => &$value ) {
+                       // Merge numeric arrays
+                       if ( is_array( $value ) && wp_is_numeric_array( $value ) && isset( $merged[ $key ] ) ) {
+                               $merged[ $key ] = array_merge( $merged[ $key ], $value );
+                       } elseif ( is_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) {
+                               $merged[ $key ] = $this->array_merge_recursive_numeric_arrays( $merged[ $key ], $value );
</ins><span class="cx" style="display: block; padding: 0 10px">                         } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                $this->error->add(
-                                       'unexpected_response_data',
-                                       'The API response did not provide a total count value.'
-                               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         $merged[ $key ] = $value;
</ins><span class="cx" style="display: block; padding: 0 10px">                         }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                } else {
-                       $this->handle_error_response( $response, $request_url );
</del><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! empty( $this->error->get_error_messages() ) ) {
-                       return $this->error;
-               }
-
-               return $count;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         return $merged;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -224,7 +258,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        protected function get_request_args() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ protected function get_request_args( $query, $variables = null ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 $oauth_token = $this->oauth_client->get_oauth_token();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! empty( $this->oauth_client->error->get_error_messages() ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -231,52 +265,22 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->error = $this->merge_errors( $this->error, $this->oauth_client->error );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                if ( is_array( $variables ) ) {
+                       $variables = wp_json_encode( $variables );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 return array(
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'timeout' => 60,
</ins><span class="cx" style="display: block; padding: 0 10px">                         'headers' => array(
</span><span class="cx" style="display: block; padding: 0 10px">                                'Accept'        => 'application/json',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                'Content-Type'  => 'application/json',
</ins><span class="cx" style="display: block; padding: 0 10px">                                 'Authorization' => "Bearer $oauth_token",
</span><span class="cx" style="display: block; padding: 0 10px">                        ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'body' => wp_json_encode( compact( 'query', 'variables' ) )
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Get the URL for the next page of results from a paginated API response.
-        *
-        * @param array $response
-        *
-        * @return string
-        */
-       protected function get_next_url( $response ) {
-               $url = '';
-
-               // First try v3.
-               $links = wp_remote_retrieve_header( $response, 'link' );
-               if ( $links ) {
-                       // Meetup.com is now returning combined link headers
-                       if ( is_string( $links ) ) {
-                               $links = preg_split( '!,\s+!', $links );
-                       }
-                       foreach ( (array) $links as $link ) {
-                               if ( false !== strpos( $link, 'rel="next"' ) && preg_match( '/^<([^>]+)>/', $link, $matches ) ) {
-                                       $url = $matches[1];
-                                       break;
-                               }
-                       }
-               }
-
-               // Then try v2.
-               if ( ! $url ) {
-                       $body = json_decode( wp_remote_retrieve_body( $response ), true );
-
-                       if ( isset( $body['meta']['next'] ) ) {
-                               $url = $body['meta']['next'];
-                       }
-               }
-
-               return esc_url_raw( $url );
-       }
-
-       /**
</del><span class="cx" style="display: block; padding: 0 10px">          * Check the rate limit status in an API response and delay further execution if necessary.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param array $headers
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -286,6 +290,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">        protected static function throttle( $response ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $headers = wp_remote_retrieve_headers( $response );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                /*
+                * NOTE: This is not in use, as GraphQL API doesn't return rate limit headers,
+                *       but does throttle requests & fail if you exceed it.
+                */
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( ! isset( $headers['x-ratelimit-remaining'], $headers['x-ratelimit-reset'] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        return;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -314,6 +323,65 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Convert a ISO8601-ish DateTime returned from the API to a timestamp.
+        *
+        * Handles timestamps in two main formats:
+        *  - 2021-11-20T17:00+05:30
+        *  - 2021-11-20T06:30-05:00[US/Eastern]
+        * Neither contains seconds.
+        *
+        * Some extra compat formats are included, just incase Meetup.com decides to return in other similar formats,
+        * or with different timezone formats, etc.
+        *
+        * @param string $datetime A DateTime string returned by the API
+        * @return int The UTC epoch timestamp.
+        */
+       public function datetime_to_time( $datetime ) {
+               if ( is_numeric( $datetime ) && $datetime > 4102444800 /* 2100-01-01 */ ) {
+                       $datetime /= 1000;
+                       return (int) $datetime;
+               } elseif ( is_numeric( $datetime ) ) {
+                       return (int) $datetime;
+               }
+
+               $datetime_formats = [
+                       'Y-m-d\TH:iP',   // 2021-11-20T17:00+05:30
+                       'Y-m-d\TH:i:sP', // 2021-11-20T17:00:00+05:30
+                       // DateTime::createFromFormat() doesn't handle the final `]` character in the following timezone format.
+                       'Y-m-d\TH:i\[e', // 2021-11-20T06:30[US/Eastern]
+                       'c',             // ISO8601, just incase the above don't cover it.
+                       'Y-m-d\TH:i:s',  // timezoneless 2021-11-20T17:00:00
+                       'Y-m-d\TH:i',    // timezoneless 2021-11-20T17:00
+               ];
+
+               // See above, just keep one timezone if the timezone format is `P\[e\]`. Simpler matching, assume the timezones are the same.
+               $datetime = preg_replace( '/([-+][0-9:]+)[[].+[]]$/', '$1', $datetime );
+
+               // See above..
+               $datetime = rtrim( $datetime, ']' );
+
+               // Just being hopeful.
+               $time = strtotime( $datetime );
+               if ( $time ) {
+                       return $time;
+               }
+
+               // Try each of the timezone formats.
+               foreach ( $datetime_formats as $format ) {
+                       $time = DateTime::createFromFormat( $format, $datetime );
+                       if ( $time ) {
+                               break;
+                       }
+               }
+
+               if ( ! $time ) {
+                       return false;
+               }
+
+               return (int) $time->format( 'U' );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Extract error information from an API response and add it to our error handler.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * Make sure you don't include the full $response in the error as data, as that could expose sensitive information
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -359,8 +427,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">                if ( isset( $data['errors'] ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        foreach ( $data['errors'] as $details ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                $error->add(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                        $details['code'],
-                                       $details['message']
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                                 $details['extensions']['code'],
+                                       $details['message'],
+                                       $details['locations'] ?? '' // TODO This isn't being passed through to the final error?
</ins><span class="cx" style="display: block; padding: 0 10px">                                 );
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px">                } elseif ( isset( $data['error'], $data['error_description'] ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -386,19 +455,76 @@
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * Retrieve data about groups in the Chapter program.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param array $args Optional. Additional request parameters.
-        *                    See https://www.meetup.com/meetup_api/docs/pro/:urlname/groups/.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param array $args Optional. 'fields' and 'filters' may be defined.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array|WP_Error
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function get_groups( array $args = array() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $request_url = $this->api_base . 'pro/wordpress/groups';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $fields = $this->get_default_fields( 'group' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! empty( $args ) ) {
-                       $request_url = add_query_arg( $args, $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( !empty( $args['fields'] ) && is_array( $args['fields'] ) ) {
+                       $fields = array_merge( $fields, $args['fields'] );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $this->send_paginated_request( $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $filters = [];
+               /*
+                *  See https://www.meetup.com/api/schema/#GroupAnalyticsFilter for valid filters.
+                */
+               if ( isset( $args['pro_join_date_max'] ) ) {
+                       $filters['proJoinDateMax'] = 'proJoinDateMax: ' . $this->datetime_to_time( $args['pro_join_date_max'] ) * 1000;
+               }
+               if ( isset( $args['last_event_min'] ) ) {
+                       $filters['lastEventMin'] = 'lastEventMin: ' . $this->datetime_to_time( $args['last_event_min'] ) * 1000;
+               }
+
+               if ( isset( $args['filters'] ) ) {
+                       foreach ( $args['filters'] as $key => $value ) {
+                               $filters[ $key ] = "{$key}: {$value}";
+                       }
+               }
+
+               $variables = [
+                       'urlname' => 'wordpress',
+                       'perPage' => 200,
+                       'cursor'  => null,
+               ];
+
+               $query = '
+               query ($urlname: String!, $perPage: Int!, $cursor: String ) {
+                       proNetworkByUrlname( urlname: $urlname ) {
+                               groupsSearch( input: { first: $perPage, after: $cursor }, filter: { ' . implode( ', ', $filters ) . '} ) {
+                                       count
+                                       '  . $this->pageInfo . '
+                                       edges {
+                                               node {
+                                                       ' . implode( ' ', $fields ) . '
+                                               }
+                                       }
+                               }
+                       }
+               }';
+
+               $result = $this->send_paginated_request( $query, $variables );
+
+               if ( is_wp_error( $result ) || ! array_key_exists( 'groupsSearch', $result['proNetworkByUrlname'] ) ) {
+                       return $result;
+               }
+
+               $results = array_column(
+                       $result['proNetworkByUrlname']['groupsSearch']['edges'],
+                       'node'
+               );
+
+               foreach ( $results as &$result ) {
+                       $result['member_count']  = $result['groupAnalytics']['totalMembers'];
+                       $result['pro_join_date'] = $this->datetime_to_time( $result['proJoinDate'] ) * 1000;
+
+                       if ( ! empty( $result['groupAnalytics']['lastEventDate'] ) ) {
+                               $result['last_event'] = $this->datetime_to_time( $result['groupAnalytics']['lastEventDate'] ) * 1000;
+                       }
+               }
+
+               return $results;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -417,8 +543,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * ISO 8601, without the timezone part. Because consistency is overrated.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param array $group_slugs The URL slugs of each group to retrieve events for. Also known as `urlname`.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param array $args        Optional. Additional request parameters.
-        *                           See https://www.meetup.com/meetup_api/docs/:urlname/events/#list
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param array $args        Optional.  'fields' and 'filters' may be defined.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array|WP_Error
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -425,6 +550,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public function get_events( array $group_slugs, array $args = array() ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $events = array();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // See get_network_events(), which should be preferred for most cases.
+               // This is kept for back-compat.
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 if ( $this->debug ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $chunked     = array_chunk( $group_slugs, 10 );
</span><span class="cx" style="display: block; padding: 0 10px">                        $group_slugs = $chunked[0];
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -444,22 +572,143 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Retrieve Event Details
+        *
+        * @param string $event_id The Event ID.
+        * @return array
+        */
+       function get_event_details( $event_id ) {
+
+               $fields = $this->get_default_fields( 'event' );
+
+               // Accepts, slug / id / slugId as the query-by fields.
+               $query = '
+               query ( $eventId: ID ) {
+                       event( id: $eventId ) {
+                               ' . implode( ' ', $fields ) . '
+                       }
+               }';
+               $variables = [
+                       'eventId' => $event_id,
+               ];
+
+               $result = $this->send_paginated_request( $query, $variables );
+
+               if ( is_wp_error( $result ) || ! array_key_exists( 'event', $result ) ) {
+                       return $result;
+               }
+
+               $event = $result['event'] ?: false;
+
+               if ( $event ) {
+                       $event = $this->apply_backcompat_fields( 'event',  $event );
+               }
+
+               return $event;
+       }
+
+       /**
+        * Retrieve the event Status for a range of given IDs.
+        * 
+        * @param array $event_ids An array of [ id => MeetupID, id2 => MeetupID2 ] to query for.
+        * @return array Array of Event Statuses if events is found, null values if MeetupID doesn't exist.
+        */
+       public function get_events_status( $event_ids ) {
+               /* $events = [ id => $meetupID, id2 => $meetupID2 ] */
+
+               $return = [];
+               $chunks = array_chunk( $event_ids, 250, true );
+
+               foreach ( $chunks as $chunked_events ) {
+                       $keys      = [];
+                       $query     = '';
+
+                       foreach ( $chunked_events as $id => $event_id ) {
+                               $key = 'e' . md5( $id );
+                               $keys[ $key ] = $id;
+
+                               $query .= sprintf(
+                                       '%s: event( id: "%s" ) { id status timeStatus }' . "\n",
+                                       $key,
+                                       esc_attr( $event_id )
+                               );
+                       }
+
+                       $result = $this->send_paginated_request( "query { $query }" );
+
+                       if ( is_wp_error( $result ) || ! isset( $result ) ) {
+                               return $result;
+                       }
+
+                       // Unwrap it.
+                       foreach ( $result as $id => $data ) {
+                               $return[ $keys[ $id ] ] = $data;
+                       }
+               }
+
+               return $return;
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Retrieve details about a group.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $group_slug The slug/urlname of a group.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param array  $args       Optional. Additional request parameters.
-        *                           See https://www.meetup.com/meetup_api/docs/:urlname/#get
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param array  $args       Optional. 'fields' and 'event_fields' may be defined.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array|WP_Error
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function get_group_details( $group_slug, $args = array() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $request_url = $this->api_base . sanitize_key( $group_slug );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $fields = $this->get_default_fields( 'group' );;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! empty( $args ) ) {
-                       $request_url = add_query_arg( $args, $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $events_fields = [
+                       'dateTime',
+                       'going',
+               ];
+
+               if ( !empty( $args['fields'] ) && is_array( $args['fields'] ) ) {
+                       $fields = array_merge( $fields, $args['fields'] );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                if ( !empty( $args['events_fields'] ) && is_array( $args['events_fields'] ) ) {
+                       $events_fields = array_merge( $events_fields, $args['events_fields'] );
+               } elseif ( !empty( $args['events_fields'] ) && true === $args['events_fields'] ) {
+                       $events_fields = array_merge( $events_fields, $this->get_default_fields( 'events' ) );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $this->send_paginated_request( $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // pastEvents cannot filter to the most recent past event, `last: 1`, `reverse:true, first: 1`, etc doesn't work.
+               // Instead, we fetch the details for every past event instead.
+
+               $query = '
+               query ( $urlname: String!, $perPage: Int!, $cursor: String ) {
+                       groupByUrlname( urlname: $urlname ) {
+                               ' . implode( ' ', $fields ) . '
+                               pastEvents ( input: { first: $perPage, after: $cursor } ) {
+                                       ' . $this->pageInfo . '
+                                       edges {
+                                               node {
+                                                       ' . implode( ' ', $events_fields ) . '
+                                               }
+                                       }
+                               }
+                       }
+               }';
+               $variables = [
+                       'urlname' => $group_slug,
+                       'perPage' => 200,
+                       'cursor'  => null,
+               ];
+
+               $result = $this->send_paginated_request( $query, $variables );
+
+               if ( is_wp_error( $result ) || ! isset( $result['groupByUrlname'] ) ) {
+                       return $result;
+               }
+
+               // Format it similar to previous response payload??
+               $result = $result['groupByUrlname'];
+
+               $result = $this->apply_backcompat_fields( 'group', $result );
+
+               return $result;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -466,38 +715,280 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Retrieve details about group members.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $group_slug The slug/urlname of a group.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param array  $args       Optional. Additional request parameters.
-        *                           See https://www.meetup.com/meetup_api/docs/:urlname/members/#list
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param array  $args       Optional. 'fields' and 'filters' may be defined.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array|WP_Error
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function get_group_members( $group_slug, $args = array() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $request_url = $this->api_base . sanitize_key( $group_slug ) . '/members';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $fields = $this->get_default_fields( 'memberships' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! empty( $args ) ) {
-                       $request_url = add_query_arg( $args, $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         if ( ! empty( $args['fields'] ) && is_array( $args['fields'] ) ) {
+                       $fields = array_merge(
+                               $fields,
+                               $args['fields']
+                       );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $this->send_paginated_request( $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Filters
+               $filters = [];
+               if ( isset( $args['role'] ) && 'leads' === $args['role'] ) {
+                       // See https://www.meetup.com/api/schema/#MembershipStatus for valid statuses.
+                       $filters[] = 'status: LEADER';
+               }
+
+               if ( isset( $args['filters'] ) ) {
+                       foreach ( $args['filters'] as $key => $value ) {
+                               $filters[] = "{$key}: {$value}";
+                       }
+               }
+
+               // 'memberships' => 'GroupUserConnection' not documented.
+               $query = '
+               query ( $urlname: String!, $perPage: Int!, $cursor: String ) {
+                       groupByUrlname( urlname: $urlname ) {
+                               memberships ( input: { first: $perPage, after: $cursor }, filter: { ' . implode( ', ', $filters ) . ' } ) {
+                                       ' . $this->pageInfo . '
+                                       edges {
+                                               node {
+                                                       ' . implode( ' ', $fields ) . '
+                                               }
+                                       }
+                               }
+                       }
+               }';
+               $variables = [
+                       'urlname' => $group_slug,
+                       'perPage' => 200,
+                       'cursor'  => null,
+               ];
+
+               $results = $this->send_paginated_request( $query, $variables );
+               if ( is_wp_error( $results ) || ! isset( $results['groupByUrlname'] ) ) {
+                       return $results;
+               }
+
+               // Select memberships.edges[*].node
+               $results = array_column(
+                       $results['groupByUrlname']['memberships']['edges'],
+                       'node'
+               );
+
+               return $results;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Query all events from the Network.
+        */
+       public function get_network_events( array $args = array() ) {
+               $defaults = [
+                       'filters'        => [],
+                       'max_event_date' => time() + YEAR_IN_SECONDS,
+                       'min_event_date' => false,
+                       'online_events'  => null, // true: only online events, false: only IRL events
+                       'status'         => 'upcoming', //  UPCOMING, PAST, CANCELLED
+                       'sort'           => '',
+               ];
+               $args = wp_parse_args( $args, $defaults );
+
+               $fields = $this->get_default_fields( 'event' );
+
+               // See https://www.meetup.com/api/schema/#ProNetworkEventsFilter
+               $filters = [];
+
+               if ( $args['min_event_date'] ) {
+                       $filters['eventDateMin'] = 'eventDateMin: ' . $this->datetime_to_time( $args['min_event_date'] ) * 1000;
+               }
+               if ( $args['max_event_date'] ) {
+                       $filters['eventDateMax'] = 'eventDateMax: ' . $this->datetime_to_time( $args['max_event_date'] ) * 1000;
+               }
+
+               if ( ! is_null( $args['online_events'] ) ) {
+                       $filters['isOnlineEvent'] = 'isOnlineEvent: ' . ( $args['online_events'] ? 'true' : 'false' );
+               }
+
+               // See https://www.meetup.com/api/schema/#ProNetworkEventStatus
+               if ( $args['status'] && in_array( $args['status'], [ 'cancelled', 'upcoming', 'past' ] ) ) {
+                       $filters['status'] = 'status: ' . strtoupper( $args['status'] );
+               }
+
+               if ( $args['filters'] ) {
+                       foreach( $args['filters'] as $key => $filter ) {
+                               $filters[ $key ] = "{$key}: {$filter}";
+                       }
+               }
+
+               $query = '
+               query ( $urlname: String!, $perPage: Int!, $cursor: String ) {
+                       proNetworkByUrlname( urlname: $urlname ) {
+                               eventsSearch ( input: { first: $perPage, after: $cursor }, filter: { ' . implode( ', ', $filters )  . ' } ) {
+                                       ' . $this->pageInfo . '
+                                       edges {
+                                               node {
+                                                       ' . implode( ' ', $fields ) . '
+                                               }
+                                       }
+                               }
+                       }
+               }';
+               $variables = [
+                       'urlname' => 'wordpress',
+                       'perPage' => 1000, // More per-page to avoid hitting request limits
+                       'cursor'  => null,
+               ];
+
+
+               $results = $this->send_paginated_request( $query, $variables );
+
+               if ( is_wp_error( $results ) || ! array_key_exists( 'eventsSearch', $results['proNetworkByUrlname'] ) ) {
+                       return $results;
+               }
+
+               if ( empty( $results['proNetworkByUrlname']['eventsSearch'] ) ) {
+                       return [];
+               }
+
+               // Select edges[*].node
+               $results = array_column(
+                       $results['proNetworkByUrlname']['eventsSearch']['edges'],
+                       'node'
+               );
+
+               $results = $this->apply_backcompat_fields( 'events', $results );
+
+               return $results;
+
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Retrieve data about events associated with one particular group.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $group_slug The slug/urlname of a group.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param array  $args       Optional. Additional request parameters.
-        *                           See https://www.meetup.com/meetup_api/docs/:urlname/events/#list
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param array  $args       Optional. 'status', 'fields' and 'filters' may be defined.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return array|WP_Error
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function get_group_events( $group_slug, array $args = array() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $request_url = $this->api_base . sanitize_key( $group_slug ) . '/events';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $defaults = [
+                       'status'          => 'upcoming',
+                       'no_earlier_than' => '',
+                       'no_later_than'   => '',
+                       'fields'          => [],
+               ];
+               $args = wp_parse_args( $args, $defaults );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! empty( $args ) ) {
-                       $request_url = add_query_arg( $args, $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         /*
+                * The GraphQL API has 4 events fields, here's some comments:
+                *  - upcomingEvents: Supports filtering via the 'GroupUpcomingEventsFilter', which allows for 'includeCancelled'.
+                *  - pastEvents: No filters.
+                *  - draftEvents: No Filters.
+                *  - unifiedEvents: Supports Filtering via the undocumented 'GroupEventsFilter', does not support status/dates?
+                *
+                * Querying for multiple of these fields results in multiple paginated subkeys, complicating the requests, not
+                * impossible but not within the spirit of this simplified query class, so we'll avoid requesting multiple paginated
+                * fields.
+                *
+                * As a result of this, if the request is for multiple statuses, we're going to recursively call ourselves.. so that
+                * we can query using the individual fields to get the statii we want, and apply the other filters directly.
+                */
+               if ( false !== strpos( $args['status'], ',' ) ) {
+                       $events = [];
+                       foreach ( explode( ',', $args['status'] ) as $status ) {
+                               $args['status'] = $status;
+                               $status_events  = $this->get_group_events( $group_slug, $args );
+
+                               // If any individual API request fails, fail it all.
+                               if ( is_wp_error( $status_events ) ) {
+                                       return $status_events;
+                               }
+
+                               $events = array_merge( $events, $status_events );
+                       }
+
+                       // Resort all items.
+                       usort( $events, function( $a, $b ) {
+                               if ( $a['time'] == $b['time'] ) {
+                                       return 0;
+                               }
+
+                               return ( $a['time'] < $b['time'] ) ? -1 : 1;
+                       } );
+
+                       return $events;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $this->send_paginated_request( $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $fields = $this->get_default_fields( 'event' );
+
+               // TODO: Check the above list against Official_WordPress_Events::parse_meetup_events()
+
+               if ( ! empty( $args['fields'] ) && is_array( $args['fields'] ) ) {
+                       $fields = array_merge(
+                               $fields,
+                               $args['fields']
+                       );
+               }
+
+               // The GraphQL field to query.
+               switch ( $args['status'] ) {
+                       case 'upcoming':
+                       case 'past':
+                       case 'draft':
+                               $event_field = $args['status'] . 'Events';
+                               break;
+                       default:
+                               // We got nothing.
+                               return [];
+               }
+
+               // No filters defined, as we have to do it ourselves. See above.
+
+               $query = '
+               query ( $urlname: String!, $perPage: Int!, $cursor: String ) {
+                       groupByUrlname( urlname: $urlname ) {
+                               ' . $event_field . ' ( input: { first: $perPage, after: $cursor } ) {
+                                       ' . $this->pageInfo . '
+                                       edges {
+                                               node {
+                                                       ' . implode( ' ', $fields ) . '
+                                               }
+                                       }
+                               }
+                       }
+               }';
+               $variables = [
+                       'urlname' => $group_slug,
+                       'perPage' => 200,
+                       'cursor'  => null,
+               ];
+
+               $results = $this->send_paginated_request( $query, $variables );
+               if ( is_wp_error( $results ) || ! isset( $results['groupByUrlname'] ) ) {
+                       return $results;
+               }
+
+               // Select {$event_field}.edges[*].node
+               $results = array_column(
+                       $results['groupByUrlname'][ $event_field ]['edges'],
+                       'node'
+               );
+
+               $results = $this->apply_backcompat_fields( 'events', $results );
+
+               // Apply filters.
+               if ( $args['no_earlier_than'] || $args['no_later_than'] ) {
+                       $args['no_earlier_than'] = $this->datetime_to_time( $args['no_earlier_than'] ) ?: 0;
+                       $args['no_later_than']   = $this->datetime_to_time( $args['no_later_than'] ) ?: PHP_INT_MAX;
+
+                       $results = array_filter(
+                               $results,
+                               function( $event ) use( $args ) {
+                                       return
+                                               $event['time'] >= $args['no_earlier_than'] &&
+                                               $event['time'] < $args['no_later_than'];
+                               }
+                       );
+               }
+
+               return $results;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -504,18 +995,285 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * Find out how many results are available for a particular request.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $route The Meetup.com API route to send a request to.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param array  $args  Optional. Additional request parameters.
-        *                      See https://www.meetup.com/meetup_api/docs/.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param array  $args  Optional.  'pro_join_date_max', 'pro_join_date_min', and 'filters' may be defined.
</ins><span class="cx" style="display: block; padding: 0 10px">          *
</span><span class="cx" style="display: block; padding: 0 10px">         * @return int|WP_Error
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function get_result_count( $route, array $args = array() ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $request_url = $this->api_base . $route;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $result  = false;
+               $filters = [];
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( ! empty( $args ) ) {
-                       $request_url = add_query_arg( $args, $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Number of groups in the Pro Network.
+               if ( 'pro/wordpress/groups' !== $route ) {
+                       return false;
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                return $this->send_total_count_request( $request_url );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // https://www.meetup.com/api/schema/#GroupAnalyticsFilter
+               if ( ! empty( $args['pro_join_date_max'] ) ) {
+                       $filters['proJoinDateMax'] = 'proJoinDateMax: ' . $this->datetime_to_time( $args['pro_join_date_max'] ) * 1000;
+               }
+               if ( ! empty( $args['pro_join_date_min'] ) ) {
+                       $filters['proJoinDateMin'] = 'proJoinDateMin: ' . $this->datetime_to_time( $args['pro_join_date_min'] ) * 1000;
+               }
+
+               if ( isset( $args['filters'] ) ) {
+                       foreach ( $args['filters'] as $key => $value ) {
+                               $filters[ $key ] = "{$key}: {$value}";
+                       }
+               }
+
+               $query = '
+               query {
+                       proNetworkByUrlname( urlname: "wordpress" ) {
+                               groupsSearch( filter: { ' .  implode( ', ', $filters ) . ' } ) {
+                                       count
+                               }
+                       }
+               }';
+
+               $results = $this->send_paginated_request( $query );
+               if ( is_wp_error( $results ) ) {
+                       return $results;
+               }
+
+               return (int) $results['proNetworkByUrlname']['groupsSearch']['count'];
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Get the default fields for each object type.
+        * 
+        * @param string $type The Object type.
+        * @return array Fields to query.
+        */
+       protected function get_default_fields( $type ) {
+               if ( 'event' === $type ) {
+                       // See https://www.meetup.com/api/schema/#Event for valid fields.
+                       return [
+                               'id',
+                               'title',
+                               'description',
+                               'eventUrl',
+                               'status',
+                               'timeStatus',
+                               'dateTime',
+                               'timezone',
+                               'endTime',
+                               'createdAt',
+                               'isOnline',
+                               'going',
+                               'group {
+                                       ' . implode( ' ', $this->get_default_fields( 'group' ) ) . '
+                               }',
+                               'venue {
+                                       id
+                                       lat
+                                       lng
+                                       name
+                                       city
+                                       state
+                                       country
+                               }'
+                       ];
+               } elseif ( 'memberships' === $type ) {
+                       // See https://www.meetup.com/api/schema/#User for valid fields.
+                       return [
+                               'id',
+                               'name',
+                               'email',
+                       ];
+               } elseif ( 'group' === $type ) {
+                       return [
+                               'id',
+                               'name',
+                               'urlname',
+                               'link',
+                               'city',
+                               'state',
+                               'country',
+                               'groupAnalytics {
+                                       totalPastEvents,
+                                       totalMembers,
+                                       lastEventDate,
+                               }',
+                               'foundedDate',
+                               'proJoinDate',
+                               'latitude',
+                               'longitude',
+                       ];
+               }
+       }
+
+       /**
+        * Apply back-compat fields/filters for previous uses of the client.
+        * 
+        * Can be removed once all uses of the library have migrated over.
+        * 
+        * @param string $type   The type of result object.
+        * @param array  $result The result to back-compat.
+        * @return The $result with back-compat.
+        */
+       protected function apply_backcompat_fields( $type, $result ) {
+               if ( 'event' === $type ) {
+
+                       $result['name'] = $result['title'];
+
+                       if ( ! empty( $result['dateTime'] ) ) {
+                               // Required for utc_offset below.
+                               $result['time'] = $this->datetime_to_time( $result['dateTime'] ) * 1000;
+                       }
+
+                       // Easier to parse the difference between start and end, than parse the ISO 'duration' the API provides for now.
+                       $result['duration'] = 0;
+                       if ( ! empty( $result['endTime'] ) ) {
+                               $result['duration'] = ( $this->datetime_to_time( $result['endTime'] ) -  $this->datetime_to_time( $result['dateTime'] ) );
+                               $result['duration'] *= 1000;
+                       }
+
+                       $result['utc_offset'] = 0;
+                       if ( ! empty( $result['timezone'] ) && isset( $result['time'] ) ) {
+                               $result['utc_offset'] = (
+                                       new DateTime(
+                                               // $result['time'] is back-compat above.
+                                               gmdate( 'Y-m-d H:i:s', $result['time']/1000 ),
+                                               new DateTimeZone( $result['timezone'] )
+                                       )
+                               )->getOffset();
+                               $result['utc_offset'] *= 1000;
+                       }
+
+                       if ( ! empty( $result['venue'] ) ) {
+                               if ( is_numeric( $result['venue']['id'] ) ) {
+                                       $result['venue']['id'] = (int) $result['venue']['id'];
+                               }
+
+                               $result['venue']['localized_location']     = $this->localise_location( $result['venue'] );
+                               $result['venue']['localized_country_name'] = $this->localised_country_name( $result['venue']['country'] );
+
+                               // For online events, disregard the Venue lat/lon. It's not correct. In back-compat methods to allow for BC for existing uses of the class.
+                               if ( ! empty( $result['venue']['lng'] ) && self::ONLINE_VENUE_ID == $result['venue']['id'] ) {
+                                       $result['venue']['lat'] = '';
+                                       $result['venue']['lon'] = '';
+                               }
+
+                               // Seriously.
+                               if ( ! empty( $result['venue']['lng'] ) ) {
+                                       $result['venue']['lon'] = $result['venue']['lng'];
+                               }
+                       }
+
+                       if ( ! empty( $result['group'] ) ) {
+                               $result['group'] = $this->apply_backcompat_fields( 'group', $result['group'] );
+                       }
+
+                       $result['status'] = strtolower( $result['status'] );
+                       if ( in_array( $result['status'], [ 'published', 'past', 'active', 'autosched' ] ) ) {
+                               $result['status'] = 'upcoming'; // Right, past is upcoming in this context
+                       }
+
+                       $result['yes_rsvp_count'] = $result['going'];
+                       $result['link']           = $result['eventUrl'];
+               }
+
+               if ( 'events' === $type ) {
+                       foreach ( $result as &$event ) {
+                               $event = $this->apply_backcompat_fields( 'event', $event );
+                       }
+               }
+
+               if ( 'group' === $type ) {
+                       // Stub in the fields that are different.
+                       $result['created']                = $this->datetime_to_time( $result['foundedDate'] ) * 1000;
+                       $result['localized_location']     = $this->localise_location( $result );
+                       $result['localized_country_name'] = $this->localised_country_name( $result['country'] );
+                       $result['members']                = $result['groupAnalytics']['totalMembers'] ?? 0;
+                       $result['member_count']           = $result['members'];
+
+                       if ( ! empty( $result['proJoinDate'] ) ) {
+                               $result['pro_join_date'] = $this->datetime_to_time( $result['proJoinDate'] ) * 1000;
+                       }
+
+                       if ( ! empty( $result['pastEvents']['edges'] ) ) {
+                               $result['last_event']         = [
+                                       'time'           => $this->datetime_to_time( end( $result['pastEvents']['edges'] )['node']['dateTime'] ) * 1000,
+                                       'yes_rsvp_count' => end( $result['pastEvents']['edges'] )['node']['going'],
+                               ];
+                       } elseif ( ! empty( $result['groupAnalytics']['lastEventDate'] ) ) {
+                               $result['last_event'] = $this->datetime_to_time( $result['groupAnalytics']['lastEventDate'] ) * 1000;
+                       }
+
+                       $result['lat'] = $result['latitude'];
+                       $result['lon'] = $result['longitude'];
+               }
+               if ( 'groups' === $type ) {
+                       foreach ( $result as &$group ) {
+                               $group = $this->apply_backcompat_fields( 'group', $group );
+                       }
+               }
+
+               return $result;
+       }
+
+       /**
+        * Generate a localised location name.
+        *
+        * For the US this is 'City, ST, USA'
+        * For Canada this is 'City, ST, Canada'
+        * For the rest of world, this is 'City, CountryName'
+        */
+       protected function localise_location( $args = array() ) {
+               // Hard-code the Online event location
+               if ( ! empty( $args['id'] ) && self::ONLINE_VENUE_ID == $args['id'] ) {
+                       return 'online';
+               }
+
+               $country = $args['country'] ?? '';
+               $state   = $args['state']   ?? '';
+               $city    = $args['city']    ?? '';
+               $country = strtoupper( $country );
+
+               // Only the USA & Canada have valid states in the response. Others have states, but are incorrect.
+               if ( 'US' === $country || 'CA' === $country ) {
+                       $state = strtoupper( $state );
+               } else {
+                       $state = '';
+               }
+
+               // Set countries to USA, AU, or Australia in that order.
+               $country = $this->localised_country_name( $country );
+
+               return implode( ', ',  array_filter( [ $city, $state, $country ] ) ) ?: false;
+       }
+
+       /**
+        * Localise a country code to a country name using WP-CLDR if present.
+        * 
+        * @param string $country Country Code.
+        * @return Country Name, or country code upon failure.
+        */
+       public function localised_country_name( $country ) {
+               $localised_country = '';
+               $country           = strtoupper( $country );
+
+               // Shortcut, CLDR isn't always what we expect here.
+               $shortcut = [
+                       'US' => 'USA',
+                       'HK' => 'Hong Kong',
+                       'SG' => 'Singapore',
+               ];
+               if ( ! empty( $shortcut[ $country ] ) ) {
+                       return $shortcut[ $country ];
+               }
+
+               if ( ! class_exists( '\WP_CLDR' ) && file_exists( WP_PLUGIN_DIR . '/wp-cldr/class-wp-cldr.php' ) ) {
+                       require WP_PLUGIN_DIR . '/wp-cldr/class-wp-cldr.php';
+               }
+
+               if ( class_exists( '\WP_CLDR' ) ) {
+                       $cldr = new \WP_CLDR();
+
+                       $localised_country = $cldr->get_territory_name( $country );
+               }
+
+               return $localised_country ?: $country;
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsofficialwordpresseventsofficialwordpresseventsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php  2021-12-21 01:50:11 UTC (rev 11392)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php    2021-12-22 02:08:56 UTC (rev 11393)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -551,10 +551,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        protected function get_meetup_events() {
</span><span class="cx" style="display: block; padding: 0 10px">                $events = array();
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Fetching events for a large number of groups from the Meetup API is currently a very inefficient process.
-               ini_set( 'memory_limit', '900M' );
-               ini_set( 'max_execution_time', 500 );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 $meetup_client = $this->get_meetup_client();
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! empty( $meetup_client->error->errors ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        $this->log( 'Failed to instantiate meetup client: ' . wp_json_encode( $meetup_client->error ), true );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -561,25 +557,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        return $events;
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $groups = $meetup_client->get_groups();
-               if ( ! empty( $meetup_client->error->errors ) ) {
-                       $this->log( 'Failed to fetch groups: ' . wp_json_encode( $meetup_client->error ), true );
-                       return $events;
-               }
-
-               $this->log( sprintf( 'received %d meetup groups', count( $groups ) ) );
-
-               $yesterday    = date( 'c', strtotime( '-1 day' ) );
-               $one_year_out = date( 'c', strtotime( '+1 year' ) );
-               $meetups      = $meetup_client->get_events(
-                       wp_list_pluck( $groups, 'urlname' ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $meetups = $meetup_client->get_network_events(
</ins><span class="cx" style="display: block; padding: 0 10px">                         array(
</span><span class="cx" style="display: block; padding: 0 10px">                                // We want cancelled events too so they will be updated in our database table.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'status'          => 'upcoming,cancelled',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'status'         => false, // We want PAST, UPCOMING, CANCELLED. We'll let the below date cutoff take care of the PAST events.
</ins><span class="cx" style="display: block; padding: 0 10px">                                 // We don't want cancelled events in the past, but need some leeway here for timezones.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'no_earlier_than' => substr( $yesterday, 0, strpos( $yesterday, '+' ) ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'min_event_date' => strtotime( '-1 day' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                                 // We don't need to cache events happening more than a year from now.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                'no_later_than'   => substr( $one_year_out, 0, strpos( $one_year_out, '+' ) ),
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         'max_event_date' => strtotime( '+1 year' ),
</ins><span class="cx" style="display: block; padding: 0 10px">                         )
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! empty( $meetup_client->error->errors ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -624,8 +609,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        $latitude        = ! empty( $meetup['venue']['lat'] ) ? $meetup['venue']['lat'] : $meetup['group']['lat'];
</span><span class="cx" style="display: block; padding: 0 10px">                        $longitude       = ! empty( $meetup['venue']['lon'] ) ? $meetup['venue']['lon'] : $meetup['group']['lon'];
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        if ( isset( $meetup['venue'] ) ) {
-                               $location = $this->format_meetup_venue_location( $meetup['venue'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( ! empty( $meetup['venue']['localized_location'] ) ) {
+                               $location = $meetup['venue']['localized_location'];
</ins><span class="cx" style="display: block; padding: 0 10px">                         } else {
</span><span class="cx" style="display: block; padding: 0 10px">                                $geocoded_location = $this->reverse_geocode( $latitude, $longitude );
</span><span class="cx" style="display: block; padding: 0 10px">                                $location_parts    = $this->parse_reverse_geocode_address( $geocoded_location );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -751,33 +736,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * Format a meetup venue's location
-        *
-        * @param object $venue
-        *
-        * @return string
-        */
-       protected function format_meetup_venue_location( $venue ) {
-               $location = array();
-
-               if ( isset( $venue['id'] ) && 26906060 === $venue['id'] ) {
-                       return 'online';
-               }
-
-               foreach ( array( 'city', 'state', 'localized_country_name' ) as $part ) {
-                       if ( ! empty( $venue[ $part ] ) ) {
-                               if ( in_array( $part, array( 'state' ) ) ) {
-                                       $location[] = strtoupper( $venue[ $part ] );
-                               } else {
-                                       $location[] = $venue[ $part ];
-                               }
-                       }
-               }
-
-               return implode( ', ', $location );
-       }
-
-       /**
</del><span class="cx" style="display: block; padding: 0 10px">          * Mark Meetup events as deleted in our database when they're deleted from Meetup.com.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * Meetup.com allows organizers to either cancel or delete events. If the event is cancelled, then the status
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -791,88 +749,35 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public function mark_deleted_meetups() {
</span><span class="cx" style="display: block; padding: 0 10px">                global $wpdb;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $chunked_db_events = array();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $meetup_client = $this->get_meetup_client();
+               if ( ! empty( $meetup_client->error->errors ) ) {
+                       $this->log( 'Failed to instantiate meetup client: ' . wp_json_encode( $meetup_client->error ), true );
+                       return;
+               }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Don't include anything before tomorrow, because time zone differences could result in past events being flagged.
</del><span class="cx" style="display: block; padding: 0 10px">                 $raw_events = $wpdb->get_results( "
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        SELECT id, source_id, meetup_url
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 SELECT id, source_id
</ins><span class="cx" style="display: block; padding: 0 10px">                         FROM `". self::EVENTS_TABLE ."`
</span><span class="cx" style="display: block; padding: 0 10px">                        WHERE
</span><span class="cx" style="display: block; padding: 0 10px">                                type      = 'meetup'    AND
</span><span class="cx" style="display: block; padding: 0 10px">                                status    = 'scheduled' AND
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                                date_utc >= DATE_ADD( NOW(), INTERVAL 24 HOUR )
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                         date_utc >= NOW()
</ins><span class="cx" style="display: block; padding: 0 10px">                         LIMIT 5000
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                " );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         ", ARRAY_A );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                foreach ( $raw_events as $event ) {
-                       $chunked_db_events[ $event->meetup_url ][] = $event;
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $events         = array_column( $raw_events, 'source_id', 'id' ); // [ id, source ], [ id, source ] => [ id => source, id => source ]
+               $event_statuses = $meetup_client->get_events_status( $events );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $meetup_client = $this->get_meetup_client();
-               if ( ! empty( $meetup_client->error->errors ) ) {
-                       $this->log( 'Failed to instantiate meetup client: ' . wp_json_encode( $meetup_client->error ), true );
-                       return;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         foreach ( $event_statuses as $id => $status ) {
+                       if ( ! $status ) {
+                               $wpdb->update(
+                                       self::EVENTS_TABLE,
+                                       array( 'status' => 'deleted' ),
+                                       array( 'id' => $id )
+                               );
+                               $this->log( "Event missing. Marked DB ID {$id} as deleted." );
+                       }       
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
-               $groups = $meetup_client->get_groups();
-               if ( ! empty( $meetup_client->error->errors ) ) {
-                       $this->log( 'Failed to fetch groups: ' . wp_json_encode( $meetup_client->error ), true );
-                       return;
-               }
-               $group_urlnames = wp_list_pluck( $groups, 'urlname' );
-
-               foreach ( $chunked_db_events as $group_url => $db_events ) {
-                       $url_name = trim( wp_parse_url( $group_url, PHP_URL_PATH ), '/' );
-
-                       if ( ! in_array( $url_name, $group_urlnames, true ) ) {
-                               // The group doesn't exist anymore, mark its events as deleted.
-                               foreach ( $db_events as $db_event ) {
-                                       $wpdb->update( self::EVENTS_TABLE, array( 'status' => 'deleted' ), array( 'id' => $db_event->id ) );
-
-                                       $this->log( "Group missing. Marked {$db_event->source_id} as deleted." );
-                               }
-
-                               continue;
-                       }
-
-                       $events = $meetup_client->get_group_events(
-                               $url_name,
-                               array(
-                                       'status' => 'upcoming,cancelled',
-                               )
-                       );
-
-                       // Make sure we have a valid API response, to avoid marking events as deleted just because the request failed.
-                       if ( is_wp_error( $events ) ) {
-                               $breaking_error_codes = array( 'invalid_grant', 'auth_fail' );
-                               if ( in_array( $events->get_error_code(), $breaking_error_codes, true ) ) {
-                                       $this->log(
-                                               sprintf(
-                                                       'Failed to check for deleted meetup events: %s',
-                                                       esc_html( $events->get_error_message() )
-                                               ),
-                                               true
-                                       );
-                                       break;
-                               }
-
-                               continue;
-                       }
-
-                       $api_events = wp_list_pluck( $events, 'id' );
-
-                       foreach ( $db_events as $db_event ) {
-                               // If the event is still appearing in the Meetup.com API results, it hasn't been deleted.
-                               if ( in_array( $db_event->source_id, $api_events, true ) ) {
-                                       continue;
-                               }
-
-                               // The event is missing from a valid response, so assume that it's been deleted.
-                               $wpdb->update( self::EVENTS_TABLE, array( 'status' => 'deleted' ), array( 'id' => $db_event->id ) );
-
-                               $this->log( "Event missing. Marked {$db_event->source_id} as deleted." );
-                       }
-               }
</del><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span></span></pre>
</div>
</div>

</body>
</html>