Changeset 1950

Show
Ignore:
Timestamp:
09/14/06 11:36:56
Author:
miyagawa
Message:

update todo.pl

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • misc/todo.pl

    r1942 r1950  
    1111 
    1212use YAML (); 
    13 use XML::Simple; 
    1413use LWP::UserAgent; 
    1514use Number::RecordLocator; 
    1615use Getopt::Long; 
    1716use Pod::Usage; 
     17use Email::Address; 
    1818use Fcntl qw(:mode); 
    1919 
     
    2222our $ua = LWP::UserAgent->new; 
    2323our $locator = Number::RecordLocator->new(); 
    24 our $default_query = "not/complete/owner/me/starts/before/tomorrow/accepted/but_first/nothing"; 
     24our $default_query = "not/complete/starts/before/tomorrow/accepted/but_first/nothing"; 
     25our $pending_query = "pending/not/complete"; 
     26our $requests_query = "requestor/me/not/owner/me/not/complete"; 
    2527our %args; 
    2628 
    2729$ua->cookie_jar({}); 
    2830 
     31# Load the user's proxy settings from %ENV 
     32$ua->env_proxy; 
     33 
     34binmode STDOUT, ":utf8"; 
     35 
    2936main(); 
    3037 
    3138sub main { 
     39    GetOptions(\%args, 
     40               "tags=s", 
     41               "tag=s@", "group=s", 
     42               "priority|pri=s", 
     43               "due=s", 
     44               "hide=s", 
     45               "owner=s", 
     46               "help", 
     47               "config=s",) 
     48      or pod2usage(2); 
     49 
     50    $CONFFILE = $args{config} if $args{config}; 
     51 
     52    pod2usage(0) if $args{help}; 
     53 
    3254    setup_config(); 
    33     setup_term_encoding(); 
    34  
    35     GetOptions(\%args, "tags=s", "tag=s@", "group=s") or pod2usage(2); 
    36      
     55 
    3756    push @{$args{tag}}, split /\s+/, $args{tags} if $args{tags}; 
    3857 
     58    if($args{priority}) { 
     59        $args{priority} = priority_from_string($args{priority}) 
     60          unless $args{priority} =~ /^[1-5]$/; 
     61        die("Invalid priority: $args{priority}") 
     62          unless$args{priority} =~ /^[1-5]$/; 
     63    } 
     64 
     65    $args{owner} ||= "me"; 
     66 
    3967    do_login() or die("Bad username/password -- edit $CONFFILE and try again."); 
    4068 
    4169    my %commands = ( 
    42         list    => \&list_tasks, 
    43         add     => \&add_task, 
    44         do      => \&do_task, 
    45         done    => \&do_task, 
    46         del     => \&del_task, 
    47         rm      => \&del_task, 
     70        list     => \&list_tasks, 
     71        ls       => \&list_tasks, 
     72        add      => \&add_task, 
     73        do       => \&do_task, 
     74        done     => \&do_task, 
     75        del      => \&del_task, 
     76        rm       => \&del_task, 
     77        edit     => \&edit_task, 
     78        tag      => \&tag_task, 
     79        pending  => sub {list_tasks($pending_query)}, 
     80        accept   => \&accept_task, 
     81        decline  => \&decline_task, 
     82        assign   => \&assign_task, 
     83        requests => sub {list_tasks($requests_query)}, 
     84        hide     => \&hide_task, 
     85        comment  => \&comment_task, 
     86        dl       => \&download_textfile, 
     87        download => \&download_textfile, 
     88        ul       => \&upload_textfile, 
     89        upload   => \&upload_textfile, 
    4890       ); 
    4991     
     
    63105 
    64106sub setup_config { 
    65     check_config_perms() unless($^O eq 'win32'); 
     107    check_config_perms() unless($^O eq 'MSWin32'); 
    66108    load_config(); 
    67109    check_config(); 
     
    81123sub load_config { 
    82124    return unless(-e $CONFFILE); 
    83     %config = %{YAML::LoadFile($CONFFILE)}; 
     125    %config = %{YAML::LoadFile($CONFFILE) || {}}; 
     126    my $sid = $config{sid}; 
     127    if($sid) { 
     128        my $uri = URI->new($config{site}); 
     129        $ua->cookie_jar->set_cookie(0, 'JIFTY_SID_' . $uri->port, 
     130                                    $sid, '/', $uri->host, $uri->port, 
     131                                    0, 0, undef, 1); 
     132    } 
     133    if($config{site}) { 
     134        # Somehow, localhost gets normalized to localhost.localdomain, 
     135        # and messes up HTTP::Cookies when we try to set cookies on 
     136        # localhost, since it doesn't send them to 
     137        # localhost.localdomain. 
     138        $config{site} =~ s/localhost/127.0.0.1/; 
     139    } 
    84140} 
    85141 
     
    91147    print <<"END_WELCOME"; 
    92148Welcome to todo.pl! before we get started, please enter your 
    93 hiveminder username and password so we can access your tasklist. 
     149Hiveminder username and password so we can access your tasklist. 
    94150 
    95151This information will be stored in $CONFFILE, should you ever need to 
     
    112168        ReadMode('restore'); 
    113169 
     170        print "\n"; 
     171 
    114172        last if do_login(); 
    115173        print "That combination doesn't seem to be correct. Try again?\n"; 
    116174    } 
    117175 
     176    save_config(); 
     177} 
     178 
     179sub save_config { 
    118180    YAML::DumpFile($CONFFILE, \%config); 
    119181    chmod 0600, $CONFFILE; 
    120182} 
    121183 
    122 sub setup_term_encoding { 
    123     my $encoding; 
    124     eval { 
    125         require Term::Encoding; 
    126         $encoding = Term::Encoding::get_encoding(); 
    127     }; 
    128     $encoding ||= "utf-8"; 
    129     binmode STDOUT, ":encoding($encoding)"; 
    130 } 
    131  
    132184=head1 TASKS 
    133185 
     
    137189 
    138190sub list_tasks { 
    139     my $query = $default_query; 
     191    my $query = shift || $default_query; 
    140192 
    141193    my $tag; 
    142194    $query .= "/tag/$tag" while $tag = shift @{$args{tag}}; 
    143     $query .= "/group/" . $args{group} if $args{group}; 
     195 
     196    for my $key qw(group priority due) { 
     197        $query .= "/$key/$args{$key}" if $args{$key}; 
     198    } 
     199 
     200    $query .= "/owner/$args{owner}"; 
    144201 
    145202    my $tasks = download_tasks($query); 
    146203     
    147204    for my $t (@$tasks) { 
    148         printf "%4s :", $locator->encode($t->{id}); 
    149         print '(' . chr(ord('A') + 5 - $t->{priority}) . ') '; 
     205        printf "%4s ", $locator->encode($t->{id}); 
     206        print '(' . priority_to_string($t->{priority}) . ') ' if $t->{priority} != 3; 
     207        print "(Due " . $t->{due} . ") " if $t->{due}; 
    150208        print $t->{summary}; 
    151209        if($t->{tags}) { 
     
    153211        } 
    154212 
    155         if($t->{group}) { 
    156             print ' (' . $t->{group} . ')'; 
     213        $t->{owner} =~ s/<nobody>/<nobody\@localhost>/; 
     214        $t->{requestor} =~ s/<nobody>/<nobody\@localhost>/; 
     215         
     216        my ($owner) = Email::Address->parse($t->{owner}); 
     217        my ($requestor) = Email::Address->parse($t->{requestor}); 
     218 
     219        my $not_owner = lc $owner->address ne lc $config{email}; 
     220        my $not_requestor = lc $requestor->address ne lc $config{email}; 
     221        if( $t->{group} || $not_owner || $not_requestor ) { 
     222            print ' ('; 
     223            print join(", ", 
     224                       $t->{group} || "personal", 
     225                       $not_requestor ? "for " . $requestor->name : (), 
     226                       $not_owner ? "by " . $owner->name : (), 
     227                      ); 
     228            print ')'; 
    157229        } 
    158230         
     
    162234 
    163235sub do_task { 
    164     my $task = shift @ARGV or pod2usage(-message => 'Need a task-id!'); 
    165     my $id = $locator->decode($task) or die("Invalid task ID: $task"); 
     236    my $task = get_task_id('complete'); 
    166237    my $result = call(UpdateTask => 
    167                       id         => $id
     238                      id         => $task
    168239                      complete   => 1); 
    169     if($result->{result}{success} == 1) { 
    170         print "Task $task completed.\n"; 
    171     } else { 
    172         die(YAML::Dump($result)); 
    173     } 
     240    result_ok($result, "Completed task"); 
    174241} 
    175242 
    176243sub add_task { 
    177244    my $summary = join(" ",@ARGV) or pod2usage(-message => "Must specify a task description"); 
    178     my %task; 
    179     $task{tags} = join(" ", map {'"' . $_ . '"'} @{$args{tag}}) if $args{tag}; 
    180     $task{group_id} = $args{group} if $args{group}; 
     245    my %task = %{args_to_task()}; 
    181246    $task{summary} = $summary; 
    182     $task{owner_id} = $config{email}; 
    183247 
    184248    my $result = call(CreateTask => %task); 
    185     if($result->{result}{success} == 1) { 
    186         print "Task created.\n"; 
    187     } else { 
    188         die(YAML::Dump($result)); 
    189     } 
     249    result_ok($result, "Created task"); 
     250
     251 
     252sub edit_task { 
     253    my $task = get_task_id('edit'); 
     254    my $summary = join(" ",@ARGV); 
     255    my %task = %{args_to_task()}; 
     256    $task{id} = $task; 
     257    $task{summary} = $summary if $summary; 
     258 
     259    my $result = call(UpdateTask => %task); 
     260    result_ok($result, "Updated task"); 
     261
     262 
     263sub tag_task { 
     264    my $task = get_task_id('tag'); 
     265    my @tags = @ARGV; 
     266 
     267    my $tasks = download_tasks("id/$task"); 
     268    my $tags = $tasks->[0]{tags}; 
     269 
     270    my $result = call(UpdateTask => 
     271                      id      => $task, 
     272                      tags    => $tags . " " . join_tags(@tags)); 
     273 
     274    result_ok($result, "Tagged task"); 
    190275} 
    191276 
    192277sub del_task { 
    193     my $task = shift @ARGV or pod2usage(-message => 'Need a task-id!'); 
    194     my $id = $locator->decode($task) or die("Invalid task ID: $task"); 
    195     my $result = call(DeleteTask => id => $id); 
    196     if($result->{result}{success} == 1) { 
    197         print "Deleted task.\n"; 
    198     } else { 
    199         die YAML::Dump($result); 
    200     } 
    201                        
     278    my $task = get_task_id('delete'); 
     279    my $result = call(DeleteTask => id => $task); 
     280 
     281    result_ok($result, "Deleted task"); 
     282
     283 
     284sub accept_task { 
     285    my $task = get_task_id('accept'); 
     286    my $result = call(UpdateTask => 
     287                      id       => $task, 
     288                      accepted => 'TRUE'); 
     289    result_ok($result, "Accepted task"); 
     290
     291 
     292 
     293sub decline_task { 
     294    my $task = get_task_id('accept'); 
     295    my $result = call(UpdateTask => 
     296                      id       => $task, 
     297                      accepted => 'FALSE'); 
     298    result_ok($result, "Declined task"); 
     299
     300 
     301sub assign_task { 
     302    my $task = get_task_id('assign'); 
     303    my $owner = shift @ARGV or die('Need an owner to assign task to'); 
     304    my $result = call(UpdateTask => id => $task, owner_id => $owner); 
     305    result_ok($result, "Assigned task to $owner"); 
     306
     307 
     308sub hide_task { 
     309    my $task = get_task_id('hide'); 
     310    my $when = join(" ", @ARGV) or die('Need a date to hide the task until'); 
     311    my $result = call(UpdateTask => 
     312                      id         => $task, 
     313                      starts     => $when); 
     314    result_ok($result, "Hid task until $when"); 
     315
     316 
     317sub comment_task { 
     318    my $task = get_task_id('comment on'); 
     319    if(-t STDIN) { 
     320        print "Type your comment now. End with end-of-file or a dot on a line by itself.\n"; 
     321    } 
     322    my $comment; 
     323    while(<STDIN>) { 
     324        chomp; 
     325        last if $_ eq "."; 
     326        $comment .= "\n$_"; 
     327    } 
     328 
     329    my $result = call(UpdateTask => 
     330                      id         => $task, 
     331                      comment    => $comment); 
     332    result_ok($result, "Commented on task"); 
     333
     334 
     335sub get_task_id { 
     336    my $action = shift; 
     337    my $task = shift @ARGV or pod2usage(-message => "Need a task-id to $action."); 
     338    return $locator->decode($task) or die("Invalid task ID"); 
     339
     340 
     341sub download_textfile { 
     342    my $query = shift || $default_query; 
     343    my $filename = shift @ARGV || 'tasks.txt'; 
     344 
     345    my $tag; 
     346    $query .= "/tag/$tag" while $tag = shift @{$args{tag}}; 
     347 
     348    for my $key qw(group priority due) { 
     349        $query .= "/$key/$args{$key}" if $args{$key}; 
     350    } 
     351 
     352    $query .= "/owner/$args{owner}"; 
     353 
     354    my $result = call(DownloadTasks => 
     355                      query  => $query, 
     356                      format => 'sync'); 
     357 
     358    # perl automatically does TRT with $filename eq '-' 
     359    open (my $file, "> $filename") || die("Can't open file '$filename': $!"); 
     360 
     361    print $file $result->{_content}{result}; 
     362
     363 
     364sub upload_textfile { 
     365    my $filename = shift @ARGV; 
     366    pod2usage(-message => "Need to specify a file to upload.", 
     367              -exitval => 1 
     368    ) unless $filename; 
     369 
     370    open (my $file, "< $filename");  
     371 
     372    local $/; 
     373    my $content = <$file>; 
     374 
     375    my $result = call(UploadTasks => 
     376                        content => $content, 
     377                        format => 'sync' ); 
     378 
     379    print STDOUT $result->{message} . "\n"; 
    202380} 
    203381 
     
    211389 
    212390sub do_login { 
     391    return 1 if $config{sid}; 
    213392    my $result = call(Login => 
    214393                      address  => $config{email}, 
    215394                      password => $config{password}); 
    216     return $result->{result}{success} == 1; 
     395    if(!$result->{failure}) { 
     396        $config{sid} = get_session_id(); 
     397        save_config(); 
     398        return 1; 
     399    } 
     400    return; 
     401
     402 
     403sub get_session_id { 
     404    return undef unless $ua->cookie_jar->as_string =~ /JIFTY_SID_\d+=([^;]+)/; 
     405    return $1; 
    217406} 
    218407 
     
    223412                      query  => $query, 
    224413                      format => 'yaml'); 
    225     return YAML::Load($result->{result}{content}{result}); 
     414    return YAML::Load($result->{_content}{result}); 
    226415} 
    227416 
     
    232421 
    233422    my $res = $ua->post( 
    234         $config{site} . "/__jifty/webservices/xml", 
     423        $config{site} . "/__jifty/webservices/yaml", 
    235424        {   "J:A-$moniker" => $class, 
    236425            map { ( "J:A:F-$_-$moniker" => $args{$_} ) } keys %args 
     
    239428 
    240429    if ( $res->is_success ) { 
    241         return XML::Simple::XMLin($res->content)
     430        return YAML::Load($res->content)->{fnord}
    242431    } else { 
    243432        die $res->status_line; 
     
    245434} 
    246435 
     436=head2 result_ok RESULT, MESSAGE 
     437 
     438Make sure that a result returned by C<call> indicates success. If so, 
     439print MESSAGE. Otherwise, die with a descriptive error. 
     440 
     441=cut 
     442 
     443sub result_ok { 
     444    my $result = shift; 
     445    my $message = shift; 
     446 
     447    if(!$result->{failure}) { 
     448        print "$message\n"; 
     449    } else { 
     450        die(YAML::Dump($result)); 
     451    } 
     452     
     453} 
     454 
     455=head2 PRIORITY 
     456 
     457Conversions between text priorities ('A' - 'Z'), and the 1-5 integer 
     458scale Hiveminder uses internally. 
     459 
     460=cut 
     461 
     462sub priority_to_string { 
     463    my $pri = shift; 
     464    return chr(ord('A') + 5 - $pri); 
     465} 
     466 
     467sub priority_from_string { 
     468    my $pri = lc shift; 
     469    return 5 + ord('a') - ord($pri) if $pri =~ /^[a-e]$/; 
     470    my %primap = ( 
     471        lowest  => 1, 
     472        low     => 2, 
     473        normal  => 3, 
     474        high    => 4, 
     475        highest => 5 
     476       ); 
     477    return $primap{$pri} || $pri; 
     478} 
     479 
     480=head2 args_to_task 
     481 
     482Convert argument passed on the command-line into a hash appropriate 
     483for passing as arguments to BTDT actions. 
     484 
     485=cut 
     486 
     487sub args_to_task { 
     488    my %task; 
     489 
     490    $task{tags} = join_tags(@{$args{tag}}) if $args{tag}; 
     491    $task{group_id} = $args{group} if $args{group}; 
     492    $task{owner_id} = $config{email}; 
     493    $task{priority} = $args{priority} if $args{priority}; 
     494    $task{due} = $args{due} if $args{due}; 
     495    $task{starts} = $args{hide} if $args{hide}; 
     496     
     497    return \%task; 
     498} 
     499 
     500sub join_tags { 
     501    my @tags = @_; 
     502    return join(" ", map {'"' . $_ . '"'} @tags); 
     503} 
     504 
    247505__END__ 
    248506 
     
    250508 
    251509todo.pl - a command-line interface to Hiveminder 
    252  
    253 =cut 
    254510 
    255511=head1 SYNOPSIS 
     
    257513  todo.pl [options] list 
    258514  todo.pl [options] add <summary> 
     515  todo.pl [options] edit <task-id> [summary] 
     516 
     517  todo.pl tag <task-id> tag1 tag2 
     518 
    259519  todo.pl done <task-id> 
    260520  todo.pl del|rm <task-id> 
     521 
     522  todo.pl [options] pending 
     523  todo.pl accept <task-id> 
     524  todo.pl decline <task-id> 
     525 
     526  todo.pl assign <task-id> <email> 
     527  todo.pl [options] requests 
     528 
     529  todo.pl hide <task-id> date 
     530 
     531  todo.pl comment <task-id> 
     532 
     533  todo.pl [options] download [file] 
     534  todo.pl upload <file> 
    261535 
    262536    Options: 
    263537       --group                          Operate on tasks in a group 
    264538       --tag                            Operate on tasks with a given tag 
     539       --pri                            Operate on tasks with a given priority 
     540       --due                            Operate on tasks due on a given day 
     541       --hide                           Operate on tasks hidden until this day 
     542       --owner                          Operate on tasks with a given owner 
     543 
    265544 
    266545  todo.pl list 
     
    270549        List all personl tasks (not in a group with tags 'home' and 'othertag'. 
    271550 
    272    
    273  
    274  
    275 =head1 OPTIONS 
    276  
    277 =over 
    278  
    279 =back 
    280  
    281 =cut 
    282  
    283 =cut 
    284  
     551  todo.pl --tag cli --group hiveminders edit 3G Implement todo.pl 
     552        Move task 3G into the hiveminders group, set its tags to 
     553        "cli", and change the summary. 
     554 
     555  todo.pl --tag "" 4J 
     556        Delete all tags from task 4J 
     557 
     558  todo.pl tag 4J home 
     559        Add the tag ``home'' to task 4J 
     560 
     561=cut