Changeset 9100
- Timestamp:
- 08/13/2019 09:08:02 PM (6 years ago)
- Location:
- sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events
- Files:
-
- 2 added
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/meetup/class-meetup-client.php
r9091 r9100 1 1 <?php 2 3 2 namespace WordCamp\Utilities; 3 4 4 use WP_Error; 5 5 … … 9 9 * Class Meetup_Client 10 10 * 11 * TODO Refactor this to use the API_Client base class. 11 * Important: This class and its dependency classes are used in multiple locations in the WordPress/WordCamp 12 * ecosystem. Because of complexities around SVN externals and the reliability of GitHub's SVN bridge during deploys, 13 * it was decided to maintain multiple copies of these files rather than have SVN externals pointing to one canonical 14 * source. 15 * 16 * If you make changes to this file, make sure they are propagated to the other locations: 17 * 18 * - wordcamp: wp-content/mu-plugins/utilities 19 * - wporg: wp-content/plugins/official-wordpress-events/meetup 12 20 */ 13 class Meetup_Client {21 class Meetup_Client extends API_Client { 14 22 /** 15 23 * @var string The base URL for the API endpoints. … … 18 26 19 27 /** 20 * @var string The API key.21 */ 22 protected $ api_key = '';28 * @var Meetup_OAuth2_Client|null 29 */ 30 protected $oauth_client = null; 23 31 24 32 /** 25 33 * @var bool If true, the client will fetch fewer results, for faster debugging. 26 34 */ 27 protected $debug_mode; 28 29 /** 30 * @var WP_Error|null Container for errors. 31 */ 32 public $error = null; 35 protected $debug = false; 33 36 34 37 /** 35 38 * Meetup_Client constructor. 36 */ 37 public function __construct() { 38 $this->error = new WP_Error(); 39 40 if ( defined( 'MEETUP_API_KEY' ) ) { 41 $this->api_key = MEETUP_API_KEY; 42 } else { 43 $this->error->add( 44 'api_key_undefined', 45 'The Meetup.com API Key is undefined.' 46 ); 47 } 48 49 $this->debug_mode = apply_filters( 'wcmc_debug_mode', false ); 39 * 40 * @param array $settings { 41 * Optional. Settings for the client. 42 * 43 * @type bool $debug If true, the client will fetch fewer results, for faster debugging. 44 * } 45 */ 46 public function __construct( array $settings = [] ) { 47 parent::__construct( array( 48 /* 49 * Response codes that should break the request loop. 50 * 51 * See https://www.meetup.com/meetup_api/docs/#errors. 52 * 53 * `200` (ok) is not in the list, because it needs to be handled conditionally. 54 * See API_Client::tenacious_remote_request. 55 * 56 * `400` (bad request) is not in the list, even though it seems like it _should_ indicate an unrecoverable 57 * error. In practice we've observed that it's common for a seemingly valid request to be rejected with 58 * a `400` response, but then get a `200` response if that exact same request is retried. 59 */ 60 'breaking_response_codes' => array( 61 401, // Unauthorized (invalid key). 62 429, // Too many requests (rate-limited). 63 404, // Unable to find group 64 ), 65 'throttle_callback' => array( __CLASS__, 'throttle' ), 66 ) ); 67 68 $settings = wp_parse_args( 69 $settings, 70 array( 71 'debug' => false, 72 ) 73 ); 74 75 $this->oauth_client = new Meetup_OAuth2_Client; 76 $this->debug = $settings['debug']; 77 78 if ( $this->debug ) { 79 self::cli_message( "Meetup Client debug is ON. Results will be truncated." ); 80 } 50 81 } 51 82 … … 68 99 69 100 while ( $request_url ) { 70 $request_url = $this->sign_request_url( $request_url ); 71 72 $response = $this->tenacious_remote_get( $request_url ); 101 $response = $this->tenacious_remote_get( $request_url, $this->get_request_args() ); 73 102 74 103 if ( 200 === wp_remote_retrieve_response_code( $response ) ) { … … 98 127 } 99 128 100 if ( $this->debug _mode) {129 if ( $this->debug ) { 101 130 break; 102 131 } … … 125 154 ), $request_url ); 126 155 127 $request_url = $this->sign_request_url( $request_url ); 128 129 $response = $this->tenacious_remote_get( $request_url ); 156 $response = $this->tenacious_remote_get( $request_url, $this->get_request_args() ); 130 157 131 158 if ( 200 === wp_remote_retrieve_response_code( $response ) ) { … … 152 179 153 180 /** 154 * Wrapper for `wp_remote_get` to retry requests that fail temporarily for various reasons. 155 * 156 * One common example of a reason a request would fail, but later succeed, is when the first request times out. 157 * 158 * Based on `wcorg_redundant_remote_get`. 159 * 160 * @param string $url 161 * @param array $args 162 * 163 * @return array|WP_Error 164 */ 165 protected function tenacious_remote_get( $url, $args = array() ) { 166 $attempt_count = 0; 167 $max_attempts = 3; 168 169 /* 170 * Response codes that should break the loop. 171 * 172 * See https://www.meetup.com/meetup_api/docs/#errors. 173 * 174 * `200` (ok) is not in the list, because it needs to be handled conditionally. See below. 175 * 176 * `400` (bad request) is not in the list, even though it seems like it _should_ indicate an unrecoverable 177 * error. In practice we've observed that it's common for a seemingly valid request to be rejected with 178 * a `400` response, but then get a `200` response if that exact same request is retried. 179 */ 180 $breaking_codes = array( 181 401, // Unauthorized (invalid key). 182 429, // Too many requests (rate-limited). 183 404, // Unable to find group 181 * Generate headers to use in a request. 182 * 183 * @return array 184 */ 185 protected function get_request_args() { 186 $oauth_token = $this->oauth_client->get_oauth_token(); 187 188 return array( 189 'headers' => array( 190 'Accept' => 'application/json', 191 'Authorization' => "Bearer $oauth_token", 192 ), 184 193 ); 185 186 // The default of 5 seconds often results in frequent timeouts.187 if ( empty( $args['timeout'] ) ) {188 $args['timeout'] = 15;189 }190 191 while ( $attempt_count < $max_attempts ) {192 $response = wp_remote_get( $url, $args );193 $response_code = wp_remote_retrieve_response_code( $response );194 195 $this->maybe_throttle( wp_remote_retrieve_headers( $response ) );196 197 /*198 * Sometimes their API inexplicably returns a success code with an empty body, but will return a valid199 * response if the exact request is retried.200 */201 if ( 200 === $response_code && ! empty( wp_remote_retrieve_body( $response ) ) ) {202 break;203 }204 205 if ( in_array( $response_code, $breaking_codes, true ) ) {206 break;207 }208 209 $attempt_count++;210 211 /**212 * Action: Fires when tenacious_remote_get fails a request attempt.213 *214 * Note that the request parameter includes the request URL that contains a query string for the API key.215 * This should be redacted before outputting anywhere public.216 *217 * @param array $response218 * @param array $request219 * @param int $attempt_count220 * @param int $max_attempts221 */222 do_action( 'meetup_client_tenacious_remote_get_attempt', $response, compact( 'url', 'args' ), $attempt_count, $max_attempts );223 224 if ( $attempt_count < $max_attempts ) {225 $retry_after = wp_remote_retrieve_header( $response, 'retry-after' ) ?: 5;226 $wait = min( $retry_after * $attempt_count, 30 );227 228 if ( 'cli' === php_sapi_name() ) {229 echo "\nRequest failed $attempt_count times. Pausing for $wait seconds before retrying.";230 }231 232 sleep( $wait );233 }234 }235 236 if ( $attempt_count === $max_attempts && 'cli' === php_sapi_name() ) {237 if ( 200 !== $response_code || is_wp_error( $response ) ) {238 echo "\nRequest failed $attempt_count times. Giving up.";239 }240 }241 242 return $response;243 }244 245 /**246 * Sign a request URL with our API key.247 *248 * @param string $request_url249 *250 * @return string251 */252 protected function sign_request_url( $request_url ) {253 return add_query_arg( array(254 'sign' => true,255 'key' => $this->api_key,256 ), $request_url );257 194 } 258 195 … … 298 235 * 299 236 * @param array $headers 300 */ 301 protected function maybe_throttle( $headers ) { 237 * 238 * @return void 239 */ 240 protected static function throttle( $response ) { 241 $headers = wp_remote_retrieve_headers( $response ); 242 302 243 if ( ! isset( $headers['x-ratelimit-remaining'], $headers['x-ratelimit-reset'] ) ) { 303 244 return; … … 305 246 306 247 $remaining = absint( $headers['x-ratelimit-remaining'] ); 307 $period = absint( $headers['x-ratelimit-reset' ] ); 308 309 // Pause more frequently than we need to, and for longer, just to be safe. 310 if ( $remaining > 2 ) { 248 $period = absint( $headers['x-ratelimit-reset'] ); 249 250 /** 251 * Don't throttle if we have sufficient requests remaining. 252 * 253 * We don't let this number get to 0, though, because there are scenarios where multiple processes are using 254 * the API at the same time, and there's no way for them to be aware of each other. 255 */ 256 if ( $remaining > 3 ) { 311 257 return; 312 258 } 313 259 260 // Pause for longer than we need to, just to be safe. 314 261 if ( $period < 2 ) { 315 262 $period = 2; 316 263 } 317 264 318 if ( 'cli' === php_sapi_name() ) { 319 echo "\nPausing for $period seconds to avoid rate-limiting."; 320 } 265 self::cli_message( "\nPausing for $period seconds to avoid rate-limiting." ); 321 266 322 267 sleep( $period ); … … 326 271 * Extract error information from an API response and add it to our error handler. 327 272 * 273 * Make sure you don't include the full $response in the error as data, as that could expose sensitive information 274 * from the request payload. 275 * 328 276 * @param array|WP_Error $response 329 277 * 330 278 * @return void 331 279 */ 332 protected function handle_error_response( $response ) { 333 if ( is_wp_error( $response ) ) { 334 $codes = $response->get_error_codes(); 335 336 foreach ( $codes as $code ) { 337 $messages = $response->get_error_messages( $code ); 338 339 foreach ( $messages as $message ) { 340 $this->error->add( $code, $message ); 341 } 342 } 343 280 public function handle_error_response( $response ) { 281 if ( parent::handle_error_response( $response ) ) { 344 282 return; 345 283 } … … 350 288 if ( isset( $data['errors'] ) ) { 351 289 foreach ( $data['errors'] as $error ) { 352 $this->error->add( $error['code'], $error['message'] ); 290 $this->error->add( 291 $error['code'], 292 $error['message'] 293 ); 353 294 } 354 295 } elseif ( isset( $data['code'] ) && isset( $data['details'] ) ) { 355 $this->error->add( $data['code'], $data['details'] ); 296 $this->error->add( 297 $data['code'], 298 $data['details'] 299 ); 356 300 } elseif ( $response_code ) { 357 301 $this->error->add( … … 360 304 ); 361 305 } else { 362 $this->error->add( 'unknown_error', 'There was an unknown error.' ); 306 $this->error->add( 307 'unknown_error', 308 'There was an unknown error.' 309 ); 363 310 } 364 311 } … … 385 332 * Retrieve data about events associated with a set of groups. 386 333 * 387 * This automatically breaks up requests into chunks of 50 groups to avoid overloading the API. 388 * 389 * @param array $group_ids The IDs of the groups to get events for. 390 * @param array $args Optional. Additional request parameters. 391 * See https://www.meetup.com/meetup_api/docs/2/events/. 334 * Because of the way that the Meetup API v3 endpoints are structured, we unfortunately have to make one request 335 * (or more, if there's pagination) for each group that we want events for. When there are hundreds of groups, and 336 * we are throttling to make sure we don't get rate-limited, this process can literally take several minutes. 337 * 338 * So, when building the array for the $group_slugs parameter, it's important to filter out groups that you know 339 * will not provide relevant results. For example, if you want all events during a date range in the past, you can 340 * filter out groups that didn't join the chapter program until after your date range. 341 * 342 * Note that when using date/time related parameters in the $args array, unlike other endpoints and fields in the 343 * Meetup API which use an epoch timestamp in milliseconds, this one requires a date/time string formatted in 344 * ISO 8601, without the timezone part. Because consistency is overrated. 345 * 346 * @param array $group_slugs The URL slugs of each group to retrieve events for. Also known as `urlname`. 347 * @param array $args Optional. Additional request parameters. 348 * See https://www.meetup.com/meetup_api/docs/:urlname/events/#list 392 349 * 393 350 * @return array|WP_Error 394 351 */ 395 public function get_events( array $group_ids, array $args = array() ) { 396 $url_base = $this->api_base . '2/events'; 397 $group_chunks = array_chunk( $group_ids, 50, true ); // Meetup API sometimes throws an error with chunk size larger than 50. 398 $events = array(); 399 400 foreach ( $group_chunks as $chunk ) { 401 $query_args = array_merge( array( 402 'group_id' => implode( ',', $chunk ), 403 ), $args ); 404 405 $request_url = add_query_arg( $query_args, $url_base ); 406 407 $data = $this->send_paginated_request( $request_url ); 408 409 if ( is_wp_error( $data ) ) { 410 return $data; 411 } 412 413 $events = array_merge( $events, $data ); 352 public function get_events( array $group_slugs, array $args = array() ) { 353 $events = array(); 354 355 if ( $this->debug ) { 356 $chunked = array_chunk( $group_slugs, 10 ); 357 $group_slugs = $chunked[0]; 358 } 359 360 foreach ( $group_slugs as $group_slug ) { 361 $response = $this->get_group_events( $group_slug, $args ); 362 363 if ( is_wp_error( $response ) ) { 364 return $response; 365 } 366 367 $events = array_merge( $events, $response ); 414 368 } 415 369 … … 418 372 419 373 /** 420 * Retrieve d ata about the group. Calls https://www.meetup.com/meetup_api/docs/:urlname/#get374 * Retrieve details about a group. 421 375 * 422 376 * @param string $group_slug The slug/urlname of a group. 423 * @param array $args Optional. Additional request parameters. 377 * @param array $args Optional. Additional request parameters. 378 * See https://www.meetup.com/meetup_api/docs/:urlname/#get 424 379 * 425 380 * @return array|WP_Error 426 381 */ 427 public function get_group_details 428 $request_url = $this->api_base . "$group_slug";382 public function get_group_details( $group_slug, $args = array() ) { 383 $request_url = $this->api_base . sanitize_key( $group_slug ); 429 384 430 385 if ( ! empty( $args ) ) { … … 436 391 437 392 /** 438 * Retrieve group members. Calls https://www.meetup.com/meetup_api/docs/:urlname/members/#list393 * Retrieve details about group members. 439 394 * 440 395 * @param string $group_slug The slug/urlname of a group. 441 * @param array $args Optional. Additional request parameters. 396 * @param array $args Optional. Additional request parameters. 397 * See https://www.meetup.com/meetup_api/docs/:urlname/members/#list 442 398 * 443 399 * @return array|WP_Error 444 400 */ 445 public function get_group_members 446 $request_url = $this->api_base . "$group_slug/members";401 public function get_group_members( $group_slug, $args = array() ) { 402 $request_url = $this->api_base . sanitize_key( $group_slug ) . '/members'; 447 403 448 404 if ( ! empty( $args ) ) { … … 458 414 * @param string $group_slug The slug/urlname of a group. 459 415 * @param array $args Optional. Additional request parameters. 460 * See https://www.meetup.com/meetup_api/docs/:urlname/events/ .416 * See https://www.meetup.com/meetup_api/docs/:urlname/events/#list 461 417 * 462 418 * @return array|WP_Error 463 419 */ 464 420 public function get_group_events( $group_slug, array $args = array() ) { 465 $request_url = $this->api_base . "$group_slug/events";421 $request_url = $this->api_base . sanitize_key( $group_slug ) . '/events'; 466 422 467 423 if ( ! empty( $args ) ) { -
sites/trunk/wordpress.org/public_html/wp-content/plugins/official-wordpress-events/official-wordpress-events.php
r9091 r9100 8 8 */ 9 9 10 use WordCamp\Utilities\API_Client; 10 11 use WordCamp\Utilities\Meetup_Client; 11 12 … … 38 39 */ 39 40 public function __construct() { 40 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 41 add_action( 'owpe_prime_events_cache', array( $this, 'prime_events_cache' ) ); 42 add_action( 'owpe_mark_deleted_meetups', array( $this, 'mark_deleted_meetups' ) ); 41 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 42 add_action( 'owpe_prime_events_cache', array( $this, 'prime_events_cache' ) ); 43 add_action( 'owpe_mark_deleted_meetups', array( $this, 'mark_deleted_meetups' ) ); 44 add_action( 'api_client_handle_error_response', array( $this, 'handle_error_response' ), 10, 3 ); 45 43 46 add_shortcode( 'official_wordpress_events', array( $this, 'render_events' ) ); 44 47 … … 50 53 wp_schedule_event( time(), 'hourly', 'owpe_mark_deleted_meetups' ); 51 54 } 55 } 56 57 /** 58 * Get an instance of the generic API Client, loading files first as necessary. 59 * 60 * @return API_Client 61 */ 62 protected function get_api_client() { 63 if ( ! class_exists( '\WordCamp\Utilities\API_Client' ) ) { 64 $files = array( 65 'class-api-client.php', 66 ); 67 68 foreach ( $files as $file ) { 69 require_once trailingslashit( __DIR__ ) . "meetup/$file"; 70 } 71 } 72 73 return new API_Client(); 52 74 } 53 75 … … 60 82 if ( ! class_exists( '\WordCamp\Utilities\Meetup_Client' ) ) { 61 83 $files = array( 84 'class-api-client.php', 85 'class-meetup-oauth2-client.php', 62 86 'class-meetup-client.php', 63 87 ); … … 209 233 */ 210 234 protected function fetch_upcoming_events() { 211 $events = array_merge( $this->get_wordcamp_events(), $this->get_meetup_events() ); 235 $wordcamp_events = $this->get_wordcamp_events(); 236 $meetup_events = $this->get_meetup_events(); 237 238 $events = array_merge( $wordcamp_events, $meetup_events ); 212 239 213 240 return $events; … … 239 266 */ 240 267 protected function get_wordcamp_events() { 241 $request_params = array( 268 $api_client = $this->get_api_client(); 269 270 // Note: With the number of WordCamps per year growing fast, we may need to batch requests in the near future, like we do for meetups. 271 $request_url = add_query_arg( array( 242 272 'status' => array( 'wcpt-scheduled', 'wcpt-cancelled' ), 243 273 'per_page' => 100, 244 // Note: With the number of WordCamps per year growing fast, we may need to batch requests in the near future, like we do for meetups 245 ); 246 247 $endpoint = add_query_arg( $request_params, self::WORDCAMP_API_BASE_URL . 'wp/v2/wordcamps' ); 248 $response = $this->remote_get( esc_url_raw( $endpoint ) ); 249 $events = $this->parse_wordcamp_events( $response ); 274 ), self::WORDCAMP_API_BASE_URL . 'wp/v2/wordcamps' ); 275 276 $response = $api_client->tenacious_remote_get( $request_url ); 277 278 $api_client->handle_error_response( $response, $request_url ); 279 280 $events = $this->parse_wordcamp_events( $response ); 250 281 251 282 $this->log( sprintf( 'returning %d events', count( $events ) ) ); … … 339 370 $events = array(); 340 371 341 $client = $this->get_meetup_client(); 342 if ( ! empty( $client->error->errors ) ) { 343 $this->log( 'Failed to instantiate meetup client: ' . wp_json_encode( $client->error ), true ); 372 // Fetching events for a large number of groups from the Meetup API is currently a very inefficient process. 373 ini_set( 'memory_limit', '900M' ); 374 ini_set( 'max_execution_time', 500 ); 375 376 $meetup_client = $this->get_meetup_client(); 377 if ( ! empty( $meetup_client->error->errors ) ) { 378 $this->log( 'Failed to instantiate meetup client: ' . wp_json_encode( $meetup_client->error ), true ); 344 379 return $events; 345 380 } 346 381 347 $groups = $ client->get_groups();348 if ( ! empty( $ client->error->errors ) ) {349 $this->log( 'Failed to fetch groups: ' . wp_json_encode( $ client->error ), true );382 $groups = $meetup_client->get_groups(); 383 if ( ! empty( $meetup_client->error->errors ) ) { 384 $this->log( 'Failed to fetch groups: ' . wp_json_encode( $meetup_client->error ), true ); 350 385 return $events; 351 386 } 352 387 353 $meetups = $client->get_events( wp_list_pluck( $groups, 'id' ) ); 354 if ( ! empty( $client->error->errors ) ) { 355 $this->log( 'Failed to fetch meetups: ' . wp_json_encode( $client->error ), true ); 388 $yesterday = date( 'c', strtotime( '-1 day' ) ); 389 $one_year_out = date( 'c', strtotime( '+1 year' ) ); 390 $meetups = $meetup_client->get_events( 391 wp_list_pluck( $groups, 'urlname' ), 392 array( 393 // We want cancelled events too so they will be updated in our database table. 394 'status' => 'upcoming,cancelled', 395 // We don't want cancelled events in the past, but need some leeway here for timezones. 396 'no_earlier_than' => substr( $yesterday, 0, strpos( $yesterday, '+' ) ), 397 // We don't need to cache events happening more than a year from now. 398 'no_later_than' => substr( $one_year_out, 0, strpos( $one_year_out, '+' ) ), 399 ) 400 ); 401 if ( ! empty( $meetup_client->error->errors ) ) { 402 $this->log( 'Failed to fetch meetups: ' . wp_json_encode( $meetup_client->error ), true ); 356 403 return $events; 357 404 } 358 405 359 foreach ( $meetups as $meetup ) { 360 if ( empty( $meetup['id'] ) || empty( $meetup['name'] ) ) { 361 $this->log( 'Malformed meetup: ' . wp_json_encode( $meetup ) ); 362 continue; 363 } 364 365 $start_timestamp = ( $meetup['time'] / 1000 ) + ( $meetup['utc_offset'] / 1000 ); // convert to seconds 366 367 if ( isset( $meetup['venue'] ) ) { 368 $location = $this->format_meetup_venue_location( $meetup['venue'] ); 369 } else { 370 $geocoded_location = $this->reverse_geocode( $meetup['group']['group_lat'], $meetup['group']['group_lon'] ); 371 $location_parts = $this->parse_reverse_geocode_address( $geocoded_location ); 372 $location = sprintf( 373 '%s%s%s', 374 $location_parts['city'] ?? '', 375 empty( $location_parts['state'] ) ? '' : ', ' . $location_parts['state'], 376 empty( $location_parts['country_name'] ) ? '' : ', ' . $location_parts['country_name'] 377 ); 378 $location = trim( $location, ", \t\n\r\0\x0B" ); 379 } 380 381 if ( ! empty( $meetup['venue']['country'] ) ) { 382 $country_code = $meetup['venue']['country']; 383 } elseif ( ! empty( $location_parts['country_code'] ) ) { 384 $country_code = $location_parts['country_code']; 385 } else { 386 $country_code = ''; 387 } 388 389 $events[] = new Official_WordPress_Event( array( 390 'type' => 'meetup', 391 'source_id' => $meetup['id'], 392 'status' => 'upcoming' === $meetup['status'] ? 'scheduled' : 'cancelled', 393 'title' => $meetup['name'], 394 'url' => $meetup['event_url'], 395 'meetup_name' => $meetup['group']['name'], 396 'meetup_url' => sprintf( 'https://www.meetup.com/%s/', $meetup['group']['urlname'] ), 397 'description' => $meetup['description'] ?? '', 398 'num_attendees' => $meetup['yes_rsvp_count'], 399 'start_timestamp' => $start_timestamp, 400 'end_timestamp' => ( empty ( $meetup['duration'] ) ? $start_timestamp : $start_timestamp + ( $meetup['duration'] / 1000 ) ), // convert to seconds 401 'location' => $location, 402 'country_code' => $country_code, 403 'latitude' => empty( $meetup['venue']['lat'] ) ? $meetup['group']['group_lat'] : $meetup['venue']['lat'], 404 'longitude' => empty( $meetup['venue']['lon'] ) ? $meetup['group']['group_lon'] : $meetup['venue']['lon'], 405 ) ); 406 } 406 $events = $this->parse_meetup_events( $meetups ); 407 407 408 408 $this->log( sprintf( 'returning %d events', count( $events ) ) ); 409 410 return $events; 411 } 412 413 /** 414 * Parse meetup events out of a response from the Meetup API. 415 * 416 * @param array $meetups 417 * 418 * @return array 419 */ 420 protected function parse_meetup_events( $meetups ) { 421 $events = array(); 422 423 foreach ( $meetups as $meetup ) { 424 if ( empty( $meetup['id'] ) || empty( $meetup['name'] ) ) { 425 $this->log( 'Malformed meetup: ' . wp_json_encode( $meetup ) ); 426 continue; 427 } 428 429 $start_timestamp = ( $meetup['time'] / 1000 ) + ( $meetup['utc_offset'] / 1000 ); // convert to seconds 430 $latitude = ! empty( $meetup['venue']['lat'] ) ? $meetup['venue']['lat'] : $meetup['group']['lat']; 431 $longitude = ! empty( $meetup['venue']['lon'] ) ? $meetup['venue']['lon'] : $meetup['group']['lon']; 432 433 if ( isset( $meetup['venue'] ) ) { 434 $location = $this->format_meetup_venue_location( $meetup['venue'] ); 435 } else { 436 $geocoded_location = $this->reverse_geocode( $latitude, $longitude ); 437 $location_parts = $this->parse_reverse_geocode_address( $geocoded_location ); 438 $location = sprintf( 439 '%s%s%s', 440 $location_parts['city'] ?? '', 441 empty( $location_parts['state'] ) ? '' : ', ' . $location_parts['state'], 442 empty( $location_parts['country_name'] ) ? '' : ', ' . $location_parts['country_name'] 443 ); 444 $location = trim( $location, ", \t\n\r\0\x0B" ); 445 } 446 447 if ( ! empty( $meetup['venue']['country'] ) ) { 448 $country_code = $meetup['venue']['country']; 449 } elseif ( ! empty( $location_parts['country_code'] ) ) { 450 $country_code = $location_parts['country_code']; 451 } else { 452 $country_code = ''; 453 } 454 455 $events[] = new Official_WordPress_Event( array( 456 'type' => 'meetup', 457 'source_id' => $meetup['id'], 458 'status' => 'upcoming' === $meetup['status'] ? 'scheduled' : 'cancelled', 459 'title' => $meetup['name'], 460 'url' => $meetup['link'], 461 'meetup_name' => $meetup['group']['name'], 462 'meetup_url' => sprintf( 'https://www.meetup.com/%s/', $meetup['group']['urlname'] ), 463 'description' => $meetup['description'] ?? '', 464 'num_attendees' => $meetup['yes_rsvp_count'], 465 'start_timestamp' => $start_timestamp, 466 'end_timestamp' => ( empty ( $meetup['duration'] ) ? $start_timestamp : $start_timestamp + ( $meetup['duration'] / 1000 ) ), // convert to seconds 467 'location' => $location, 468 'country_code' => $country_code, 469 'latitude' => $latitude, 470 'longitude' => $longitude, 471 ) ); 472 } 409 473 410 474 return $events; … … 431 495 usleep( 75000 ); 432 496 433 $response = $this->remote_get( sprintf( 497 $api_client = $this->get_api_client(); 498 $request_url = sprintf( 434 499 'https://maps.googleapis.com/maps/api/geocode/json?latlng=%s,%s&sensor=false&key=%s', 435 500 $latitude, 436 501 $longitude, 437 502 OFFICIAL_WP_EVENTS_GOOGLE_MAPS_API_KEY 438 ) ); 439 $body = json_decode( wp_remote_retrieve_body( $response ) ); 503 ); 504 505 $response = $api_client->tenacious_remote_get( $request_url ); 506 $body = json_decode( wp_remote_retrieve_body( $response ) ); 440 507 441 508 if ( ! is_wp_error( $response ) && isset( $body->results ) && empty( $body->error_message ) ) { … … 448 515 } 449 516 else { 517 $api_client->handle_error_response( $response, $request_url ); 450 518 $this->log( 'geocode failed: ' . wp_json_encode( $response ) ); 451 519 } … … 571 639 $wpdb->update( self::EVENTS_TABLE, array( 'status' => 'deleted' ), array( 'id' => $db_event->id ) ); 572 640 573 if ( 'cli' === php_sapi_name() ) { 574 echo "\nMarked {$db_event->source_id} as deleted."; 575 } 576 } 577 } 578 } 579 580 /** 581 * Wrapper for wp_remote_get() 582 * 583 * This adds error logging/notification. 584 * 585 * @param string $url 586 * @param array $args 587 * 588 * @return false|array|WP_Error False if a valid $url was not passed; otherwise the results from wp_remote_get() 589 */ 590 protected function remote_get( $url, $args = array() ) { 591 $response = $error = false; 592 593 if ( $url ) { 594 $args['timeout'] = 30; 595 $response = wp_remote_get( $url, $args ); 596 597 $this->maybe_pause( wp_remote_retrieve_headers( $response ) ); 598 599 $response_code = wp_remote_retrieve_response_code( $response ); 600 $response_message = wp_remote_retrieve_response_message( $response ); 601 $response_body = wp_remote_retrieve_body( $response ); 602 603 if ( is_wp_error( $response ) ) { 604 $error_messages = implode( ', ', $response->get_error_messages() ); 605 606 if ( false === strpos( $error_messages, 'Operation timed out' ) ) { 607 $error = sprintf( 608 'Received WP_Error message: %s; Request was to %s; Arguments were: %s', 609 $error_messages, 610 $url, 611 print_r( $args, true ) 612 ); 613 } 614 } elseif ( 200 != $response_code ) { 615 // trigger_error() has a message limit of 1024 bytes, so we truncate $response['body'] to make sure that $body doesn't get truncated. 641 $this->log( "Marked {$db_event->source_id} as deleted." ); 642 } 643 } 644 } 645 646 /** 647 * Error logging and notification. 648 * 649 * Hooked to `api_client_handle_error_response`. 650 * 651 * @param array|WP_Error $response 652 * 653 * @return void 654 */ 655 protected function handle_error_response( $response, $request_url, $request_args ) { 656 $error = null; 657 658 $response_code = wp_remote_retrieve_response_code( $response ); 659 $response_message = wp_remote_retrieve_response_message( $response ); 660 $response_body = wp_remote_retrieve_body( $response ); 661 662 if ( is_wp_error( $response ) ) { 663 $error_messages = implode( ', ', $response->get_error_messages() ); 664 665 if ( false === strpos( $error_messages, 'Operation timed out' ) ) { 616 666 $error = sprintf( 617 "HTTP Code: %d\nMessage: %s\nBody: %s\nRequest URL: %s\nArgs: %s", 618 $response_code, 619 sanitize_text_field( $response_message ), 620 substr( sanitize_text_field( $response_body ), 0, 500 ), 621 $url, 622 print_r( $args, true ) 667 'Received WP_Error message: %s; Request was to %s; Arguments were: %s', 668 $error_messages, 669 $request_url, 670 print_r( $request_args, true ) 623 671 ); 624 625 $response = new WP_Error( 'owe_invalid_http_response', 'Invalid HTTP response code', $response ); 626 } 627 628 if ( $error ) { 629 $error = preg_replace( '/&key=[a-z0-9]+/i', '&key=[redacted]', $error ); 630 trigger_error( sprintf( 631 '%s error for %s: %s', 632 __METHOD__, 633 parse_url( site_url(), PHP_URL_HOST ), 672 } 673 } elseif ( 200 !== $response_code ) { 674 // trigger_error() has a message limit of 1024 bytes, so we truncate $response['body'] to make sure that $body doesn't get truncated. 675 $error = sprintf( 676 "HTTP Code: %d\nMessage: %s\nBody: %s\nRequest URL: %s\nArgs: %s", 677 $response_code, 678 sanitize_text_field( $response_message ), 679 substr( sanitize_text_field( $response_body ), 0, 500 ), 680 $request_url, 681 print_r( $request_args, true ) 682 ); 683 } 684 685 if ( $error ) { 686 $error = preg_replace( '/&key=[a-z0-9]+/i', '&key=[redacted]', $error ); 687 688 $this->log( sanitize_text_field( $error ), true ); 689 690 trigger_error( sprintf( 691 '%s error for %s: %s', 692 __METHOD__, 693 parse_url( site_url(), PHP_URL_HOST ), 694 sanitize_text_field( $error ) 695 ), E_USER_WARNING ); 696 697 $to = apply_filters( 'owe_error_email_addresses', array() ); 698 699 if ( $to && ( ! defined( 'WPORG_SANDBOXED_REQUEST' ) || ! WPORG_SANDBOXED_REQUEST ) ) { 700 wp_mail( 701 $to, 702 sprintf( 703 '%s error for %s', 704 __METHOD__, 705 parse_url( site_url(), PHP_URL_HOST ) 706 ), 634 707 sanitize_text_field( $error ) 635 ), E_USER_WARNING ); 636 637 $to = apply_filters( 'owe_error_email_addresses', array() ); 638 if ( $to && ( ! defined( 'WPORG_SANDBOXED_REQUEST' ) || ! WPORG_SANDBOXED_REQUEST ) ) { 639 wp_mail( 640 $to, 641 sprintf( 642 '%s error for %s', 643 __METHOD__, 644 parse_url( site_url(), PHP_URL_HOST ) 645 ), 646 sanitize_text_field( $error ) 647 ); 648 } 649 } 650 } 651 652 return $response; 653 } 654 655 /** 656 * Maybe pause the script to avoid rate limiting 657 * 658 * @param array $headers 659 */ 660 protected function maybe_pause( $headers ) { 661 if ( ! isset( $headers['x-ratelimit-remaining'], $headers['x-ratelimit-reset'] ) ) { 662 return; 663 } 664 665 $remaining = absint( $headers['x-ratelimit-remaining'] ); 666 $period = absint( $headers['x-ratelimit-reset'] ); 667 668 // Pause more frequently than we need to, and for longer, just to be safe 669 if ( $remaining > 2 ) { 670 return; 671 } 672 673 if ( $period < 2 ) { 674 $period = 2; 675 } 676 677 if ( 'cli' == php_sapi_name() ) { 678 echo "\nPausing for $period seconds to avoid rate-limiting."; 679 } 680 681 $this->log( 'sleeping to avoid api rate limit' ); 682 sleep( $period ); 708 ); 709 } 710 } 683 711 } 684 712 … … 697 725 $limit = 500; 698 726 $api_keys = array( MEETUP_API_KEY, OFFICIAL_WP_EVENTS_GOOGLE_MAPS_API_KEY ); 727 728 if ( 'cli' === php_sapi_name() ) { 729 echo $message; 730 } 699 731 700 732 if ( $write_to_disk ) {
Note: See TracChangeset
for help on using the changeset viewer.