Module: postgraphs

This module finds the graph and wordcloud images and alt text files that were created by graph and wordcloud, and it creates a Mastodon post with these images attached. It assigns the alt text to each image and also uses that alt text as the body of the post.

Example post

Configuration

The postgraphs module is controlled by the [postgraphs] section of the configuration file:

[postgraphs]
debug        = 20
dry_run      = false
visibility   = public
message      = Here are the graphs!

Configuration Options

Option Description
dry_run If true, don't actually post (default: false)
visibility Post visibility: public, unlisted, private, or direct (default: public)
message Custom message to include with the post

It will pull the standard Mastodon authentication parameters. But you can include different parameters in the [postgraphs] section of the config file.

[postgraphs]
api_base_url = https://example.social
cred_file    = /path/to/your/access_token.secret
botusername  = yourbotname

Usage

To post graphs to Mastodon:

mastoscore --debug=info ini/yourevent.ini graph
mastoscore --debug=info ini/yourevent.ini wordcloud
mastoscore --debug=info ini/yourevent.ini postgraphs

Code Reference

Module for posting graph and wordcloud images to Mastodon with their alt text.

This module finds the graph and wordcloud images along with their corresponding alt text files, and creates a post with these images attached.

find_graph_files(config)

Find the graph image and its corresponding alt text file.

Parameters

  • config: A ConfigParser object from the config module

Returns

A tuple containing (graph_image_path, graph_alt_text) or (None, None) if not found

Source code in mastoscore/postgraphs.py
def find_graph_files(config):
    """
    Find the graph image and its corresponding alt text file.

    # Parameters
    - **config**: A ConfigParser object from the [config](module-config.md) module

    # Returns
    A tuple containing (graph_image_path, graph_alt_text) or (None, None) if not found
    """
    hashtag = config.get('mastoscore', 'hashtag')
    year = config.get('mastoscore', 'event_year')
    month = config.get('mastoscore', 'event_month')
    day = config.get('mastoscore', 'event_day')
    date_str = f"{year}{month}{day}"

    # Define paths
    graphs_dir = create_journal_directory(config)
    graph_pattern = os.path.join(graphs_dir, f"{hashtag}-{date_str}.png")
    alt_text_pattern = os.path.join(graphs_dir, f"{hashtag}-{date_str}.txt")

    # Find files
    graph_files = glob.glob(graph_pattern)
    alt_text_files = glob.glob(alt_text_pattern)

    if graph_files and os.path.exists(graph_files[0]):
        graph_image_path = graph_files[0]

        # Read alt text if available
        if alt_text_files and os.path.exists(alt_text_files[0]):
            with open(alt_text_files[0], 'r') as f:
                graph_alt_text = f.read()
        else:
            graph_alt_text = f"Graph of #{hashtag} activity on {date_str}"

        return graph_image_path, graph_alt_text

    return None, None

find_wordcloud_files(config)

Find the wordcloud image and its corresponding alt text file.

Parameters

  • config: A ConfigParser object from the config module

Returns

A tuple containing (wordcloud_image_path, wordcloud_alt_text) or (None, None) if not found

Source code in mastoscore/postgraphs.py
def find_wordcloud_files(config):
    """
    Find the wordcloud image and its corresponding alt text file.

    # Parameters
    - **config**: A ConfigParser object from the [config](module-config.md) module

    # Returns
    A tuple containing (wordcloud_image_path, wordcloud_alt_text) or (None, None) if not found
    """
    hashtag = config.get('mastoscore', 'hashtag')
    year = config.get('mastoscore', 'event_year')
    month = config.get('mastoscore', 'event_month')
    day = config.get('mastoscore', 'event_day')
    date_str = f"{year}{month}{day}"

    # Get hashtag_fix value, default to 'as-is'
    hashtag_fix = config.get('wordcloud', 'hashtag_fix', fallback='as-is')

    # Define paths
    wordcloud_dir = create_journal_directory(config)
    wordcloud_pattern = os.path.join(wordcloud_dir, f"wordcloud-{hashtag}-{date_str}-{hashtag_fix}.png")
    alt_text_pattern = os.path.join(wordcloud_dir, f"wordcloud-{hashtag}-{date_str}-{hashtag_fix}.txt")

    # Find files
    wordcloud_files = glob.glob(wordcloud_pattern)
    alt_text_files = glob.glob(alt_text_pattern)

    if wordcloud_files and os.path.exists(wordcloud_files[0]):
        wordcloud_image_path = wordcloud_files[0]

        # Read alt text if available
        if alt_text_files and os.path.exists(alt_text_files[0]):
            with open(alt_text_files[0], 'r') as f:
                wordcloud_alt_text = f.read()
        else:
            wordcloud_alt_text = f"Word cloud for #{hashtag} on {date_str}"

        return wordcloud_image_path, wordcloud_alt_text

    return None, None

post_graphs(config)

Find graph and wordcloud images with their alt text and post them to Mastodon.

Parameters

  • config: A ConfigParser object from the config module

Config Parameters Used

Option Description
postgraphs:debug Debug level for logging
postgraphs:dry_run Boolean. If True, don't actually post.
postgraphs:visibility Post visibility (public, unlisted, private, direct)
postgraphs:message Custom message to include with the post
mastoscore:hashtag Hashtag used for the analysis
mastoscore:event_year Year of the event (YYYY)
mastoscore:event_month Month of the event (MM)
mastoscore:event_day Day of the event (DD)

Returns

Boolean indicating success or failure

Source code in mastoscore/postgraphs.py
def post_graphs(config):
    """
    Find graph and wordcloud images with their alt text and post them to Mastodon.

    # Parameters
    - **config**: A ConfigParser object from the [config](module-config.md) module

    # Config Parameters Used
    | Option | Description |
    | ------- | ------- |
    | `postgraphs:debug` | Debug level for logging |
    | `postgraphs:dry_run` | Boolean. If True, don't actually post. |
    | `postgraphs:visibility` | Post visibility (public, unlisted, private, direct) |
    | `postgraphs:message` | Custom message to include with the post |
    | `mastoscore:hashtag` | Hashtag used for the analysis |
    | `mastoscore:event_year` | Year of the event (YYYY) |
    | `mastoscore:event_month` | Month of the event (MM) |
    | `mastoscore:event_day` | Day of the event (DD) |

    # Returns
    Boolean indicating success or failure
    """
    # Get configuration values
    debug = config.getint('postgraphs', 'debug', fallback=20)
    dry_run = config.getboolean('postgraphs', 'dry_run', fallback=False)
    visibility = config.get('postgraphs', 'visibility', fallback='public')
    custom_message = config.get('postgraphs', 'message', fallback='')
    hashtag = config.get('mastoscore', 'hashtag')

    # Set up logging
    logger = logging.getLogger(__name__)
    logging.basicConfig(format='%(levelname)s\t%(message)s')
    logger.setLevel(debug)

    # Find graph and wordcloud files
    graph_image_path, graph_alt_text = find_graph_files(config)
    wordcloud_image_path, wordcloud_alt_text = find_wordcloud_files(config)

    if not graph_image_path and not wordcloud_image_path:
        logger.error("No graph or wordcloud images found")
        return False

    # Prepare media list
    media_ids = []
    media_files = []

    # Create message text
    if custom_message:
        # get rid of any embedded new lines
        message = " ".join(custom_message.splitlines())
    else:
        message = f"Visualizations for #{hashtag}"

    message += f"\n\n{graph_alt_text}\n\n"
    message += f"{wordcloud_alt_text}\n\n"

    # Add hashtag to the message
    message += f"#{hashtag}"

    if dry_run:
        logger.info("DRY RUN - Would post the following:")
        logger.info(f"Message: {message}")

        if graph_image_path:
            logger.info(f"Graph image: {graph_image_path}")
            logger.info(f"Graph alt text: {graph_alt_text}")

        if wordcloud_image_path:
            logger.info(f"Wordcloud image: {wordcloud_image_path}")
            logger.info(f"Wordcloud alt text: {wordcloud_alt_text}")

        return True

    # Create Tooter object
    try:
        tooter = Tooter(config, 'postgraphs')
    except Exception as e:
        logger.critical("Failed to create Tooter object")
        logger.critical(e)
        return False

    # Upload media files
    if graph_image_path:
        try:
            media = tooter.media_post(
                graph_image_path,
                description=graph_alt_text
            )
            media_ids.append(media['id'])
            media_files.append(graph_image_path)
            logger.info(f"Uploaded graph image: {graph_image_path}")
        except Exception as e:
            logger.error(f"Failed to upload graph image: {e}")

    if wordcloud_image_path:
        try:
            media = tooter.media_post(
                wordcloud_image_path,
                description=wordcloud_alt_text
            )
            media_ids.append(media['id'])
            media_files.append(wordcloud_image_path)
            logger.info(f"Uploaded wordcloud image: {wordcloud_image_path}")
        except Exception as e:
            logger.error(f"Failed to upload wordcloud image: {e}")

    # Post status with media
    if media_ids:
        try:
            status = tooter.status_post(
                message,
                media_ids=media_ids,
                visibility=visibility
            )
            logger.info(f"Posted status with {len(media_ids)} images: {status['url']}")
            return True
        except Exception as e:
            logger.error(f"Failed to post status: {e}")
            return False
    else:
        logger.error("No media files were successfully uploaded")
        return False