I’m working on a module to add OctoPrint status to my zsh prompt, which I’ll probably write about in the future as a bigger post about my prompt customizations.
To start with that though, I need to play around with accessing the API via curl.
So here’s my super alpha version that will request the current status via HTTP and output it:
#!/bin/bash
OCTOPRINT_API="hunter2"
BASE_URL="http://octoprint.example.com"
JOB_ENDPOINT="${BASE_URL}/api/job"
CURL_TIMEOUT="0.5"# Fetch the JSON from OctoPrint
response="$(curl --max-time ${CURL_TIMEOUT} --silent --fail \
--header "X-Api-Key: ${OCTOPRINT_API}" \
"${JOB_ENDPOINT}")"# Extract fields with jq
file_name=$(jq -r '.job.file.display' <<< "$response")
completion=$(jq -r '.progress.completion' <<< "$response")
state=$(jq -r '.state' <<< "$response")
time_elapsed=$(jq -r '.progress.printTime' <<< "$response")
time_left=$(jq -r '.progress.printTimeLeft' <<< "$response")
# Round the completion percentage to two decimals
completion_str=$(printf"%.2f""$completion")
# Convert seconds to H:MM:SSfunctionfmt_time() {
local total_seconds="$1"local hours=$((total_seconds / 3600))
local minutes=$(((total_seconds % 3600) / 60))
local seconds=$((total_seconds % 60))
printf"%02d:%02d:%02d""$hours""$minutes""$seconds"
}
# Convert the times
time_elapsed_str=$(fmt_time "$time_elapsed")
time_left_str=$(fmt_time "$time_left")
# Print a readable summaryecho"File: ${file_name}"echo"State: ${state}"echo"Completion: ${completion_str}%"echo"Time Elapsed: ${time_elapsed_str}"echo"Time Left: ${time_left_str}"Code language:Bash(bash)
Do you ever find yourself doing some debugging with error_log() or its friends? Does that debugging ever involve SQL queries? Are you tired of staring at grey queries all the time? I have just the product for you!
Introducing Syntax Highlighting SQL in Terminal! Brought to you by our friends at Large Language Models, Incorporated, this new PHP function will use ANSI escape sequences to colorize your SQL queries for easier viewing and debugging!
I’ve been playing around with hooking up ChatGPT/Dall-E to WordPress and WP-CLI. To do this, I whipped up a super simple class to make this easier:
<?phpclassOpenAI_API{
publicconst API_KEY = 'hunter2'; // Get your own darn key!/**
* Generates an image based on the provided prompt using the OpenAI API.
*
* @param string $prompt The text prompt to generate the image from. Default is an empty string.
* @return string The response body from the OpenAI API, or a JSON-encoded error message if the request fails.
*/publicstaticfunctiongenerate_image( string $prompt = '' ): string{
$data = array(
'model' => 'dall-e-3',
'prompt' => trim( $prompt ),
'quality' => 'hd',
'n' => 1,
'size' => '1024x1024',
);
$args = array(
'body' => wp_json_encode( $data ),
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . OpenAI_API::API_KEY,
),
'method' => 'POST',
'data_format' => 'body',
);
$response = wp_remote_post( 'https://api.openai.com/v1/images/generations', $args );
if ( is_wp_error( $response ) ) {
return wp_json_encode( $response );
} else {
$body = wp_remote_retrieve_body( $response );
return $body;
}
}
/**
* Creates a chat completion using the OpenAI GPT-3.5-turbo model.
*
* @param string $prompt The user prompt to be sent to the OpenAI API.
* @param string $system_prompt Optional. The system prompt to be sent to the OpenAI API. Defaults to a predefined prompt.
*
* @return string The response body from the OpenAI API, or a JSON-encoded error message if the request fails.
*/publicstaticfunctioncreate_chat_completion( string $prompt = '', string $system_prompt = '' ): string{
if ( empty( $system_prompt ) ) {
$system_prompt = 'You are a virtual assistant designed to provide general support across a wide range of topics. Answer concisely and directly, focusing on essential information only. Maintain a friendly and approachable tone, adjusting response length based on the complexity of the question.';
}
// The data to send in the request body
$data = array(
'model' => 'gpt-3.5-turbo',
'messages' => array(
array(
'role' => 'system',
'content' => trim( $system_prompt ),
),
array(
'role' => 'user',
'content' => trim( $prompt ),
),
),
);
$args = array(
'body' => wp_json_encode( $data ),
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . OpenAI_API::API_KEY,
),
'method' => 'POST',
'data_format' => 'body',
'timeout' => 15,
);
// Perform the POST request
$response = wp_remote_post( 'https://api.openai.com/v1/chat/completions', $args );
// Error handlingif ( is_wp_error( $response ) ) {
return wp_json_encode( $response );
} else {
if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
return wp_json_encode( array( 'error' => 'API returned non-200 status code', 'response' => wp_remote_retrieve_body( $response ) ) );
}
// Assuming the request was successful, you can access the response body as follows:
$body = wp_remote_retrieve_body( $response );
return $body;
}
}
}Code language:PHP(php)
I can generate images and get back text from the LLM. Here’s some examples ChatGPT made to show how you can use these:
Example 1: Generating an Image
This example generates an image of a “cozy cabin in the snowy woods at sunset” using the generate_image method and displays it in an <img> tag.
<?php
$image_url = OpenAI_API::generate_image("A cozy cabin in the snowy woods at sunset");
if ( ! empty( $image_url ) ) {
echo'<img src="' . esc_url( $image_url ) . '" alt="Cozy cabin in winter">';
} else {
echo'Image generation failed.';
}
?>Code language:PHP(php)
Example 2: Simple Chat Completion
This example sends a question to the create_chat_completion method and prints the response directly.
Example 3: Chat Completion with Custom System Prompt
This example sets a custom system prompt for a specific tone, here focusing on culinary advice, and asks a relevant question.
<?php
$system_prompt = "You are a culinary expert. Please provide advice on healthy meal planning.";
$response = OpenAI_API::create_chat_completion("What are some good meals for weight loss?", $system_prompt);
echo $response;
?>Code language:PHP(php)
Here are some key limitations of this simple API implementation and why these are crucial considerations for production:
Lack of Robust Error Handling:
This API implementation has basic error handling that only checks if an error occurred during the request. It doesn’t provide specific error messages for different types of failures (like rate limits, invalid API keys, or network issues).
Importance: In production, detailed error handling allows for clearer diagnostics and faster troubleshooting when issues arise.
No Caching:
The current API makes a fresh request for each call, even if the response might be identical to a recent query.
Importance: Caching can reduce API usage costs, improve response times, and reduce server load, particularly for commonly repeated queries.
No API Rate Limiting:
This implementation doesn’t limit the number of requests sent within a certain time frame.
Importance: Rate limiting prevents hitting API request quotas and helps avoid unexpected costs or blocked access if API limits are exceeded.
No Logging for Debugging:
There’s no logging in place for tracking request errors or failed attempts.
Importance: Logs provide an audit trail that helps diagnose issues over time, which is crucial for maintaining a stable application in production.
Lack of Security for API Key Management:
The API key is currently hard coded into the class.
Importance: In production, it’s best to use environment variables or a secure key management system to protect sensitive information and prevent accidental exposure of the API key.
No Response Parsing or Validation:
The code assumes that the API response format is always correct, without validation.
Importance: Inconsistent or unexpected responses can cause failures. Validation ensures the app handles different cases gracefully.
Why Not Use in Production?
Due to these limitations, this API should be considered a prototype or learning tool rather than a production-ready solution. Adding robust error handling, caching, rate limiting, and logging would make it more resilient, secure, and efficient for a production environment.
Alright, so listen to the LLM and don’t do anything stupid with this, like I am doing.
Something that I do often is run PHPCS on code I’m working on, almost always inside a git repository. Even more likely is that PHPCS was installed via composer, which means it will live in $GIT_ROOT/vendor/bin. So I always end up doing something like ../../../vendor/bin/phpcs file.php which is hugely annoying.
Which is why I made this monstrosity:
# PHPCSfunctionphpcs() {
if git rev-parse --git-dir > /dev/null 2>&1; then
git_root=$(git rev-parse --show-toplevel 2>/dev/null)
# Check if vendor/bin/phpcs existsif [[ -x "$git_root/vendor/bin/phpcs" ]]; then# Call the local vendor/bin/phpcs"$git_root/vendor/bin/phpcs""$@"fielse# Fall back to the system's default phpcscommand phpcs "$@"fi
}
# PHPCBFfunctionphpcbf() {
if git rev-parse --git-dir > /dev/null 2>&1; then
git_root=$(git rev-parse --show-toplevel 2>/dev/null)
# Check if vendor/bin/phpcbf existsif [[ -x "$git_root/vendor/bin/phpcbf" ]]; then# Call the local vendor/bin/phpcbf"$git_root/vendor/bin/phpcbf""$@"fielse# Fall back to the system's default phpcbfcommand phpcbf "$@"fi
}Code language:Bash(bash)
Basically, what this is doing is any time I run phpcs or phpcbf it will first check if I am inside a git repository. If I am, and $GIT_ROOT/vendor/bin/phpcs exists, it will automatically find it and use it.
Saves me seconds a day. SECONDS! Maybe you can find it useful as well. Or not. Who knows.
I recently had to work with a third-party integration that used the WordPress REST API to interact with a website. We use this tool internally to move data around from other integrations, and finally into WordPress.
One of the things that I was worried about was the fact that this plugin would then have full access to the website, which is a private site. We only wanted to use it to post and then update WordPress posts, but there was always the concern that if the third party were to be hacked, then someone could read all of our posts on the private site through the REST API.
My solution was to hook into user_has_cap so that the user that was set up for the plugin to integrate with, through an application password, would only have access to read and edit its own posts. A bonus is that we wanted to be able to change the author of a post so that it would show up as specific users or project owners. That meant the authorized plugin user would also need access to these posts after the author was changed–so to get past that I scoured each post revision, and if the plugin user was the author of a revision it was also allowed access.
Finally, to make sure no other published posts were readable, I hooked into posts_results to set any post that didn’t meat the above criteria were marked as private. Below is a cleaned up version of that as an example if anyone else needs this type of functionality–feel free to use it as a starting point:
<?php/**
* Restricts post capabilities for a specific user.
*
* @param bool[] $allcaps The current user capabilities.
* @param string[] $caps The requested capabilities.
* @param array $args {
* Arguments that accompany the requested capability check.
*
* @type string $0 Requested capability.
* @type int $1 Concerned user ID.
* @type mixed ...$2 Optional second and further parameters, typically object ID.
* }
* @param WP_User $user The user object.
*
* @return bool[] The modified user capabilities.
*/functionemrikol_restrict_post_capabilities( array $allcaps, array $caps, array $args, WP_User $user ): array{
$post_id = isset( $args[2] ) ? absint( $args[2] ) : false;
if ( false === $post_id || ! get_post( $post_id ) ) {
return $allcaps;
}
if ( 'restricted' === get_user_meta( $user->ID, 'emrikol_restricted_post_capabilities', true ) ) {
$allowed_caps = array( 'read', 'read_private_posts', 'read_post', 'edit_post', 'delete_post', 'edit_others_posts', 'delete_others_posts' );
$requested_cap = isset( $caps[0] ) ? $caps[0] : '';
if ( in_array( $requested_cap, $allowed_caps, true ) ) {
if ( emrikol_user_is_author_or_revision_author( $user->ID, $post_id ) ) {
$allcaps[ $requested_cap ] = true;
} else {
$allcaps[ $requested_cap ] = false;
}
}
}
return $allcaps;
}
add_filter( 'user_has_cap', 'emrikol_restrict_post_capabilities', 10, 4 );
/**
* Restricts the public posts results based on the query.
*
* @param WP_Post[] $posts The array of posts returned by the query.
* @param WP_Query $query The WP_Query instance (passed by reference).
*
* @return array The filtered array of posts.
*/functionemrikol_restrict_public_posts_results( array $posts, WP_Query $query ): array{
if ( ! is_admin() && $query->is_main_query() ) {
$current_user = wp_get_current_user();
if ( 'restricted' === get_user_meta( $user->ID, 'emrikol_restricted_post_capabilities', true ) ) {
foreach ( $posts as $key => $post ) {
if ( ! emrikol_user_is_author_or_revision_author( $current_user->ID, $post->ID ) ) {
$posts[ $key ]->post_status = 'private';
}
}
}
}
return $posts;
}
add_filter( 'posts_results', 'emrikol_restrict_public_posts_results', 10, 2 );
/**
* Checks if the user is the author of the post or the author of a revision.
*
* @param int $user_id The ID of the user.
* @param int $post_id The ID of the post.
*
* @return bool True if the user is the author or revision author, false otherwise.
*/functionemrikol_user_is_author_or_revision_author( int $user_id, int $post_id ): bool{
$post_author_id = (int) get_post_field( 'post_author', $post_id );
if ( $user_id === $post_author_id ) {
returntrue;
}
$revisions = wp_get_post_revisions( $post_id );
foreach ( $revisions as $revision ) {
if ( $user_id === $revision->post_author ) {
returntrue;
}
}
returnfalse;
}
Code language:PHP(php)
I set up a few defaults I like to have for my Pis:
# Updates, new software, and cleanup
sudo apt update && sudo apt upgrade
sudo apt install mc screen ack zsh locate git htop cockpit -y
sudo apt autoremove
# Add dotfile customizations. Sorry, it's currently private :D
git clone git@github.com:emrikol/dotfiles.git
cp -r ~/dotfiles/. ~/
sudo usermod --shell /bin/zsh derrick
zsh
# Set up root access so I can SCP in from Transmit if I need to.
sudo sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
sudo passwd root
# Customize Raspberry Pi settings.
sudo raspi-configCode language:Bash(bash)
After installing cockpit I believe, I had an issue where the MAC address of my Wifi kept changing randomly on each reboot. I had to follow these instructions and add this to my /etc/NetworkManager/NetworkManager.conf:
From here, we should have my default “base” Raspberry Pi setup. And now, we can work on figuring out how to install Pibooth. According to the install docs, we need to run a few commands:
$ sudo apt install "libsdl2-*"
Reading package lists... Done
Building dependency tree
Reading state information... Done
Note, selecting 'libsdl2-mixer-dev'for glob 'libsdl2-*'
Note, selecting 'libsdl2-image-dev'for glob 'libsdl2-*'
Note, selecting 'libsdl2-gfx-dev'for glob 'libsdl2-*'
Note, selecting 'libsdl2-gfx-doc'for glob 'libsdl2-*'
Note, selecting 'libsdl2-mixer-2.0-0'for glob 'libsdl2-*'
Note, selecting 'libsdl2-dbg:armhf'for glob 'libsdl2-*'
Note, selecting 'libsdl2-dev'for glob 'libsdl2-*'
Note, selecting 'libsdl2-doc'for glob 'libsdl2-*'
Note, selecting 'libsdl2-ttf-dev'for glob 'libsdl2-*'
Note, selecting 'libsdl2-net-2.0-0'for glob 'libsdl2-*'
Note, selecting 'libsdl2-net-dev'for glob 'libsdl2-*'
Note, selecting 'libsdl2-image-2.0-0'for glob 'libsdl2-*'
Note, selecting 'libsdl2-2.0-0-dbgsym:armhf'for glob 'libsdl2-*'
Note, selecting 'libsdl2-2.0-0'for glob 'libsdl2-*'
Note, selecting 'libsdl2-gfx-1.0-0'for glob 'libsdl2-*'
Note, selecting 'libsdl2-ttf-2.0-0'for glob 'libsdl2-*'
libsdl2-2.0-0 is already the newest version (2.0.9+dfsg1-1+deb10u1).
libsdl2-2.0-0set to manually installed.
Some packages could not be installed. This may mean that you have
requested an impossible situation or if you are using the unstable
distribution that some required packages have not yet been created
or been moved out of Incoming.
The following information may help to resolve the situation:
The following packages have unmet dependencies:
libsdl2-2.0-0-dbgsym:armhf : Depends: libsdl2-2.0-0:armhf (= 2.0.9+dfsg1-1+rpt1) but it is not going to be installed
E: Unable to correct problems, you have held broken packages.Code language:JavaScript(javascript)
Meh. Okay. Let’s just install all of them but the trouble package. Hopefully that won’t come back to bite us.
I recently worked on profiling a customer site for performance problems, and one of the issues that I had seen was that calls to get_the_excerpt() were running HORRIBLY slow due to filters.
I ended up writing a really hacky workaround to cache excerpts to help reduce the burden they had on the page generation time, but we ended up not using this solution. Instead we found another filter in a plugin that removed the painfully slow stuff happening in the excerpt.
So below is one potential way to cache your excerpt, but be warned it’s only barely tested:
/**
* Checks the excerpt cache for a post and returns the cached excerpt if available.
* If the post is password protected, the original post excerpt is returned.
*
* @param string $post_excerpt The original post excerpt.
* @param WP_Post $post The post object.
*
* @return string The post excerpt, either from the cache or the original.
*/functionblarg_check_excerpt_cache( string $post_excerpt, WP_Post $post ): string{
// We do not want to cache password protected posts.if ( post_password_required( $post ) ) {
return $post_excerpt;
}
$cache_key = $post->ID;
$cache_group = 'blarg_cached_post_excerpt';
$cache_data = wp_cache_get( $cache_key, $cache_group );
if ( false !== $cache_data ) {
remove_all_filters( 'get_the_excerpt' );
add_filter( 'get_the_excerpt', 'blarg_check_excerpt_cache', PHP_INT_MIN, 2 );
$post_excerpt = $cache_data;
} else {
add_filter( 'get_the_excerpt', 'blarg_cache_the_excerpt', PHP_INT_MAX, 2 );
}
// At this point, do not modify anything and return.return $post_excerpt;
}
add_filter( 'get_the_excerpt', 'blarg_check_excerpt_cache', PHP_INT_MIN, 2 );
/**
* Caches the post excerpt in the WordPress object cache.
*
* @param string $post_excerpt The post excerpt to cache.
* @param WP_Post $post The post object.
*
* @return string The cached post excerpt.
*/functionblarg_cache_the_excerpt( string $post_excerpt, WP_Post $post ): string{
$cache_key = $post->ID;
$cache_group = 'blarg_cached_post_excerpt';
wp_cache_set( $cache_key, $post_excerpt, $cache_group );
return $post_excerpt;
}
/**
* Deletes the cached excerpt for a given post.
*
* @param int|WP_Post $post The post ID or WP_Post object.
*
* @return void
*/functionblarg_delete_the_excerpt_cache( int|WP_Post $post ): void{
if ( $post instanceof WP_Post ) {
$post_id = $post->ID;
} else {
$post_id = $post;
}
$cache_key = $post_id;
$cache_group = 'blarg_cached_post_excerpt';
wp_cache_delete( $cache_key, $cache_group );
}
add_action( 'clean_post_cache', 'blarg_delete_the_excerpt_cache', 10, 1 );
Code language:PHP(php)
This should automatically clear out the cache any time the post is updated, but if the excerpt gets updated in any other way you may need to purge the cache in other ways.
I’ve been rebuilding my Raspberry Pi collection from scratch, and moving from Ubuntu Server to Debian/Raspbian Bookworm. One of the tasks that I quickly tried to automate was reconnecting my CIFS mounts. I wanted to do it better, and came across this method, with the help of ChatGPT, to mount them at boot:
#!/bin/bash
# Check if the script is run as rootif [[ $EUID -ne 0 ]]; thenecho"This script must be run as root"exit 1
fi# Check for correct number of argumentsif [ "$#" -ne 4 ]; thenecho"Usage: $0 <RemoteDirectory> <MountDirectory> <Username> <Password>"exit 1
fi
REMOTE_DIR="$1"
MOUNT_DIR="$2"
USERNAME="$3"
PASSWORD="$4"
CREDENTIALS_PATH="/etc/samba/credentials-$(basename "$MOUNT_DIR")"# Escape the mount directory for systemd
UNIT_NAME=$(systemd-escape -p --suffix=mount "$MOUNT_DIR")
# Create mount directory
mkdir -p "$MOUNT_DIR"# Create credentials file
touch "$CREDENTIALS_PATH"echo"username=$USERNAME" > "$CREDENTIALS_PATH"echo"password=$PASSWORD" >> "$CREDENTIALS_PATH"
chmod 600 "$CREDENTIALS_PATH"# Create systemd unit file
UNIT_FILE_PATH="/etc/systemd/system/$UNIT_NAME"echo"[Unit]
Description=Mount Share at $MOUNT_DIR
After=network-online.target
Wants=network-online.target
[Mount]
What=$REMOTE_DIR
Where=$MOUNT_DIR
Type=cifs
Options=_netdev,iocharset=utf8,file_mode=0777,dir_mode=0777,credentials=$CREDENTIALS_PATH
TimeoutSec=30
[Install]
WantedBy=multi-user.target" > "$UNIT_FILE_PATH"# Reload systemd, enable and start the unit
systemctl daemon-reload
systemctl enable"$UNIT_NAME"
systemctl start "$UNIT_NAME"echo"Mount setup complete. Mounted $REMOTE_DIR at $MOUNT_DIR"Code language:Bash(bash)
I’m sure this is totally insecure and a terrible idea, but it works for me so back off, buddy!
Please don’t follow me as an example of what to do, but take this code for anything you need.