Michael Burkhardt’s Weblog

Automating My Now Page

What is a Now page?

According to nownownow.com, a now page “tells you what this person is focused on at this point in their life.” I use my now page to share my current status and the things I’ve recently read, watched, or listened to.

Keeping It Simple

A lot of folks use a variety of tools to automate the capture of their various listening/viewing/watching activity. I’m too lazy to set all that up, so I’m starting simple. I created a YAML file in my pastebin that I can easily edit by hand. Here's a sample:

listen:
  -
    date: 20230212
    icon: microphone-lines
    title: The Foreign Desk No. 476
    url: https://monocle.com/radio/shows/the-foreign-desk/476
read:
  -
    date: 20230124
    icon: book
    title: "*The Dark Forest*, by Cixin Liu"
    url: https://www.goodreads.com/book/show/23168817-the-dark-forest
watch:
  -
    date: 20230211
    icon: futbol
    title: Brentford FC (EPL)
    url: https://www.brentfordfc.com/en

As you can see, each entry stores the date, icon, link text, and URL. I’m not actually doing anything with the date right now. Maybe someday. The icon can be any of the available FontAwesome free icons.

Scripting the Automation

I use Python a lot these days, so it was the easiest for me to work with. The idea is simple (and so, in fact, is the script): load the YAML file, generate markdown, upload to the omg.lol /now API.

Here's the code nowbot.py:

import boto3
import hashlib
import json
import urllib3
import yaml

def lambda_handler(event, context):

    # OMG.LOL API CONFIG
    omg_now_url = 'https://api.omg.lol/address/mihobu/now'
    omg_api_key = 'XXXXXXXXXXXXXXXXXXXXXXX'
    omg_headers = {
        'Authorization': f'Bearer {omg_api_key}'
    }
    
    # CONFIGURE HEADINGS
    heading = {
        "listen": "What I’m Listening To",
        "read": "What I’m Reading",
        "watch": "What I’m Watching"
    }
    
    # GET A CONNECTION POOL
    http = urllib3.PoolManager()
    
    # GET AN SSM CLIENT
    ssm_client = boto3.client('ssm')
    
    # OBTAIN OLD YAML DIGEST FROM THE PARAMETER STORE
    try:
        response = ssm_client.get_parameter(Name='nowbot_digest')
        old_digest = response['Parameter']['Value']
    except Exception as e:
        print(e)
        print("Ignored.")
        old_digest = 'UndefinedDigestValue'

    # GET THE MEDIA YAML FILE
    resp = http.request('GET', 'https://mihobu.paste.lol/media.yaml/raw')
    media2 = yaml.safe_load(resp.data)
    new_digest = hashlib.md5(resp.data).hexdigest()
    
    # HAVE THE CONTENTS CHANGED?
    if old_digest == new_digest:
        # YAML hasn't changed
        print('YAML content has not changed. Exiting.')
        return None

    # BEGIN TO CONSTRUCT THE NEW NOW CONTENT
    now = '''{profile-picture}
    
# Michael Burkhardt
    
## What I’m Doing Now
    
<script src="https://status.lol/mihobu.js?time&fluent&pretty&link"></script>
'''

    # LOOP OVER THE YAML CONTENT TO BUILD THE NEW NOW CONTENT
    max_items = 5
    for t in media2.keys():
        now += f"\n## {heading[t]}\n\n"
        c = 0
        for item in media2[t]:
            c = c + 1
            now += "- [{}]({}) {{{}}}\n".format(item['title'], item['url'], item['icon'])
            if c >= max_items:
                break

    # WRAP IT UP
    now += '''
<div class="nowlol">BACK TO <a href="https://mihobu.monkeywalk.com/">ALL THINGS MIHOBU</a></div>
{last-updated}
'''

    # CALL THE OMG.LOL API TO UPDATE THE NOW PAGE CONTENTS
    payload = {
        'content': now,
        'listed': 1
    }
    resp2 = http.request('POST', omg_now_url, body=json.dumps(payload), headers=omg_headers)

    # UPDATE THE MD5 DIGEST IN THE PARAMETER STORE
    response = ssm_client.put_parameter(
        Name='nowbot_digest',
        Value=new_digest,
        Overwrite=True
    )

Missing Library

Setting up the Lambda function was very easy using the AWS Console, but I ran into one snag:

[ERROR] Runtime.ImportModuleError: Unable to import module 'lambda_function': No module named 'yaml'
Traceback (most recent call last):

Crud. That means I need to create a Lambda Layer containing the missing library (or libraries).

Lambda provides a lot Python libraries by default, but not everything you might every want. A Lambda Layers is basically just a ZIP file with a site-packages directory containing additional libraries you want to use.

To create my Lambda Layer, I simply installed packages to a specified directory on my Mac.

% pip install pyyaml --target ./python/lib/python3.9/site-packages --no-deps --no-binary=:all:

The directory must be one that AWS Lambda recognizes for the particular runtime in use—in this case, Python 3.9. I wanted to make sure I was explicitly adding all the libraries I needed, so that’s why I used the --no-deps option. Binary packages must be handled a little differently, thus the --no-binary option. (I didn’t any binary packages for this example, so I don’t cover that here. Maybe another post some day.)

Since I didn’t run into any other dependencies and Lambda didn’t complain about any of my other libraries, all that was left was to ZIP up the contents.

% zip -r layer.zip python

Creating the Layer in the Lambda service page of the AWS Console is very easy. Give the layer a name, upload the ZIP file, and define the compatible architecture (x86_64) and runtime (Python 3.9). Click “Create” and we’re done!

From there, we simply attach the Layer to the Lambda Function. A quick test: success!

Scheduling

To auto-generate the now page content, I set up a scheduler in Amazon EventBridge to invoke my Lambda function every 8 hours.

Acknowledgements

Thanks to Adam and the entire omg.lol for providing a platform that is full of fun and energy, as well as to Cory and Robb who inspired this little project.

Recent Posts

HOMENOWBLOG