getTimestamp() : (int) $when; return gmdate( 'Y-m-d\TH:i:s\Z', $timestamp ); } public function get_events( $user_id, $api_filters = array() ) { $access_token = timetics_get_google_access_token($user_id); if ( ! $access_token ) { return ['error' => 'Access token not found or expired.']; } // Define the time range for the last 3 months. // // Always formatted as UTC with a trailing "Z" rather than an offset: // an offset like "+06:00" carries a plus sign that survives into the // query string, where Google reads it as a space. The bounds then fail // to parse and the API answers with no items at all — which this class // cannot distinguish from "no events", so every event silently // disappeared. "Z" sidesteps the escaping problem entirely. $three_months_ago = self::to_rfc3339_utc( strtotime( '-3 months' ) ); $three_months_ahead = self::to_rfc3339_utc( strtotime( '+3 months' ) ); $filters = array( 'timeMin' => $three_months_ago, 'timeMax' => $three_months_ahead, 'orderBy' => 'startTime', 'singleEvents' => 'true', ); // add additional api filters if needed $filters = array_merge( $filters, $api_filters ); // API URL with time range filter $api_url = add_query_arg( $filters, self::TIMETICS_CALENDAR_EVENT); // Set headers $args = [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json', ], ]; // Fetch data from Google Calendar API $response = wp_remote_get( $api_url, $args ); if ( is_wp_error( $response ) ) { return []; } $body = wp_remote_retrieve_body( $response ); $events = json_decode($body, true); if ( empty( $events['items'] ) ) { return []; } // Filter required fields $filtered_events = []; foreach ( $events['items'] as $event ) { if ( empty( $event['start'] ) ) { continue; } // Google sends `dateTime` for timed events and a date-only `date` // for all-day ones. All-day bounds have no time and no timezone, so // they must not be run through setTimezone() — that shifts the // wall-clock and used to collapse them to 00:00:00-00:00:00, which // blocked nothing at all. Note Google's all-day end date is // EXCLUSIVE: a single day off is 08-10 to 08-11. $all_day = empty( $event['start']['dateTime'] ); $start = $all_day ? $event['start']['date'] : $event['start']['dateTime']; $end = $all_day ? $event['end']['date'] : $event['end']['dateTime']; if ( $all_day ) { $filtered_events[] = [ 'id' => $event['id'] ?? '', 'all_day' => true, 'start_date' => $start, 'start_time' => '00:00:00', 'end_date' => $end, 'end_time' => '00:00:00', 'summary' => $event['summary'] ?? '', 'description' => $event['description'] ?? '', ]; continue; } $timezone = $event['start']['timeZone'] ?? timetics_wp_timezone_string(); $timezone = new \DateTimeZone( $timezone ); $start_dt = new \DateTime( $start ); $end_dt = new \DateTime( $end ); // Absolute instants, captured before the display conversion below, // so overlap maths never has to reason about wall-clock strings. $start_timestamp = $start_dt->getTimestamp(); $end_timestamp = $end_dt->getTimestamp(); $start_dt->setTimezone( $timezone ); $end_dt->setTimezone( $timezone ); $filtered_events[] = [ 'id' => $event['id'] ?? '', 'all_day' => false, 'start_date' => $start_dt->format( 'Y-m-d' ), 'start_time' => $start_dt->format( 'H:i:s' ), 'end_date' => $end_dt->format( 'Y-m-d' ), 'end_time' => $end_dt->format( 'H:i:s' ), 'start_timestamp' => $start_timestamp, 'end_timestamp' => $end_timestamp, 'timezone' => $timezone->getName(), 'summary' => $event['summary'] ?? '', 'description' => $event['description'] ?? '', ]; } return $filtered_events; } /** * Get event by ID * * @param string $event_id * * @return JSON | WP_Error */ public function get_event( $event_id , $user_id = null ) { if ( ! $user_id ) { $user_id = get_current_user_id(); } $access_token = timetics_get_google_access_token( $user_id ); $data = [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, ], ]; $response = wp_remote_get(self::TIMETICS_CALENDAR_EVENT . '/' . $event_id, $data); if ( is_wp_error( $response ) ) { return ['error' => $response->get_error_message()]; } $body = wp_remote_retrieve_body( $response ); $event = json_decode($body, true); return $event; } /** * Create event * * @param array $args Event data * * @return JSON | WP_Error */ public function create_event( $args = [] ) { $defaults = [ 'summary' => '', 'description' => '', 'location' => '', 'start' => '', 'end' => '', 'attendees' => [], 'google_meet' => true, 'access_token' => '', ]; $args = apply_filters( 'timetics/booking/create/google-event', $args); $args = wp_parse_args( $args, $defaults ); $data = $this->prepare_request_data( $args ); $query_params = build_query( [ 'conferenceDataVersion' => '1', // 'sendUpdates' => 'all', ] ); $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . '?' . $query_params, $data ); if ( is_wp_error( $response ) ) { return false; } $status_code = wp_remote_retrieve_response_code( $response ); if ( 200 != $status_code ) { return false; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); return $data; } /** * Update calender event * * @param array $args * * @return array */ public function update_event( $event_id, $args ) { $defaults = [ 'summary' => '', 'description' => '', 'location' => '', 'start' => '', 'end' => '', 'attendees' => [], 'google_meet' => true, 'access_token' => '', 'method' => 'PUT', ]; $args = wp_parse_args( $args, $defaults ); $query_params = build_query( [ 'conferenceDataVersion' => '1', 'sendUpdates' => 'all', ] ); $data = $this->prepare_request_data( $args ); $data['method'] = 'PUT'; $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . '/' . $event_id . '?' . $query_params, $data ); if ( is_wp_error( $response ) ) { return false; } $status_code = wp_remote_retrieve_response_code( $response ); if ( 200 != $status_code ) { return false; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); return $data; } /** * Get timzeson * * @return string | WP_Error */ public function get_timezone( $access_token ) { $data = [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, ], ]; $response = wp_remote_get( self::TIMETICS_TIMEZONE_URI, $data ); if ( ! is_wp_error( $response ) ) { $data = json_decode( wp_remote_retrieve_body( $response ), true ); return $data['value']; } return $response; } /** * Delete google calendar event * * @param string $event_id * * @return array */ public function delete_event( $event_id, $access_token ) { $query_params = build_query( [ 'conferenceDataVersion' => '1', 'sendUpdates' => 'all', ] ); $response = wp_remote_post( self::TIMETICS_CALENDAR_EVENT . '/' . $event_id . '?' . $query_params, [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json; charset=utf-8', ], 'method' => 'DELETE', ] ); if ( is_wp_error( $response ) ) { return false; } $status_code = wp_remote_retrieve_response_code( $response ); if ( 200 != $status_code ) { return false; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); return $data; } /** * Get timezone offset * * @param string $timezone * * @return string */ public function get_timezone_offset( $timezone ) { $current = timezone_open( $timezone ); $utc_time = new \DateTime( 'now', new \DateTimeZone( 'UTC' ) ); $offset_insecs = timezone_offset_get( $current, $utc_time ); $hours_and_sec = gmdate( 'H:i', abs( $offset_insecs ) ); return stripos( $offset_insecs, '-' ) === false ? "+{$hours_and_sec}" : "-{$hours_and_sec}"; } /** * Prepare time for calendar event * * @param array $data * * @return array */ private function prepare_time( $data, $access_token ) { $start_date = isset( $data['start']['date'] ) ? $data['start']['date'] : gmdate( 'Y-m-d' ); $start_time = isset( $data['start']['time'] ) ? $data['start']['time'] : gmdate( 'H:i:s' ); $end_date = isset( $data['end']['date'] ) ? $data['end']['date'] : gmdate( 'Y-m-d' ); $end_time = isset( $data['end']['time'] ) ? $data['end']['time'] : gmdate( 'H:i:s' ); $timezone = isset( $data['timezone'] ) ? $data['timezone'] : timetics_wp_timezone_string(); // Create DateTime objects with proper timezone to avoid double conversion. $start_datetime = new \DateTime( $start_date . ' ' . $start_time, new \DateTimeZone( $timezone ) ); $end_datetime = new \DateTime( $end_date . ' ' . $end_time, new \DateTimeZone( $timezone ) ); return [ 'start' => [ 'dateTime' => $start_datetime->format( \DateTime::RFC3339 ), 'timeZone' => $timezone, ], 'end' => [ 'dateTime' => $end_datetime->format( \DateTime::RFC3339 ), 'timeZone' => $timezone, ], ]; } /** * Convet 12 hours format to 24 hours format * * @param string $time * * @return string */ public function convertTo24HourFormat( $time ) { // Use gmdate() instead of wp_date() to avoid timezone conversion // since we're building an RFC3339 datetime string with explicit timezone offset. return gmdate( 'H:i:s', strtotime( $time ) ); } /** * Prepare event create requested data * * @param array $args * * @return array */ private function prepare_request_data( $args = [] ) { $access_token = $args['access_token']; $date = $this->prepare_time( [ 'start' => $args['start'], 'end' => $args['end'], 'timezone' => $args['timezone'], ], $access_token ); $args['start'] = [$date['start']]; $args['end'] = [$date['end']]; if ( $args['google_meet'] ) { // requestId must be unique per request; Google ignores the // conference create request (no Meet link generated) if it // matches a previously used id. $request_id = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : uniqid( 'tt-meet-', true ); $args['conferenceData'] = [ 'createRequest' => [ 'requestId' => $request_id, 'conferenceSolutionKey' => ['type' => 'hangoutsMeet'], ], ]; } unset( $args['access_token'] ); $data = [ 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'Content-Type' => 'application/json; charset=utf-8', ], 'body' => wp_json_encode( $args ), ]; return $data; } /** * Get google calendar auth scope * * @return string */ public static function scope() { return 'https://www.googleapis.com/auth/calendar'; } }