package Angerwhale::Model::UserStore; use strict; use warnings; use base qw(Catalyst::Model); use NEXT; use YAML::Syck qw(LoadFile DumpFile); use Angerwhale::User; use File::Slurp qw(read_file write_file); use Carp; use Crypt::OpenPGP::KeyRing; use Crypt::OpenPGP::KeyServer; =head1 NAME Angerwhale::Model::UserStore - Manages Blog users. =head1 SYNOPSIS Keeps track of the blog's users. my $pgp_id = "cafebabe"; my $user = $c->model('UserStore')->get_user_by_nice_id($pgp_id); print "$pgp_id is ". $user->fullname; See also L. Note that users are cached; they are refreshed from the keyserver according to the config's C in seconds. Defaults to one hour. If a user exists, but a keyserver can't be contacted, the old data will still be used. =head1 CONFIGURATION =head2 keyserver The keyserver to fetch PGP keys from. Defaults to C, since other keyservers can't manage to properly store and retieve the author's key. =head2 update_interval Try to update user info from C after this many seconds. Defaults to 3600, one hour. =cut __PACKAGE__->mk_accessors(qw|update_interval keyserver|); =head1 METHODS =head2 new Called by Catalyst to create and initialize userstore. =cut # XXX: TODO: allow for storing duplicate keyids. keyids don't have to # be unique. sub new { my ( $self, $c ) = @_; $self = $self->NEXT::new(@_); my $dir = $self->{users} = $c->config->{base} . '/.users'; # read the config, first from $self->whatever, then from # c->config->whatever, and finally fall back to some # clever defaults $self->update_interval( $c->config->{update_interval} || 3600 ) if !$self->update_interval; $self->keyserver( $c->config->{keyserver} || 'stinkfoot.org' ) if !$self->keyserver; mkdir $dir; if ( !-d $dir || !-w _ ) { $c->log->fatal("no user store at $dir ($!)"); die "no user store at $dir"; } return $self; } =head2 create_user_by_real_id =head2 create_user_by_nice_id Creates a new user in the user store (by the OpenPGP keyid "cafebabe" [nice] or the Crypt::OpenPGP representation of that number [real]). Returns the C on success, exception on failure. =cut sub create_user_by_real_id { my $self = shift; my $real_id = shift; my $nice_id = unpack( 'H*', $real_id ); return $self->create_user_by_nice_id($nice_id); } sub create_user_by_nice_id { my $self = shift; my $nice_id = shift; return $self->get_user_by_nice_id($nice_id); } =head2 get_user_by_real_id =head2 get_user_by_nice_id Retrieves the user, creating it if necessary. =cut sub get_user_by_real_id { my $self = shift; my $real_id = shift; my $nice_id = unpack( 'H*', $real_id ); return $self->get_user_by_nice_id($nice_id); } sub get_user_by_nice_id { my $self = shift; my $nice_id = shift; my $real_id = pack( 'H*', $nice_id ); my $dir = $self->{users}; my $base = "$dir/$nice_id"; my $user = {}; my $last_updated = 0; $user->{nice_id} = $nice_id; eval { $user->{public_key} = read_file("$base/key") }; eval { $user->{fullname} = read_file("$base/fullname") }; eval { $user->{fingerprint} = read_file("$base/fingerprint") }; eval { $user->{email} = read_file("$base/email") }; eval { $last_updated = read_file("$base/last_updated") }; bless $user, 'Angerwhale::User'; $user->{keyserver} = $self->{keyserver}; my $outdated = ( ( time() - $last_updated ) > $self->{update_interval} ); eval { _user_ok($user); }; if ( !$@ && !$outdated ) { # refreshed OK return $user; } # create a user if the data was bad # or it's time to update eval { delete $user->{public_key}; delete $user->{fullname}; delete $user->{fingerprint}; delete $user->{email}; $user->refresh; $self->store_user($user); _user_ok($user); }; warn "could not refresh or retrieve user 0x$nice_id: $@" if $@; die "user isnta a user" if !$user->isa('Angerwhale::User'); return $user; } sub _user_ok { my $user = shift; die "no name" if !$user->fullname; die "no key" if !$user->public_key; die "no email" if !$user->email; die "no fingerprint" if !$user->key_fingerprint; return 1; } =head2 refresh_user Refresh the user's details from the keyserver =cut sub refresh_user { my $self = shift; my $user = shift; $user->refresh; $self->store_user($user); $user->{refreshed} = 1; } =head2 store_user Write the user's data to disk, so that attributes can be changed and so that the blog will work if the keyserver goes offline. =cut # stores by nice id sub store_user { my $self = shift; my $user = shift; my $dir = $self->{users}; my $uid = $user->nice_id; my $base = "$dir/$uid"; mkdir $base if !-d $base; die "couldn't create userdir $base for $uid" if !-d $base; eval { write_file( "$base/key", $user->public_key ); write_file( "$base/fullname", $user->fullname ); write_file( "$base/fingerprint", $user->key_fingerprint ); write_file( "$base/email", $user->email ); write_file( "$base/last_updated", time() ); }; if ($@) { die "Error writing user: $!"; } return 1; } =head2 last_updated Returns the time of the most recent refresh of all users. =cut sub last_updated { my $self = shift; my $user = shift; my $dir = $self->{users}; my $uid = $user->nice_id; my $base = "$dir/$uid"; my $updated; eval { $updated = read_file("$base/last_updated"); }; return $updated; } =head2 users Returns a list of all the users (Cs) the system knows about. The users are refreshed if they've expired. =cut sub users { my $self = shift; my $dir = $self->{users}; my @users; opendir( my $dirhandle, $dir ) or die "Couldn't open $dir for reading"; while ( my $uid = readdir $dirhandle ) { next if $uid =~ /^[.][.]?$/; # .. and . aren't users :) eval { my $user = $self->get_user_by_nice_id($uid); push @users, $user; }; } return @users; } =head1 NAME Angerwhale::Model::UserStore - Catalyst Model =head1 SYNOPSIS See L =head1 DESCRIPTION Catalyst Model. =head1 AUTHOR Jonathan Rockway =head1 LICENSE This library is free software, you can redistribute it and/or modify it under the same terms as Perl itself. =cut 1;