Yet Another Small はてダラ暫定版
まだまだテスト不足な上に、ソースもあまり綺麗じゃありませんが、とりあえず暫定版を作成してみました。
仕様を考えてから(11/19)、10日以上たったのですが、なかなか時間が取れず、この程度しかできておりません(面目ない)。
の2つのモジュールが必要です。
perl -MCPAN -e shell install Atompub::Client install XML::Atom::Entry
してください。そして、以下のソースを、仮にyanhw.plとして保存し、以下のようなconfig.txtファイルをそのディレクトリに置き、2012-12-01.txtを作成してみてください。
perl yanhw.pl 2012-12-01.txt
すれば、登録できます(保証できませんが・汗)。
ちなみに、2012-12-01.txtの1行目はタイトル行で「*」はいりません。送信後、ファイル内を書き換えるので、エディターで開いている場合は開き直してください。
config.txt
# # はてなのユーザID(デフォルトは空) id:chikkun530 # # パスワード(デフォルトは空) password:hogehoge # # ディレクトリ:YYYY-MM-DD.txtのファイルを置いておく場所(デフォルトは . ) txt_dir:~/Dropbox/blog/gijutu # # タッチファイル:送信時に更新されるファイル(デフォルトは touch.txt) touch:~/Dropbox/blog/gijutu/hw.touch # # HTTPプロキシー(デフォルトは空) #proxy:http://www.example.com:8080/ # # クライアント(ローカル)側の文字コード(デフォルトは空) #client_encoding:Shift_JIS # # フィルタコマンド #filter:iconv -f euc-jp -t utf-8 %s
yanhw.pl
use strict; my $VERSION = "0.0.1"; use File::Basename; use Getopt::Std; use Atompub::Client; use XML::Atom::Entry; use utf8; use FileHandle; use Data::Dumper;#for dbug my $enable_encode = eval('use Encode; 1'); # Prototypes. sub login(); sub update_diary_entry($$$$$); sub delete_diary_entry($); sub doit_and_retry($$); sub replace_image_name($); sub replace_image_name_string($); sub draft_it($$$$$); sub insert_id($$); sub delete_it($); sub post_it($$$$$); sub send_image(); sub get_timestamp(); sub print_debug(@); sub print_message(@); sub read_title_body($); sub find_image_file($); sub replace_timestamp($); sub error_exit(@); sub load_config(); # Hatena user id (if empty, I will ask you later). my $username = ''; # Hatena password (if empty, I will ask you later). my $password = ''; # Hatena group name (for hatena group user only). my $groupname = ''; # AtomPub client my $client = Atompub::Client->new; # Default file names. my $touch_file = 'touch.txt'; my $config_file = 'config.txt'; my $target_file = ''; # Filter command. # e.g. 'iconv -f euc-jp -t utf-8 %s' # where %s is filename, output is stdout. my $filter_command = ''; # Proxy setting. my $http_proxy = ''; # Directory for "YYYY-MM-DD.txt". my $txt_dir = ".";#last / is no need my $image_dir = '.'; # target file name my $fname = ""; # Client and server encodings. my $client_encoding = 'UTF-8';#overriding is possible my $server_encoding = 'UTF-8';#overriding is impossible # Hatena URL. my $hatena_url = 'http://d.hatena.ne.jp'; my $draft_url = '';#after login will be assigned my $post_url = '';#after login will be assigned my $image_url = 'http://f.hatena.ne.jp/atom/post'; my @suffixlist = ("jpg","JPG","png","PNG","gif","GIF"); my $hatena = XML::Atom::Namespace->new(hatena => 'http://www.hatena.ne.jp/info/xmlns#'); # Other variables. my $delete_title = ":delete:"; my $delete_regex = qr/.*$delete_title$/i; my $draft_title = "draft:"; my $draft_regex = qr/^$draft_title.+/i; my $delete_flag = 0; my $draft_flag = 0; my $update_flag = 0; my $blog_id = ""; my $image_flag = 0; my @images = (); my %image_ids = (); my $rkm; # session id for posting. # Handle command-line option. my %cmd_opt = ( 'u' => "", # "username" option. 'p' => "", # "password" option. 'g' => "", # "groupname" option. 'f' => "", # "file" option. 'M' => 0, # "no timestamp" flag. 'n' => "", # "config file" option. ); $Getopt::Std::STANDARD_HELP_VERSION = 1; getopts("du:p:g:f:Mn:", \%cmd_opt) or error_exit("Unknown option."); if ($cmd_opt{d}) { print_debug("Debug flag on."); &VERSION_MESSAGE(); } # Override config file name (before load_config). $config_file = $cmd_opt{n} if $cmd_opt{n}; # Override global vars with config file. load_config() if -e($config_file); # Override global vars with command-line options. $username = $cmd_opt{u} if $cmd_opt{u}; $password = $cmd_opt{p} if $cmd_opt{p}; $groupname = $cmd_opt{g} if $cmd_opt{g}; $target_file = $cmd_opt{f} if $cmd_opt{f}; # Change $hatena_url to Hatena group URL if ($groupname is defined). if ($groupname) { $hatena_url = "http://$groupname.g.hatena.ne.jp"; } # Start. &main; # no-error exit. exit(0); # Main sequence. sub main { my $count = 0; my @files; # Setup file list. if ( $cmd_opt{f} ) { # Do not check timestamp. push( @files, $cmd_opt{f} ); print_debug("main: files: option -f: @files"); } else { while ( glob("$txt_dir/*.txt") ) { # Check timestamp. next if ( -e ($touch_file) and ( -M ($_) > -M ($touch_file) ) ); push( @files, $_ ); } print_debug("main: files: current dir ($txt_dir): @files"); } # Process it. login(); $draft_url = "$hatena_url/$username/atom/draft"; $post_url = "$hatena_url/$username/atom/blog"; print_debug("draft_url:$draft_url"); print_debug("post_url:$post_url"); for (@files) { # Check file name. $draft_flag = 0; $delete_flag = 0; $update_flag = 0; $image_flag = 0; @images = (); next unless (/\b(\d\d\d\d)-(\d\d)-(\d\d)\.txt$/); my ( $year, $month, $day ) = ( $1, $2, $3 ); my $date = $year . $month . $day; $fname = $_; # Check if it is a file. next unless ( -f $fname ); # Login if necessary. # Replace "*t*" unless suppressed. replace_timestamp($fname) unless ( $cmd_opt{M} ); # Read title and body. my ( $title, $body ) = read_title_body($fname); # Find image files. #my $imgfile = find_image_file($_); if ($delete_flag) { # Delete entry. print_message("Delete $year-$month-$day."); delete_diary_entry($date); print_message("Delete OK."); sleep(1); $count++; next; } # print_message("Post $year-$month-$day. " . ($imgfile ? " (image: $imgfile)" : "")); send_image(); update_diary_entry( $year, $month, $day, $title, $body ); print_message("Post OK."); sleep(1); $count++; } if ( $count == 0 ) { print_message("No files are posted."); } else { unless ( $cmd_opt{f} ) { # Touch file. open( FILE, "> $touch_file" ) or error_exit("touch_file error, $!:$touch_file"); print FILE get_timestamp; close(FILE); } } } # Delete. sub delete_it($) { my ($date) = @_; print_debug("delete_it: $date"); my $entry = XML::Atom::Entry->new; my $entry_uri; if ($draft_flag) { $entry_uri = $client->deleteEntry( $draft_url . "/$blog_id", $entry ); } else { $entry_uri = $client->deleteEntry( $post_url."/$date" . "/$blog_id", $entry ); } print_debug("delete_it: "); # (Note that delete error != post error) if ( !$entry_uri ) { print_debug("delete_it: returns 0 (ERROR)."); return 0; } else { print_debug("delete_it: returns 1 (OK)."); return 1; } } #write the blog_id in the target file sub insert_id($$) { my ( $filename, $epoch ) = @_; # Read. open( FILE, $filename ) or error_exit("cannot open target file(1:insert image id), $!: $filename"); my $contents = ""; my $linenum = 0; while ( my $line = readline FILE ) { $linenum++; if ( $linenum == 2 && !$update_flag ) { if ($draft_flag) { $contents .= "<!--epoch=$epoch-->\n"; } else { $contents .= "<!--blog_id=$epoch-->\n"; } } foreach my $im ( keys(%image_ids) ) { while ( $line =~ /$im/gi ) { #f:id:hatenadiary:20041007101545j:image $line =~ s/$im/$image_ids{$im}/gi; print_debug("replace_image_name: $im"); } } $contents .= $line; } close(FILE); print_debug("insert_id: $filename"); open( FILE, "> $filename" ) or error_exit("2:cannot open target file(insert image id), $!: $filename"); print FILE $contents; close(FILE); } #replace the image filename with the image syntax in the target file sub replace_image_name($) { my ($filename) = @_; # Read. open( FILE, $filename ) or error_exit("cannot open target file, $!: $filename"); my $contents = ""; while ( my $line = readline FILE ) { foreach my $im ( keys(%image_ids) ) { while ( $line =~ /$im/gi ) { #f:id:hatenadiary:20041007101545j:image $line =~ s/$im/$image_ids{$im}/gi; print_debug("replace_image_name: $im"); } } $contents .= $line; } close(FILE); print_debug("replaced image name: $filename"); open( FILE, "> $filename" ) or error_exit("cannot open target file, $!: $filename"); print FILE $contents; close(FILE); } #send image sub send_image(){ if($image_flag){ print_debug("image file sending..."); for my $im (@images){ local $/; # slurp mode my $fh = FileHandle->new("$image_dir/$im") or error_exit("cannnot open $im: $!"); my $image = $fh->getline; my ($name,$path,$suffix) = fileparse($im, @suffixlist); my $entry = XML::Atom::Entry->new; $entry->content($image); my $s = lc($suffix); $entry->content->type("image/$s"); print_debug("content_type: image/$s"); $entry->title($im); my $image_id = $client->createEntry($image_url, $entry) or error_exit("image file not send:$client->errstr"); my $res = XML::Atom::Entry->new(Stream => \$client->{response}->content); $image_ids{$im} = $res->get($hatena, "syntax"); print_debug("post image: $im : $image_id:$image_ids{$im}"); } } } sub replace_image_name_string($) { my ($body) = @_; foreach my $im ( keys(%image_ids) ) { while ( $body =~ /$im/gi ) { #f:id:hatenadiary:20041007101545j:image $body =~ s/$im/$image_ids{$im}/gi; } } return $body; } #post the blog sub post_it($$$$$) { my ( $year, $month, $day, $title, $body ) = @_; my $entry; print_debug("post_it: $year-$month-$day."); if ($image_flag) { $body = replace_image_name_string($body); } $entry = XML::Atom::Entry->new; $entry->title($title); $entry->content($body); my $entry_uri = ""; if ( !$update_flag ) { $entry->updated("$year-$month-$day"); $entry_uri = $client->createEntry( $post_url, $entry ); $entry_uri =~ /(\d+)$/; warn $client->errstr || '\n'; insert_id( $fname, $1 ); print_debug("blog_id:$1 blog created($entry_uri)"); } else { $entry_uri = $client->updateEntry( $post_url . "/$year$month$day/$blog_id", $entry ); if ($image_flag) { replace_image_name($fname); } warn $client->errstr || '\n'; print_debug("blog_id:$1 blog updated($entry_uri)"); } print_debug("post_it"); # Check the result. OK if the location ends with the date. if ($entry_uri) { print_debug("post_it: returns 1 (OK)."); return 1; } else { print_debug("post_it: returns 0 (ERROR)."); return 0; } } #post the draft blog sub draft_it($$$$$) { my ( $year, $month, $day, $title, $body ) = @_; print_debug("draft_it: $year-$month-$day."); $title =~ s/$draft_title//; if ($image_flag) { $body = replace_image_name_string($body); } my $entry = XML::Atom::Entry->new; $entry->title($title); $entry->content($body); my $entry_uri = ""; if ( !$update_flag ) { $entry_uri = $client->createEntry( $draft_url, $entry ); $entry_uri =~ /(\d+)$/; insert_id( $fname, $1 ); print_debug("entry_id:$1 draft created"); } else { $entry_uri = $client->updateEntry( $draft_url . "/$blog_id", $entry ); print_debug("entry_id:$1 draft updated"); } #http://d.hatena.ne.jp/chikkun530/atom/draft/1353480488 warn $client->errstr || '\n'; } # read title and body, and check (draft|read) and (create|update) sub read_title_body($) { my ($file) = @_; # Execute filter command, if any. my $input = $file; if ($filter_command) { $input = sprintf( "$filter_command |", $file ); } print_debug("read_title_body: input: $input"); if ( not open( FILE, $input ) ) { error_exit("cannot open, $!:$input"); } my $title = <FILE>; # first line. chomp($title); #タイトル行の最後に「:delete:」があったら、削除する if ( $title =~ /$delete_regex/ ) { $delete_flag = 1; print_debug("delete blog"); } # タイトル行の最初に「draft:」があったら、それは「下書き保存」 if ( $title =~ /$draft_regex/ ) { $draft_flag = 1; print_debug("draft blog"); } my $second = <FILE>; # すでに登録している場合は、以下のような「<!--epoch=***-->」とか「<!--blog_id=***-->」が2行目にある。 # TODO 登録されていないのに「ある」場合と、登録されているのに「ない」場合の仕様決め if ( $second =~ /<! *--epoch *= *(\d+) *-->/ || $second =~ /<! *--blog_id *= *(\d+) *-->/ ) { $update_flag = 1; $blog_id = $1; print_debug("update blog"); } my $body = ""; #join('', <FILE>); # rest of all. while (<FILE>) { while ( $_ =~ /(\w+\.(?:jpg|png|gif))/ig ) { if ( -e "$image_dir/$1" ) { push( @images, $1 ); $image_flag = 1; print_debug("Image File:$1 exists in the $file"); } } $body .= $_; } $body = $second . $body; close(FILE); # Convert encodings. if ( $enable_encode and ( $client_encoding ne $server_encoding ) ) { print_debug("Convert from $client_encoding to $server_encoding."); Encode::from_to( $title, $client_encoding, $server_encoding ); Encode::from_to( $body, $client_encoding, $server_encoding ); } return ( $title, $body ); } # Login. sub login() { if ($http_proxy) { $client->proxy($http_proxy); print_debug("login: proxy for http: $http_proxy"); } # Ask username if not set. unless ($username) { print "Username: "; chomp( $username = <STDIN> ); } # Ask password if not set. unless ($password) { print "Password: "; chomp( $password = <STDIN> ); } $client->username($username); $client->password($password); print_message("Login OK."); } # Update entry. sub update_diary_entry($$$$$) { my ( $year, $month, $day, $title, $body ) = @_; if ( !$draft_flag ) { # Post. doit_and_retry( "update_diary_entry: POST.", sub { return post_it( $year, $month, $day, $title, $body ) } ); } else { doit_and_retry( "draft_diary_entry: POST.", sub { return draft_it( $year, $month, $day, $title, $body ) } ); } } # Delete entry. sub delete_diary_entry($) { my ($date) = @_; # Delete. doit_and_retry( "delete_diary_entry: DELETE.", sub { return delete_it($date) } ); } # Do the $funcref, and retry if fail. sub doit_and_retry($$) { my ( $msg, $funcref ) = @_; my $retry = 0; my $ok = 0; while ( $retry < 2 ) { $ok = $funcref->(); if ( $ok or not $cmd_opt{c} ) { last; } print_debug("try_it: $msg"); print_message("Something wrong! Retry login--retry $retry."); $retry++; } if ( not $ok ) { error_exit("giving up!."); } } # Get "YYYYMMDDhhmmss" for now. sub get_timestamp() { my (@week) = qw(Sun Mon Tue Wed Thu Fri Sat); my ( $sec, $min, $hour, $day, $mon, $year, $weekday ) = localtime(time); $year += 1900; $mon++; $mon = "0$mon" if $mon < 10; $day = "0$day" if $day < 10; $hour = "0$hour" if $hour < 10; $min = "0$min" if $min < 10; $sec = "0$sec" if $sec < 10; $weekday = $week[$weekday]; return "$year$mon$day$hour$min$sec"; } # Show version message. This is called by getopts. sub VERSION_MESSAGE { print <<"EOD"; Yet Another Hatena Diary Writer Version $VERSION Copyright (C) 2012 by Chiku Kazuro. EOD } # Debug print. sub print_debug(@) { if ( $cmd_opt{d} ) { print "DEBUG: ", @_, "\n"; } } # Print message. sub print_message(@) { print @_, "\n"; } # Error exit. sub error_exit(@) { print "ERROR: ", @_, "\n"; exit(1); } # Read title and body. # Replace "*t*" with timestamp. sub replace_timestamp($) { my ($filename) = @_; # Read. open( FILE, $filename ) or error_exit("timestamp replace, $!: $filename"); my $file = join( '', <FILE> ); close(FILE); # Replace. my $newfile = $file; $newfile =~ s/^\*t\*/"*" . time() . "*"/gem; # Write if replaced. if ( $newfile ne $file ) { print_debug("replace_timestamp: $filename"); open( FILE, "> $filename" ) or error_exit("timestamp replace,$!: $filename"); print FILE $newfile; close(FILE); } } # Show help message. This is called by getopts. sub HELP_MESSAGE { print <<"EOD"; Usage: perl $0 [Options] Options: --version Show version. --help Show this message. -d Debug. Use this switch for verbose log. -u username Username. Specify username. -p password Password. Specify password. -T seconds Timeout. Default value is 180. -g groupname Groupname. Specify groupname. -f filename File. Send only this file without checking timestamp. -M Do NOT replace *t* with current time. -n config_file Config file. Default value is $config_file. Config file example: # # $config_file # id:yourid password:yourpassword # txt_dir:/usr/yourid/diary # touch:/usr/yourid/diary/hw.touch # proxy:http://www.example.com:8080/ # client_encoding:Shift_JIS # filter:iconv -f euc-jp -t utf-8 %s EOD } # Load config file. sub load_config() { print_debug("Loading config file ($config_file)."); if ( not open( CONF, $config_file ) ) { error_exit("Can't open $config_file."); } while (<CONF>) { chomp; if (/^\#/) { # skip comment. } elsif (/^$/) { # skip blank line. } elsif (/^id:([^:]+)$/) { $username = $1; print_debug("load_config: id:$username"); } elsif (/^g:([^:]+)$/) { $groupname = $1; print_debug("load_config: g:$groupname"); } elsif (/^password:(.*)$/) { $password = $1; print_debug("load_config: password:********"); } elsif (/^proxy:(.*)$/) { $http_proxy = $1; print_debug("load_config: proxy:$http_proxy"); } elsif (/^client_encoding:(.*)$/) { $client_encoding = $1; print_debug("load_config: client_encoding:$client_encoding"); } elsif (/^filter:(.*)$/) { $filter_command = $1; print_debug("load_config: filter:$filter_command"); } elsif (/^txt_dir:(.*)$/) { $txt_dir = glob($1); print_debug("load_config: txt_dir:$txt_dir"); } elsif (/^touch:(.*)$/) { $touch_file = glob($1); print_debug("load_config: touch:$touch_file"); } else { error_exit("Unknown command '$_' in $config_file."); } } close(CONF); } __END__