diff --git a/src/APnutI.php b/src/APnutI.php index 60e3526..dade426 100644 --- a/src/APnutI.php +++ b/src/APnutI.php @@ -3,6 +3,7 @@ namespace APnutI; use APnutI\Entities\Post; +use APnutI\Entities\Poll; use APnutI\Entities\User; use APnutI\Exceptions\PnutException; use APnutI\Exceptions\NotFoundException; @@ -17,7 +18,7 @@ use Psr\Log\NullLogger; class APnutI { - protected string $api_url = 'https://api.pnut.io/v1/'; + protected string $api_url = 'https://api.pnut.io/v1'; protected string $auth_url = 'https://pnut.io/oauth/authenticate'; protected string $client_secret; protected string $client_id; @@ -35,6 +36,7 @@ class APnutI protected ?string $access_token; protected LoggerInterface $logger; protected string $token_session_key; + protected string $token_redirect_after_auth; protected ?string $server_token_file_path = null; public ?Meta $meta = null; @@ -68,6 +70,8 @@ class APnutI { $this->logger = empty($log_path) ? 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'; $handler = new RotatingFileHandler($log_path, 5, Logger::DEBUG, true); $this->logger->pushHandler($handler); $this->server_token = null; @@ -297,7 +301,183 @@ class APnutI "Checking auth status for app: {$this->app_name}: {$log_str}" ); $this->logger->info('Referrer: '.($_SERVER['HTTP_REFERER'] ?? 'Unknown')); - $_SESSION['redirect_after_auth'] = $_SERVER['HTTP_REFERER']; + $_SESSION[$this->token_redirect_after_auth] = $_SERVER['HTTP_REFERER']; return $is_authenticated; } + + public function authenticate(string $auth_code): bool + { + $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' + ]; + $resp = $this->post( + '/oauth/access_token', + $parameters, + 'application/x-www-form-urlencoded' + ); + + if ($resp === null || !isset($resp['access_token'])) { + $this->logger->error("No access token ".json_encode($resp)); + return false; + } else { + $this->logger->debug('Received access token ' . $resp['access_token']); + $_SESSION[$this->token_session_key] = $resp['access_token']; + $this->logger->debug('Saved access token'); + $this->access_token = $resp['access_token']; + return true; + } + } + + public function logout() + { + unset($_SESSION[$this->token_session_key]); + $this->access_token = null; + } + + // TODO + public function getPostsForUser(User $user, $count = null) + { + } + + // TODO + public function getPostsForUsername(string $username, int $count = 0) + { + /* + if(!$this->isAuthenticated()) { + throw new NotAuthorizedException("Cannot retrieve posts, "); + } + */ + if (mb_substr($username, 0, 1) !== '@') { + $username = '@'.$username; + } + $params = []; + if ($count > 0) { + $params['count'] = $count; + } + $posts = $this->get('/users/' . $username . '/posts', $params); + $p = []; + foreach ($posts as $post) { + $p[] = new Post($post); + } + var_dump($p); + } + + public function searchPosts( + array $args, + bool $order_by_id = true, + int $count = 0 + ): array { + if ($order_by_id) { + $args['order'] = 'id'; + } + if ($count > 0) { + $args['count'] = $count; + } + $post_obj = []; + /* + * Stop fetching if: + * - count($posts) >= $count and $count != 0 + * - OR: meta['more'] is false + */ + do { + $posts = $this->get('/posts/search', $args); + if ($this->meta->more) { + $args['before_id'] = $this->meta->min_id; + } + foreach ($posts as $post) { + $post_obj[] = new Post($post); + } + } while ($this->meta != null + && $this->meta->more + && (count($post_obj) < $count || $count !== 0)); + return $post_obj; + } + + // 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 + ]; + foreach ($params as $param => $value) { + $parameters[$param] = $value; + } + $response = $this->get('posts/search', $parameters); + if (count($response) === 0) { + return []; + } + $polls = []; + 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']); + } + } + } + } + return $polls; + } + + public function getPoll(int $poll_id): Poll + { + return new Poll($this->get('/polls/' . $poll_id)); + } + + public function getAuthorizedUser(): User + { + return $this->getUser('/me'); + } + + public function getUser(int $user_id, array $args = []) + { + return new User($this->get('/users/'.$user_id, $args)); + } + + public function getPost(int $post_id, array $args = []) + { + if (!empty($this->access_token)) { + #$this->logger->info("AT:".$this->access_token); + } else { + $this->logger->info("No AT"); + } + + // Remove in p roduction again + try { + $p = new Post($this->get('/posts/'.$post_id, $args)); + $this->logger->debug(json_encode($p)); + return $p; + } catch (NotAuthorizedException $nae) { + $this->logger->warning( + 'NotAuthorizedException when getting post, trying without access token' + ); + //try again not authorized + $r = $this->make_request( + '/get', + '/posts/' . $post_id, + $args, + 'application/json', + true + ); + return new Post($r); + } + } + + public function getAvatar(int $user_id, array $args = []): string + { + return $this->get('/users/'.$user_id.'/avatar', $args); + } } diff --git a/src/Entities/Poll.php b/src/Entities/Poll.php new file mode 100644 index 0000000..123440d --- /dev/null +++ b/src/Entities/Poll.php @@ -0,0 +1,110 @@ +options = []; + $this->type = $data['type']; + if ($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 (in_array($data['type'], Poll::$poll_types)) { + $this->created_at = new \DateTime($data['created_at']); + $this->closed_at = new \DateTime($data['closed_at']); + $this->id = (int)$data['id']; + $this->is_anonymous = (bool)$data['is_anonymous']; + $this->is_public = (bool)$data['is_public']; + 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']); + } + if (!empty($data['source'])) { + $this->source = new Source($data['source']); + } + } else { + throw new NotSupportedPollException($data['type']); + } + } + + /** + * 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)) { + $optns = []; + $most_voted_option = $option; + $optns[] = $option; + } elseif ($option->greaterThanOrSame($most_voted_option)) { + $optns[] = $option; + } + } + return $optns; + } + + public static function isValidPoll(string $type): bool + { + return $type === Poll::$notice_type || in_array($type, Poll::$poll_types); + } + + 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'); + } +} diff --git a/src/Entities/PollOption.php b/src/Entities/PollOption.php new file mode 100644 index 0000000..f4abf8a --- /dev/null +++ b/src/Entities/PollOption.php @@ -0,0 +1,65 @@ +text = $data['text']; + $this->position = (int)$data['position']; + if (!empty($data['is_your_response'])) { + $this->is_your_response = (bool)$data['is_your_response']; + } + if (!empty($data['respondents'])) { + $this->respondents = (int)$data['respondents']; + } + if (!empty($data['respondent_ids'])) { + $this->respondent_ids = $data['respondent_ids']; + } + } + + public function greaterThan(?PollOption $option): bool + { + return empty($option) + || ($this->text != $option->text + && $this->respondents > $option->respondents); + } + + public function greaterThanOrSame(?PollOption $option): bool + { + return empty($option) + || ($this->text != $option->text + && $this->respondents >= $option->respondents); + } + + public function smallerThan(?PollOption $option): bool + { + return empty($option) + || ($this->text != $option->text + && $this->respondents < $option->respondents); + } + + public function smallerThanOrSame(?PollOption $option): bool + { + return empty($option) + || ($this->text != $option->text + && $this->respondents <= $option->respondents); + } + + public function equals(?PollOption $option): bool + { + return $this->text == $option->text + && $this->respondents == $option->respondents; + } + + public function __toString(): string + { + return $this->text . ' with ' . $this->respondents .' respondents'; + } +} diff --git a/src/Exceptions/NotSupportedPollException.php b/src/Exceptions/NotSupportedPollException.php new file mode 100644 index 0000000..8c2e792 --- /dev/null +++ b/src/Exceptions/NotSupportedPollException.php @@ -0,0 +1,7 @@ +