Profiles for users now saved when successfully creating a key, or if it doesn't exist already. Synced with profile on sever if available or use sane defaults if not. You can download the updated profile if changed on server /profiledown, or upload it if you make changes, /profileup, or let ttyverse smart sync changes, /syncprofile.

This commit is contained in:
Storm Dragon
2025-07-30 19:27:55 -04:00
parent ab28e58018
commit 1df027c20f
+582
View File
@@ -203,6 +203,7 @@ BEGIN {
# user= # Username override
# leader= # Command leader character
# === EXTENSION SETTINGS ===
# Extensions can be loaded with the -exts option
# Extension preferences use the extpref_ prefix:
@@ -220,6 +221,7 @@ EOF
exit 0;
}
$space_pad = " " x 1024;
$background_is_ready = 0;
@@ -2032,6 +2034,9 @@ if (length($credentials)) {
# Update server configuration (character limits, etc.)
&update_server_config;
# Check and sync profile if enabled
&check_and_sync_profile($keyfile);
}
#### BOT/DAEMON MODE STARTUP ####
@@ -4406,6 +4411,19 @@ EOF
my $choices = $2;
return &vote_on_poll($post_code, $choices);
}
# profile management commands
if (m#^/syncprofile\s*$#i) {
&cmd_syncprofile;
return 0;
}
if (m#^/profileup\s*$#i) {
&cmd_profileup;
return 0;
}
if (m#^/profiledown\s*$#i) {
&cmd_profiledown;
return 0;
}
# reply to all mentioned users
if (m#^/(replyall|ra|replytoall)\s+([a-z]\d+)\s+(.+)$#i) {
@@ -11266,4 +11284,568 @@ sub save_dm_seen_status {
}
}
#########################################################################
# Profile Management System Functions
#########################################################################
# Simple URL encoding for form data
sub url_encode {
my $str = shift;
$str =~ s/([^A-Za-z0-9\-_\.~])/sprintf("%%%02X", ord($1))/ge;
return $str;
}
# Get the profile file path for a given keyfile
sub get_profile_file_path {
my $keyfile_path = shift;
return "${keyfile_path}.profile";
}
# Check if profile file exists for given keyfile
sub profile_file_exists {
my $keyfile_path = shift;
my $profile_path = get_profile_file_path($keyfile_path);
return -f $profile_path;
}
# Parse profile file into structured hash
sub parse_profile_file {
my $profile_path = shift;
my %profile = ();
my @custom_fields = ();
unless (open(PROFILE, '<', $profile_path)) {
print $stdout "-- Error: Cannot read profile file $profile_path: $!\n";
return undef;
}
print $stdout "-- DEBUG: Parsing profile file with multi-line support\n" if ($verbose);
my $current_field = '';
my $current_value = '';
while (my $line = <PROFILE>) {
chomp($line);
# Skip comments and empty lines
next if ($line =~ /^\s*#/ || $line =~ /^\s*$/);
# Handle continuation lines (indented OR unindented for bio field)
if ($line =~ /^\s+(.*)$/ && $current_field) {
# Standard indented continuation
$current_value .= "\n$1";
next;
} elsif ($current_field eq 'bio' && $line !~ /^\s*[^=]+\s*=/) {
# Bio field continuation (non-indented lines that aren't new field definitions)
$current_value .= "\n$line";
next;
}
# Finish previous field if we have one
if ($current_field) {
$current_value =~ s/^\s+|\s+$//g; # trim whitespace
if ($current_field eq 'custom_field') {
push @custom_fields, $current_value;
} else {
$profile{$current_field} = $current_value;
}
}
# Parse new field
if ($line =~ /^\s*([^=]+?)\s*=\s*(.*)$/) {
$current_field = $1;
$current_value = $2;
} else {
$current_field = '';
$current_value = '';
}
}
# Don't forget the last field
if ($current_field) {
$current_value =~ s/^\s+|\s+$//g;
if ($current_field eq 'custom_field') {
push @custom_fields, $current_value;
} else {
$profile{$current_field} = $current_value;
}
}
close(PROFILE);
$profile{'custom_fields'} = \@custom_fields;
return \%profile;
}
# Generate profile template file
sub generate_profile_template {
my $keyfile_path = shift;
my $server_profile = shift || {};
my $server_info = shift || {};
my $profile_path = get_profile_file_path($keyfile_path);
unless (open(TEMPLATE, '>', $profile_path)) {
print $stdout "-- Error: Cannot create profile template $profile_path: $!\n";
return 0;
}
my $timestamp = scalar(localtime());
my $server_name = $fediverseserver || 'your fediverse server';
my $username = $server_profile->{'username'} || $server_profile->{'acct'} || 'your username';
# Clean HTML from bio for template
my $clean_bio = $server_profile->{'note'} || 'Your bio/description here';
$clean_bio =~ s/<br\s*\/?>/\n/gi; # Convert <br> tags to newlines
$clean_bio =~ s/<\/p>\s*<p>/\n\n/gi; # Convert paragraph breaks to double newlines
$clean_bio =~ s/<[^>]*>//g; # Remove remaining HTML tags
$clean_bio =~ s/&lt;/</g; # Decode HTML entities
$clean_bio =~ s/&gt;/>/g;
$clean_bio =~ s/&amp;/&/g;
$clean_bio =~ s/^\s+|\s+$//g; # Trim whitespace
print TEMPLATE <<EOF;
# TTYverse Profile Template - Generated for $server_name
# Change the following line to true to enable profile management
use_profile = false
# Generated: $timestamp
# Server: $server_name
# Account: $username
# === Basic Profile Fields ===
display_name = @{[$server_profile->{'display_name'} || 'Your Display Name']}
bio = $clean_bio
website = @{[$server_profile->{'url'} || 'https://example.com']}
location = @{[$server_profile->{'location'} || 'Your Location']}
# === Custom Fields ===
# Your server supports custom profile fields
# Format: custom_field = Field Name = Field Value
# Copy/paste this line for more custom fields
EOF
# Add existing custom fields as examples
if ($server_profile->{'fields'} && ref($server_profile->{'fields'}) eq 'ARRAY') {
foreach my $field (@{$server_profile->{'fields'}}) {
my $name = $field->{'name'} || '';
my $value = $field->{'value'} || '';
# Remove HTML tags from value for template
$value =~ s/<[^>]*>//g;
print TEMPLATE "custom_field = $name = $value\n";
}
}
# Always add commented examples for guidance
print TEMPLATE "# \n";
print TEMPLATE "# Examples (uncomment and edit to use):\n";
print TEMPLATE "# custom_field = GitHub = yourusername\n";
print TEMPLATE "# custom_field = Matrix = \@you:matrix.org\n";
print TEMPLATE "# custom_field = Blog = https://myblog.com\n";
print TEMPLATE "# custom_field = Website = https://yoursite.com\n";
close(TEMPLATE);
chmod(0600, $profile_path); # Keep private
return 1;
}
# Validate profile configuration
sub validate_profile_config {
my $profile = shift;
my @errors = ();
# Check display_name length (varies by server, but 30 is common)
if ($profile->{'display_name'} && length($profile->{'display_name'}) > 100) {
push @errors, "Display name too long (max ~100 characters)";
}
# Check bio length (varies by server, but 500 is common for Mastodon)
if ($profile->{'bio'} && length($profile->{'bio'}) > 1000) {
push @errors, "Bio too long (max ~500-1000 characters depending on server)";
}
# Validate website URL format
if ($profile->{'website'} && $profile->{'website'} !~ /^https?:\/\//) {
push @errors, "Website must be a valid HTTP/HTTPS URL";
}
# Validate custom fields format
if ($profile->{'custom_fields'}) {
foreach my $field (@{$profile->{'custom_fields'}}) {
unless ($field =~ /^([^=]+)\s*=\s*(.+)$/) {
push @errors, "Invalid custom field format: '$field' (should be 'Name = Value')";
}
}
}
return \@errors;
}
# Fetch current user profile from server
sub fetch_user_profile {
print $stdout "-- Fetching current profile from server...\n" if ($verbose);
my $profile_data = &grabjson($credurl, 0, 0, 0, undef, 1);
if (!$profile_data) {
print $stdout "-- Error: Failed to fetch user profile from server\n";
return undef;
}
print $stdout "-- Profile fetched successfully\n" if ($verbose);
return $profile_data;
}
# Update server profile with changes
sub update_server_profile {
my $changes = shift;
unless (keys %$changes) {
print $stdout "-- No profile changes to update\n" if ($verbose);
return 1;
}
print $stdout "-- Updating profile on server...\n" if ($verbose);
# Build direct curl command (bypass TTYverse backticks due to form data issues)
my $update_url = "${apibase}/accounts/update_credentials";
my $curl_cmd = "$baseagent";
# Add standard TTYverse curl options
foreach my $arg (@wend) {
$curl_cmd .= " '$arg'";
}
# Add OAuth Bearer token authentication
my $bearer_header = &signrequest($update_url, '');
if ($bearer_header) {
$curl_cmd .= " $bearer_header";
}
# Add PATCH method
$curl_cmd .= " -X PATCH";
# Add multipart form data fields
$curl_cmd .= " -F 'display_name=" . $changes->{'display_name'} . "'" if defined($changes->{'display_name'});
$curl_cmd .= " -F 'note=" . $changes->{'bio'} . "'" if defined($changes->{'bio'});
# Website is handled as a custom field, not direct 'url' parameter
if (defined($changes->{'website'})) {
# Find existing Website field or add as new field
my $website_field_index = 0;
$curl_cmd .= " -F 'fields_attributes[${website_field_index}][name]=Website'";
$curl_cmd .= " -F 'fields_attributes[${website_field_index}][value]=" . $changes->{'website'} . "'";
}
# Custom fields (Mastodon multipart format)
if ($changes->{'custom_fields'}) {
my $field_index = 0;
foreach my $field_data (@{$changes->{'custom_fields'}}) {
if ($field_data =~ /^([^=]+)\s*=\s*(.+)$/) {
my ($name, $value) = ($1, $2);
$name =~ s/^\s+|\s+$//g;
$value =~ s/^\s+|\s+$//g;
# Escape single quotes in field values
$name =~ s/'/'\\''/g;
$value =~ s/'/'\\''/g;
$curl_cmd .= " -F 'fields_attributes[${field_index}][name]=$name'";
$curl_cmd .= " -F 'fields_attributes[${field_index}][value]=$value'";
$field_index++;
}
}
}
# Add the URL
$curl_cmd .= " '$update_url'";
print $stdout "-- DEBUG: Profile update command: $curl_cmd\n" if ($superverbose);
# Execute the request directly
my $return = `$curl_cmd 2>&1`;
my $exit_code = $?;
print $stdout "-- DEBUG: HTTP exit code: $exit_code\n" if ($verbose);
print $stdout "-- DEBUG: API response length: " . length($return) . " bytes\n" if ($verbose);
print $stdout "-- DEBUG: API response: $return\n" if ($superverbose);
# Check if update was successful - look for valid profile JSON response
if ($return =~ /"id":\s*"[^"]+"/s && $return =~ /"username":\s*"[^"]+"/s) {
print $stdout "-- Profile updated successfully\n";
return 1;
} elsif ($return =~ /"error":\s*"([^"]+)"/s) {
print $stdout "-- Profile update failed: $1\n";
return 0;
} else {
print $stdout "-- Profile update failed\n";
print $stdout "-- Response: $return\n" if ($verbose);
return 0;
}
}
# Compare local profile config with server profile
sub compare_profiles {
my $local_config = shift;
my $server_profile = shift;
my %changes = ();
# Compare basic fields
my %field_map = (
'display_name' => 'display_name',
'bio' => 'note',
'website' => 'url'
);
foreach my $local_field (keys %field_map) {
my $server_field = $field_map{$local_field};
my $local_value = $local_config->{$local_field} || '';
my $server_value = $server_profile->{$server_field} || '';
# Clean server value (remove HTML tags from bio)
if ($server_field eq 'note') {
$server_value =~ s/<[^>]*>//g;
$server_value =~ s/&lt;/</g;
$server_value =~ s/&gt;/>/g;
$server_value =~ s/&amp;/&/g;
}
if ($local_value ne $server_value) {
$changes{$local_field} = $local_value;
print $stdout "-- Profile change detected in $local_field: '$server_value' -> '$local_value'\n" if ($verbose);
}
}
# Compare custom fields
my @local_fields = @{$local_config->{'custom_fields'} || []};
my @server_fields = @{$server_profile->{'fields'} || []};
# Convert server fields to same format as local fields
my @server_field_strings = ();
foreach my $server_field (@server_fields) {
my $name = $server_field->{'name'} || '';
my $value = $server_field->{'value'} || '';
# Remove HTML tags
$value =~ s/<[^>]*>//g;
$value =~ s/&lt;/</g;
$value =~ s/&gt;/>/g;
$value =~ s/&amp;/&/g;
push @server_field_strings, "$name = $value";
}
# Check if custom fields have changed
my $local_fields_str = join('||', sort @local_fields);
my $server_fields_str = join('||', sort @server_field_strings);
if ($local_fields_str ne $server_fields_str) {
$changes{'custom_fields'} = \@local_fields;
print $stdout "-- Profile change detected in custom fields\n" if ($verbose);
}
return \%changes;
}
# Interactive profile sync command - smart bi-directional sync
sub cmd_syncprofile {
print $stdout "-- Manual profile sync requested\n";
my $profile_file = get_profile_file_path($keyfile);
unless (profile_file_exists($keyfile)) {
print $stdout "-- Error: No profile file found. Create one first by restarting TTYverse.\n";
return;
}
my $profile_config = parse_profile_file($profile_file);
unless ($profile_config) {
print $stdout "-- Error: Could not parse profile file\n";
return;
}
if ($profile_config->{'use_profile'} ne 'true') {
print $stdout "-- Profile sync disabled. Set use_profile=true in $profile_file\n";
return;
}
# Validate and sync
my $validation_errors = validate_profile_config($profile_config);
if (@$validation_errors) {
print $stdout "-- Profile validation errors:\n";
foreach my $error (@$validation_errors) {
print $stdout " $error\n";
}
return;
}
my $server_profile = fetch_user_profile();
unless ($server_profile) {
print $stdout "-- Could not fetch server profile\n";
return;
}
my $changes = compare_profiles($profile_config, $server_profile);
if (keys %$changes) {
print $stdout "-- Syncing profile changes: " . join(", ", keys %$changes) . "\n";
my $success = update_server_profile($changes);
if ($success) {
print $stdout "-- Profile synced successfully\n";
} else {
print $stdout "-- Profile sync failed\n";
}
} else {
print $stdout "-- Profile already up to date\n";
}
}
# Force upload local profile to server
sub cmd_profileup {
print $stdout "-- Forcing profile upload (local → server)\n";
my $profile_file = get_profile_file_path($keyfile);
unless (profile_file_exists($keyfile)) {
print $stdout "-- Error: No profile file found\n";
return;
}
my $profile_config = parse_profile_file($profile_file);
unless ($profile_config) {
print $stdout "-- Error: Could not parse profile file\n";
return;
}
if ($profile_config->{'use_profile'} ne 'true') {
print $stdout "-- Profile sync disabled. Set use_profile=true in $profile_file\n";
return;
}
# Validate local config
my $validation_errors = validate_profile_config($profile_config);
if (@$validation_errors) {
print $stdout "-- Profile validation errors:\n";
foreach my $error (@$validation_errors) {
print $stdout " $error\n";
}
return;
}
# Force upload all local fields
my %force_changes = ();
$force_changes{'display_name'} = $profile_config->{'display_name'} if $profile_config->{'display_name'};
$force_changes{'bio'} = $profile_config->{'bio'} if $profile_config->{'bio'};
$force_changes{'website'} = $profile_config->{'website'} if $profile_config->{'website'};
$force_changes{'custom_fields'} = $profile_config->{'custom_fields'} if $profile_config->{'custom_fields'};
if (keys %force_changes) {
print $stdout "-- Uploading all profile fields to server\n";
my $success = update_server_profile(\%force_changes);
if ($success) {
print $stdout "-- Profile uploaded successfully\n";
} else {
print $stdout "-- Profile upload failed\n";
}
} else {
print $stdout "-- No profile data to upload\n";
}
}
# Force download server profile to local file
sub cmd_profiledown {
print $stdout "-- Forcing profile download (server → local)\n";
my $profile_file = get_profile_file_path($keyfile);
print $stdout "-- Fetching current server profile\n";
my $server_profile = fetch_user_profile();
unless ($server_profile) {
print $stdout "-- Could not fetch server profile\n";
return;
}
print $stdout "-- Regenerating local profile file with server data\n";
if (generate_profile_template($keyfile, $server_profile)) {
print $stdout "-- Profile downloaded and saved to: $profile_file\n";
print $stdout "-- Note: use_profile is set to false. Edit file and enable to use.\n";
} else {
print $stdout "-- Failed to download profile\n";
}
}
# Main profile sync function called during startup
sub check_and_sync_profile {
my $keyfile_path = shift;
return unless length($keyfile_path);
my $profile_file = get_profile_file_path($keyfile_path);
# Check if profile file exists
unless (profile_file_exists($keyfile_path)) {
# Missing profile file - create template
print $stdout "-- Profile template not found, creating: $profile_file\n";
print $stdout "-- DEBUG: Fetching current server profile to populate template\n" if ($verbose);
my $server_profile = fetch_user_profile();
if (generate_profile_template($keyfile_path, $server_profile)) {
print $stdout "-- Profile template created: $profile_file\n";
print $stdout "-- Edit file and set use_profile=true to enable profile management\n";
print $stdout "-- DEBUG: Template populated with server data: display_name='" .
($server_profile->{'display_name'} || 'none') . "', custom_fields=" .
scalar(@{$server_profile->{'fields'} || []}) . "\n" if ($verbose);
} else {
print $stdout "-- ERROR: Failed to create profile template\n";
}
return;
}
# Parse existing profile file
print $stdout "-- DEBUG: Parsing profile file: $profile_file\n" if ($verbose);
my $profile_config = parse_profile_file($profile_file);
unless ($profile_config) {
print $stdout "-- Error: Could not parse profile file $profile_file\n";
return;
}
print $stdout "-- DEBUG: Profile config loaded - use_profile='" .
($profile_config->{'use_profile'} || 'unset') . "', fields=" .
scalar(keys %$profile_config) . "\n" if ($verbose);
# Check if profile management is enabled
if ($profile_config->{'use_profile'} ne 'true') {
print $stdout "-- Profile file found but disabled (use_profile=false)\n" if ($verbose);
return;
}
# Validate profile configuration
my $validation_errors = validate_profile_config($profile_config);
if (@$validation_errors) {
print $stdout "-- Profile validation errors:\n";
foreach my $error (@$validation_errors) {
print $stdout " $error\n";
}
return;
}
# Fetch current server profile and compare
print $stdout "-- DEBUG: Fetching current server profile for comparison\n" if ($verbose);
my $server_profile = fetch_user_profile();
unless ($server_profile) {
print $stdout "-- Could not fetch server profile for comparison\n";
return;
}
print $stdout "-- DEBUG: Comparing local config with server profile\n" if ($verbose);
my $changes = compare_profiles($profile_config, $server_profile);
if (keys %$changes) {
print $stdout "-- Updating profile: " . join(", ", keys %$changes) . "\n";
print $stdout "-- DEBUG: Changes detected: " . scalar(keys %$changes) . " fields\n" if ($verbose);
my $success = update_server_profile($changes);
if ($success) {
print $stdout "-- Profile updated successfully\n";
} else {
print $stdout "-- Profile update failed - check network and authentication\n";
}
} else {
print $stdout "-- Profile up to date\n" if ($verbose);
}
}
}