initial commit

This commit is contained in:
Max Nuding 2025-05-15 14:25:39 +02:00
commit e57af3ad36
Signed by: phlaym
SSH Key Fingerprint: SHA256:mionmF+5trOUI1AxqzAU1ZK3tv6IiDcdKGXcMWwa1nQ
9 changed files with 1073 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
history.json

735
Check_PCA.php Normal file
View File

@ -0,0 +1,735 @@
<?php
namespace PCA;
function write_pca_log($string, $level = 'INFO')
{
echo '[' . $level . '] ' . date('H:i:s') . ' ' . $string . PHP_EOL . '<br />';
file_put_contents(
get_log_file(),
'[' . $level . '] ' . date('H:i:s') . ' ' . $string . PHP_EOL,
FILE_APPEND | LOCK_EX
);
}
function get_log_file()
{
return realpath(__DIR__ . '/../pca_logs') . '/pca.log';
//return realpath(__DIR__ . '/../pca_logs') . '/' . date('Y-m-d') . '.log';
}
class User
{
public $user_name;
public $name;
public $number_posts;
public $following;
public $follows_bot;
public $last_club_notification = '';
private static function clean_api_response($api_reponse)
{
$data = [];
if (array_key_exists('data', $api_reponse)) {
$data = $api_reponse['data'];
} else {
$data = $api_reponse;
}
return $data;
}
public function __construct($api_reponse)
{
$data = self::clean_api_response($api_reponse);
$this->user_name = '@' . $data['username'];
$this->number_posts = preg_replace('/\s+/', '', $data['counts']['posts']);
$this->follows_bot = $data['follows_you'];
$this->name = array_key_exists('name', $data) ? $data['name'] : '';
}
public static function get_users_from_api_reponse($api_reponse)
{
$data = self::clean_api_response($api_reponse);
$users = [];
// Single user
if (count(array_filter(array_keys($data), 'is_string')) > 0) {
$users[] = new User($data);
} else {
// Multiple users
foreach ($data as $key => $value) {
$users[] = new User($value);
}
}
return $users;
}
public function get_highest_pca($clubs)
{
$key = $this->get_next_pca_key($clubs) - 1;
if ($key == -1) {
return [];
}
return $clubs[$key];
}
public function get_next_pca($clubs)
{
return $clubs[$this->get_next_pca_key($clubs)];
}
private function get_next_pca_key($clubs)
{
if (preg_replace('/\s+/', '', $clubs[0]['post_count']) > $this->number_posts
) {
return 0;
}
foreach ($clubs as $key => $club) {
if (preg_replace('/\s+/', '', $club['post_count']) > $this->number_posts
) {
return $key;
}
}
}
}
class API
{
public $access_token = '';
private static $api_endpoint = 'https://api.pnut.io/v1';
public $max_posttext_length = 256;
private static $settings_file_location = __DIR__
. '/..'
. '/pca_settings/user_settings.json';
private static $settings = [];
private static $default_notification_text = 'Congratulations {user.username}, you are now a member of #{pca.name} {pca.emoji} ({pca.postcount} posts)! Next: {nextpca.emoji} at {nextpca.postcount} posts';
private static $notification_tokens = [
'{user.username}',
'{user.name}',
'{pca.name}',
'{pca.emoji}',
'{pca.postcount}',
'{nextpca.name}',
'{nextpca.emoji}',
'{nextpca.postcount}',
'{posts_to_pca}',
];
public $me;
public function init()
{
write_pca_log('');
write_pca_log('=================================');
write_pca_log('');
$this->max_posttext_length = $this->get_data(
'https://api.pnut.io/v1/sys/config'
)['data']['post']['max_length'];
$this->me = User::get_users_from_api_reponse(
$this->get_data('https://api.pnut.io/v1/users/me')
)[0];
write_pca_log('Hi, my name is ' . $this->me->user_name, 'DEBUG');
$sapi_type = php_sapi_name();
$mode = !empty($_SERVER['DOCUMENT_ROOT']) &&
(substr($sapi_type, 0, 3) == 'cli' || empty($_SERVER['REMOTE_ADDR']))
? 'Debug'
: 'Production';
write_pca_log('I am currently in ' . $mode . ' mode');
if (!file_exists(self::$settings_file_location)) {
file_put_contents(self::$settings_file_location, json_encode([]));
write_pca_log(
'No user settings file found. Creating empty one at ' .
self::$settings_file_location
);
}
self::$settings = json_decode(
file_get_contents(self::$settings_file_location),
true
);
write_pca_log('User settings file loaded');
}
private function get_data(
$endpoint,
$parameters = [],
$method = 'GET',
$contenttype = 'application/x-www-form-urlencoded',
$force = false
) {
write_pca_log('Making request to pnut API at ' . $endpoint);
$postdata = $this->build_query_string($parameters);
if ($contenttype == 'application/json') {
$postdata = json_encode($parameters);
}
$context_options = [
'http' => [
'method' => $method,
'header' =>
'Content-type: ' .
$contenttype .
"\r\n" .
'Authorization: Bearer ' .
$this->access_token,
'content' => $postdata,
],
];
$sapi_type = php_sapi_name();
if (!$force
&& !empty($_SERVER['DOCUMENT_ROOT'])
&& (substr($sapi_type, 0, 3) == 'cli' || empty($_SERVER['REMOTE_ADDR']))
&& $method == 'POST'
) {
write_pca_log(
'Running from shell instead of server. Debug mode assumed. Not submitting POST requests to server!',
'DEBUG'
);
write_pca_log(
'Would have posted if run on a server: ' . $postdata,
'DEBUG'
);
return;
}
$context = stream_context_create($context_options);
$response = @file_get_contents($endpoint, false, $context);
if ($response === false) {
write_pca_log('Received no or invalid response from server', 'ERROR');
return [
'meta' => ['code' => '401', 'error_message' => 'Invalid request'],
];
}
$resp_dict = json_decode($response, true);
write_pca_log(
'Got server response. Meta: ' . json_encode($resp_dict['meta'])
);
$response_code = $resp_dict['meta']['code'];
// Success
if ($response_code >= 200 && $response_code <= 208) {
return $resp_dict;
} else {
write_pca_log(
'Received error-response from server. ' .
json_encode($resp_dict['meta']),
'ERROR'
);
// die();
}
}
public function get_messages($clubs)
{
$num_unread_endpoint =
self::$api_endpoint . '/users/me/channels/num_unread/pm';
$num_unread_pms = $this->get_data($num_unread_endpoint)['data'];
$messages = []; //Keys: username, values: message-array of messages by the user
write_pca_log($num_unread_pms . ' unread PMs');
if ($num_unread_pms > 0) {
$channels_endpoint = self::$api_endpoint .
'/users/me/channels/subscribed?include_read=0&channel_types=io.pnut.core.pm';
$channels = $this->get_data($channels_endpoint);
foreach ($channels['data'] as $channel) {
write_pca_log('Channel: ' . $channel['id'], 'DEBUG');
$messages_endpoint =
self::$api_endpoint .
'/channels/' .
$channel['id'] .
'/messages?include_deleted=0&include_html=0&include_client=0&include_marker=1';
$unread_messages = $this->get_data($messages_endpoint);
$msg = [];
$sender = '';
$last_read = $unread_messages['meta']['marker']['last_read_id'];
foreach ($unread_messages['data'] as $message) {
if ($message['id'] <= $last_read) {
//Stop when reaching already read messages
write_pca_log(
'Message ' .
$message['id'] .
'is <= last read message ' .
$last_read .
'. Skipping rest of the messages.',
'DEBUG'
);
break;
}
$sender_tmp = $message['user']['username'];
if (substr($this->me->user_name, 1) == $sender_tmp) {
//Ignore messages sent by the bot
write_pca_log(
'Ignoring message ' .
$message['id'] .
', because it was sent by myself',
'DEBUG'
);
continue;
}
$sender = $sender_tmp;
$message_text = $message['content']['text'];
$msg[] = $message['content']['text'];
write_pca_log(
'Message from ' . $sender . ': ' . $message_text,
'DEBUG'
);
}
if ($sender != '') {
$messages[$sender] = $msg;
}
}
}
return $messages;
}
public function send_message($user, $message)
{
$send_message_endpoint =
self::$api_endpoint . '/channels/pm/messages?update_marker=1';
$parameters = ['text' => $message, 'destinations' => ['@' . $user]];
$this->get_data(
$send_message_endpoint,
$parameters,
'POST',
'application/json',
$user == 'hutattedonmyarm'
);
}
public function set_notification_text_template($user_id, $text)
{
// echo json_encode(self::$settings);
self::$settings[$user_id] = $text;
$this->save_settings();
}
public function get_notification_text_template($user_id)
{
$notification_text = API::$default_notification_text;
if (array_key_exists($user_id, self::$settings)) {
write_pca_log(
$user_id .
' has custom settings: ' .
json_encode(self::$settings[$user_id])
);
$notification_text = self::$settings[$user_id];
} else {
write_pca_log($user_id . ' does not have custom settings!');
}
return $notification_text;
}
private function build_notification_text(
$user,
$clubs,
$current_pca,
$next_pca
) {
$notification_text = $this->get_notification_text_template(
$user->user_name
);
$token_values = [
$user->user_name,
$user->name,
$current_pca['pca'],
$current_pca['emoji'],
$current_pca['post_count'],
$next_pca['pca'],
$next_pca['emoji'],
$next_pca['post_count'],
$next_pca['post_count'] - $user->number_posts,
];
if (array_key_exists($user->user_name, self::$settings)) {
write_pca_log(
$user->user_name .
' has custom settings: ' .
json_encode(self::$settings[$user->username])
);
$notification_text = self::$settings[$user->username];
} else {
write_pca_log($user->user_name . ' does not have custom settings!');
}
foreach (self::$notification_tokens as $index => $token) {
$notification_text = str_replace(
$token,
$token_values[$index],
$notification_text
);
}
return $notification_text;
}
public function reset_notification_text_for_user($user_id)
{
if (array_key_exists($user_id, self::$settings)) {
unset(self::$settings[$user_id]);
$this->save_settings();
}
}
private function save_settings()
{
file_put_contents(
self::$settings_file_location,
json_encode(self::$settings)
);
}
public function write_post($posttext, $reply_to = -1)
{
$post_endpoint = self::$api_endpoint . '/posts';
$txt = mb_strimwidth($posttext, 0, $this->max_posttext_length, '');
// $txt = $posttext;
$parameters = ['text' => $txt];
if ($reply_to != -1) {
$parameters['reply_to'] = $reply_to;
}
write_pca_log(
"Writing post: '" . $txt . "' => " . json_encode($parameters)
);
$this->get_data($post_endpoint, $parameters, 'POST');
}
public function get_user($user_id)
{
if ($user_id[0] != '@') {
$user_id = '@' . $user_id;
}
$user_endpoint = self::$api_endpoint . '/users/' . $user_id;
return new User($this->get_data($user_endpoint));
}
public function get_bot_followers()
{
$before_id = null;
$users = [];
do {
write_pca_log('Getting followers before id: ' . $before_id);
$followers_endpoint = self::$api_endpoint . '/users/me/followers';
if ($before_id != null) {
$followers_endpoint .= '?before_id=' . $before_id;
}
$followers_data = $this->get_data(
$followers_endpoint, [
'before_id' => $before_id,
]
);
$before_id = $followers_data['meta']['min_id'];
$users = array_merge(
$users,
User::get_users_from_api_reponse($followers_data)
);
} while ($followers_data['meta']['more'] == '1');
return $users;
}
function build_query_string($array)
{
foreach ($array as $k => &$v) {
if ($v === true) {
$v = '1';
} elseif ($v === false) {
$v = '0';
}
unset($v);
}
return http_build_query($array);
}
public function notify_user($user, $clubs)
{
// Get current PCA
$current_pca_dict = $user->get_highest_pca($clubs);
$current_pca = '';
if (array_key_exists('pca', $current_pca_dict)) {
$current_pca = $current_pca_dict['pca'];
} else {
// If key doesn't exist, user hasn't reached a club yet
return null;
}
$last_notification = $user->last_club_notification;
$next_pca = $user->get_next_pca($clubs);
$next = [
'club' => $next_pca,
'posts_until' => $next_pca['post_count'] - $user->number_posts
];
if ($current_pca != $last_notification) {
// Haven't notified about the current club yet
// Stitch together the post itself
$this->build_notification_text(
$user,
$clubs,
$current_pca_dict,
$next_pca
);
$text_components = [];
$posttext = 'Congratulations ' .
$user->user_name .
', you are now a member of #' .
preg_replace('/\s+/', '', $current_pca) .
' ' .
$current_pca_dict['emoji'];
$text_components[] = ' ('
. $current_pca_dict['post_count']
. '+ posts)!';
$text_components[] = ' Next: ' .
$next_pca['emoji'] .
' at ' .
$next_pca['post_count'] .
' posts';
foreach ($text_components as $component) {
$available_length = $this->max_posttext_length - strlen($component);
if (strlen($posttext) < $available_length) {
$posttext .= $component;
}
}
/*
* Steps to write post as a reply to the post which made a user enter a new club:
* 1) Get number of user posts (we already have that info)
* 2) Subtract pca post count value from number posts. This will give us an offset
* 3) Get their posts with ?count=OFFSET&include_html=0&include_counts=0&include_client=0
* 4) Check if counts match. If not, get more of their posts with before_id set to the relevant ID
* 5) Repeat 4 until we have the correct post
* 6) Reply to it
*/
$pca_offset = $user->number_posts - $current_pca_dict['post_count'] + 1;
$before_id = -1;
$loop_counts = 10;
do {
$ep = self::$api_endpoint .
'/users/' .
$user->user_name .
'/posts?count=' .
$pca_offset .
'&include_html=0&include_counts=0&include_client=0&include_deleted=1';
// write_pca_log($ep, "DEBUG");
$response = $this->get_data($ep, []);
$pca_offset -= count($response['data']);
$before_id = $response['meta']['min_id'];
write_pca_log(
'Received ' . count($response['data']) . ' posts',
'DEBUG'
);
$loop_counts--;
// write_pca_log("Remaining offset: ".$pca_offset);
} while ($pca_offset > 0 && $loop_counts > 0);
$reply_to = $response['data'][count($response['data']) - 1]['id'];
// reached mac loop iterations
if (!isset($reply_to) || $reply_to == 0 || $loop_counts <= 0) {
$reply_to = -1;
}
$del_ar = $response['data'][count($response['data']) - 1];
$deleted = array_key_exists('is_deleted', $del_ar) &&
$del_ar['is_deleted'] == 'true';
$log_string = 'Post that made them reach ' .
preg_replace('/\s+/', '', $current_pca) .
': ' .
$reply_to .
'. Deleted?: ';
$log_string .= $deleted ? 'yes' : 'no';
write_pca_log($log_string);
$this->write_post($posttext, $reply_to);
$user->last_club_notification = $current_pca;
write_pca_log($posttext);
write_pca_log($log_string);
$now = \DateTime::createFromFormat('U.u', microtime(true));
$recent_changes_dict = [
'date' => $response['data'][count($response['data']) - 1]['created_at'],
'user' => $user->user_name,
'pca' => $current_pca,
'post_id' => $reply_to,
];
$history_file = 'history.json';
$inp = file_get_contents($history_file);
$tempArray = json_decode($inp, true);
if ($tempArray == null) {
$tempArray = [];
}
array_push($tempArray, $recent_changes_dict);
file_put_contents($history_file, json_encode($tempArray));
} else {
// Already notified, nothing to do
write_pca_log(
$user->user_name .
' has already been notified for reaching ' .
$current_pca
);
}
return $next;
}
}
// Get all clubs
write_pca_log('');
write_pca_log('');
write_pca_log('');
write_pca_log('');
write_pca_log('Log file: ' . get_log_file());
$clubs = json_decode(file_get_contents('https://pca.phlaym.net/pca.php'), true);
write_pca_log('Found clubs: ' . implode(', ', array_column($clubs, 'pca')));
// $last_notification_file = '../last_notification.json';
$last_notification_file_dir = realpath(__DIR__ . '/../pca_settings');
$last_notification_file_name = '/last_notification.json';
$last_notification_file = $last_notification_file_dir
. $last_notification_file_name;
write_pca_log('Last notification file: ' . $last_notification_file);
$last_notification_dict = [];
if (file_exists($last_notification_file)) {
$last_notification_dict = json_decode(
file_get_contents($last_notification_file),
true
);
write_pca_log('Loaded last notification info from file');
} else {
write_pca_log('Last notification info file could not be found');
}
// Get users
$access_token = '-1';
$token_file = realpath(__DIR__ . '/../pca_settings/access_token');
if ($token_file !== false) {
$access_token = file_get_contents($token_file);
write_pca_log('Using saved authentication');
if ($access_token == false) {
write_pca_log('Re-authenticating');
header('Location: http://pca.phlaym.net/pnutauth.php');
}
} else {
write_pca_log('Re-authenticating');
header('Location: http://pca.phlaym.net/pnutauth.php');
}
$api = new API();
$api->access_token = $access_token;
$api->init();
$messages = $api->get_messages($clubs);
/*
* Message command syntax:
* "Help" or "?" => prints help
* "Get notification text" => replies with notification text
* "Reset notification text" => resets to default
* "Set notification text" followed by a space and then the text. Replies with sample text. Available tokens see class var
*/
foreach ($messages as $sender => $message) {
$sender_obj = $api->get_user($sender);
foreach ($message as $msg) {
if (mb_strtolower($msg) == '?' || mb_strtolower($msg) == 'help') {
$helptext = 'Welcome to ' . $api->me->user_name . "!\n";
$helptext .= "The following commands are available: \n";
$helptext .=
"- 'Get notification text', which prints the current notification text for you\n";
$helptext .=
"- 'Set notification text', followed by a space and a custom notification text\n";
$helptext .=
"- 'Reset notification text', resets the notification text to the default\n";
$helptext .= "\n";
$helptext .= " Available tokens for the custom notification text\n";
$helptext .= " - '{user.username}', your username (including the @)\n";
$helptext .= " - '{user.name}', your real name\n";
$helptext .=
" - '{pca.name}', the name of the PCA you achieved, excluding the #\n";
$helptext .= " - '{pca.emoji}', the emoji of the PCA you achieved\n";
$helptext .=
" - '{pca.postcount}', the number of posts you need to achieve the PCA\n";
$helptext .= " - '{nextpca.name}', the name of the next PCA\n";
$helptext .= " - '{nextpca.emoji}', the emoji of the next PCA\n";
$helptext .=
" - '{nextpca.postcount}', the total number of posts needed for the next PCA\n";
$helptext .=
" - '{posts_to_pca}', the number of posts missing to the next PCA\n";
$api->send_message($sender, $helptext);
//Print help text
} elseif (substr(mb_strtolower($msg), 0, 21) === 'get notification text') {
//Print current notification text for user
//TODO: build example notification text. Create user object and do 'get_highest_pca($clubs)' and 'get_next_pca($clubs)'
$api->send_message(
$sender,
"You current notification text is: \n" .
$api->get_notification_text_template($sender)
);
} elseif (substr(mb_strtolower($msg), 0, 23) === 'reset notification text'
) {
//reset notification text for user to default
$api->reset_notification_text_for_user($sender);
$api->send_message(
$sender,
"Your notification text has been reset back to the default: \n" .
$api->get_notification_text_template($sender)
);
} elseif (substr(mb_strtolower($msg), 0, 21) === 'set notification text') {
$notification_text = substr($msg, 22);
$api->set_notification_text_template($sender, $notification_text);
$api->send_message(
$sender,
"Your notification text has been set to:\n" .
$api->get_notification_text_template($sender)
);
} else {
write_pca_log(
"Unknown command: '"
. $msg
. "' from "
. $sender,
'DEBUG'
);
$api->send_message(
$sender,
"I'm sorry, I don't recognize that. Try PMing me 'help'"
);
}
}
}
$followers = $api->get_bot_followers();
// Check and notify. Also build notification dict
$next_club_infos = [];
foreach ($followers as $user) {
if ($last_notification_dict != null
&& array_key_exists($user->user_name, $last_notification_dict)
) {
write_pca_log(
$user->user_name .
' has been notified in the past. Last time when reaching: ' .
$last_notification_dict[$user->user_name]
);
$user->last_club_notification = $last_notification_dict[$user->user_name];
} else {
write_pca_log($user->user_name . ' has never been notified in the past');
}
$next_club_info = $api->notify_user($user, $clubs);
if ($next_club_info !== null) {
$next_club_info['user'] = $user->user_name;
$next_club_infos[] = $next_club_info;
}
if ($user->last_club_notification != '') {
write_pca_log(
'Saving last notification ' .
$user->last_club_notification .
' for user ' .
$user->user_name
);
$last_notification_dict[$user->user_name] = $user->last_club_notification;
} else {
write_pca_log(
$user->user_name .
' will not be saved, as they have not reached a club yet'
);
}
}
usort(
$next_club_infos,
fn($a, $b) => intval($a['posts_until']) - intval($b['posts_until'])
);
foreach ($next_club_infos as $idx => $value) {
if ($idx >= 10) {
break;
}
write_pca_log(
'Next club will be: '
. $value['club']['pca']
. ' for user '
. $value['user']
. ' in '
. $value['posts_until']
. ' posts'
);
}
// Save changes
write_pca_log('Saving updated notification info');
$f = fopen($last_notification_file, 'w');
fwrite($f, json_encode($last_notification_dict));
fclose($f);
?>

18
README.md Normal file
View File

@ -0,0 +1,18 @@
A small [https://pnut.io](https://pnut.io) bot.
Can be pretty much cloned and run as-is,
with the only exception being a file called clientsecret in `../pca_settings/`, containing the client secret (duh).
If you don't have one, get it [here](https://pnut.io/dev)
The bot is currently called roughly every 15 minutes by a cronjob,
so any follows/new club memberships might take a while to show up.
Used to live at https://wedro.online/Check_PCA.php,
now lives at https://pca.phlaym.net/Check_PCA.php.
Source used to be [on Github](https://github.com/hutattedonmyarm/pcabot),
but has now moved to [my Gitea instance](https://git.phlaym.net/phlaym/pca).
Without its history, mostly because the old repo contains a various assortment of Pnut related
stuff and this is much more split up now.
Required PHP 8.x, tested with >=8.4

1
pca.json Normal file
View File

@ -0,0 +1 @@
[{"pca":"RollClub","emoji":"\ud83c\udf5e","post_count":"500","inventor":"@mlv"},{"pca":"MetalClub","emoji":"\ud83e\udd18","post_count":"666","inventor":"@hutattedonmyarm"},{"pca":"CrumpetClub","emoji":"\ud83c\udf70","post_count":"1000","inventor":"@irina"},{"pca":"NoonClub","emoji":"\ud83d\udd5b","post_count":"1200","inventor":"@samweinberg"},{"pca":"EnterpriseClub","emoji":"\ud83d\ude80","post_count":"1701","inventor":"@thedoctor,@ludolphus,@wife"},{"pca":"BitesizeCookieClub","emoji":"\ud83c\udf65","post_count":"2000","inventor":"@saket"},{"pca":"PnutClub","emoji":"\ud83e\udd5c","post_count":"2016","inventor":"@hutattedonmyarm"},{"pca":"CrunchClub","emoji":"\u260e\ufe0f","post_count":"2600","inventor":"@infodriveway"},{"pca":"MysteryScienceClub","emoji":"\ud83d\udce1","post_count":"3000","inventor":"@blt"},{"pca":"LDRClub","emoji":"\ud83d\udc5f","post_count":"5000","inventor":"@kym"},{"pca":"CPCClub","emoji":"\u2328\ufe0f","post_count":"6128","inventor":"@papierzeit"},{"pca":"IBMPCClub","emoji":"\ud83d\udcbb","post_count":"8088","inventor":"@duerig"},{"pca":"CookieClub","emoji":"\ud83c\udf6a","post_count":"10000","inventor":"@bondman"},{"pca":"SpinalTapClub","emoji":"\ud83d\udc89","post_count":"11000","inventor":"@paulyhedral"},{"pca":"BreakfastClub","emoji":"\ud83c\udf73","post_count":"20000","inventor":"@trine"},{"pca":"CaratClub","emoji":"\ud83d\udc8e","post_count":"24000","inventor":"@saket"},{"pca":"PeshawarClub","emoji":"\ud83c\udf5b","post_count":"25000","inventor":"@peemee"},{"pca":"MileHighClub","emoji":"\u2708\ufe0f","post_count":"30000","inventor":"@alicia"},{"pca":"PiClub","emoji":"\u2b55\ufe0f","post_count":"31416","inventor":"@kdfrawg"},{"pca":"TowelClub","emoji":"\ud83d\udc33","post_count":"42000","inventor":"@mps"},{"pca":"HitmanClub","emoji":"\ud83d\udd2a","post_count":"47000","inventor":"@remus"},{"pca":"BaconClub","emoji":"\ud83d\udc37","post_count":"50000","inventor":"@orangesn0w"},{"pca":"BrowncoatClub","emoji":"\ud83d\ude80","post_count":"57000","inventor":"@thedoctor"},{"pca":"CommodoreClub","emoji":"\ud83d\udd31","post_count":"64000","inventor":"@saket"},{"pca":"MotorolaClub","emoji":"\u24c2\ufe0f","post_count":"68000","inventor":"@kohlmannj"},{"pca":"TromboneClub","emoji":"\ud83c\udfb6","post_count":"76000","inventor":"@charlesg"},{"pca":"WiFiClub","emoji":"\ud83d\udcf6","post_count":"80211","inventor":"@rdo"},{"pca":"PajamaClub","emoji":"\ud83d\udcb7","post_count":"90000","inventor":"@mps"},{"pca":"TowerOfBabble","emoji":"\ud83d\uddfc","post_count":"100000","inventor":"@sham"},{"pca":"MacClub","emoji":"\ud83d\udcbb","post_count":"128000","inventor":"@fields"},{"pca":"TwitterLeaverClub","emoji":"","post_count":"140000","inventor":"@nhat"},{"pca":"GetALifeNoSrslyClub","emoji":"\ud83d\udc40","post_count":"200000","inventor":"@saket"},{"pca":"MeaninglessPostCountClub","emoji":"\ud83d\udcaf","post_count":"231568","inventor":"@cunarders"},{"pca":"ADNClub","emoji":"","post_count":"256000","inventor":"@trine"},{"pca":"PensionersClub","emoji":"\ud83c\udf97","post_count":"401000","inventor":"@sham"},{"pca":"MaglevClub","emoji":"\ud83d\ude84","post_count":"430000","inventor":"@remus"},{"pca":"LaughterClub","emoji":"\ud83e\udd23","post_count":"555000","inventor":"@saket"},{"pca":"GatesClub","emoji":"\ud83d\udcac","post_count":"640000","inventor":"@adiabatic"},{"pca":"JoyLuckClub","emoji":"\ud83c\udfa2","post_count":"888000","inventor":"@alicia"},{"pca":"MillionairesClub","emoji":"\ud83d\udcb0","post_count":"1000000","inventor":"@mps"},{"pca":"ToTheMoonClub","emoji":"\ud83d\udcc8","post_count":"21000000","inventor":"@blumenkraft"},{"pca":"GoogolplexClub","emoji":"\u03a9","post_count":"10000\u00ad000\u00ad\u00ad000000000\u00ad\u00ad000\u00ad000\u00ad000\u00ad\u00ad000\u00ad000\u00ad000\u00ad000\u00ad000\u00ad000\u00ad\u00ad000\u00ad000\u00ad000\u00ad\u00ad000\u00ad000000\u00ad\u00ad000\u00ad000\u00ad000\u00ad000\u00ad000\u00ad000\u00ad\u00ad000000\u00ad000\u00ad\u00ad000\u00ad000\u00ad000\u00ad\u00ad000","inventor":"@saket"}]

37
pca.php Normal file
View File

@ -0,0 +1,37 @@
<?php
$filename = 'pca.json';
if (file_exists($filename) && (time()-720 < filemtime($filename))) {
echo file_get_contents($filename);
} else {
output_pca($filename);
}
function output_pca($filename) {
$html = file_get_contents('https://wiki.pnut.io/PCA');
$doc = new DOMDocument();
$doc->loadHTML($html);
$tables = $doc->getElementsByTagName('table');
$pca = array();
foreach ($tables as $table) {
if ($table->hasAttribute('class') && $table->getAttribute('class') == 'wikitable') {
foreach ($table->getElementsByTagName('tr') as $childNode) {
$entry = $childNode->getElementsByTagName('td');
if ($entry->length > 0) {
$achievement["pca"] = preg_replace('/\s+/', '', $entry->item(0)->textContent);
$achievement["emoji"] = preg_replace('/\s+/', '', $entry->item(1)->textContent);
$achievement["post_count"] = preg_replace('/\s+/', '', $entry->item(2)->textContent);
$achievement["inventor"] = preg_replace('/\s+/', '', $entry->item(3)->textContent);
$pca[] = $achievement;
}
}
}
}
$myfile = fopen($filename, "w");
$file_content = json_encode($pca);
fwrite($myfile, $file_content);
fclose($myfile);
echo $file_content;
}
?>

39
pnutauth.php Normal file
View File

@ -0,0 +1,39 @@
<?php
$redirect_uri = 'https://pca.phlaym.net/pnutauth.php';
$client_id = 'RwHDh73PtU0It4DdKhwh2GEagBoO1ELD';
$client_secret = file_get_contents('../pca_settings/clientsecret');
if (isset($_GET['code'])) {
$code = $_GET['code'];
// set post fields
$post = [
'client_id' => $client_id,
'client_secret' => $client_secret,
'code' => $code,
'redirect_uri' => $redirect_uri,
'grant_type' => 'authorization_code'
];
$ch = curl_init('https://api.pnut.io/v1/oauth/access_token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
// execute!
$response = curl_exec($ch);
// close the connection, release resources used
curl_close($ch);
$resp = json_decode($response, true);
file_put_contents('../pca_settings/access_token', $resp['access_token']);
header('Location: https://pca.phlaym.net/Check_PCA.php');
} else {
header(
'Location: https://pnut.io/oauth/authenticate?client_id='
. $client_id
. '&redirect_uri='
. urlencode($redirect_uri)
. '&scope=write_post,messages&response_type=code'
);
}
?>

204
recent_progressions.php Normal file
View File

@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Pnut.io PCA progression history</title>
<link rel="stylesheet" href="styles/style_index.css" />
<script>
var startRow;
var tableID = "progressionTable"
var filterStyles = ["str", "<=", ">="];
var filterStyleIdx = 0;
function init() {
var numTH = document.getElementsByTagName('th').length;
startRow = (document.getElementsByTagName('tr').length * numTH) / (document.getElementsByTagName('td').length + numTH);
//Make filter row visible. (Hidden by default, so users with JS disabled don't see it)
var sr = document.getElementById('searchRow').style.display = "";
var datalist = document.getElementById('filterPCA');
if ('options' in datalist) {
xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function() {
if (xmlhttp.readyState==4 && xmlhttp.status==200) {
var pcaDict = JSON.parse(xmlhttp.responseText);
var options = '';
pcaDict.forEach(function(element) {
options += '<option value="' + element.pca + '" />';
});
datalist.innerHTML = options;
}
}
xmlhttp.open("GET","pca.json");
xmlhttp.send();
}
sortTable(3, "desc");
}
function isDate(d) {
return (d !== "Invalid Date") && !isNaN(d);
}
function changeIdFilterStyle(sender) {
filterStyleIdx++;
if (filterStyleIdx >= filterStyles.length) {
filterStyleIdx = 0;
}
sender.textContent = filterStyles[filterStyleIdx];
if (document.getElementById('filterPost').value != "") {
filter();
}
}
function sortTable(n, dir) {
var rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
var table = document.getElementById(tableID);
switching = true;
dir = dir || "asc";
while (switching) {
switching = false;
rows = table.getElementsByTagName("tr");
for (i = startRow; i < (rows.length - 1); i++) {
shouldSwitch = false;
x = rows[i].getElementsByTagName("td")[n];
y = rows[i + 1].getElementsByTagName("td")[n];
if (dir == "asc") {
if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
shouldSwitch= true;
break;
}
} else if (dir == "desc") {
if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
shouldSwitch= true;
break;
}
}
}
if (shouldSwitch) {
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
if (switchcount == 0) {
var headers = Array.from(rows[0].getElementsByTagName("th"));
headers.forEach(function(element) {
element.innerHTML = element.innerHTML.replace('▲', '');
element.innerHTML = element.innerHTML.replace('▼', '');
});
headers[n].innerHTML += dir == 'asc' ? '▲' : '▼';
}
switching = true;
switchcount ++;
} else {
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
}
function filter() {
var table = document.getElementById(tableID);
//Get values to filter for
var filterUser = document.getElementById('filterUser').value.toUpperCase();
var filterPCA = document.getElementById('filterPCAinput').value.toUpperCase();
var filterPost = document.getElementById('filterPost').value;
var filterDateBefore = document.getElementById('filterDateBefore').value;
var filterDateAfter = document.getElementById('filterDateAfter').value;
//Only filter if entered dates are valid
var filterBefore = new Date(filterDateBefore);
var filterAfter = new Date(filterDateAfter);
var doFilterBefore = isDate(filterBefore);
var doFilterAfter = isDate(filterAfter);
var filterArray = [filterUser, filterPCA, filterPost];
var tr = table.getElementsByTagName("tr");
var tableData, i, j, userData, showRow, postIdString, postID;
var filterStyle = filterStyles[filterStyleIdx];
//Loop through every row, skipping the headers
for (i = startRow; i < tr.length; i++) {
tableData = tr[i].getElementsByTagName("td");
showRow = true;
postIdString = tableData[2].children[0].innerHTML;
postID = parseInt(postIdString);
//Filter post ID depending on the selected filter style
switch(filterStyle) {
case "str":
showRow = showRow && ((postIdString.toUpperCase().indexOf(filterPost) > -1));
break;
case "<=":
showRow = showRow && !isNaN(postID) && postID <= filterPost;
break;
case ">=":
showRow = showRow && !isNaN(postID) && postID >= filterPost;
break;
}
//Filter other columns (except date)
for (j = 0; j < tableData.length - 2; j++) {
showRow = showRow && ((tableData[j].innerHTML.toUpperCase().indexOf(filterArray[j]) > -1));
}
//Filter date, depending on the presence and validity of the entered filter date
var postDate = new Date(tableData[tableData.length - 1].innerHTML.split(' ')[0]);
var doFilterDate = doFilterBefore || doFilterAfter;
var filterBeforeHit = !doFilterBefore || (doFilterBefore && postDate <= filterBefore)
var filterAfterHit = !doFilterAfter || (doFilterAfter && postDate >= filterAfter)
//Show/hide row
showRow = showRow && (!doFilterDate || (doFilterDate && filterBeforeHit && filterAfterHit));
tr[i].style.display = showRow ? "" : "none";
}
}
</script>
</head>
<body onload="init()">
<p>Recent PCA progressions:</p>
<?php
$history_file = 'history.json';
if (!file_exists($history_file)) {
echo "None :( </body></html>";
die();
}
$recent_changes = json_decode(file_get_contents($history_file), true);
if (count($recent_changes) == 0) {
echo "None :( </body></html>";
die();
}
?>
<table id="progressionTable">
<tbody>
<tr>
<th onclick="sortTable(0)">User</th>
<th onclick="sortTable(1)">PCA</th>
<th onclick="sortTable(2)">Post ID</th>
<th onclick="sortTable(3)">Date</th>
</tr>
<tr id="searchRow" style="display: none;">
<th><input type="text" id="filterUser" onpaste="filter()" onkeyup="filter()" placeholder="Filter User"></th>
<th>
<input list="filterPCA" id="filterPCAinput" onchange="filter()" placeholder="Filter PCA">
<datalist id="filterPCA">
<option value="test"></option>
</datalist>
<!-- <input type="text" id="filterPCA" onpaste="filter()" onkeyup="filter()" placeholder="Filter PCA"> !-->
</th>
<th>
<button onclick="changeIdFilterStyle(this)" title="Filter post ID by&#013;str: String&#013;<= and >=: Numeric">str</button>
<input type="text" id="filterPost" onpaste="filter()" onkeyup="filter()" placeholder="Filter Post ID" style="width: 60%">
</th>
<th>
<input type="date" id="filterDateBefore" onkeyup="filter()" onchange="filter()" placeholder="Posts on/before" style=" width:45%; margin-right: 1%">
<input type="date" id="filterDateAfter" onkeyup="filter()" onchange="filter()" placeholder="Posts on/after" style=" width: 45%; margin-left: 1%">
</th>
</tr>
<?php
foreach ($recent_changes as $entry) {
$user = '<a href="https://pnut.io/'.$entry['user'].'">'.$entry['user'].'</a>';
$pca = $entry['pca'];
$datetime = new DateTime($entry['date']);
$date = $datetime->format('Y-m-d H:i:s P');
$post_id = '<a href="https://posts.pnut.io/'.$entry['post_id'].'">'.$entry['post_id'].'</a>';
echo '<tr><td>'.$user.'</td><td>'.$pca.'</td><td>'.$post_id.'</td><td>'.$date.'</td></tr>';
}
?>
</tbody>
</table>
</body>
</html>

2
styles/.htaccess Normal file
View File

@ -0,0 +1,2 @@
Options All -Indexes

36
styles/style_index.css Normal file
View File

@ -0,0 +1,36 @@
body {
max-width: 700px;
margin: 0 auto;
padding: 10px;
font-family: "arial";
}
table, th, td {
border: 1px solid black;
border-collapse: collapse;
}
th, td {
padding: 5px;
}
th {
cursor: pointer;
}
.arrow {
position: relative;
}
.arrow:after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 0;
height: 0;
display: block;
border-left: 5px solid transparent;
border-bottom: 5px solid transparent;
border-top: 5px solid #000;
}