diff --git a/src/APnutI.php b/src/APnutI.php index 6f0bdca..a3d4a4d 100644 --- a/src/APnutI.php +++ b/src/APnutI.php @@ -48,7 +48,7 @@ class APnutI public string $app_name = 'Abstract API'; public LoggerInterface $logger; - /* + /* * Error codes: * 3XX: Pnut error * - 300: Cannot fetch post creator @@ -94,8 +94,8 @@ class APnutI $log_level = constant('Monolog\Logger::' . $log_level); } $this->logger = empty($log_path) - ? new NullLogger() - : new Logger($this->app_name); + ? new NullLogger() + : new Logger($this->app_name); $this->token_session_key = $this->app_name . 'access_token'; $this->token_redirect_after_auth = $this->app_name . 'redirect_after_auth'; if ($use_rotating_file_handler) { @@ -132,16 +132,16 @@ class APnutI } } - /** - * Internal function, parses out important information pnut.io adds - * to the headers. Mostly taken from PHPnut - */ + /** + * Internal function, parses out important information pnut.io adds + * to the headers. Mostly taken from PHPnut + */ protected function parseHeaders(string $response): string { - // take out the headers - // set internal variables - // return the body/content - /*$this->rate_limit = null; + // take out the headers + // set internal variables + // return the body/content + /*$this->rate_limit = null; $this->rate_limit_remaining = null; $this->rate_limit_reset = null;*/ $this->scopes = []; @@ -157,9 +157,9 @@ class APnutI } else { $content = ''; } - // this is not a good way to parse http headers - // it will not (for example) take into account multiline headers - // but what we're looking for is pretty basic, so we can ignore those shortcomings + // this is not a good way to parse http headers + // it will not (for example) take into account multiline headers + // but what we're looking for is pretty basic, so we can ignore those shortcomings $this->headers = explode("\r\n", $headers); foreach ($this->headers as $header) { $header = explode(': ', $header, 2); @@ -210,13 +210,13 @@ class APnutI $headers = []; $use_server_token = false; if ($method !== 'GET') { - // if they passed an array, build a list of parameters from it + // if they passed an array, build a list of parameters from it curl_setopt($ch, CURLOPT_POST, true); if (is_array($parameters) && $method !== 'POST-RAW') { $parameters = - $content_type === 'application/json' - ? json_encode($parameters) - : http_build_query($parameters); + $content_type === 'application/json' + ? json_encode($parameters) + : http_build_query($parameters); } curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters); $headers[] = 'Content-Type: ' . $content_type; @@ -263,7 +263,7 @@ class APnutI if (!empty($response)) { $response = $this->parseHeaders($response); if ($http_status == 302) { - #echo json_encode(preg_match_all('/^Location:(.*)$/mi', $response, $matches)); + #echo json_encode(preg_match_all('/^Location:(.*)$/mi', $response, $matches)); $this->logger->debug("302 Redirect to {$this->redirect_target}"); throw new HttpPnutRedirectException($this->redirect_target); } @@ -279,7 +279,7 @@ class APnutI $headers_string = print_r($this->headers, true); $this->logger->error("Error, not authorized: {$nae->getMessage()}"); $this->logger->error("Headers: {$headers_string}"); - # Force re-auth + # Force re-auth if (!$use_server_token) { $this->logout(); } @@ -296,7 +296,7 @@ class APnutI throw $pe; } - // look for errors + // look for errors if (isset($response['error'])) { if (is_array($response['error'])) { throw new PnutException( @@ -306,7 +306,7 @@ class APnutI } else { throw new PnutException($response['error']); } - // look for response migration errors + // look for response migration errors } elseif (isset($response['meta'], $response['meta']['error_message'])) { throw new PnutException( $response['meta']['error_message'], @@ -326,8 +326,8 @@ class APnutI } else { throw new PnutException( 'No response ' . - json_encode($response) . - ", http status: {$http_status}" + json_encode($response) . + ", http status: {$http_status}" ); } } @@ -374,26 +374,26 @@ class APnutI $redirect_uri .= $append_redirect_query_string; } $url = - $this->auth_url . - '?client_id=' . - $this->client_id . - '&redirect_uri=' . - urlencode($redirect_uri) . - '&scope=' . - $this->needed_scope . - '&response_type=code'; + $this->auth_url . + '?client_id=' . + $this->client_id . + '&redirect_uri=' . + urlencode($redirect_uri) . + '&scope=' . + $this->needed_scope . + '&response_type=code'; $this->logger->debug('Auth URL: ' . $url); return $url; } - //TODO: Ping server and validate token + //TODO: Ping server and validate token public function isAuthenticated( bool $allow_server_token = false, bool $skip_verify_token = false ): bool { $is_authenticated = - ($allow_server_token && !empty($this->server_token)) || - isset($this->access_token); + ($allow_server_token && !empty($this->server_token)) || + isset($this->access_token); $log_str = $is_authenticated ? 'Authenticated' : 'Not authenticated'; $this->logger->info( "Checking auth status for app: {$this->app_name}: {$log_str}" @@ -411,11 +411,11 @@ class APnutI { $this->logger->debug("Authenticating: {$auth_code}"); $parameters = [ - 'client_id' => $this->client_id, - 'client_secret' => $this->client_secret, - 'code' => $auth_code, - 'redirect_uri' => $this->redirect_uri, - 'grant_type' => 'authorization_code', + 'client_id' => $this->client_id, + 'client_secret' => $this->client_secret, + 'code' => $auth_code, + 'redirect_uri' => $this->redirect_uri, + 'grant_type' => 'authorization_code', ]; $resp = $this->post( '/oauth/access_token', @@ -442,15 +442,15 @@ class APnutI $this->current_user = null; } - // TODO + // TODO public function getPostsForUser(User $user, $count = null) { } - // TODO + // TODO public function getPostsForUsername(string $username, int $count = 0) { - /* + /* if(!$this->isAuthenticated()) { throw new NotAuthorizedException("Cannot retrieve posts, "); } @@ -482,7 +482,7 @@ class APnutI $args['count'] = $count; } $post_obj = []; - /* + /* * Stop fetching if: * - count($posts) >= $count and $count != 0 * - OR: meta['more'] is false @@ -499,19 +499,19 @@ class APnutI return $post_obj; } - // TODO Maybe support additional polls? + // TODO Maybe support additional polls? public function getPollsFromUser(int $user_id, array $params = []): array { $parameters = [ - 'raw_types' => 'io.pnut.core.poll-notice', - 'creator_id' => $user_id, - 'include_deleted' => false, - 'include_client' => false, - 'include_counts' => false, - 'include_html' => false, - 'include_mention_posts' => false, - 'include_copy_mentions' => false, - 'include_post_raw' => true, + 'raw_types' => 'io.pnut.core.poll-notice', + 'creator_id' => $user_id, + 'include_deleted' => false, + 'include_client' => false, + 'include_counts' => false, + 'include_html' => false, + 'include_mention_posts' => false, + 'include_copy_mentions' => false, + 'include_post_raw' => true, ]; foreach ($params as $param => $value) { $parameters[$param] = $value; @@ -524,8 +524,23 @@ class APnutI foreach ($response as $post) { if (!empty($post['raw'])) { foreach ($post['raw'] as $raw) { - if (Poll::$notice_type === $raw['type']) { - $polls[] = $this->getPoll($raw['value']['poll_id']); + if (!empty($raw[Poll::$notice_type])) { + try { + $this->logger->debug('Parsing poll from raw'); + $polls[] = new Poll($raw, $this); + } catch (NotSupportedPollException) { + $this->logger->warning('Parsing poll from raw failed. Loading from poll id'); + $polls[] = $this->getPoll($raw[Poll::$notice_type]['poll_id']); + } + // if (empty($raw[Poll::$notice_type]['prompt'])) { + // $polls[] = $this->getPoll($raw[Poll::$notice_type]['poll_id']); + // } else { + // /*$poll_parsing_data = [ + // 'type' => Poll::$notice_type, + // 'value' => $raw[Poll::$notice_type] + // ];*/ + // $polls[] = new Poll($raw, $this); + // } } } } @@ -542,10 +557,10 @@ class APnutI $this->logger->error('Poll not supported: ' . json_encode($res)); throw $e; } catch (HttpPnutForbiddenException $fe) { - $this->logger->error('Poll token required and not provided!'); + $this->logger->error('Poll token required and not provided!', ['ex' => $fe]); throw new PollAccessRestrictedException(); } catch (NotAuthorizedException $nauth) { - $this->logger->error('Not authorized when fetching poll'); + $this->logger->error('Not authorized when fetching poll', ['ex' => $nauth]); throw new PollAccessRestrictedException(); } } @@ -566,16 +581,16 @@ class APnutI $this->logger->debug('Poll token provided'); $re = - '/((http(s)?:\/\/)?((posts)|(beta))\.pnut\.io\/(@.*\/)?)?(?(1)|^)(?\d+)/$'; + '/((http(s)?:\/\/)?((posts)|(beta))\.pnut\.io\/(@.*\/)?)?(?(1)|^)(?\d+)/$'; preg_match($re, $poll_token, $matches); if (!empty($matches['postid'])) { $this->logger->debug('Poll token is post ' . $matches['postid']); $post_id = (int) $matches['postid']; $args = [ - 'include_raw' => true, - 'include_counts' => false, - 'include_html' => false, - 'include_post_raw' => true, + 'include_raw' => true, + 'include_counts' => false, + 'include_html' => false, + 'include_post_raw' => true, ]; return $this->getPollFromEndpoint('/posts/' . $post_id, $args); } else { @@ -597,14 +612,14 @@ class APnutI $poll_types_param = implode(',', $poll_types); $this->logger->info( 'No list of polls provided, using post search for poll types: ' . - $poll_types_param + $poll_types_param ); $endpoint = '/posts/search?raw_types=' . $poll_types_param; $params = [ - 'include_raw' => true, - 'include_counts' => false, - 'include_html' => false, - 'include_post_raw' => true, + 'include_raw' => true, + 'include_counts' => false, + 'include_html' => false, + 'include_post_raw' => true, ]; } try { @@ -633,7 +648,7 @@ class APnutI ?string $poll_token ): Poll { $params = [ - 'positions' => $options, + 'positions' => $options, ]; if (!empty($poll_token)) { $params['poll_token'] = $poll_token; @@ -663,12 +678,12 @@ class APnutI public function getPost(int $post_id, array $args = []) { if (!empty($this->access_token)) { - #$this->logger->info("AT:".$this->access_token); + #$this->logger->info("AT:".$this->access_token); } else { $this->logger->info('No AT'); } - // Remove in production again + // Remove in production again try { $p = new Post($this->get('/posts/' . $post_id, $args), $this); $this->logger->debug(json_encode($p)); @@ -677,7 +692,7 @@ class APnutI $this->logger->warning( 'NotAuthorizedException when getting post, trying without access token' ); - //try again not authorized + //try again not authorized $r = $this->makeRequest( '/get', '/posts/' . $post_id, @@ -691,7 +706,7 @@ class APnutI public function getAvatar(int $user_id, array $args = []): string { - //get returns an array with the url at idx 0 + //get returns an array with the url at idx 0 $r = null; try { $r = $this->get( @@ -714,7 +729,7 @@ class APnutI ?int $width = null, ?int $height = null ): string { - //get returns an array with the url at idx 0 + //get returns an array with the url at idx 0 $args = []; if (!empty($width)) { $args['w'] = $width; @@ -772,9 +787,9 @@ class APnutI ): Post { $text = $auto_crop ? substr($text, 0, $this->getMaxPostLength()) : $text; $parameters = [ - 'text' => $text, - 'reply_to' => $reply_to, - 'is_nsfw' => $is_nsfw, + 'text' => $text, + 'reply_to' => $reply_to, + 'is_nsfw' => $is_nsfw, ]; if (!empty($raw)) { $parameters['raw'] = $parameters; @@ -792,7 +807,7 @@ class APnutI ): Post { $text = $auto_crop ? substr($text, 0, $this->getMaxPostLength()) : $text; $parameters = [ - 'text' => $text, + 'text' => $text, ]; $parameters = array_merge($parameters, $params); $this->logger->debug('Post with params'); @@ -805,9 +820,9 @@ class APnutI public function getChannel(int $channel_id): Channel { - # Always include channel raw, it contains the channel name + # Always include channel raw, it contains the channel name $parameters = [ - 'include_channel_raw' => true, + 'include_channel_raw' => true, ]; return new Channel( $this->get('/channels/' . $channel_id, $parameters), @@ -819,7 +834,7 @@ class APnutI bool $include_pms = true, bool $include_channels = true ): array { - # Always include channel raw, it contains the channel name + # Always include channel raw, it contains the channel name $channel_types = []; if ($include_pms) { $channel_types[] = 'io.pnut.core.pm'; @@ -829,8 +844,8 @@ class APnutI } $parameters = [ - 'include_channel_raw' => true, - 'channel_types' => implode(',', $channel_types), + 'include_channel_raw' => true, + 'channel_types' => implode(',', $channel_types), ]; $channels = []; $resp = $this->get('/users/me/channels/subscribed', $parameters); @@ -844,14 +859,14 @@ class APnutI { $config = $this->get('/sys/config'); self::$POST_MAX_LENGTH = $config['post']['max_length']; - //self::$POST_MAX_LENGTH_REPOST = $config['post']['repost_max_length']; + //self::$POST_MAX_LENGTH_REPOST = $config['post']['repost_max_length']; self::$POST_MAX_LENGTH_REPOST = self::$POST_MAX_LENGTH; self::$POST_SECONDS_BETWEEN_DUPLICATES = - $config['post']['seconds_between_duplicates']; + $config['post']['seconds_between_duplicates']; self::$MESSAGE_MAX_LENGTH = $config['message']['max_length']; self::$RAW_MAX_LENGTH = $config['raw']['max_length']; self::$USER_DESCRIPTION_MAX_LENGTH = - $config['user']['description_max_length']; + $config['user']['description_max_length']; self::$USER_USERNAME_MAX_LENGTH = $config['user']['username_max_length']; $this->logger->info('-----------Pnut API config-----------'); $this->logger->info(''); @@ -859,7 +874,7 @@ class APnutI $this->logger->info('Max repost length: ' . self::$POST_MAX_LENGTH_REPOST); $this->logger->info( 'Seconds between post duplicates: ' . - self::$POST_SECONDS_BETWEEN_DUPLICATES + self::$POST_SECONDS_BETWEEN_DUPLICATES ); $this->logger->info('Max raw length: ' . self::$RAW_MAX_LENGTH); $this->logger->info( @@ -890,9 +905,9 @@ class APnutI { $this->logger->info('Requesting server access token from pnut'); $params = [ - 'client_id' => $this->client_id, - 'client_secret' => $this->client_secret, - 'grant_type' => 'client_credentials', + 'client_id' => $this->client_id, + 'client_secret' => $this->client_secret, + 'grant_type' => 'client_credentials', ]; $resp = $this->post('/oauth/access_token', $params); if (!empty($resp['access_token'])) { diff --git a/src/Entities/Poll.php b/src/Entities/Poll.php index 5b865fe..f978d94 100644 --- a/src/Entities/Poll.php +++ b/src/Entities/Poll.php @@ -1,4 +1,5 @@ api = $api; - $this->options = []; - $type = ''; - if (array_key_exists('type', $data) && $data['type'] === Poll::$notice_type) { - $val = $data['value']; - $this->closed_at = new \DateTime($val['closed_at']); - foreach ($val['options'] as $option) { - $this->options[] = new PollOption($option); - } - $this->id = (int)$val['poll_id']; - $this->token = $val['poll_token']; - $this->prompt = $val['prompt']; - } elseif (array_key_exists('type', $data) &&in_array($data['type'], Poll::$poll_types)) { - $this->parsePoll($data); - } elseif (array_key_exists('type', $data) &&strpos($data['type'], '.poll') !== false) { - // Try parsing unknown types if they *might* be a poll - try { - $this->parsePoll($data); - } catch (\Exception $e) { - throw new NotSupportedPollException($data['type']); - } - } elseif (array_key_exists('raw', $data) && #Polls included in posts - array_key_exists(Poll::$notice_type, $data['raw']) && - count($data['raw'][Poll::$notice_type]) > 0 - ) { - $poll_data = $data['raw'][Poll::$notice_type][0]; - if (!empty($data['source'])) { #Source is attached to post, not to poll raw - $poll_data['source'] = $data['source']; - } - if (!empty($data['user'])) { #User is attached to post, not to poll raw - $poll_data['user'] = $data['user']; - } - $type = Poll::$notice_type; - $this->parsePoll($poll_data); - } else { - throw new NotSupportedPollException($data['type']); + public function __construct(array $data, APnutI $api) + { + $this->api = $api; + $this->options = []; + $type = ''; + if (array_key_exists('type', $data) && + $data['type'] === Poll::$notice_type + ) { + $val = $data['value']; + $this->closed_at = new \DateTime($val['closed_at']); + foreach ($val['options'] as $option) { + $this->options[] = new PollOption($option); + } + $this->id = (int) $val['poll_id']; + $this->token = $val['poll_token']; + $this->prompt = $val['prompt']; + } elseif (array_key_exists('type', $data) && + in_array($data['type'], Poll::$poll_types) + ) { + $this->parsePoll($data); + } elseif (array_key_exists('type', $data) && + strpos($data['type'], '.poll') !== false + ) { + // Try parsing unknown types if they *might* be a poll + try { + $this->parsePoll($data); + } catch (\Exception $e) { + throw new NotSupportedPollException($data['type']); + } + } elseif (array_key_exists('raw', $data) && #Polls included in posts + array_key_exists(Poll::$notice_type, $data['raw']) && + count($data['raw'][Poll::$notice_type]) > 0 + ) { + $poll_data = $data['raw'][Poll::$notice_type][0]; + if (!empty($data['source'])) { + #Source is attached to post, not to poll raw + $poll_data['source'] = $data['source']; + } + if (!empty($data['user'])) { + #User is attached to post, not to poll raw + $poll_data['user'] = $data['user']; + } + $type = Poll::$notice_type; + $this->parsePoll($poll_data); + } else { + throw new NotSupportedPollException($data['type']); + } + $this->type = empty($type) ? $data['type'] : $type; } - $this->type = empty($type) ? $data['type'] : $type; - } - private function parsePoll(array $data) - { - $this->created_at = new \DateTime($data['created_at']); - $this->closed_at = new \DateTime($data['closed_at']); - $this->id = (int)$data['id']; - $this->is_anonymous = array_key_exists('is_anonymous', $data) ? (bool)$data['is_anonymous'] : false; - $this->max_options = array_key_exists('max_options', $data) ? (int)$data['max_options'] : 1; - $this->is_public = array_key_exists('is_public', $data) ? (bool)$data['is_public'] : false; - foreach ($data['options'] as $option) { - $this->options[] = new PollOption($option); + private function parsePoll(array $data) + { + $this->created_at = new \DateTime($data['created_at']); + $this->closed_at = new \DateTime($data['closed_at']); + $this->id = (int) $data['id']; + $this->is_anonymous = array_key_exists('is_anonymous', $data) + ? (bool) $data['is_anonymous'] + : false; + $this->max_options = array_key_exists('max_options', $data) + ? (int) $data['max_options'] + : 1; + $this->is_public = array_key_exists('is_public', $data) + ? (bool) $data['is_public'] + : false; + foreach ($data['options'] as $option) { + $this->options[] = new PollOption($option); + } + if (!empty($data['poll_token'])) { + $this->token = $data['poll_token']; + } + $this->prompt = $data['prompt']; + if (!empty($data['user'])) { + $this->user = new User($data['user'], $this->api); + } + if (!empty($data['source'])) { + $this->source = new Source($data['source']); + } } - if (!empty($data['poll_token'])) { - $this->token = $data['poll_token']; - } - $this->prompt = $data['prompt']; - if (!empty($data['user'])) { - $this->user = new User($data['user'], $this->api); - } - if (!empty($data['source'])) { - $this->source = new Source($data['source']); - } - } - /** - * Returns the most voted option. If multiple options have the same amount - * of voted, return all of them. Always returns an array! - */ - public function getMostVotedOption(): array - { - if (count($this->options) === 0) { - return []; - } - $optns = []; - //$most_voted_option = $this->options[0]; - $most_voted_option = null; - foreach ($this->options as $option) { - if ($option->greaterThan($most_voted_option)) { + /** + * Returns the most voted option. If multiple options have the same amount + * of voted, return all of them. Always returns an array! + */ + public function getMostVotedOption(): array + { + if (count($this->options) === 0) { + return []; + } $optns = []; - $most_voted_option = $option; - $optns[] = $option; - } elseif ($option->greaterThanOrSame($most_voted_option)) { - $optns[] = $option; - } + //$most_voted_option = $this->options[0]; + $most_voted_option = null; + foreach ($this->options as $option) { + if ($option->greaterThan($most_voted_option)) { + $optns = []; + $most_voted_option = $option; + $optns[] = $option; + } elseif ($option->greaterThanOrSame($most_voted_option)) { + $optns[] = $option; + } + } + return $optns; } - return $optns; - } - public function getMyVotes(): array - { - $optns = []; - foreach ($this->options as $option) { - if ($option->is_your_response) { - $optns[] = $option; - } + public function getMyVotes(): array + { + $optns = []; + foreach ($this->options as $option) { + if ($option->is_your_response) { + $optns[] = $option; + } + } + return $optns; } - return $optns; - } - public static function isValidPoll(string $type): bool - { - return $type === Poll::$notice_type || in_array($type, Poll::$poll_types); - } + public static function isValidPoll(string $type): bool + { + return $type === Poll::$notice_type || in_array($type, Poll::$poll_types); + } - public function canVote() - { - $is_authenticated = $this->api->isAuthenticated(false, true); - return $is_authenticated && !$this->isClosed(); - } + public function canVote() + { + $is_authenticated = $this->api->isAuthenticated(false, true); + return $is_authenticated && !$this->isClosed(); + } - public function isClosed() - { - return $this->closed_at <= new \DateTime(); - } + public function isClosed() + { + return $this->closed_at <= new \DateTime(); + } - public function vote(array $options): Poll - { - return $this->api->voteInPoll($this->id, $options, $this->token); - } + public function vote(array $options): Poll + { + return $this->api->voteInPoll($this->id, $options, $this->token); + } - /* + /* * This is inconsistend with most other functions (which are provided directly by the API object. * I should probably settle on one of the two styles. * I prefer having the methods in here, but having to pass the API object along is not so great, * neither is having to make the API's logger public * TODO for v2 I guess */ - public static function create( - APnutI $api, - string $prompt, - array $options, - int $max_options, - int $duration_minutes, - bool $is_anonymous, - bool $is_public - ): Poll { - $options = array_filter($options); - $options = array_map( - function ($v) { - return ['text' => $v]; - }, - $options - ); - $params = [ - 'duration' => $duration_minutes, - 'options' => array_filter($options), #filters empty options - 'prompt' => $prompt, - 'type' => 'io.pnut.core.poll', - 'is_anonymous' => $is_anonymous, - 'is_public' => $is_public, - 'max_options' => $max_options - ]; - $api->logger->debug('Creating poll'); - $api->logger->debug(json_encode($params)); - $resp = $api->post('/polls', $params, 'application/json'); - #TODO: Use getPollFromEndpoint - return new Poll($resp, $api); - } - - public static function makePollNoticeRaw(int $poll_id, string $poll_token) - { - return [ - 'io.pnut.core.poll-notice' => [ - [ - '+io.pnut.core.poll' => [ - 'poll_id' => $poll_id, - 'poll_token' => $poll_token - ] - ] - ] - ]; - } - - public function __toString(): string - { - if (!empty($this->user)) { - $str = $this->user->username; - #$str = 'Unknown user'; - } else { - $str = 'Unknown user'; + public static function create( + APnutI $api, + string $prompt, + array $options, + int $max_options, + int $duration_minutes, + bool $is_anonymous, + bool $is_public + ): Poll { + $options = array_filter($options); + $options = array_map(function ($v) { + return ['text' => $v]; + }, $options); + $params = [ + 'duration' => $duration_minutes, + 'options' => array_filter($options), #filters empty options + 'prompt' => $prompt, + 'type' => 'io.pnut.core.poll', + 'is_anonymous' => $is_anonymous, + 'is_public' => $is_public, + 'max_options' => $max_options, + ]; + $api->logger->debug('Creating poll'); + $api->logger->debug(json_encode($params)); + $resp = $api->post('/polls', $params, 'application/json'); + #TODO: Use getPollFromEndpoint + return new Poll($resp, $api); + } + + public static function makePollNoticeRaw(int $poll_id, string $poll_token) + { + return [ + 'io.pnut.core.poll-notice' => [ + [ + '+io.pnut.core.poll' => [ + 'poll_id' => $poll_id, + 'poll_token' => $poll_token, + ], + ], + ], + ]; + } + + public function __toString(): string + { + if (!empty($this->user)) { + $str = $this->user->username; + #$str = 'Unknown user'; + } else { + $str = 'Unknown user'; + } + return $str . + " asked: '" . + $this->prompt . + "', closed at " . + $this->closed_at->format('Y-m-d H:i:s T'); } - return $str - . " asked: '" - . $this->prompt - . "', closed at " - . $this->closed_at->format('Y-m-d H:i:s T'); - } }