Compare commits

...

13 Commits

Author SHA1 Message Date
6346aae533 Decrese log level padding 2025-05-24 13:21:15 +02:00
d6c646e45c improve log level formtting 2025-05-23 08:20:23 +02:00
d7030d3dd4 allow strings for user ids 2025-05-22 04:57:51 +02:00
2ab290c712 allow username in getUser 2025-05-20 20:31:04 +02:00
b18e2ceea2 remove commented out code 2025-05-20 19:46:39 +02:00
56078fabad fixed poll parsing 2025-05-20 19:46:05 +02:00
5d009163f0 add debug logging 2025-05-20 18:59:17 +02:00
4ceeba7f4b updated poll parsing 2025-05-20 18:57:01 +02:00
7f0f8ac95b Add User::getPresenceString 2025-05-20 17:51:25 +02:00
848c1ae91e update dependencies 2025-05-15 13:50:37 +02:00
3cc7c812e8 add more logging options 2025-05-13 07:13:04 +02:00
77ae2cc627 set app name before creating logger 2025-05-06 21:44:57 +02:00
62820fd272 fix missing / 2025-05-06 20:34:40 +02:00
7 changed files with 415 additions and 334 deletions

View File

@ -3,7 +3,7 @@
"ignoredFilePatterns" : [ "ignoredFilePatterns" : [
"logs" "logs"
], ],
"remotePath" : "\/var\/www\/html\/Dragonpolls\/vendor\/hutattedonmyarm\/apnuti", "remotePath" : "\/var\/www\/dragonpolls\/vendor\/hutattedonmyarm\/apnuti",
"server" : "Phlaym", "server" : "Rabenberger Photos",
"usesPublishing" : true "usesPublishing" : true
} }

View File

@ -6,9 +6,9 @@
"keywords": ["pnut", "api"], "keywords": ["pnut", "api"],
"license": "MIT", "license": "MIT",
"require": { "require": {
"monolog/monolog": "^2.0", "monolog/monolog": "^3.0",
"psr/log": "^1.1", "psr/log": "^2.0 || ^3.0",
"php": "^7.4|^8.0", "php": "^8.3",
"ext-pdo": "*", "ext-pdo": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-json": "*", "ext-json": "*",

83
composer.lock generated
View File

@ -4,63 +4,70 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "3c041ce633113232f43e991a77d802f9", "content-hash": "fd959c4dda47577b06ccb0f69ab89de8",
"packages": [ "packages": [
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "2.2.0", "version": "3.9.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Seldaek/monolog.git", "url": "https://github.com/Seldaek/monolog.git",
"reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084" "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/1cb1cde8e8dd0f70cc0fe51354a59acad9302084", "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6",
"reference": "1cb1cde8e8dd0f70cc0fe51354a59acad9302084", "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.2", "php": ">=8.1",
"psr/log": "^1.0.1" "psr/log": "^2.0 || ^3.0"
}, },
"provide": { "provide": {
"psr/log-implementation": "1.0.0" "psr/log-implementation": "3.0.0"
}, },
"require-dev": { "require-dev": {
"aws/aws-sdk-php": "^2.4.9 || ^3.0", "aws/aws-sdk-php": "^3.0",
"doctrine/couchdb": "~1.0@dev", "doctrine/couchdb": "~1.0@dev",
"elasticsearch/elasticsearch": "^7", "elasticsearch/elasticsearch": "^7 || ^8",
"graylog2/gelf-php": "^1.4.2", "ext-json": "*",
"graylog2/gelf-php": "^1.4.2 || ^2.0",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.2",
"mongodb/mongodb": "^1.8", "mongodb/mongodb": "^1.8",
"php-amqplib/php-amqplib": "~2.4", "php-amqplib/php-amqplib": "~2.4 || ^3",
"php-console/php-console": "^3.1.3", "php-console/php-console": "^3.1.8",
"phpspec/prophecy": "^1.6.1", "phpstan/phpstan": "^2",
"phpstan/phpstan": "^0.12.59", "phpstan/phpstan-deprecation-rules": "^2",
"phpunit/phpunit": "^8.5", "phpstan/phpstan-strict-rules": "^2",
"predis/predis": "^1.1", "phpunit/phpunit": "^10.5.17 || ^11.0.7",
"rollbar/rollbar": "^1.3", "predis/predis": "^1.1 || ^2",
"ruflin/elastica": ">=0.90 <7.0.1", "rollbar/rollbar": "^4.0",
"swiftmailer/swiftmailer": "^5.3|^6.0" "ruflin/elastica": "^7 || ^8",
"symfony/mailer": "^5.4 || ^6",
"symfony/mime": "^5.4 || ^6"
}, },
"suggest": { "suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server", "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
"ext-mbstring": "Allow to work properly with unicode symbols", "ext-mbstring": "Allow to work properly with unicode symbols",
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
"ext-openssl": "Required to send log messages using SSL",
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
"php-console/php-console": "Allow sending log messages to Google Chrome",
"rollbar/rollbar": "Allow sending log messages to Rollbar", "rollbar/rollbar": "Allow sending log messages to Rollbar",
"ruflin/elastica": "Allow sending log messages to an Elastic Search server" "ruflin/elastica": "Allow sending log messages to an Elastic Search server"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "2.x-dev" "dev-main": "3.x-dev"
} }
}, },
"autoload": { "autoload": {
@ -88,7 +95,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Seldaek/monolog/issues", "issues": "https://github.com/Seldaek/monolog/issues",
"source": "https://github.com/Seldaek/monolog/tree/2.2.0" "source": "https://github.com/Seldaek/monolog/tree/3.9.0"
}, },
"funding": [ "funding": [
{ {
@ -100,34 +107,34 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2020-12-14T13:15:25+00:00" "time": "2025-03-24T10:02:05+00:00"
}, },
{ {
"name": "psr/log", "name": "psr/log",
"version": "1.1.3", "version": "3.0.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/php-fig/log.git", "url": "https://github.com/php-fig/log.git",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=5.3.0" "php": ">=8.0.0"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "1.1.x-dev" "dev-master": "3.x-dev"
} }
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Psr\\Log\\": "Psr/Log/" "Psr\\Log\\": "src"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
@ -137,7 +144,7 @@
"authors": [ "authors": [
{ {
"name": "PHP-FIG", "name": "PHP-FIG",
"homepage": "http://www.php-fig.org/" "homepage": "https://www.php-fig.org/"
} }
], ],
"description": "Common interface for logging libraries", "description": "Common interface for logging libraries",
@ -148,24 +155,24 @@
"psr-3" "psr-3"
], ],
"support": { "support": {
"source": "https://github.com/php-fig/log/tree/1.1.3" "source": "https://github.com/php-fig/log/tree/3.0.2"
}, },
"time": "2020-03-23T09:12:05+00:00" "time": "2024-09-11T13:17:53+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": [], "stability-flags": {},
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^7.4|^8.0", "php": "^8.3",
"ext-pdo": "*", "ext-pdo": "*",
"ext-curl": "*", "ext-curl": "*",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*" "ext-mbstring": "*"
}, },
"platform-dev": [], "platform-dev": {},
"plugin-api-version": "2.0.0" "plugin-api-version": "2.6.0"
} }

View File

@ -14,8 +14,12 @@ use APnutI\Exceptions\NotSupportedPollException;
use APnutI\Exceptions\HttpPnutForbiddenException; use APnutI\Exceptions\HttpPnutForbiddenException;
use APnutI\Exceptions\PollAccessRestrictedException; use APnutI\Exceptions\PollAccessRestrictedException;
use APnutI\Meta; use APnutI\Meta;
use APnutI\Logger\LevelNamePaddingProcessor;
use Monolog\Logger; use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler; use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogHandler;
use Monolog\Formatter\LineFormatter;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
@ -46,7 +50,7 @@ class APnutI
public string $app_name = 'Abstract API'; public string $app_name = 'Abstract API';
public LoggerInterface $logger; public LoggerInterface $logger;
/* /*
* Error codes: * Error codes:
* 3XX: Pnut error * 3XX: Pnut error
* - 300: Cannot fetch post creator * - 300: Cannot fetch post creator
@ -78,20 +82,45 @@ class APnutI
?string $app_name = null, ?string $app_name = null,
?string $redirect_uri = null, ?string $redirect_uri = null,
?string $log_path = null, ?string $log_path = null,
$log_level = null string|int|null $log_level = null,
bool $use_rotating_file_handler = true,
bool $use_stream_handler = false,
bool $use_syslog_handler = false,
) { ) {
if (!empty($app_name)) {
$this->app_name = $app_name;
}
if (empty($log_level)) { if (empty($log_level)) {
$log_level = Logger::INFO; $log_level = Logger::INFO;
} elseif (is_string($log_level)) { } elseif (is_string($log_level)) {
$log_level = constant('Monolog\Logger::' . $log_level); $log_level = constant('Monolog\Logger::' . $log_level);
} }
$this->logger = empty($log_path) $this->logger = empty($log_path)
? new NullLogger() ? new NullLogger()
: new Logger($this->app_name); : new Logger($this->app_name);
$this->token_session_key = $this->app_name . 'access_token'; $this->token_session_key = $this->app_name . 'access_token';
$this->token_redirect_after_auth = $this->app_name . 'redirect_after_auth'; $this->token_redirect_after_auth = $this->app_name . 'redirect_after_auth';
$handler = new RotatingFileHandler($log_path, 5, $log_level, true);
$this->logger->pushHandler($handler); $this->logger->pushProcessor(new LevelNamePaddingProcessor());
$formatter = new LineFormatter("[%datetime%] %channel%.%extra.level_padded%: %message% %context% %extra%\n");
if ($use_rotating_file_handler) {
$handler = new RotatingFileHandler($log_path, 5, $log_level, true);
$handler->setFormatter($formatter);
$this->logger->pushHandler($handler);
}
if ($use_stream_handler) {
$handler = new StreamHandler($log_path, $log_level, true);
$handler->setFormatter($formatter);
$this->logger->pushHandler($handler);
}
if ($use_syslog_handler) {
$handler = new SyslogHandler($this->app_name, \LOG_USER, $log_level, true);
$handler->setFormatter($formatter);
$this->logger->pushHandler($handler);
}
$this->server_token = null; $this->server_token = null;
$this->logger->debug('__construct API'); $this->logger->debug('__construct API');
if (isset($_SESSION[$this->token_session_key])) { if (isset($_SESSION[$this->token_session_key])) {
@ -112,21 +141,18 @@ class APnutI
if (!empty($redirect_uri)) { if (!empty($redirect_uri)) {
$this->redirect_uri = $redirect_uri; $this->redirect_uri = $redirect_uri;
} }
if (!empty($app_name)) {
$this->app_name = $app_name;
}
} }
/** /**
* Internal function, parses out important information pnut.io adds * Internal function, parses out important information pnut.io adds
* to the headers. Mostly taken from PHPnut * to the headers. Mostly taken from PHPnut
*/ */
protected function parseHeaders(string $response): string protected function parseHeaders(string $response): string
{ {
// take out the headers // take out the headers
// set internal variables // set internal variables
// return the body/content // return the body/content
/*$this->rate_limit = null; /*$this->rate_limit = null;
$this->rate_limit_remaining = null; $this->rate_limit_remaining = null;
$this->rate_limit_reset = null;*/ $this->rate_limit_reset = null;*/
$this->scopes = []; $this->scopes = [];
@ -142,9 +168,9 @@ class APnutI
} else { } else {
$content = ''; $content = '';
} }
// this is not a good way to parse http headers // this is not a good way to parse http headers
// it will not (for example) take into account multiline 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 // but what we're looking for is pretty basic, so we can ignore those shortcomings
$this->headers = explode("\r\n", $headers); $this->headers = explode("\r\n", $headers);
foreach ($this->headers as $header) { foreach ($this->headers as $header) {
$header = explode(': ', $header, 2); $header = explode(': ', $header, 2);
@ -195,13 +221,13 @@ class APnutI
$headers = []; $headers = [];
$use_server_token = false; $use_server_token = false;
if ($method !== 'GET') { 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); curl_setopt($ch, CURLOPT_POST, true);
if (is_array($parameters) && $method !== 'POST-RAW') { if (is_array($parameters) && $method !== 'POST-RAW') {
$parameters = $parameters =
$content_type === 'application/json' $content_type === 'application/json'
? json_encode($parameters) ? json_encode($parameters)
: http_build_query($parameters); : http_build_query($parameters);
} }
curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters); curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters);
$headers[] = 'Content-Type: ' . $content_type; $headers[] = 'Content-Type: ' . $content_type;
@ -248,7 +274,7 @@ class APnutI
if (!empty($response)) { if (!empty($response)) {
$response = $this->parseHeaders($response); $response = $this->parseHeaders($response);
if ($http_status == 302) { 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}"); $this->logger->debug("302 Redirect to {$this->redirect_target}");
throw new HttpPnutRedirectException($this->redirect_target); throw new HttpPnutRedirectException($this->redirect_target);
} }
@ -264,7 +290,7 @@ class APnutI
$headers_string = print_r($this->headers, true); $headers_string = print_r($this->headers, true);
$this->logger->error("Error, not authorized: {$nae->getMessage()}"); $this->logger->error("Error, not authorized: {$nae->getMessage()}");
$this->logger->error("Headers: {$headers_string}"); $this->logger->error("Headers: {$headers_string}");
# Force re-auth # Force re-auth
if (!$use_server_token) { if (!$use_server_token) {
$this->logout(); $this->logout();
} }
@ -281,7 +307,7 @@ class APnutI
throw $pe; throw $pe;
} }
// look for errors // look for errors
if (isset($response['error'])) { if (isset($response['error'])) {
if (is_array($response['error'])) { if (is_array($response['error'])) {
throw new PnutException( throw new PnutException(
@ -291,7 +317,7 @@ class APnutI
} else { } else {
throw new PnutException($response['error']); throw new PnutException($response['error']);
} }
// look for response migration errors // look for response migration errors
} elseif (isset($response['meta'], $response['meta']['error_message'])) { } elseif (isset($response['meta'], $response['meta']['error_message'])) {
throw new PnutException( throw new PnutException(
$response['meta']['error_message'], $response['meta']['error_message'],
@ -311,8 +337,8 @@ class APnutI
} else { } else {
throw new PnutException( throw new PnutException(
'No response ' . 'No response ' .
json_encode($response) . json_encode($response) .
", http status: {$http_status}" ", http status: {$http_status}"
); );
} }
} }
@ -359,26 +385,26 @@ class APnutI
$redirect_uri .= $append_redirect_query_string; $redirect_uri .= $append_redirect_query_string;
} }
$url = $url =
$this->auth_url . $this->auth_url .
'?client_id=' . '?client_id=' .
$this->client_id . $this->client_id .
'&redirect_uri=' . '&redirect_uri=' .
urlencode($redirect_uri) . urlencode($redirect_uri) .
'&scope=' . '&scope=' .
$this->needed_scope . $this->needed_scope .
'&response_type=code'; '&response_type=code';
$this->logger->debug('Auth URL: ' . $url); $this->logger->debug('Auth URL: ' . $url);
return $url; return $url;
} }
//TODO: Ping server and validate token //TODO: Ping server and validate token
public function isAuthenticated( public function isAuthenticated(
bool $allow_server_token = false, bool $allow_server_token = false,
bool $skip_verify_token = false bool $skip_verify_token = false
): bool { ): bool {
$is_authenticated = $is_authenticated =
($allow_server_token && !empty($this->server_token)) || ($allow_server_token && !empty($this->server_token)) ||
isset($this->access_token); isset($this->access_token);
$log_str = $is_authenticated ? 'Authenticated' : 'Not authenticated'; $log_str = $is_authenticated ? 'Authenticated' : 'Not authenticated';
$this->logger->info( $this->logger->info(
"Checking auth status for app: {$this->app_name}: {$log_str}" "Checking auth status for app: {$this->app_name}: {$log_str}"
@ -396,11 +422,11 @@ class APnutI
{ {
$this->logger->debug("Authenticating: {$auth_code}"); $this->logger->debug("Authenticating: {$auth_code}");
$parameters = [ $parameters = [
'client_id' => $this->client_id, 'client_id' => $this->client_id,
'client_secret' => $this->client_secret, 'client_secret' => $this->client_secret,
'code' => $auth_code, 'code' => $auth_code,
'redirect_uri' => $this->redirect_uri, 'redirect_uri' => $this->redirect_uri,
'grant_type' => 'authorization_code', 'grant_type' => 'authorization_code',
]; ];
$resp = $this->post( $resp = $this->post(
'/oauth/access_token', '/oauth/access_token',
@ -427,15 +453,15 @@ class APnutI
$this->current_user = null; $this->current_user = null;
} }
// TODO // TODO
public function getPostsForUser(User $user, $count = null) public function getPostsForUser(User $user, $count = null)
{ {
} }
// TODO // TODO
public function getPostsForUsername(string $username, int $count = 0) public function getPostsForUsername(string $username, int $count = 0)
{ {
/* /*
if(!$this->isAuthenticated()) { if(!$this->isAuthenticated()) {
throw new NotAuthorizedException("Cannot retrieve posts, "); throw new NotAuthorizedException("Cannot retrieve posts, ");
} }
@ -467,7 +493,7 @@ class APnutI
$args['count'] = $count; $args['count'] = $count;
} }
$post_obj = []; $post_obj = [];
/* /*
* Stop fetching if: * Stop fetching if:
* - count($posts) >= $count and $count != 0 * - count($posts) >= $count and $count != 0
* - OR: meta['more'] is false * - OR: meta['more'] is false
@ -484,33 +510,40 @@ class APnutI
return $post_obj; return $post_obj;
} }
// TODO Maybe support additional polls? // TODO Maybe support additional polls?
public function getPollsFromUser(int $user_id, array $params = []): array public function getPollsFromUser(int|string $user_id, array $params = []): array
{ {
$parameters = [ $parameters = [
'raw_types' => 'io.pnut.core.poll-notice', 'raw_types' => 'io.pnut.core.poll-notice',
'creator_id' => $user_id, 'creator_id' => $user_id,
'include_deleted' => false, 'include_deleted' => false,
'include_client' => false, 'include_client' => false,
'include_counts' => false, 'include_counts' => false,
'include_html' => false, 'include_html' => false,
'include_mention_posts' => false, 'include_mention_posts' => false,
'include_copy_mentions' => false, 'include_copy_mentions' => false,
'include_post_raw' => true, 'include_post_raw' => true,
]; ];
foreach ($params as $param => $value) { foreach ($params as $param => $value) {
$parameters[$param] = $value; $parameters[$param] = $value;
} }
$response = $this->get('posts/search', $parameters); $response = $this->get('/posts/search', $parameters);
if (count($response) === 0) { if (count($response) === 0) {
return []; return [];
} }
$polls = []; $polls = [];
$this->logger->debug('Parsing ' . count($response) . ' results');
foreach ($response as $post) { foreach ($response as $post) {
if (!empty($post['raw'])) { if (!empty($post['raw'])) {
foreach ($post['raw'] as $raw) { foreach ($post['raw'] as $raw_type => $raw) {
if (Poll::$notice_type === $raw['type']) { if ($raw_type === Poll::$notice_type) {
$polls[] = $this->getPoll($raw['value']['poll_id']); try {
$this->logger->debug('Parsing poll from raw', $raw);
$polls[] = new Poll($post, $this);
} catch (NotSupportedPollException) {
$this->logger->warning('Parsing poll from raw failed. Loading from poll id');
$polls[] = $this->getPoll($raw[Poll::$notice_type]['poll_id']);
}
} }
} }
} }
@ -527,10 +560,10 @@ class APnutI
$this->logger->error('Poll not supported: ' . json_encode($res)); $this->logger->error('Poll not supported: ' . json_encode($res));
throw $e; throw $e;
} catch (HttpPnutForbiddenException $fe) { } 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(); throw new PollAccessRestrictedException();
} catch (NotAuthorizedException $nauth) { } catch (NotAuthorizedException $nauth) {
$this->logger->error('Not authorized when fetching poll'); $this->logger->error('Not authorized when fetching poll', ['ex' => $nauth]);
throw new PollAccessRestrictedException(); throw new PollAccessRestrictedException();
} }
} }
@ -551,16 +584,16 @@ class APnutI
$this->logger->debug('Poll token provided'); $this->logger->debug('Poll token provided');
$re = $re =
'/((http(s)?:\/\/)?((posts)|(beta))\.pnut\.io\/(@.*\/)?)?(?(1)|^)(?<postid>\d+)/$'; '/((http(s)?:\/\/)?((posts)|(beta))\.pnut\.io\/(@.*\/)?)?(?(1)|^)(?<postid>\d+)/$';
preg_match($re, $poll_token, $matches); preg_match($re, $poll_token, $matches);
if (!empty($matches['postid'])) { if (!empty($matches['postid'])) {
$this->logger->debug('Poll token is post ' . $matches['postid']); $this->logger->debug('Poll token is post ' . $matches['postid']);
$post_id = (int) $matches['postid']; $post_id = (int) $matches['postid'];
$args = [ $args = [
'include_raw' => true, 'include_raw' => true,
'include_counts' => false, 'include_counts' => false,
'include_html' => false, 'include_html' => false,
'include_post_raw' => true, 'include_post_raw' => true,
]; ];
return $this->getPollFromEndpoint('/posts/' . $post_id, $args); return $this->getPollFromEndpoint('/posts/' . $post_id, $args);
} else { } else {
@ -582,14 +615,14 @@ class APnutI
$poll_types_param = implode(',', $poll_types); $poll_types_param = implode(',', $poll_types);
$this->logger->info( $this->logger->info(
'No list of polls provided, using post search for poll types: ' . '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; $endpoint = '/posts/search?raw_types=' . $poll_types_param;
$params = [ $params = [
'include_raw' => true, 'include_raw' => true,
'include_counts' => false, 'include_counts' => false,
'include_html' => false, 'include_html' => false,
'include_post_raw' => true, 'include_post_raw' => true,
]; ];
} }
try { try {
@ -618,7 +651,7 @@ class APnutI
?string $poll_token ?string $poll_token
): Poll { ): Poll {
$params = [ $params = [
'positions' => $options, 'positions' => $options,
]; ];
if (!empty($poll_token)) { if (!empty($poll_token)) {
$params['poll_token'] = $poll_token; $params['poll_token'] = $poll_token;
@ -640,7 +673,8 @@ class APnutI
return $this->current_user; return $this->current_user;
} }
public function getUser(int $user_id, array $args = []) // User ID or user name
public function getUser(int|string $user_id, array $args = [])
{ {
return new User($this->get('/users/' . $user_id, $args), $this); return new User($this->get('/users/' . $user_id, $args), $this);
} }
@ -648,12 +682,12 @@ class APnutI
public function getPost(int $post_id, array $args = []) public function getPost(int $post_id, array $args = [])
{ {
if (!empty($this->access_token)) { if (!empty($this->access_token)) {
#$this->logger->info("AT:".$this->access_token); #$this->logger->info("AT:".$this->access_token);
} else { } else {
$this->logger->info('No AT'); $this->logger->info('No AT');
} }
// Remove in production again // Remove in production again
try { try {
$p = new Post($this->get('/posts/' . $post_id, $args), $this); $p = new Post($this->get('/posts/' . $post_id, $args), $this);
$this->logger->debug(json_encode($p)); $this->logger->debug(json_encode($p));
@ -662,7 +696,7 @@ class APnutI
$this->logger->warning( $this->logger->warning(
'NotAuthorizedException when getting post, trying without access token' 'NotAuthorizedException when getting post, trying without access token'
); );
//try again not authorized //try again not authorized
$r = $this->makeRequest( $r = $this->makeRequest(
'/get', '/get',
'/posts/' . $post_id, '/posts/' . $post_id,
@ -674,9 +708,9 @@ class APnutI
} }
} }
public function getAvatar(int $user_id, array $args = []): string public function getAvatar(int|string $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; $r = null;
try { try {
$r = $this->get( $r = $this->get(
@ -695,11 +729,11 @@ class APnutI
} }
public function getAvatarUrl( public function getAvatarUrl(
int $user_id, int|string $user_id,
?int $width = null, ?int $width = null,
?int $height = null ?int $height = null
): string { ): string {
//get returns an array with the url at idx 0 //get returns an array with the url at idx 0
$args = []; $args = [];
if (!empty($width)) { if (!empty($width)) {
$args['w'] = $width; $args['w'] = $width;
@ -757,9 +791,9 @@ class APnutI
): Post { ): Post {
$text = $auto_crop ? substr($text, 0, $this->getMaxPostLength()) : $text; $text = $auto_crop ? substr($text, 0, $this->getMaxPostLength()) : $text;
$parameters = [ $parameters = [
'text' => $text, 'text' => $text,
'reply_to' => $reply_to, 'reply_to' => $reply_to,
'is_nsfw' => $is_nsfw, 'is_nsfw' => $is_nsfw,
]; ];
if (!empty($raw)) { if (!empty($raw)) {
$parameters['raw'] = $parameters; $parameters['raw'] = $parameters;
@ -777,7 +811,7 @@ class APnutI
): Post { ): Post {
$text = $auto_crop ? substr($text, 0, $this->getMaxPostLength()) : $text; $text = $auto_crop ? substr($text, 0, $this->getMaxPostLength()) : $text;
$parameters = [ $parameters = [
'text' => $text, 'text' => $text,
]; ];
$parameters = array_merge($parameters, $params); $parameters = array_merge($parameters, $params);
$this->logger->debug('Post with params'); $this->logger->debug('Post with params');
@ -790,9 +824,9 @@ class APnutI
public function getChannel(int $channel_id): Channel 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 = [ $parameters = [
'include_channel_raw' => true, 'include_channel_raw' => true,
]; ];
return new Channel( return new Channel(
$this->get('/channels/' . $channel_id, $parameters), $this->get('/channels/' . $channel_id, $parameters),
@ -804,7 +838,7 @@ class APnutI
bool $include_pms = true, bool $include_pms = true,
bool $include_channels = true bool $include_channels = true
): array { ): array {
# Always include channel raw, it contains the channel name # Always include channel raw, it contains the channel name
$channel_types = []; $channel_types = [];
if ($include_pms) { if ($include_pms) {
$channel_types[] = 'io.pnut.core.pm'; $channel_types[] = 'io.pnut.core.pm';
@ -814,8 +848,8 @@ class APnutI
} }
$parameters = [ $parameters = [
'include_channel_raw' => true, 'include_channel_raw' => true,
'channel_types' => implode(',', $channel_types), 'channel_types' => implode(',', $channel_types),
]; ];
$channels = []; $channels = [];
$resp = $this->get('/users/me/channels/subscribed', $parameters); $resp = $this->get('/users/me/channels/subscribed', $parameters);
@ -829,14 +863,14 @@ class APnutI
{ {
$config = $this->get('/sys/config'); $config = $this->get('/sys/config');
self::$POST_MAX_LENGTH = $config['post']['max_length']; 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_MAX_LENGTH_REPOST = self::$POST_MAX_LENGTH;
self::$POST_SECONDS_BETWEEN_DUPLICATES = self::$POST_SECONDS_BETWEEN_DUPLICATES =
$config['post']['seconds_between_duplicates']; $config['post']['seconds_between_duplicates'];
self::$MESSAGE_MAX_LENGTH = $config['message']['max_length']; self::$MESSAGE_MAX_LENGTH = $config['message']['max_length'];
self::$RAW_MAX_LENGTH = $config['raw']['max_length']; self::$RAW_MAX_LENGTH = $config['raw']['max_length'];
self::$USER_DESCRIPTION_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']; self::$USER_USERNAME_MAX_LENGTH = $config['user']['username_max_length'];
$this->logger->info('-----------Pnut API config-----------'); $this->logger->info('-----------Pnut API config-----------');
$this->logger->info(''); $this->logger->info('');
@ -844,7 +878,7 @@ class APnutI
$this->logger->info('Max repost length: ' . self::$POST_MAX_LENGTH_REPOST); $this->logger->info('Max repost length: ' . self::$POST_MAX_LENGTH_REPOST);
$this->logger->info( $this->logger->info(
'Seconds between post duplicates: ' . '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('Max raw length: ' . self::$RAW_MAX_LENGTH);
$this->logger->info( $this->logger->info(
@ -875,9 +909,9 @@ class APnutI
{ {
$this->logger->info('Requesting server access token from pnut'); $this->logger->info('Requesting server access token from pnut');
$params = [ $params = [
'client_id' => $this->client_id, 'client_id' => $this->client_id,
'client_secret' => $this->client_secret, 'client_secret' => $this->client_secret,
'grant_type' => 'client_credentials', 'grant_type' => 'client_credentials',
]; ];
$resp = $this->post('/oauth/access_token', $params); $resp = $this->post('/oauth/access_token', $params);
if (!empty($resp['access_token'])) { if (!empty($resp['access_token'])) {

View File

@ -1,4 +1,5 @@
<?php <?php
namespace APnutI\Entities; namespace APnutI\Entities;
use APnutI\Entities\PollOption; use APnutI\Entities\PollOption;
@ -9,216 +10,227 @@ use APnutI\Exceptions\NotSupportedPollException;
class Poll class Poll
{ {
public \DateTime $created_at; public \DateTime $created_at;
public \DateTime $closed_at; public \DateTime $closed_at;
public int $id = 0; public int $id = 0;
public int $max_options = 0; public int $max_options = 0;
public bool $is_anonymous = false; public bool $is_anonymous = false;
public bool $is_public = false; public bool $is_public = false;
public array $options = []; public array $options = [];
public ?string $token = null; public ?string $token = null;
public string $prompt = ""; public string $prompt = '';
public ?User $user = null; public ?User $user = null;
public ?Source $source = null; public ?Source $source = null;
public string $type; public string $type;
private APnutI $api; private APnutI $api;
public static string $notice_type = 'io.pnut.core.poll-notice'; public static string $notice_type = 'io.pnut.core.poll-notice';
public static array $poll_types = [ public static array $poll_types = [
'general.poll', 'general.poll',
'net.unsweets.beta', 'net.unsweets.beta',
'io.pnut.core.poll', 'io.pnut.core.poll',
'io.broadsword.poll', 'io.broadsword.poll',
'nl.chimpnut.quizbot.attachment.poll' 'nl.chimpnut.quizbot.attachment.poll',
]; ];
public function __construct(array $data, APnutI $api) public function __construct(array $data, APnutI $api)
{ {
$this->api = $api; $this->api = $api;
$this->options = []; $this->options = [];
$type = ''; $type = '';
if (array_key_exists('type', $data) && $data['type'] === Poll::$notice_type) { if (array_key_exists('type', $data) &&
$val = $data['value']; $data['type'] === Poll::$notice_type
$this->closed_at = new \DateTime($val['closed_at']); ) {
foreach ($val['options'] as $option) { $val = $data['value'];
$this->options[] = new PollOption($option); $this->closed_at = new \DateTime($val['closed_at']);
} foreach ($val['options'] as $option) {
$this->id = (int)$val['poll_id']; $this->options[] = new PollOption($option);
$this->token = $val['poll_token']; }
$this->prompt = $val['prompt']; $this->id = (int) $val['poll_id'];
} elseif (array_key_exists('type', $data) &&in_array($data['type'], Poll::$poll_types)) { $this->token = $val['poll_token'];
$this->parsePoll($data); $this->prompt = $val['prompt'];
} elseif (array_key_exists('type', $data) &&strpos($data['type'], '.poll') !== false) { } elseif (array_key_exists('type', $data) &&
// Try parsing unknown types if they *might* be a poll in_array($data['type'], Poll::$poll_types)
try { ) {
$this->parsePoll($data); $this->parsePoll($data);
} catch (\Exception $e) { } elseif (array_key_exists('type', $data) &&
throw new NotSupportedPollException($data['type']); strpos($data['type'], '.poll') !== false
} ) {
} elseif (array_key_exists('raw', $data) && #Polls included in posts // Try parsing unknown types if they *might* be a poll
array_key_exists(Poll::$notice_type, $data['raw']) && try {
count($data['raw'][Poll::$notice_type]) > 0 $this->parsePoll($data);
) { } catch (\Exception $e) {
$poll_data = $data['raw'][Poll::$notice_type][0]; throw new NotSupportedPollException($data['type']);
if (!empty($data['source'])) { #Source is attached to post, not to poll raw }
$poll_data['source'] = $data['source']; } elseif (array_key_exists('raw', $data) && #Polls included in posts
} array_key_exists(Poll::$notice_type, $data['raw']) &&
if (!empty($data['user'])) { #User is attached to post, not to poll raw count($data['raw'][Poll::$notice_type]) > 0
$poll_data['user'] = $data['user']; ) {
} $poll_data = $data['raw'][Poll::$notice_type][0];
$type = Poll::$notice_type; if (!empty($data['source'])) {
$this->parsePoll($poll_data); #Source is attached to post, not to poll raw
} else { $poll_data['source'] = $data['source'];
throw new NotSupportedPollException($data['type']); }
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) private function parsePoll(array $data)
{ {
$this->created_at = new \DateTime($data['created_at']); $this->created_at = new \DateTime($data['created_at']);
$this->closed_at = new \DateTime($data['closed_at']); $this->closed_at = new \DateTime($data['closed_at']);
$this->id = (int)$data['id']; $this->id = (int) $data['id'];
$this->is_anonymous = array_key_exists('is_anonymous', $data) ? (bool)$data['is_anonymous'] : false; $this->is_anonymous = array_key_exists('is_anonymous', $data)
$this->max_options = array_key_exists('max_options', $data) ? (int)$data['max_options'] : 1; ? (bool) $data['is_anonymous']
$this->is_public = array_key_exists('is_public', $data) ? (bool)$data['is_public'] : false; : false;
foreach ($data['options'] as $option) { $this->max_options = array_key_exists('max_options', $data)
$this->options[] = new PollOption($option); ? (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 * Returns the most voted option. If multiple options have the same amount
* of voted, return all of them. Always returns an array! * of voted, return all of them. Always returns an array!
*/ */
public function getMostVotedOption(): array public function getMostVotedOption(): array
{ {
if (count($this->options) === 0) { if (count($this->options) === 0) {
return []; return [];
} }
$optns = [];
//$most_voted_option = $this->options[0];
$most_voted_option = null;
foreach ($this->options as $option) {
if ($option->greaterThan($most_voted_option)) {
$optns = []; $optns = [];
$most_voted_option = $option; //$most_voted_option = $this->options[0];
$optns[] = $option; $most_voted_option = null;
} elseif ($option->greaterThanOrSame($most_voted_option)) { foreach ($this->options as $option) {
$optns[] = $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 public function getMyVotes(): array
{ {
$optns = []; $optns = [];
foreach ($this->options as $option) { foreach ($this->options as $option) {
if ($option->is_your_response) { if ($option->is_your_response) {
$optns[] = $option; $optns[] = $option;
} }
}
return $optns;
} }
return $optns;
}
public static function isValidPoll(string $type): bool public static function isValidPoll(string $type): bool
{ {
return $type === Poll::$notice_type || in_array($type, Poll::$poll_types); return $type === Poll::$notice_type || in_array($type, Poll::$poll_types);
} }
public function canVote() public function canVote()
{ {
$is_authenticated = $this->api->isAuthenticated(false, true); $is_authenticated = $this->api->isAuthenticated(false, true);
return $is_authenticated && !$this->isClosed(); return $is_authenticated && !$this->isClosed();
} }
public function isClosed() public function isClosed()
{ {
return $this->closed_at <= new \DateTime(); return $this->closed_at <= new \DateTime();
} }
public function vote(array $options): Poll public function vote(array $options): Poll
{ {
return $this->api->voteInPoll($this->id, $options, $this->token); return $this->api->voteInPoll($this->id, $options, $this->token);
} }
/* /*
* This is inconsistend with most other functions (which are provided directly by the API object. * 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 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, * 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 * neither is having to make the API's logger public
* TODO for v2 I guess * TODO for v2 I guess
*/ */
public static function create( public static function create(
APnutI $api, APnutI $api,
string $prompt, string $prompt,
array $options, array $options,
int $max_options, int $max_options,
int $duration_minutes, int $duration_minutes,
bool $is_anonymous, bool $is_anonymous,
bool $is_public bool $is_public
): Poll { ): Poll {
$options = array_filter($options); $options = array_filter($options);
$options = array_map( $options = array_map(function ($v) {
function ($v) { return ['text' => $v];
return ['text' => $v]; }, $options);
}, $params = [
$options 'duration' => $duration_minutes,
); 'options' => array_filter($options), #filters empty options
$params = [ 'prompt' => $prompt,
'duration' => $duration_minutes, 'type' => 'io.pnut.core.poll',
'options' => array_filter($options), #filters empty options 'is_anonymous' => $is_anonymous,
'prompt' => $prompt, 'is_public' => $is_public,
'type' => 'io.pnut.core.poll', 'max_options' => $max_options,
'is_anonymous' => $is_anonymous, ];
'is_public' => $is_public, $api->logger->debug('Creating poll');
'max_options' => $max_options $api->logger->debug(json_encode($params));
]; $resp = $api->post('/polls', $params, 'application/json');
$api->logger->debug('Creating poll'); #TODO: Use getPollFromEndpoint
$api->logger->debug(json_encode($params)); return new Poll($resp, $api);
$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 [
public static function makePollNoticeRaw(int $poll_id, string $poll_token) 'io.pnut.core.poll-notice' => [
{ [
return [ '+io.pnut.core.poll' => [
'io.pnut.core.poll-notice' => [ 'poll_id' => $poll_id,
[ 'poll_token' => $poll_token,
'+io.pnut.core.poll' => [ ],
'poll_id' => $poll_id, ],
'poll_token' => $poll_token ],
] ];
] }
]
]; public function __toString(): string
} {
if (!empty($this->user)) {
public function __toString(): string $str = $this->user->username;
{ #$str = 'Unknown user';
if (!empty($this->user)) { } else {
$str = $this->user->username; $str = 'Unknown user';
#$str = 'Unknown user'; }
} else { return $str .
$str = 'Unknown user'; " 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');
}
} }

View File

@ -52,6 +52,17 @@ class User
} }
} }
public function getPresenceString()
{
if ($this->presence === true) {
return "online";
} elseif ($this->presence === false) {
return "offline";
} else {
return "presence unknown";
}
}
public function getAvatarUrl( public function getAvatarUrl(
?int $width = null, ?int $width = null,
?int $height = null ?int $height = null

View File

@ -0,0 +1,17 @@
<?php
namespace APnutI\Logger;
use Monolog\Processor\ProcessorInterface;
use Monolog\LogRecord;
// See: https://stackoverflow.com/a/78901964
class LevelNamePaddingProcessor implements ProcessorInterface
{
public function __invoke(LogRecord $record): LogRecord
{
$record->extra['level_padded'] = str_pad($record->level->getName(), 5, ' ', STR_PAD_RIGHT);
return $record;
}
}