2
Extension Development
Storm Dragon edited this page 2025-07-29 15:00:35 -04:00

Extension Development

TTYverse extensions are Perl scripts that integrate with TTYverse's event system to add custom functionality. This guide covers creating, testing, and deploying custom extensions.

Extension Architecture

Extensions hook into TTYverse's multi-process architecture through defined callback functions:

  • $handle - Called for each incoming post (main processing hook)
  • $addaction - Called for user commands (adds new slash commands)
  • $prepost - Called before sending posts
  • $postpost - Called after sending posts
  • $heartbeat - Called periodically for background tasks
  • $notifier - Called for notification events (sounds, desktop alerts, etc.)

Available Variables

Extensions have access to these TTYverse variables:

Core Variables

  • $whoami - Current user's username
  • $silent - Silent mode flag (suppress output when true)
  • $verbose - Verbose mode flag (show debug info when true)
  • $stdout - Output filehandle for user messages
  • $streamout - Output filehandle for timeline content

File Paths

  • $data - XDG data directory (~/.local/share/ttyverse)
  • $config - XDG config directory (~/.config/ttyverse)
  • $store - Extension persistent storage hash

Post Processing

  • &defaulthandle($ref) - Default post display function
  • &standardpost($ref, $text_only) - Format post for notifications
  • &findtarget($code) - Find post by menu code (a1, b3, etc.)
  • &descape($text) - Decode HTML entities and escape sequences safely
  • &common_split_post($text, $reply_to, $media) - Send posts via TTYverse

Timeline State

  • $last_id - Current timeline position (0 during initial load)
  • $initial_load_in_progress - True during startup timeline fetch

Environment Access

  • $ENV{'HOME'} - User's home directory
  • $ENV{'DISPLAY'} - X11 display (for GUI detection)
  • $ENV{'XDG_DATA_HOME'} - XDG data directory override
  • $ENV{'XDG_CONFIG_HOME'} - XDG config directory override

Extension Types

1. Post Processing Extensions

Handle incoming posts and modify display:

$handle = sub {
    my $post_ref = shift;
    my $class = shift;  # Notification class (default, mention, dm, etc.)
    
    # Your custom processing here
    # Access post data: $post_ref->{'content'}, $post_ref->{'account'}->{'username'}
    
    # Always call default handler to display the post
    &defaulthandle($post_ref);
    return 1;
};

2. Command Extensions

Add new slash commands:

$addaction = sub {
    my $command_line = $_;  # Full command line input
    
    if ($command_line =~ /^\/mycommand\s+(.*)$/) {
        my $args = $1;
        
        # Process your command
        print $streamout "-- My command executed with: $args\n";
        return 1;  # Command handled
    }
    
    return 0;  # Not our command, let TTYverse handle it
};

3. Notification Extensions

Handle sound, visual, or other notifications:

sub notifier_mynotifier {
    my $class = shift;    # Notification type (dm, mention, boost, etc.)
    my $text = shift;     # Formatted notification text  
    my $post_ref = shift; # Original post data
    
    # Handle initialization (called with no $class)
    if (!defined($class)) {
        print $stdout "-- My notifier loaded\n";
        return 1;
    }
    
    # Process notification based on class
    if ($class eq 'dm') {
        # Handle direct messages with higher priority
    } elsif ($class eq 'mention') {
        # Handle mentions
    }
    
    return 1;
}

# Register the notifier (must be done globally)
# TTYverse automatically finds functions named notifier_*

4. Time-based Extensions

Extensions that perform actions based on time intervals:

# Extension global variables for state tracking
my $last_action_time = 0;
my $first_run = 0;

$handle = sub {
    my $post_ref = shift;
    
    # Initialize on first run
    if (!$first_run) {
        $first_run = 1;
        $last_action_time = time;
        print $stdout "-- Time-based extension loaded\n" if (!$silent);
    }
    
    # Check if enough time has passed (5 minutes = 300 seconds)
    my $current_time = time;
    if ($current_time - $last_action_time >= 300) {
        $last_action_time = $current_time;
        
        # Perform periodic action
        my ($sec,$min,$hour,$day,$mon,$year) = localtime($current_time);
        my $timestamp = sprintf("%02d:%02d", $hour, $min);
        print $streamout "-- $timestamp --\n" if (!$silent);
    }
    
    # Always call default handler
    &defaulthandle($post_ref);
    return 1;
};

Configuration and State Management

RC File Configuration

Extensions can use configuration variables from the user's RC file:

# In user's ~/.config/ttyverse/ttyverserc:
# extpref_myext_setting=value
# extpref_myext_enabled=1

# In your extension:
my $setting = $extpref_myext_setting || "default_value";
my $enabled = $extpref_myext_enabled || 0;

File-based Configuration Storage

For settings that change during runtime, use separate config files:

# Read extension-specific config file
my $config_file = "$ENV{'HOME'}/.config/ttyverse/myext_settings";
my $setting_value;

if (-e $config_file) {
    open my $fh, '<', $config_file or die "Cannot read config: $!";
    $setting_value = <$fh>;
    close $fh;
    chomp($setting_value) if defined($setting_value);
} else {
    $setting_value = "default";
}

# Write extension-specific config file
sub save_setting {
    my $value = shift;
    my $config_file = "$ENV{'HOME'}/.config/ttyverse/myext_settings";
    
    open my $fh, '>', $config_file or die "Cannot write config: $!";
    print $fh $value;
    close $fh;
}

XDG Directory Handling

Use XDG standards for cross-platform compatibility:

# Get XDG-compliant data directory
my $data_dir = $ENV{'XDG_DATA_HOME'} || "$ENV{'HOME'}/.local/share";
my $ext_data_dir = "$data_dir/ttyverse";

# Get XDG-compliant config directory  
my $config_dir = $ENV{'XDG_CONFIG_HOME'} || "$ENV{'HOME'}/.config";
my $ext_config_dir = "$config_dir/ttyverse";

# Create directories if needed
mkdir $ext_data_dir unless -d $ext_data_dir;
mkdir $ext_config_dir unless -d $ext_config_dir;

Persistent Storage with $store

Use the $store hash for data that persists during the TTYverse session:

# Store data (survives between function calls)
$store->{'my_counter'} = 0;
$store->{'user_preferences'} = { volume => 50, enabled => 1 };

# Access stored data
my $count = $store->{'my_counter'} || 0;
$count++;
$store->{'my_counter'} = $count;

# Complex data structures
my $prefs = $store->{'user_preferences'} || {};
$prefs->{'last_used'} = time;
$store->{'user_preferences'} = $prefs;

Post Data Structure

Post references contain Mastodon API data:

$post_ref = {
    'id' => '12345',
    'content' => 'Post text with <em>HTML</em>',
    'account' => {
        'username' => 'user',
        'acct' => 'user@domain.com',
        'display_name' => 'Display Name'
    },
    'mentions' => [
        {
            'username' => 'mentioned_user',
            'acct' => 'mentioned_user@domain.com'
        }
    ],
    'created_at' => '2024-01-01T12:00:00Z',
    'visibility' => 'public'
};

Notification Classes

TTYverse uses these notification classes:

  • default - Regular timeline posts
  • mention - Posts that mention your username
  • dm - Direct messages (highest priority)
  • me - Your own posts
  • follow - New followers
  • boost - Your posts were boosted/shared
  • favourite - Your posts were favourited/liked
  • poll - Poll notifications
  • announcement - Server announcements

Example: Complete Extension

Here's a comprehensive example extension that demonstrates multiple patterns:

# Advanced Custom Extension
# Adds /greet command and tracks greeting statistics
# Demonstrates configuration, state management, and safe text processing

# Extension configuration with defaults
my $enabled = $extpref_greet_enabled || 1;
my $greeting_style = $extpref_greet_style || "friendly";

# Initialize extension state
if (!$store->{'greet_loaded'}) {
    $store->{'greet_loaded'} = 1;
    $store->{'greet_count'} = 0;
    print $stdout "-- Greeting extension loaded (style: $greeting_style)\n" if (!$silent);
}

# Command handler
$addaction = sub {
    my $command = $_;
    
    # Toggle extension on/off
    if ($command eq '/greet toggle') {
        my $config_file = "$ENV{'HOME'}/.config/ttyverse/greet_enabled";
        my $new_state = $enabled ? 0 : 1;
        
        open my $fh, '>', $config_file or die "Cannot save config: $!";
        print $fh $new_state;
        close $fh;
        
        $enabled = $new_state;
        my $status = $enabled ? "enabled" : "disabled";
        print $streamout "-- Greeting extension $status\n";
        return 1;
    }
    
    # Show greeting statistics
    if ($command eq '/greet stats') {
        my $count = $store->{'greet_count'} || 0;
        print $streamout "-- Total greetings sent: $count\n";
        return 1;
    }
    
    # Send greeting
    if ($command =~ /^\/greet\s+(\S+)\s*(.*)$/) {
        return 0 unless $enabled;  # Respect user preference
        
        my $username = $1;
        my $custom_message = $2;
        
        # Clean and validate username
        $username = &descape($username);
        $username =~ s/^@//;  # Remove @ if user included it
        
        # Choose greeting based on style and custom message
        my $greeting;
        if ($custom_message) {
            $greeting = &descape($custom_message);
        } elsif ($greeting_style eq "formal") {
            $greeting = "Good day!";
        } elsif ($greeting_style eq "casual") {
            $greeting = "Hey there! 😊";
        } else {  # friendly (default)
            $greeting = "Hello! 👋";
        }
        
        # Send the greeting
        my $post_text = "\@$username $greeting";
        my $result = &common_split_post($post_text, undef, undef);
        
        # Update statistics on success
        if ($result) {
            $store->{'greet_count'}++;
            print $stdout "-- Greeting sent to $username (total: $store->{'greet_count'})\n" if ($verbose);
        }
        
        return $result;
    }
    
    return 0;  # Not our command
};

# Post handler (optional - could add auto-greeting detection)
$handle = sub {
    my $post_ref = shift;
    my $class = shift;
    
    # Skip during initial load
    if ($last_id eq 0) {
        &defaulthandle($post_ref);
        return 1;
    }
    
    # Could add logic here to detect greetings and respond
    # For now, just process normally
    &defaulthandle($post_ref);
    return 1;
};

# Extension successfully loaded
$store->{'loaded'} = 1;

# Required return value
1;

Extension Development Workflow

1. Create Extension File

# Create extension in the extensions directory
~/.local/share/ttyverse/extensions/myextension.pl

2. Test Extension

# Test Perl syntax
perl -c ~/.local/share/ttyverse/extensions/myextension.pl

# Load extension with TTYverse
./ttyverse.pl -exts=myextension -verbose

3. Debug Extension

Use verbose mode to see debug output:

./ttyverse.pl -exts=myextension -verbose

Add debug output in your extension:

print $stdout "-- DEBUG: My extension called\n" if ($verbose);

Advanced Extension Patterns

Handling Initial Timeline Load

Prevent extension spam during startup by checking timeline state:

$handle = sub {
    my $post_ref = shift;
    my $class = shift;
    
    # Skip processing during initial timeline load
    if ($last_id eq 0) {
        &defaulthandle($post_ref);
        return 1;
    }
    
    # Or use the more explicit flag
    if ($initial_load_in_progress) {
        &defaulthandle($post_ref);
        return 1;
    }
    
    # Your extension logic here...
    &defaulthandle($post_ref);
    return 1;
};

Safe Text Processing

Always use &descape() when processing post content:

# Extract and clean post text
my $author = &descape($post_ref->{'account'}->{'display_name'} || 
                     $post_ref->{'account'}->{'username'});
my $content = &descape($post_ref->{'content'});

# Additional cleaning for text-to-speech or notifications
$content =~ s/<[^>]*>//g;           # Remove HTML tags
$content =~ s/&[a-zA-Z0-9]+;//g;    # Remove remaining entities  
$content =~ s/https?:\/\/\S+/ link /g; # Replace URLs
$content =~ s/#(\w+)/ hashtag $1 /g;   # Make hashtags readable

Environment Detection

Check system capabilities before using features:

# Check for GUI environment
if (!$ENV{'DISPLAY'}) {
    print $stdout "-- Warning: No GUI display available\n" if (!$silent);
    return 1;
}

# Check for required commands
sub command_exists {
    my $cmd = shift;
    return system("which $cmd >/dev/null 2>&1") == 0;
}

if (!command_exists('paplay')) {
    print $stdout "-- Warning: paplay not found, trying alternative\n" if (!$silent);
    # Try alternative commands...
}

Notification Function Registration

Proper naming convention for notification handlers:

# Function name must be: notifier_extensionname
sub notifier_soundpack {
    my $class = shift;
    my $text = shift; 
    my $post_ref = shift;
    
    # TTYverse automatically discovers and calls functions matching this pattern
    # No explicit registration needed
}

# Multiple notifiers can exist - TTYverse calls all of them
sub notifier_myother {
    # Another notification handler
}

Best Practices

Error Handling

$handle = sub {
    my $post_ref = shift;
    
    eval {
        # Your processing code here
        # ...
    };
    
    if ($@) {
        print $stdout "-- Extension error: $@\n" if (!$silent);
    }
    
    # Always call default handler
    &defaulthandle($post_ref);
    return 1;
};

Persistent Storage

# Store data between sessions
$store->{'my_data'} = "persistent value";

# Access stored data
my $saved_value = $store->{'my_data'} || "default";

Respecting User Preferences

# Check if user wants your extension to be quiet
return 1 if ($silent);

# Only show messages in verbose mode
print $stdout "-- Extension info\n" if ($verbose && !$silent);

Extension Loading

Extensions must end with:

# Mark extension as successfully loaded
$store->{'loaded'} = 1;

# Required Perl module return value
1;

Testing Your Extension

  1. Syntax Check: perl -c yourextension.pl
  2. Load Test: ./ttyverse.pl -exts=yourextension -verbose
  3. Function Test: Try your commands/hooks with real data
  4. Error Test: Test with invalid inputs and network failures

Distribution

To share your extension:

  1. Add clear documentation header with license
  2. List any dependencies
  3. Provide configuration examples
  4. Test on different systems

See existing extensions in ~/.local/share/ttyverse/extensions/ for examples.


See also: Extensions | Configuration | Commands Reference