$did, 'password' => $password, ), null, $deadline ); if ( is_wp_error( $session ) ) { return $session; } if ( ! isset( $session['accessJwt'], $session['did'] ) || ! is_string( $session['accessJwt'] ) || $session['did'] !== $did ) { return self::error( 'atproto_comments_server' ); } return array( 'access_jwt' => $session['accessJwt'], 'did' => $session['did'], ); } /** * POST request to an XRPC method. * * @param string $pds PDS URL. * @param string $nsid Method. * @param string $body Request body. * @param string $content_type Content type. * @param string|null $token Access token, if any. * @param Deadline|null $deadline Global deadline, if any. * @param int $timeout Maximum timeout. * @return array|\WP_Error */ private static function post( $pds, $nsid, $body, $content_type, $token, $deadline, $timeout ) { return self::request( 'POST', self::url( $pds, $nsid ), $body, $content_type, $token, $deadline, $timeout ); } /** * URL of an XRPC method. * * @param string $pds PDS URL. * @param string $nsid Method. * @return string */ private static function url( $pds, $nsid ) { return rtrim( $pds, '/' ) . '/xrpc/' . $nsid; } /** * Request to an XRPC method. * * @param string $method `GET` or `POST`. * @param string $url Method URL. * @param string $pds PDS URL. * @param string|null $body Request body (POST only). * @param string|null $content_type Content type (POST only). * @param string|null $token Access token, if any. * @param Deadline|null $deadline Global deadline, if any. * @param int $timeout Maximum timeout. * @return array|\WP_Error */ private static function request( $method, $url, $body, $content_type, $token, $deadline, $timeout ) { if ( $deadline ) { if ( ! $deadline->has( Deadline::MIN_REQUEST ) ) { return Deadline::error(); } $timeout = $deadline->timeout( $timeout ); } $headers = array( 'Accept' => 'application/json' ); if ( null !== $content_type ) { $headers['Content-Type'] = $content_type; } if ( null !== $token ) { $headers['Authorization'] = 'Bearer ' . $token; } $args = array( 'timeout' => $timeout, 'redirection' => 0, 'limit_response_size' => self::MAX_RESPONSE_BYTES, 'headers' => $headers, ); if ( 'POST' === $method ) { $args['body'] = $body; $response = wp_safe_remote_post( $url, $args ); } else { $response = wp_safe_remote_get( $url, $args ); } if ( is_wp_error( $response ) ) { return self::error( 'atproto_comments_network' ); } $status = (int) wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $status >= 200 && $status < 300 ) { return is_array( $data ) ? $data : self::error( 'atproto_comments_server' ); } $name = is_array( $data ) && isset( $data['error'] ) && is_string( $data['error'] ) ? $data['error'] : ''; $error = self::error( self::error_code( $status, $name ), $name ); $error->add_data( array_merge( $error->get_error_data(), array( 'pds_status' => $status ) ) ); return $error; } /** * Plugin error code for a PDS error response. * * @param int $status HTTP status. * @param string $name XRPC `error` value. * @return string */ private static function error_code( $status, $name ) { if ( 'RecordNotFound' === $name ) { return 'atproto_comments_record_not_found'; } if ( 'AuthFactorTokenRequired' === $name ) { return 'atproto_comments_auth_factor'; } if ( in_array( $name, array( 'AccountTakedown', 'AccountDeactivated' ), true ) ) { return 'atproto_comments_account_unavailable'; } if ( 429 === $status ) { return 'atproto_comments_rate_limited'; } if ( 401 === $status || in_array( $name, array( 'AuthenticationRequired', 'InvalidPassword' ), true ) ) { return 'atproto_comments_auth'; } return 'atproto_comments_server'; } /** * Builds an error with its translated message and REST status. The XRPC * error name, if it is a plain identifier, is appended for support. * * @param string $code Error code. * @param string $name XRPC `error` value, if any. * @return \WP_Error */ public static function error( $code, $name = '' ) { switch ( $code ) { case 'atproto_comments_network': $message = __( 'The AT Protocol server did not respond. Try again later.', 'parolas' ); break; case 'atproto_comments_auth': $message = __( 'The AT Protocol server rejected the credentials. Check the account in the settings.', 'parolas' ); break; case 'atproto_comments_auth_factor': $message = __( 'The account asks for a sign-in code. Use an app password instead of the main password.', 'parolas' ); break; case 'atproto_comments_account_unavailable': $message = __( 'The AT Protocol account is suspended or deactivated.', 'parolas' ); break; case 'atproto_comments_rate_limited': $message = __( 'The AT Protocol server has received too many requests. Try again in a few minutes.', 'parolas' ); break; case 'atproto_comments_record_not_found': $message = __( 'The post does not exist on the AT Protocol server.', 'parolas' ); break; default: $code = 'atproto_comments_server'; $message = __( 'The AT Protocol server returned an unexpected response.', 'parolas' ); } if ( '' !== $name && preg_match( '/^[A-Za-z][A-Za-z0-9]{0,63}\z/', $name ) ) { $message .= ' (' . $name . ')'; } return new \WP_Error( $code, $message, array( 'status' => 'atproto_comments_rate_limited' === $code ? 429 : 502 ) ); } }