initial commit
This commit is contained in:
commit
e57af3ad36
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
history.json
|
735
Check_PCA.php
Normal file
735
Check_PCA.php
Normal 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
18
README.md
Normal 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
1
pca.json
Normal 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
37
pca.php
Normal 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
39
pnutauth.php
Normal 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
204
recent_progressions.php
Normal 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
str: String
<= 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
2
styles/.htaccess
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
Options All -Indexes
|
36
styles/style_index.css
Normal file
36
styles/style_index.css
Normal 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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user