Sunday, 26 November 2023

Web Tutorial: The Self-affirmations WordPress Plugin (Part 4/4)

Having written a function to generate email, we will now send it!

Email in WordPress can be a tricky issue. Sure, we can use the native function wp_mail(). However, it is, half the time, going to fail depending on your server settings which you do not always have control over. Thus, it's advised to use an email plugin.

The one I am using for this, is WP Mail SMTP. Feel free to use your own. I used my email account at Google to configure WP Mail SMTP. Again, you may use any other method to configure it; this just happened to be the most convenient for me. Just read through the documentation, do a test send, and Bob's your uncle!


Once you have this out of the way, let's focus on writing the job that will send email on a regular basis. This is the function tt_selfaffirmations(). We begin by declaring list, and using it to store the result returned by running tt_get_readytoreceive(). Then we iterate through each element of list. There should be only one, though if you haven't reset the LAST_SENT column in your Oracle APEX database, it will be 0.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_selfaffirmations()
{
    $list = tt_get_readytoreceive();

    foreach($list as $l)
    {

    }

}


In the Foreach loop, we get the string name by concatenating the first_name and last_name properties of the current element, with a space in between. Then using the value of name, and the current element's email, gender and dob properties, we get the email array by running tt_generate_mail().

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_selfaffirmations()
{
    $list = tt_get_readytoreceive();

    foreach($list as $l)
    {
        $name = $l->first_name . " " . $l->last_name;
        $email = tt_generate_mail($l->email, $name, $l->gender, $l->dob);

    }
}


In this If block, we run wp_mail() using the email property, the title property of email and the body property of email with an "unsubscribe" line appended to the end. The rest of the arguments aren't that crucial. See the wp_email() function description here.

In essence, we're checking if the email was sent with no error.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_selfaffirmations()
{
    $list = tt_get_readytoreceive();

    foreach($list as $l)
    {
        $name = $l->first_name . " " . $l->last_name;
        $email = tt_generate_mail($l->email, $name, $l->gender, $l->dob);

        if (wp_mail($l->email, $email["title"], $email["body"] . "\n\nTo unsubscribe to the Self-affirmations Mailing List, please reply to this email with the subject 'UNSUBSCRIBE'.", "", [] ))
        {

        }

    }
}


If so, we should run the tt_set_lastsent() function to set the LAST_SENT column of that record to today's date so the email won't get sent a second time.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_selfaffirmations()
{
    $list = tt_get_readytoreceive();

    foreach($list as $l)
    {
        $name = $l->first_name . " " . $l->last_name;
        $email = tt_generate_mail($l->email, $name, $l->gender, $l->dob);

        if (wp_mail($l->email, $email["title"], $email["body"] . "\n\nTo unsubscribe to the Self-affirmations Mailing List, please reply to this email with the subject 'UNSUBSCRIBE'.", "", [] ))
        {
            tt_set_lastsent($l->email);
        }
    }
}


If you run the Test job link, this is what you should see. tt_get_readytoreceive() was run, followed by tt_get_terms() followed by tt_generate_email().


More importantly, this should appear in your inbox!


So the sending of email is good to go. Time to set up the CRON job. For this, I use the WP Crontrol plugin. After activating the plugin, go to Tools, then Cron Events.


Once you click on Add New, you should be redirected to this interface. We'll use "cron_selfaffirmations" as the hook name, and set it to run once daily.


Once you confirm, you should see it in the list.


What next? Well, go back to your plugin file. Add this line. It basically uses the add_action() function to link the tt_selfaffirmations() function with the cron_selfaffirmations hook. Thus when the CRON job kicks in, it will run tt_selfaffirmations()!

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_selfaffirmations()
{
    $list = tt_get_readytoreceive();

    foreach($list as $l)
    {
        $name = $l->first_name . " " . $l->last_name;
        $email = tt_generate_mail($l->email, $name, $l->gender, $l->dob);

        if (wp_mail($l->email, $email["title"], $email["body"] . "\n\nTo unsubscribe to the Self-affirmations Mailing List, please reply to this email with the subject 'UNSUBSCRIBE'.", "", [] ))
        {
            tt_set_lastsent($l->email);
        }
    }
}

add_action("cron_selfaffirmations", "tt_selfaffirmations");


If you reset your LAST_SENT column in the Oracle APEX database, the email will be sent when the job next runs. Set it to tun 5 minutes from now and see if it works!

That's all!

Thank you for staying with me! This was fun, y'all. And I didn't even have to write all that much code!


Your Greatest Fan,
T___T

Friday, 24 November 2023

Web Tutorial: The Self-affirmations WordPress Plugin (Part 3/4)

The next funciton we need to deal with, is tt_generate_email(). Before we begin, you will need a CharGPT Developer Account, and an API key. It doesn't cost all that much, honestly, so I would absolutely recommend putting that credit card to good use.

We begin by adding in parameters for the function. We wat the email address ($id), the name, gender and birthday.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_generate_mail($id, $name, $gender, $dob)
{

}


For all the parameters other than the first, we will have default values. Thus, when you run this function without passing any values in, it will use these values.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_generate_mail($id, $name = "Teochew Thunder", $gender = "M", $dob = "01-01-1980")
{

}
-

We begin the function by declaring the variable terms, and running tt_get_terms() with the value of id passed in as an argument, to set terms to the returned value. Bear in mind that if there is no value given for id, tt_get_terms() will use "teochewthunder@gmail.com" as a default value.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_generate_mail($id, $name = "Teochew Thunder", $gender = "M", $dob = "01-01-1980")
{
    $terms = tt_get_terms($id);
}


We then declare interests as an empty string. And then we check if the interests array of terms is not empty. We'll only alter the value of interests if so.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_generate_mail($id, $name = "Teochew Thunder", $gender = "M", $dob = "01-01-1980")
{
    $terms = tt_get_terms($id);

    $interests = "";
    if (count($terms["interests"]) > 0)
    {
    
    }

}


Firstly, we use the value of gender to ensure that the string interests is correct in the gender sense.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_generate_mail($id, $name = "Teochew Thunder", $gender = "M", $dob = "01-01-1980")
{
    $terms = tt_get_terms($id);

    $interests = "";
    if (count($terms["interests"]) > 0)
    {
        $interests = ($gender == "M" ? "He" : "She") . " is interested in ";    
    }
}


Then we iterate through the interests array of the terms array using a For loop.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_generate_mail($id, $name = "Teochew Thunder", $gender = "M", $dob = "01-01-1980")
{
    $terms = tt_get_terms($id);

    $interests = "";
    if (count($terms["interests"]) > 0)
    {
        $interests = ($gender == "M" ? "He" : "She") . " is interested in ";
        for ($i = 0; $i < sizeof($terms["interests"]); $i++)
        {

        }    
    
    }
}


What we do next, is append the value of the current element to the interests string. Thus, if gender is "M" and the first element of interests is "rollerblading", the resultant string would be "He is interested in rollerblading". After that, we check if the current element is the final element of the interests array.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_generate_mail($id, $name = "Teochew Thunder", $gender = "M", $dob = "01-01-1980")
{
    $terms = tt_get_terms($id);

    $interests = "";
    if (count($terms["interests"]) > 0)
    {
        $interests = ($gender == "M" ? "He" : "She") . " is interested in ";
        for ($i = 0; $i < sizeof($terms["interests"]); $i++)
        {
            $interests .= $terms["interests"][$i];
            if ($i == sizeof($terms["interests"]) - 1)
            {

            }
            else
            {

            }
        }   
     
    }
}


If so, append a full-stop. If not, append the string "and" preceded and superseded by a space. Thus, if interests contains the values "jazz music". "spiders" and "surfing", and gender is "F", the resultant string would be "She is interested in jazz music and spiders and surfing". Not a perfect sentence, but it should make sense to ChatGPT.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_generate_mail($id, $name = "Teochew Thunder", $gender = "M", $dob = "01-01-1980")
{
    $terms = tt_get_terms($id);

    $interests = "";
    if (count($terms["interests"]) > 0)
    {
        $interests = ($gender == "M" ? "He" : "She") . " is interested in ";
        for ($i = 0; $i < sizeof($terms["interests"]); $i++)
        {
            $interests .= $terms["interests"][$i];
            if ($i == sizeof($terms["interests"]) - 1)
            {
                $interests .= ". ";
            }
            else
            {
                $interests .= " and ";
            }
        }        
    }
}


We'll do something similar for the descriptions array.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
$interests = "";
if (count($terms["interests"]) > 0)
{
    $interests = ($gender == "M" ? "He" : "She") . " is interested in ";
    for ($i = 0; $i < sizeof($terms["interests"]); $i++)
    {
        $interests .= $terms["interests"][$i];
        if ($i == sizeof($terms["interests"]) - 1)
        {
            $interests .= ". ";
        }
        else
        {
            $interests .= " and ";
        }
    }        
}

$descriptions = "";
if (count($terms["descriptions"]) > 0)
{
    $descriptions = ($gender == "M" ? "He" : "She") . " is described as ";        
    for ($i = 0; $i < sizeof($terms["descriptions"]); $i++)
    {
        $descriptions .= $terms["descriptions"][$i];
        if ($i == sizeof($terms["descriptions"]) - 1)
        {
            $descriptions .= ". ";
        }
        else
        {
            $descriptions .= " and ";
        }
    }        
}



After that, we use name, gender and dob to formulate the string about.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
$descriptions = "";
if (count($terms["descriptions"]) > 0)
{
    $descriptions = ($gender == "M" ? "He" : "She") . " is described as ";        
    for ($i = 0; $i < sizeof($terms["descriptions"]); $i++)
    {
        $descriptions .= $terms["descriptions"][$i];
        if ($i == sizeof($terms["descriptions"]) - 1)
        {
            $descriptions .= ". ";
        }
        else
        {
            $descriptions .= " and ";
        }
    }        
}

$about = ($gender == "M" ? "man" : "woman");
$about .= " named '" . $name . "'";
$about .= " born on " . explode("T", $dob)[0];



We then declare prompt and title as empty strings, and tokens as an integer with an initial value of 50. prompt_type is declared as a random number from 0 to 20. The future values of prompt, tokens and title will depend on the value of prompt_type.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
$about = ($gender == "M" ? "man" : "woman");
$about .= " named '" . $name . "'";
$about .= " born on " . explode("T", $dob)[0];

$prompt_type = rand(0, 20);
$prompt = "";
$tokens = 50;
$title = "";



As you can see, in here I use a Switch statement on prompt_type and use that to determine values.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
$prompt_type = rand(0, 20);
$prompt = "";
$tokens = 50;
$title = "";

switch($prompt_type)
{
    case 0: $prompt = "Generate a complimentary poem about"; $title = "A poem for you!"; $tokens = 3000; break;
    case 1: $prompt = "Generate some positive life advice for"; $title = "Some life advice"; $tokens = 1000; break;
    case 2: $prompt = "Generate a sample horoscope for"; $title = "Your Zodiac advice"; $tokens = 3000; break;
    case 3: $prompt = "Generate a Chinese zodiac horoscope for"; $title = "Your Zodiac advice"; $tokens = 3000; break;
    case 4: $prompt = "Generate an encouraging two paragraph letter to self for"; $title = "Your self-affirmation"; $tokens = 2000; break;
    case 5: $prompt = "Generate an encouraging one paragraph letter to self for"; $title = "Your self-affirmation"; $tokens = 1000; break;
    case 6: $prompt = "Generate an encouraging three paragraph letter to self for"; $title = "Your self-affirmation"; $tokens = 3000; break;            
    case 7: $prompt = "Generate a funny and uplifting short story about"; $title = "The Story of You"; $tokens = 3000; break;
    case 8: $prompt = "Generate five inspirational quotes from famous people for"; $title = "Five Quotes to make your day"; $tokens = 2500; break;
    case 9: $prompt = "Generate five fictitious short reviews from fictitious publications about"; $title = "Your reviews from public media"; $tokens = 2500; break;
    case 10: $prompt = "Generate five fictitious one-sentence reviews from fictitious people from diverse races and their occupations about"; $title = "Public Opinion About You"; $tokens = 2500; break;
    case 11: $prompt = "Generate five fictitious one-sentence reviews from fictitious people from diverse races complimenting the personality of"; $title = "Public Opinion About You"; $tokens = 2500; break;                
    case 12: $prompt = "Generate five corny pickup lines from random people for"; $title = "Pickup Lines For You"; $tokens = 2500; break;
    case 13: $prompt = "Generate a character testimonial (from self) for"; $title = "Your testimonial!"; $tokens = 3000; break;
    case 14: $prompt = "Write a love letter (from self) to"; $title = "Some self-love"; $tokens = 3000; break;
    case 15: $prompt = "Create a short movie synopsis with famous actors about"; $title = "A movie was made about you!"; $tokens = 3000; break;            
    case 16: $prompt = "Create a movie role, with famous co-stars, for"; $title = "A movie role for you"; $tokens = 3000; break;            
    case 17: $prompt = "Write a welcoming letter from the President of a Fan Club centered around"; $title = "Welcome Address From " . $name . " Fan Club"; $tokens = 1000; break;
    case 18: $prompt = "Generate a sensational and funny article from a fictitious publication about"; $title = "An article About You"; $tokens = 1000; break;            
    default: $prompt = "Generate a complimentary poem about"; $title = "A poem for you!"; $tokens = 3000; break;            
}



tokens will need to be larger depending on how many values interests and descriptions have.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php

switch($prompt_type)
{
    case 0: $prompt = "Generate a complimentary poem about"; $title = "A poem for you!"; $tokens = 3000; break;
    case 1: $prompt = "Generate some positive life advice for"; $title = "Some life advice"; $tokens = 1000; break;
    case 2: $prompt = "Generate a sample horoscope for"; $title = "Your Zodiac advice"; $tokens = 3000; break;
    case 3: $prompt = "Generate a Chinese zodiac horoscope for"; $title = "Your Zodiac advice"; $tokens = 3000; break;
    case 4: $prompt = "Generate an encouraging two paragraph letter to self for"; $title = "Your self-affirmation"; $tokens = 2000; break;
    case 5: $prompt = "Generate an encouraging one paragraph letter to self for"; $title = "Your self-affirmation"; $tokens = 1000; break;
    case 6: $prompt = "Generate an encouraging three paragraph letter to self for"; $title = "Your self-affirmation"; $tokens = 3000; break;            
    case 7: $prompt = "Generate a funny and uplifting short story about"; $title = "The Story of You"; $tokens = 3000; break;
    case 8: $prompt = "Generate five inspirational quotes from famous people for"; $title = "Five Quotes to make your day"; $tokens = 2500; break;
    case 9: $prompt = "Generate five fictitious short reviews from fictitious publications about"; $title = "Your reviews from public media"; $tokens = 2500; break;
    case 10: $prompt = "Generate five fictitious one-sentence reviews from fictitious people from diverse races and their occupations about"; $title = "Public Opinion About You"; $tokens = 2500; break;
    case 11: $prompt = "Generate five fictitious one-sentence reviews from fictitious people from diverse races complimenting the personality of"; $title = "Public Opinion About You"; $tokens = 2500; break;                
    case 12: $prompt = "Generate five corny pickup lines from random people for"; $title = "Pickup Lines For You"; $tokens = 2500; break;
    case 13: $prompt = "Generate a character testimonial (from self) for"; $title = "Your testimonial!"; $tokens = 3000; break;
    case 14: $prompt = "Write a love letter (from self) to"; $title = "Some self-love"; $tokens = 3000; break;
    case 15: $prompt = "Create a short movie synopsis with famous actors about"; $title = "A movie was made about you!"; $tokens = 3000; break;            
    case 16: $prompt = "Create a movie role, with famous co-stars, for"; $title = "A movie role for you"; $tokens = 3000; break;            
    case 17: $prompt = "Write a welcoming letter from the President of a Fan Club centered around"; $title = "Welcome Address From " . $name . " Fan Club"; $tokens = 1000; break;
    case 18: $prompt = "Generate a sensational and funny article from a fictitious publication about"; $title = "An article About You"; $tokens = 1000; break;            
    default: $prompt = "Generate a complimentary poem about"; $title = "A poem for you!"; $tokens = 3000; break;            
}

$tokens += (100 * count($terms["interests"]));
$tokens += (100 * count($terms["descriptions"]));



And here, we declare final_prompt. We use the values of the strings prompt, about, interests and descriptions. Thus, a typical string would be "Generate a complimentary poem about a woman named 'Venus Chen', born on 02-11-1988. She is interested in positive thinking and flowers. She is described as happy and optimistic and emotional.". Add a line that displays final_prompt on screen.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
$tokens += (100 * count($terms["interests"]));
$tokens += (100 * count($terms["descriptions"]));

$final_prompt = $prompt . " " . $about . ". " . $interests . $descriptions;

echo $final_prompt;


Test this!


This was the text returned. Because we first ran tt_get_terms(), it returned us the list of terms. Then you see that we have the prompt at the end.
{"items":[{"type":"DESCRIPTIONS","term":"acidic"},{"type":"DESCRIPTIONS","term":"cynical"},{"type":"DESCRIPTIONS","term":"jolly"},{"type":"DESCRIPTIONS","term":"patient"},{"type":"DESCRIPTIONS","term":"pragmatic"},{"type":"INTERESTS","term":"comedy"},{"type":"INTERESTS","term":"programming"},{"type":"INTERESTS","term":"soccer"},{"type":"INTERESTS","term":"superheroes"}],"hasMore":false,"limit":25,"offset":0,"count":9,"links":[{"rel":"self","href":"https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/terms/teochewthunder@gmail.com"},{"rel":"describedby","href":"https://apex.oracle.com/pls/apex/teochewthunder/metadata-catalog/mailinglist/terms/item"},{"rel":"first","href":"https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/terms/teochewthunder@gmail.com"}]}Generate a Chinese zodiac horoscope for man named 'Teochew Thunder' born on 01-01-1980. He is interested in programming and soccer. He is described as acidic and cynical and jolly and pragmatic.


Now that you have a prompt, it's time to send it to ChatGPT. For this, you will need an account and a key, Declare the variables key, org and url. key is the key provided for your ChatGPT developer account. org is the account id. url is the endpoint for ChatGPT. Of course your key isn't going to be "sk=xxx", but I'm not about to share my key in public. That would defeat the entire purpose of having a key, amirite?

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
$final_prompt = $prompt . " " . $about . ". " . $interests . $descriptions;

echo $final_prompt;

$key = "sk-xxx";
$org = "org-FUOhDblZb1pxvaY6YylF54gl";
$url = "https://api.openai.com/v1/chat/completions";



Declare headers as an array with three strings. The first string is for authorization and includes key. The next one is to identify the account, and includes org. And the last line explicitly states that the content type is JSON.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
$key = "sk-xxx";
$org = "org-FUOhDblZb1pxvaY6YylF54gl";
$url = "https://api.openai.com/v1/chat/completions";

$headers = [
    "Authorization: Bearer " . $key,
    "OpenAI-Organization: " . $org,
    "Content-Type: application/json"
];



Declare messages and obj as empty arrays. Declare keys role and content in obj. The value of role should be "user", while the value of content should be final_prompt. And then make obj the first element of messages.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php

$headers = [
    "Authorization: Bearer " . $key,
    "OpenAI-Organization: " . $org,
    "Content-Type: application/json"
];

$messages = [];
$obj = [];
$obj["role"] = "user";
$obj["content"] = $final_prompt;
$messages[] = $obj;



Then declare data as an empty array. Add keys model, messages and max_tokens. The value of model is the ChatGPT version. The value of messages is messages. The value of max_tokens is tokens, which we defined earlier based on the amount of information given.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
$messages = [];
$obj = [];
$obj["role"] = "user";
$obj["content"] = $final_prompt;
$messages[] = $obj;

$data = [];
$data["model"] = "gpt-3.5-turbo";
$data["messages"] = $messages;
$data["max_tokens"] = $tokens;


Now begin cURL. It should be using the POST method. We send in data in JSON string format using the json_encode() function. We also set the headers to headers, and ensure that it returns a value.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php

$data = array();
$data["model"] = "gpt-3.5-turbo";
$data["messages"] = $messages;
$data["max_tokens"] = $tokens;

$curl = curl_init($url);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);



Then we run curl_exec() passing in curl as an argument, and assign the returned value to result. If there's an error, display it. If there's no error, print result. Finally, clean up using curl_close().

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php

$curl = curl_init($url);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

$result = curl_exec($curl);
if (curl_errno($curl))
{
    echo "Error:" . curl_error($curl);
}
else
{
    echo print_r($result);
}

curl_close($curl);    



Now test this!


Try it again! Is the prompt and result different?


That's it for the test, but we still need to return a value. So use json_decode() on result.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
curl_close($curl);    
    
$result = json_decode($result);


Then declare sanitized_content. It should be the value of the content property of the message object of the first element in choices, of result. If that sounds convoluted, refer to the displayed printout on screen earlier and you'll see the hierarchy.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
curl_close($curl);    

$result = json_decode($result);
$sanitized_content = $result->choices[0]->message->content;


And then we use str_replace() to replace all the template strings in the output with reasonable data. There's no real exhaustive list of this.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
curl_close($curl);    

$result = json_decode($result);
$sanitized_content = $result->choices[0]->message->content;
$sanitized_content = str_replace("[Your Name]", $name, $sanitized_content);
$sanitized_content = str_replace("[President's Name]", $name, $sanitized_content);
$sanitized_content = str_replace("[Fan Club Name]", $name . " Fan Club", $sanitized_content);
$sanitized_content = str_replace("[Date]", date("j F Y"), $sanitized_content);
$sanitized_content = str_replace("[Insert Date]", date("j F Y"), $sanitized_content);
$sanitized_content = str_replace("[President's Logo]", "", $sanitized_content);
$sanitized_content = str_replace("[P.O Box]", "", $sanitized_content);
$sanitized_content = str_replace("[City, State, Zip Code]", "", $sanitized_content);
$sanitized_content = str_replace("[Email Address]", "", $sanitized_content);
$sanitized_content = str_replace("[Website]", "", $sanitized_content);
$sanitized_content = str_replace("[Social Media Handles]", "", $sanitized_content);    


And finally, return an array with two keys - title and body. The value of title is title, which we defined earlier. And the value of body is sanitized_content.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
$result = json_decode($result);
$sanitized_content = $result->choices[0]->message->content;
$sanitized_content = str_replace("[Your Name]", $name, $sanitized_content);
$sanitized_content = str_replace("[President's Name]", $name, $sanitized_content);
$sanitized_content = str_replace("[Fan Club Name]", $name . " Fan Club", $sanitized_content);
$sanitized_content = str_replace("[Date]", date("j F Y"), $sanitized_content);
$sanitized_content = str_replace("[Insert Date]", date("j F Y"), $sanitized_content);
$sanitized_content = str_replace("[President's Logo]", "", $sanitized_content);
$sanitized_content = str_replace("[P.O Box]", "", $sanitized_content);
$sanitized_content = str_replace("[City, State, Zip Code]", "", $sanitized_content);
$sanitized_content = str_replace("[Email Address]", "", $sanitized_content);
$sanitized_content = str_replace("[Website]", "", $sanitized_content);
$sanitized_content = str_replace("[Social Media Handles]", "", $sanitized_content);    

return ["title" => $title, "body" => $sanitized_content];


We have written a function to generate email content. Now we need to send the email, and schedule the task!

Next

Setting up the CRON job.

Tuesday, 21 November 2023

Web Tutorial: The Self-affirmations WordPress Plugin (Part 2/4)

The next part of this web tutorial involves creating a WordPress plugin. This is entirely dependent upon you having a WordPress instance installed on your server.

In essence, you create a folder (let's call it tt_selfaffirmations) under the plugins directory of the wp-content directory, then create a PHP file in there. The starting comments help WordPress identify that this is the plugin file.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
/**
* Plugin Name: Self-affirmations
* Plugin URI: http://www.teochewthunder.com
* Description: This plugin implements self-affirmations
* Version: 1.0.0
* Author: TeochewThunder
* Author URI: http://www.teochewthunder.com
* License: GPL2
*/


This next function, wc_admin_menu(), is added to the admin_menu hook. The final effect is that we will add menu items into the side menu for testing purposes.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
/**
* Plugin Name: Self-affirmations
* Plugin URI: http://www.teochewthunder.com
* Description: This plugin implements self-affirmations
* Version: 1.0.0
* Author: TeochewThunder
* Author URI: http://www.teochewthunder.com
* License: GPL2
*/

function wc_admin_menu()
{

}


add_action("admin_menu", "wc_admin_menu", 11);


In here, we call the built-in function add_submenu_page(). We use index.php as the page to which to execute the code in. The next two arguments are just the label for the link. "manage_options" is the minimum user capability required, and this particular value ensures that only admins can see the link added. "tt_get_readytoreceive" is the name of the function we will call when clicking that link, and we will create that function soon.

Here's some info on the add_submenu_page() function.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function wc_admin_menu()
{
    add_submenu_page("index.php", "Test Readytoreceive", "Test Readytoreceive", "manage_options", "tt_get_readytoreceive", "tt_get_readytoreceive");
}

add_action("admin_menu", "wc_admin_menu", 11);


And of course, we will need to create the tt_get_readytoreceive() function.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function wc_admin_menu()
{
    add_submenu_page("index.php", "Test Readytoreceive", "Test Readytoreceive", "manage_options", "tt_get_readytoreceive", "tt_get_readytoreceive");
}

add_action("admin_menu", "wc_admin_menu", 11);

function tt_get_readytoreceive()
{

}



If you refresh your WordPress console, you should see the new link at the sidebar.

While we're at it, we may as well create four more links.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function wc_admin_menu()
{
    add_submenu_page("index.php", "Test Readytoreceive", "Test Readytoreceive", "manage_options", "tt_get_readytoreceive", "tt_get_readytoreceive");
    add_submenu_page("index.php", "Test Terms", "Test Terms", "manage_options", "tt_get_terms", "tt_get_terms");
    add_submenu_page("index.php", "Test Lastsent", "Test Lastsent", "manage_options", "tt_set_lastsent", "tt_set_lastsent");
    add_submenu_page("index.php", "Test Generate Email", "Test Generate Email", "manage_options", "tt_generate_mail", "tt_generate_mail");
    add_submenu_page("index.php", "Test Job", "Test Job", "manage_options", "tt_selfaffirmations", "tt_selfaffirmations");

}


We will, of course, need to create all these functions. Of all these functions, in this part of the web tutorial, we will only concentrate on the first three. These are the tests that will test the REST endpoints we created in the previous part of this web tutorial.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function wc_admin_menu()
{
add_submenu_page("index.php", "Test Readytoreceive", "Test Readytoreceive", "manage_options", "tt_get_readytoreceive", "tt_get_readytoreceive");
    add_submenu_page("index.php", "Test Terms", "Test Terms", "manage_options", "tt_get_terms", "tt_get_terms");
    add_submenu_page("index.php", "Test Lastsent", "Test Lastsent", "manage_options", "tt_set_lastsent", "tt_set_lastsent");
    add_submenu_page("index.php", "Test Generate Email", "Test Generate Email", "manage_options", "tt_generate_mail", "tt_generate_mail");
    add_submenu_page("index.php", "Test Job", "Test Job", "manage_options", "tt_selfaffirmations", "tt_selfaffirmations");
}

add_action("admin_menu", "wc_admin_menu", 11);

function tt_get_readytoreceive()
{

}

function tt_get_terms()
{

}

function tt_set_lastsent()
{

}

function tt_generate_mail()
{

}

function tt_selfaffirmations()
{

}



And here's five links instead of one!


We continue with the tt_get_readytoreceive() function. We're going to use cURL to get data via the REST endpoints. Thus, we begin by calling the curl_init() function and set it to the variable cURLConnection.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_readytoreceive()
{
    $cURLConnection = curl_init();
}


We follow up by using the curl_setopt() function to set the variables CURLOPT_URL and CURLOPT_RETURNTRANSFER for cURLConnection. For CURLOPT_URL, we use the URL that we defined for the endpoint readytoreceive. And CURLOPT_RETURNTRANSFER has to be true.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_readytoreceive()
{
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/readytoreceive");
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

}


We then run the curl_exec() function using cURLConnection as an argument. The result is assigned to the variable json_list. After that, we use the function curl_close() just as a matter of neatness.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_readytoreceive()
{
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/readytoreceive");
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

}


OK, so over here, we display json_list on screen.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_readytoreceive()
{
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/readytoreceive");
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    echo $json_list;
}


Run the code by clicking on the "Test readytoreceive" link. See what happens? Look familiar? If you didn't get anything, then you need to get back to the Oracle APEX database to change the LAST_SENT value back to "01/01/1970".


In case the screenshot was unclear, here's the JSON object.
{"items":[{"email":"teochewthunder@gmail.com","first_name":"Teochew","last_name":"Thunder","dob":"1977-11-05T00:00:00Z","gender":"M"}],"hasMore":false,"limit":25,"offset":0,"count":1,"links":[{"rel":"self","href":"https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/readytoreceive"},{"rel":"describedby","href":"https://apex.oracle.com/pls/apex/teochewthunder/metadata-catalog/mailinglist/item"},{"rel":"first","href":"https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/readytoreceive"}]}


Well, you know what the JSON looks like now. So we use json_decode() to decode json_list, and assign the result to list. Then we return the items array from the list object.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_readytoreceive()
{
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/readytoreceive");
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    echo $json_list;
    
    $list = json_decode($json_list);
    
    return $list->items;

}


Now we will work on tt_get_terms(). This is the function that calls the REST endpoint that returns the user's list of terms. It's largely the same as the last one, so just copy everything over first.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_terms()
{
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/readytoreceive");
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    echo $json_list;
    
    $list = json_decode($json_list);
    
    return $list->items;

}


For this function, there is a parameter, id, which is the user's email address. We will then change the URL, and append the value of id at the end.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_terms($id)
{
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/terms/" . $id);
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    echo $json_list;
    
    $list = json_decode($json_list);
    
    return $list->items;
}


We add this line to ensure that if id is NULL, as would be the case in a test, we'll use "teochewthunder@gmail.com".

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_terms($id)
{
    $id = ($id ? $id : "teochewthunder@gmail.com");
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/terms/" . $id);
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    echo $json_list;
    
    $list = json_decode($json_list);
    
    return $list->items;
}


Test this. You should get the following output.


This is the JSON output. It could potentially be quite lengthy.
{"items":[{"type":"DESCRIPTIONS","term":"acidic"},{"type":"DESCRIPTIONS","term":"cynical"},{"type":"DESCRIPTIONS","term":"jolly"},{"type":"DESCRIPTIONS","term":"patient"},{"type":"DESCRIPTIONS","term":"pragmatic"},{"type":"INTERESTS","term":"comedy"},{"type":"INTERESTS","term":"programming"},{"type":"INTERESTS","term":"soccer"},{"type":"INTERESTS","term":"superheroes"}],"hasMore":false,"limit":25,"offset":0,"count":9,"links":[{"rel":"self","href":"https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/terms/teochewthunder@gmail.com"},{"rel":"describedby","href":"https://apex.oracle.com/pls/apex/teochewthunder/metadata-catalog/mailinglist/terms/item"},{"rel":"first","href":"https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/terms/teochewthunder@gmail.com"}]}


Now, for this function, we don't want to return the entire list. We want to randomly include terms, so as to vary the output more. We begin by declaring two arrays, interests and descriptions. We'll change the returned output to an array containing the arrays interests and descriptions, named the same.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_terms($id)
{
    $id = ($id ? $id : "teochewthunder@gmail.com");
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/terms/" . $id);
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    echo $json_list;
    
    $list = json_decode($json_list);
    
    $interests = [];
    $descriptions = [];
    
    return ["interests" => $interests, "descriptions" => $descriptions];

}


Then we use a Foreach loop on the items array of the list object.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_terms($id)
{
    $id = ($id ? $id : "teochewthunder@gmail.com");
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/terms/" . $id);
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    echo $json_list;
    
    $list = json_decode($json_list);
    
    $interests = [];
    $descriptions = [];
    foreach($list->items as $i) {

    }

    
    return ["interests" => $interests, "descriptions" => $descriptions];
}


Depending on the value of the type property of the current element, add the term property to either interests or descriptions.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_terms($id)
{
    $id = ($id ? $id : "teochewthunder@gmail.com");
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/terms/" . $id);
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    echo $json_list;
    
    $list = json_decode($json_list);
    
    $interests = [];
    $descriptions = [];
    foreach($list->items as $i) {
        if ($i->type == "INTERESTS") $interests[] = $i->term;
        if ($i->type == "DESCRIPTIONS") $descriptions[] = $i->term;

    }
    
    return ["interests" => $interests, "descriptions" => $descriptions];
}


We then use the rand() function in each If block, giving a 50% probability of the term being added to their respective arrays.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_get_terms($id)
{
    $id = ($id ? $id : "teochewthunder@gmail.com");
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/terms/" . $id);
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    echo $json_list;
    
    $list = json_decode($json_list);
    
    $interests = [];
    $descriptions = [];
    foreach($list->items as $i) {
        if ($i->type == "INTERESTS" && rand(0, 1) == 1) $interests[] = $i->term;
        if ($i->type == "DESCRIPTIONS" && rand(0, 1) == 1) $descriptions[] = $i->term;
    }
    
    return ["interests" => $interests, "descriptions" => $descriptions];
}


For the last REST endpoint, we again copy the contents of tt_get_readytoreceive() into tt_set_lastsent().

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_set_lastsent()
{
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/readytoreceive");
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    echo $json_list;
    
    $list = json_decode($json_list);
    
    return $list->items;

}


As with tt_get_terms(), we add a parameter and change the URL. We'll also add the line to use "teochewthunder@gmail.com" as the default value of id.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_set_lastsent($id)
{
    $id = ($id ? $id : "teochewthunder@gmail.com");
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/setreceived/" . $id);
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    echo $json_list;
    
    $list = json_decode($json_list);
    
    return $list->items;
}


We don't want to display anything, or return anything.

wp-content/plugins/tt_selfaffirmations/tt_selfaffirmations.php
function tt_set_lastsent($id)
{
    $id = ($id ? $id : "teochewthunder@gmail.com");
    $cURLConnection = curl_init();

    curl_setopt($cURLConnection, CURLOPT_URL, "https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/setreceived/" . $id);
    curl_setopt($cURLConnection, CURLOPT_RETURNTRANSFER, true);

    $json_list = curl_exec($cURLConnection);
    curl_close($cURLConnection);

    //echo $json_list;
    
    //$list = json_decode($json_list);
    
    return; //$list->items;
}


Now test this. You should get nothing in the display.


If you test the first link again, this time you should not see the user in the list, because the value of LAST_SENT has been set to today's date.


In the JSON object, you see that the items property is now an empty array.
{"items":[],"hasMore":false,"limit":25,"offset":0,"count":0,"links":[{"rel":"self","href":"https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/readytoreceive"},{"rel":"describedby","href":"https://apex.oracle.com/pls/apex/teochewthunder/metadata-catalog/mailinglist/item"},{"rel":"first","href":"https://apex.oracle.com/pls/apex/teochewthunder/mailinglist/readytoreceive"}]}


This wasn't very hard, was it? We basically built on what we already created in Oracle APEX. The next part will also deal with REST endpoints, but it will be ChatGPT's REST endpoint.

Next

Generating email content using ChatGPT.

Sunday, 19 November 2023

Web Tutorial: The Self-affirmations WordPress Plugin (Part 1/4)

Last September, I walked you through setting up a Mailing List Registration Form in Oracle APEX. This month is a follow up. We are going to use the database created then, to drive what we're making today. So if you didn't pay attention the last time, tough luck, chum! We'll be continuing on our work from the last round.

We want to set up a script in WordPress to send out those emails on a scheduled basis, to the users in the database. For that, first of all, we have to set up REST endpoints for the database in Oracle APEX.
Log in to your workspace and go straight to SQL Workshop. There should be a range of big buttons. We want the one that says RESTful Services.

Over here you should see this screen, with no Modules yet. Click the Create Module button.

This should come up. Make up a name for the Module, and an endpoint name. Click Save.

Now you should see this. Click on the Module name.

Now you will see the Module. You'll want to create Templates. The button is on the lower right, in a nice aqua color.

Here we add the template. For this, we use the name "readytoreceive". This is where we get a list of all users that have not received an email in the last n number of days, determined by the value of DAYS.

Now it's there, but this is just a Template. We'll need to create a Handler for it.

In here, set the Method to "GET". Then click Create Handler.

Now you'll see this Handler for "GET" has been created. Click on it...

...and you're in this interface. Set Source Type to "Collection Query". And input the SQL below.

This is the SQL. We basically say that if the user's LAST_SENT value is less than the value of today's date minus DAYS minus 1, it's added to the list. Why minus 1? Well, DAYS could be 1, and if user's LAST_SENT value was yesterday, it would not get entered into the list. Thus, we do a minus 1 to take care of that case.
SELECT EMAIL, FIRST_NAME, LAST_NAME, DOB, GENDER FROM MAILING_LIST WHERE LAST_SENT < TO_DATE(CURRENT_DATE - (DAYS - 1))

So that's your first endpoint!

The next endpoint is called "terms". We set it up the same way, first creating a Template. Unlike the last time, we have ":email" in the URI Template setting. This is the parameter which we will use later.

Then we create a Handler for it by clicking Create Handler. For this, everything is the same as previously, except for the SQL.

The SQL is as follows. We want to get everything from the MAILING_LIST_TERMS table where the EMAIL field matches the value of email.
SELECT TYPE, TERM FROM MAILING_LIST_TERMS WHERE EMAIL = :email

We need to define the parameter, email, for the query. In the Parameters section right at the bottom, click the Add Row button and fill in the details as shown. This defines the parameter for the query as a string.

Done with the second endpoint!

The third one also uses a parameter, but it will update the database. Create a Template for it, like this.

Then create a Handler. This time, the Source Type is "PL/SQL".

This is the SQL. It sets the value of LAST_SENT for the MAILING_LIST table where the EMAIL value matches email, to today's date.
UPDATE MAILING_LIST SET LAST_SENT = CURRENT_DATE  WHERE EMAIL = :email

Of course, we will need to set up the parameter, email.

Time to test!

In your browser, test this URL for readytoreceive. You should get this.

Now test terms using an email address in the database. You should have this list, assuming you've entered data for it.

Test setreceived with the email. There's no visual indication, but if you check the database, the LAST_SENT column for that email should be today's date. Now if you test setreceived again, this email should not appear in the list!

In this part of the web tutorial, we set up REST endpoints to get and set data. Subsequently, we will use these endpoints for the script.

Next

The WordPress plugin, setting up testing.

Saturday, 11 November 2023

Why I did it my way

Being a software developer has been a rewarding, if not spectacularly successful, career. All the sweeter because it could have turned out differently if not for a couple key decisions I made in my youth.

When I graduated with a Bachelor's Degree in Information Technology more than two decades ago, my father wanted me to help with the family business. His plan was for me to come in, be a Manager, maybe even a General Manager, in the company. I declined, and Mom said this was pure ego on my part. Dad said nothing. I think (I hope) he understood.

So, why? Why take that risk?

I like to think ego had very little to do with it. The considerations were mostly of a practical nature. Let's start with the pettier reasons.

Dad's business was in second-hand cars. This may not be the expected reaction of a young hot-blooded heterosexual male, but there were few things I could be less interested in. I just didn't see myself with my spanking new Bachelor's Degree in Information Technology dealing in used cars. Honestly, if I was going to use that Degree on what looked like totally unrelated stuff, there were tons of things that were a lot more fun.

The next reason was distance, or lack thereof. Not that Dad was an unbearable guy to be around, but at the same time he wasn't such a delight that I wanted to see him in the office and at home. Boundaries are a thing.

Finding my path.

The more substantial reason was about me wanting, in Frank Sinatra's famous words, to do it my way. Get my own career. Start wherever I started, go wherever that took me, all without trading on the family name. I could wind up having a shit career (and for a long time, I did) but at least that shit career would be -my- career, not one that was handed to me. Again, Mom said it was about ego. Grandma said it was about having backbone. I liked her take better.

Finally, and perhaps most importantly, time is finite for all mortals. My father would not be around forever, and if I allowed myself to grow dependent on his presence, I would be doing everyone a huge disservice.

Regrets?

There were times I had to remind myself that I wasn't the only one who was encountering difficulty; everyone had their own struggles. Plenty of people had it worse than me. All I ever wanted to do was code for a living. I absolutely refused to believe that such a simple goal was beyond me.

It's not been a bed of roses. There were times it got so rough I wondered to myself why I didn't simply take the easy path offered. Every asshole employer I worked for, every meager paycheck I tried to stretch, every time I had to work with people I didn't like. Every night spent on a build that just wouldn't compile. Every time I made an elementary mistake that led to a system crash.

Tumultuous seas.

But for every job I lost, I found a higher paying one. For every colleague I hated, there were three others I made friends with. For every difficult customer I had to deal with, I refined my skills at dealing with them. For every obstacle encountered, I obtained good lessons that often were able to be applied elsewhere. Every ticking off I got made my skin thicker. Even unpleasant employers were an experience. Not experiences I necessarily want to repeat, but valuable nonetheless. I discovered a resilience; indeed, even an antifragility, I had never suspected myself of being able to muster.

Recently, I was in a Microsoft Teams meeting, and with a start, I realized that I recognized the name of the vendor in the meeting, and the voice. The vendor speaking was an ex-boss from 2011. My ex-boss was now one of my current company's vendors. Twelve years ago, I was working for him, and drawing less than half my current pay. Talk about mind-boggling. That's how far this unexceptional software developer has traveled.

Conclusion

Now when I look back and think about how hard I had to work for less money, I marvel at how long I've lasted. And I congratulate myself, because no one else will. And no one else should have to.

My career, mediocre as it may be, is mine. And it was no mistake.

Doing it my way like an absolute boss,
T___T