MAICO Twitterツイートシステム詳細です。
MAICO 2010 Twitter botは、CentOS 6.4(64bit)/Perl 5.10.1/Apache 2.2.24 の環境でDaemonとして稼働しています。
■起動に必要なファイル /home/www/.pit/default.yaml (Twitter botアプリを使うアカウントのConsumerKey/Secret) /home/www/mamesibori.net/html/maico2010_repeat/ (このサイト) /home/www/mamesibori.net/html/cuesheets/*.yaml (ツイートシナリオファイル) /home/www/mamesibori.net/maico_repeat/MAICO.pl /home/www/mamesibori.net/maico_repeat/MAICO2010.pm /home/www/mamesibori.net/maico_repeat/Next.pm /home/www/mamesibori.net/maico_repeat/consumer_keys.yaml (Twitter botアプリのkey) ■起動前に準備するもの ・Bot用Twitterアプリの申請(consumer_keysを取得しておく) ・Bot用TwitterアカウントからBot用Twitterアプリの仕様認証を通しておく(keyを取得しておく) ■起動方法 (root権限は不要)必要なファイルを設置したら、MAICO.pl内の実行者のUIDを確認し、 $ cd /home/www/mamesibori.net/maico_repeat/ $ ./MAICO.pl ■終了方法 $ killall MAICO.pl (サービス起動用スクリプトを/etc/rc.d に用意すると便利です。)
ラジオドラマ版MAICO-2010のシナリオファイル(不足あり)です。
1997年当時のAMラジオ放送の録音から文字起こしして、作品内時間に合わせて発言時間を調整しています。
MAICO-2010 ツイートシステムのプログラムです。
プログラムのライセンスはPerlライセンスです。ライセンスの範囲内でご自由にご利用いただけます。
Filename: system/MAICO.pl
#!/usr/bin/perl
# MAICO.pl: Twitterで放送するアンドロイドスクリプト
# 移設するときは、MAICO.pm, Next.pm,
# /etc/rc.d/init.d/MAICO も忘れずに。
use strict;
use warnings;
use FindBin; # $FindBin::Realbin が使えるようになります
use lib "$FindBin::RealBin"; # モジュール探索パスに起動dirを追加
use Date::Parse; # Twitterからの日付文字列解析
use DateTime::Format::HTTP; # JavaScript用日付文字列生成
use Encode;
use File::Spec::Functions; # exports catdir/catfile
use JSON;
use Proc::Daemon; # PerlスクリプトをDaemon化
use Time::HiRes qw(sleep); # Perlのsleepを高解像度のsleepに置き換え
use utf8;
use YAML;
use MAICO2010; # MAICO Twitter Interface
use Next; # MAICOスクリプトとWebサイトとの連携プラグイン
our $DAEMON = 1; # Daemonとして動作:1
our $SLEEP_INTERVAL = 1; # インターバル:1秒ごと
my $offset_year = 2; # 2年分ツイートをずらす
# ファイルパス関係。デーモンとして実行するため絶対パスで用意する
my $realbin = $FindBin::RealBin;
my $html_dir = catdir ( $realbin, '../html/maico2010_repeat');
my $log_file = catfile( $realbin, 'out.log');
my $err_file = catfile( $realbin, 'err.log');
my $cuesheets_dir = catdir ( $html_dir, 'cuesheets');
# 初回公演時は放送中はTwitterプロフィール画像にONAIRと入っていました
my $icon_file = catfile( $html_dir, 'images', 'maicoicon012.gif');
my $icon_onair = catfile( $html_dir, 'images', 'maicoicon011_onair_animation.png');
# Twitterインタフェース。登場人物の分だけ用意する
my $support = MAICO2010->new(pit => 'twitter.com@MAICO2ksupp_rpt');
my $maico = MAICO2010->new(pit => 'twitter.com@MAICO2010_rpt' );
my $matsuo = MAICO2010->new(pit => 'twitter.com@matsuo_dens_rpt');
my $tatsumoto = MAICO2010->new(pit => 'twitter.com@tatsumoto_rpt' );
my $masudamasu = MAICO2010->new(pit => 'twitter.com@masudamasu_rpt' );
my $akai = MAICO2010->new(pit => 'twitter.com@akai_yuriko_rpt');
my $satsuki = MAICO2010->new(pit => 'twitter.com@satsuki2010_rpt');
my $satoru = MAICO2010->new(pit => 'twitter.com@satoru2010_rpt' );
my $debug;# = 1;
my $onair;# = 1; # ON AIR中は1にする: 本番中は他の処理はしない
#my $onair = 1; # 2011/3/3以降は本番しかない(リプライ返答はしない)
#ので、1にする
#my $onair = 1; # リピート放送はリプライ返答はしないので、1にする
$onair = 1 if $debug;
# プラグインルーチン初期化。現在はNext.pmしかありませんが、
# 初回放送では、目覚まし用MorningCall.pm、自動リプライ用Reply.pm、
# MAICOにロボットという単語を含むreplyしたら否定ツイートするNotbot.pm
# などありました。
# なお、プラグインは実質的には変数管利用のオブジェクトで、actionに動作
# を組み込まないと動きません><
# 次公演予告・MAICO位置情報記録JSONファイルと対応する変数$next
my $nextjson_file = catfile( $html_dir, 'next.json');
my $next = Next->new( nextjson_file => $nextjson_file, m => $maico );
# キューシートが時刻文字列をキーとしたハッシュで格納されます
my $cuesheets = load_cuesheet( $cuesheets_dir );
#print Dump($cuesheets); # デバッグ用:キューシートの中身確認
# TwitterのMAICOサポートのTLをmaico_timeline.jsonに取得
maicotimeline();
# MAICOの現在時刻をつかさどる$now。1秒ごとのイベントループで進めていく
# 重要:MAICO自身は現在時刻で生きています。キューシートの時刻を進める
# ことでリピート再生を実現しています。また、発言失敗のときは時刻
# を退行させて発言をリトライします。
# Daemon起動時に1回だけ、MAICOの時刻を世界時刻に設定します。
my $now = DateTime->now( time_zone => 'Asia/Tokyo' );
#my $now = DateTime::Format::HTTP
# ->parse_datetime('2010-05-20T07:30:00')
# ->set_time_zone('Asia/Tokyo'); # MAICOの時刻指定(デバッグ)用
# ツイート失敗リカバリ用変数
my $before_res; # 一個前のAPI呼び出し時の状態が入る
my $forbidden_counter = 0; # fobiddenカウンタ
&init; # Proc::Daemon のイニシャライズ
&run; # Proc::Daemon 実行開始
# メインルーチンEND
### 以下 Proc::Daemon 用サブルーチン
sub action { # run内のイベントループから呼ばれるルーチン
# 現在のMAICOの時刻に応じて動作をさせた後、MAICOの時刻を進めます。
my $sec = $now->second; # 秒数で実行を分岐させるための変数(0~59)
# MAICOの誤差修正
if ( $now >= DateTime->now() ) { # MAICOの時刻が世界時刻と同じ
$SLEEP_INTERVAL = 1; # MAICOの次の時刻割り込みは1秒後
}
if ( (!$onair) && ( $now < DateTime->now() ) ) { # ずれている
$SLEEP_INTERVAL = 0.2; # MAICOの次の時刻割り込みは0.2秒
}
# ON AIR中でないときの処理 時報、時刻指定ツイート
if ( !$onair ) { # ON AIR中でないとき
if ( $now->hms eq '11:59:59') {
# $maico->tweet( 'ひるほー。' );
}
if ( $now->hms eq '23:59:59') {
#if ( $now->ymd eq '2010-09-30' ) {
# $maico->tweet( 'ニッポン放送のMAICOです。もう、10月ですね。'
# . '10月20日は、MAICO・直木賞発表をツイートしますよー。'
# . 'ニコニコ動画UP分がソースですので不完全版ですが、'
# . '楽しみにしていてくださいね!キューシートは'
# . 'http://bit.ly/9lBgcBです。' );
#}
#else {
# # $maico->tweet( 'よるほー' );
#}
}
}
# デバッグ用(MAICOタイムライン生成2012/12/28追加)
#if ( $now->second == 0 ) { # 毎分0秒に実行させる
# maicotimeline();
#}
my $res = q(); # キューシートで発言があった場合API呼び出し状態が入る
$before_res = $res; # 1回目のTweetの直前はクリアする
# キューシートを見てTwitterで発言させる
$res = tell_cuesheet( $cuesheets ); # 後ろのほうのサブルーチンです
# Twitterの投稿の成否確認、失敗時のリカバリ(時間退行)
if ( $res ) { # キューシートで発言があった場合
if ( ("$res" eq 1 ) # 投稿できた場合、または
|| ( $forbidden_counter == 3 ) # Forbiddenが3回カウントされた場合
) {
# WAIT調整 Twitter POSTで消費した時間と次の発言までの時間調整
# 例えば 次の発言が10秒後で、Twitterで2秒かかったら、
# 次の秒に進むための時刻割り込みは、(10-2)/10 = 0.8 となる。
my @keys = sort keys %$cuesheets;
my $next_time = $keys[1];
if ($next_time) {
print "$now $next_time\n";
my $dt_next = DateTime::Format::HTTP
->parse_datetime( $next_time )
->set_time_zone('Asia/Tokyo');
my $delta = $dt_next->epoch - $now->epoch;
$delta = 0.01 if $delta < 0.01;
my $delta2 = $dt_next->epoch
- DateTime->now()
->set_time_zone('Asia/Tokyo')->epoch;
$SLEEP_INTERVAL = int( ( $delta2 / $delta ) *1000 ) / 1000;
$SLEEP_INTERVAL = 0.2 if $SLEEP_INTERVAL < 0.2;
print "$delta2 $delta $SLEEP_INTERVAL\n";
$forbidden_counter = 0;
}
}
elsif ( ($res =~ m/Bad gateway/i )
|| ( $res =~ m/Can't connect to api/i )
|| ( $res =~ m/Service Temporarily Unavailable/i )
|| ( $res =~ m/タイムアウト/)
) { #くじらの時=>何回でもリトライ
$now->add( seconds => -20 ); # MAICOの時刻を20秒遅らせる(再投稿)
$SLEEP_INTERVAL = 1;
return 1;
}
elsif ( $res eq 'Forbidden' ) # Forbiddenが出た
{
if ( ( $before_res =~ m/Bad gateway/i ) # 直前が Bad gatewayだった
) { # 何もしない(Bad gateway=>Forbiddenの流れは、
# 投稿が成功しているはず
}
else { # 直前がBad gatewayじゃない=>3回リトライ
$forbidden_counter++;
print "forbidden_counter: $forbidden_counter\n";
$now->add( seconds => -20 ); # MAICOの時刻を20秒遅らせる(再投稿)
$SLEEP_INTERVAL = 1;
return 1;
}
}
else {
}
}
# ここまでのif文で、リトライ・時間退行分はreturnしているので、
# ここに来るということは、正常に投稿できたということ。
delete $cuesheets->{"$now"}; # 投稿済みのキューをはずす
$now->add(seconds =>1); # MAICOの時刻を1秒分進める(本来の時刻とは
# 合っていない)
return 1;
}
sub interrupt { # Proc:Daemon の割り込み処理ルーチン
my $sig = shift;
setpgrp; # I *am* the leader
$SIG{$sig} = 'IGNORE';
kill $sig, 0; # death to all-comers
die "killed by $sig";
exit(0);
}
sub init { # Proc::Daemon の初期化ルーチン
$SIG{INT } = 'interrupt'; # Ctrl-C
$SIG{HUP } = 'interrupt'; # HUP SIGNAL
$SIG{QUIT} = 'interrupt'; # QUIT SIGNAL
$SIG{KILL} = 'interrupt'; # KILL SIGNAL
$SIG{TERM} = 'interrupt'; # TERM SIGNAL
if ($DAEMON) { # as a daemon
Proc::Daemon::Init( {
setuid => 500,
work_dir => '/var/run',
pid_file => 'MAICO.pid',
child_STDOUT => ">$log_file",
child_STDERR => ">$err_file",
});
#open STDERR, '>', $err_file or die $!;
#open STDOUT, '>', $log_file or die $!;
$|=1; # 標準出力をauto flush
}
}
sub run { # Proc:Daemon の実行ルーチン。イベントループを発生
while(1) {
&action;
sleep($SLEEP_INTERVAL);
}
}
### 以降 Proc::Daemonとは関係ないサブルーチン
sub load_cuesheet { # cuesheetファイルををparseして取り込み
# cuesheetを未来にずらすことで、リピート放送を実現する
my $dir = shift; # cuesheet取り込みディレクトリ
my $res = {}; # cuesheetから取り込んだ時刻イベントハッシュ
my $now = DateTime->now( time_zone => 'Asia/Tokyo' ); # 現在時刻
foreach ( glob File::Spec->catdir( $dir, '*.yaml' ) ) {
print "$_\n";
my $yaml = YAML::LoadFile( $_ );
foreach my $key ( keys %{ $yaml->{ cue } } ) {
# $dt はcuesheetに書かれた時間のDateTimeオブジェクト
my $dt = DateTime::Format::HTTP->parse_datetime( $key )
->add( years => $offset_year ); # 年単位でずらす
if ( $dt > $now ) { # cuesheetの項目は現在の時刻より新しい項目か?
$res->{"$dt"}->{ cue } = $yaml->{ cue }->{ $key };
$res->{"$dt"}->{ title } = $yaml->{ title };
$res->{"$dt"}->{hashtag} = $yaml->{hashtag};
}
}
}
# Webサイト用次回放送予定JSON作成。
my $first = ( sort keys %$res )[0]; #取り込んだcuesheetの最初のもの
my $dt_next = DateTime::Format::HTTP
->parse_datetime( $first )
->set_time_zone('Asia/Tokyo');
my $firststr = DateTime::Format::HTTP->format_datetime( $dt_next );
$next->update({
date => $firststr,
title => $res->{ $first }->{ title },
});
return $res;
}
sub tell_cuesheet { # キューシートを読み、応じて発言。actionから呼ばれる
my $cuesheets = shift;
print "$now $SLEEP_INTERVAL\n"; # 現在時刻表示(out.logに入ります)
#(my $next = Dump($cuesheets)) =~s/---\n//sgo;
#$next =~s/\n.*//sgo;
#print "$next\n";
my $str = $cuesheets->{"$now"}->{cue};
if ($str) {
if ($str eq 'ON AIR') { # cuesheetに"ON AIR"と書いていた時の処理
print "[ON AIR]\n";
unless ($debug) {
$onair = 1;
#$maico->update_profile_image($icon_onair);
}
return;
}
elsif ($str eq 'OFF AIR') { # cuesheetに"OFF AIR"と書かれていた時の処理
print "[OFF AIR]\n";
unless ( $debug ) { # デバッグフラグ立っていないとき
$onair = undef;
#$onair = 1; # 2011年3月以降はReplyなどできない
# Webサイト用に次回放送予定のJSONを生成
my $first = ( sort keys %$cuesheets )[1];
my $dt_next = DateTime::Format::HTTP
->parse_datetime( $first )
->set_time_zone('Asia/Tokyo');
my $firststr = DateTime::Format::HTTP->format_datetime( $dt_next );
$next->update({
date => $firststr,
title => $cuesheets->{ $first }->{ title },
});
# MAICO顔アイコンをOFFAIR(通常)に戻す
#$maico->update_profile_image($icon_file);
}
return;
}
elsif ( $str =~ m/LOCATION /i ) { # cuesheetにLOCATION
unless ($debug) { # デバッグフラグ立っていないとき
# LOCATION のあとのコンマ区切り文字列から位置情報をParse
(my $loc = $str ) =~ s/LOCATION //i;
(my $lat, my $long, my $place ) = split ',', $loc;
# $nextはNext.pmのオブジェクト。
# Webサイト用に位置情報(次回放送予定と兼ねている)のJSONを生成
$next->update({ lat => $lat, long => $long, place => $place, });
}
return;
}
# ここに来るまでのif文でTwitter発言以外の処理はreturnしているはず
# よってここからはTwitterで発言する処理
# 登場人物ごとに用意したオブジェクトを使い発言します。
$str .= ' ' . $cuesheets->{"$now"}->{ hashtag }
if $cuesheets->{"$now"}->{ hashtag };
print encode_utf8("$str\n");
if ( $str =~ m/^サトル\s*?:(.+)/ ) {
return $satoru->tweet( $1 );
}
if ( $str =~ m/^さつき\s*?:(.+)/ ) {
return $satsuki->tweet( $1 );
}
if ( $str =~ m/^赤井\s*?:(.+)/ ) {
return $akai->tweet( $1 );
}
if ( $str =~ m/^マツオ\s*?:(.+)/ ) {
return $matsuo->tweet( $1 );
}
if ( $str =~ m/^タツモト\s*?:(.+)/ ) {
return $tatsumoto->tweet( $1 );
}
if ( $str =~ m/^マスダマス\s*?:(.+)/ ) {
#return $support->tweet( $1 );
return $masudamasu->tweet( $1 );
}
if ( $str =~ m/^MAICO\s*?:(.+)/ ) {
#return $support->tweet( $1 );
return $maico->tweet( $1 );
}
# 登場人物以外のツイートは人物名含めsupport_botがツイートする
return $support->tweet( $str );
}
}
sub maicotimeline { # デバッグ用・Webサイト用MAICO-2010TimeLine用意
# MAICOサポート($support)はメンバー全員followしているのでTL取得
my $maico_timeline = $support->home_timeline({ count => 37 });
my $maicores; # MAICOサポートのツイート格納
foreach my $item ( @$maico_timeline ) {
my $dt = DateTime->from_epoch(
time_zone => 'Asia/Tokyo',
epoch => str2time( $item->{ created_at } )
);
push @$maicores, {
id => $item->{id} + 0,
id_str => qq($item->{id}),
created_at => DateTime::Format::HTTP->format_datetime( $dt ),
text => $item->{ text },
from_user => $item->{ user }->{ screen_name },
profile_image_url => $item->{ user }->{ profile_image_url },
profile_image_url_https => $item->{user}->{profile_image_url_https},
};
}
splice( @$maicores, 20);
my $maico_localtimeline = { # Webサイト表示用に最低限の要素の構造体
results => $maicores,
max_id => $maicores->[0]->{ id },
max_id_str => $maicores->[0]->{ id_str },
};
my $maicotl_file = catfile( $html_dir, 'maico_timeline.json');
MAICO2010::JSON_DumpFile( $maicotl_file, $maico_localtimeline );
}
1;
__END__
=head1 NAME
MAICO.pl - Multi Account Tweet Program
=head1 AUTHOR
L<http://mamesibori.net/maico2010_repeat/system/>
=head1 LICENCE
It is distributed under GPL and the Artistic License 2.0..
=cut
Filename: system/MAICO2010.pm
package MAICO2010;
use strict;
use warnings;
use Config::Pit;
use Date::Parse;
use Encode;
use File::Spec::Functions; #exports catdir/catfile
use FindBin;
use JSON;
use Net::Twitter;
use utf8;
use YAML;
use WWW::Mechanize;
sub new {
my $class = shift;
my %args = ( @_ );
my $p;
if ( defined $args{pit} ) {
$p = pit_get( $args{pit} );
die "not preset account data in Pit." if !%$p;
} else {
$p->{access_token } = $args{access_token};
$p->{access_token_secret} = $args{access_token_secret};
$p->{username} = $args{username};
$p->{password} = $args{password};
}
my $consumerkeys_file = catfile( $FindBin::RealBin, 'consumer_keys.yaml' );
my $consumerkeys = YAML::LoadFile( $consumerkeys_file );
$p->{consumer_key } = $consumerkeys->{consumer_key};
$p->{consumer_key_secret} = $consumerkeys->{consumer_key_secret};
my $self;
$self->{c} = $p;
login($self);
# $self->{c} : consumer_key関係
# $self->{tw} : Net::Twitterオブジェクト
# $self->{geo}: 位置情報
return bless $self, $class;
}
sub login {
my $self = shift;
my $tw = Net::Twitter->new(
traits => [qw/API::REST OAuth WrapError/],
consumer_key => $self->{c}->{consumer_key},
consumer_secret => $self->{c}->{consumer_key_secret},
ssl => 1,
);
$tw->access_token ($self->{c}->{access_token});
$tw->access_token_secret($self->{c}->{access_token_secret});
$self->{tw} = $tw;
return;
}
sub tweet {
my $self = shift;
my $status = shift;
my $reply_to = shift;
return unless $status;
my $arg;
#$arg->{status} = decode_utf8($status);
$arg->{status} = $status;
$arg->{in_reply_to_status_id} = $reply_to if $reply_to;
$arg->{lat } = $self->{geo}->{lat } if $self->{geo}->{lat };
$arg->{long} = $self->{geo}->{long} if $self->{geo}->{long};
warn Dump($arg);
my $res = $self->{tw}->update( $arg );
#warn Dump($res);
#if( 1 ) { # Over Capacity test
if( defined $res ) {
#eval "{ $res }";
#if ( $@ ) {
# warn "update failed because: $@\n";
#} else {
warn "update Successful.\n";
#}
_maicotimeline($res);
return 1;
#return 'Bad Gateway'; # Over Capacity test
}
else{
warn "update failed.\n";
warn $self->{tw}->http_message ."\n";
return $self->{tw}->http_message;
# 'Over Capacity' => 'Bad Gateway'
}
return;
}
sub home_timeline {
my $self = shift;
my $arg = shift;
my $res = $self->{tw}->home_timeline( $arg );
return $res;
}
sub search {
my $self = shift;
my $search_term = { q => shift };
my $tw = Net::Twitter->new(
traits => ['API::Search', 'OAuth', 'WrapError'],
consumer_key => $self->{c}->{consumer_key},
consumer_secret => $self->{c}->{consumer_key_secret},
ssl => 1,
);
my $res = $tw->search($search_term);
my $status;
if( defined $res ){
warn "search Successful.\n";
}
else{
warn "search failed.\n";
warn $tw->http_message ."\n";
$status = $tw->http_message;
# 'Over Capacity' => 'Bad Gateway'
}
return { res => $res, status => $status };
}
sub mentions {
my $self = shift;
my $param = shift;
my $tw = $self->{tw};
my $res = $tw->mentions($param);
my $status;
if( defined $res ){
warn "mentions Successful.\n";
}
else{
warn "mentions failed.\n";
warn $tw->http_message ."\n";
$status = $tw->http_message;
# 'Over Capacity' => 'Bad Gateway'
}
return { res => $res, status => $status };
}
sub make_friend {
my $self = shift;
my $screen_name = shift;
my $tw = Net::Twitter->new(
username => $self->{c}->{username},
password => $self->{c}->{password},
ssl => 1,
);
my $res = $tw->create_friend({ screen_name => $screen_name });
return $res;
}
sub followers {
my $self = shift;
my %args = ( @_ );
my $res = $self->{tw}->followers( \%args );
return $res;
}
sub friends {
my $self = shift;
my %args = ( @_ );
my $res = $self->{tw}->friends( \%args );
return $res;
}
sub friends_ids {
my $self = shift;
my %args = ( @_ );
my $res = $self->{tw}->friends_ids( \%args );
return $res;
}
sub retweet {
my $self = shift;
my $id = shift;
my $res = $self->{tw}->retweet( $id );
return $res;
}
sub update_profile_image2 {
my $self = shift;
my $file = shift;
my $mech = WWW::Mechanize->new();
$mech->agent_alias( 'Windows IE 6' );
$mech = WWW::Mechanize->new(timeout => 1);
eval {
$mech->get('https://twitter.com/settings/profile');
};
if ($@) {
# 失敗
warn $mech->status, " : $@";
return undef;
}
return undef unless $mech->success;
return undef if $mech->uri() ne 'https://twitter.com/login';
eval {
$mech->submit_form(
form_number => 2,
fields => {
'session[username_or_email]' => $self->{c}->{username},
'session[password]' => $self->{c}->{password},
},
button => 'commit',
);
};
if ($@) {
# 失敗
warn $mech->status, " : $@";
return undef;
}
print $mech->status()."\n";
return undef unless $mech->success;
eval {
$mech->submit_form(
form_number => 2,
fields => {'profile_image[uploaded_data]' => $file, }
);
};
if ($@) {
# 失敗
warn $mech->status, " : $@";
return undef;
}
print $mech->status()."\n";
return undef unless $mech->success;
return;
}
sub update_profile_image {
my $self = shift;
my $file = shift;
my $res = $self->{tw}->update_profile_image([$file]);
return $res;
}
sub update_location {
my $self = shift;
my $lat = shift;
my $long = shift;
if ($lat && $long ) {
$self->{geo} = { lat => $lat, long => $long };
} else {
delete $self->{geo};
}
return;
}
sub update_profile {
my $self = shift;
my %args = ( @_ );
my $res = $self->{tw}->update_profile( \%args );
return $res;
}
### ここ以降は普通のサブルーチンです
sub _maicotimeline { # 2012/12/31に突貫で作ったローカルTimeline生成ルーチン
my $item = shift; # Twitter POST後のresposeが入ります
my $html_dir = catdir( $FindBin::RealBin, '../html/maico2010_repeat');
my $maicotl_file = catfile( $html_dir, 'maico_timeline.json');
my $maico_mytimeline = MAICO2010::JSON_LoadFile( $maicotl_file );
my $maicores = $maico_mytimeline->{results};
my $dt = DateTime->from_epoch(
time_zone => 'Asia/Tokyo',
epoch => str2time( $item->{created_at} ),
);
# Twitter APIのJSON出力に似せた構造体を作る
unshift @$maicores, {
id => $item->{id} + 0,
id_str => qq($item->{id}),
created_at => DateTime::Format::HTTP->format_datetime( $dt ),
text => $item->{text},
from_user => $item->{user}->{screen_name},
profile_image_url => $item->{user}->{profile_image_url},
profile_image_url_https => $item->{user}->{profile_image_url_https},
};
splice( @$maicores, 20); # 1発言追加するので、発言を20に詰める
$maico_mytimeline = {
results => $maicores,
max_id => $maicores->[0]->{id},
max_id_str => $maicores->[0]->{id_str},
};
MAICO2010::JSON_DumpFile( $maicotl_file, $maico_mytimeline );
}
sub JSON_LoadFile { # JSONファイルを読み込んで構造体を返す
my $file = shift;
my $data = _read_file( $file );
return JSON::decode_json( $data );
}
sub JSON_DumpFile { # 構造体をJSONファイルに書き出す
my $file = shift;
my $hash = shift;
_write_file( $file, encode_json( $hash ) );
return;
}
sub _read_file { # ファイルを読み込む:一気読み
my $file = shift;
local $_;
if ( open my $fh, '<', $file ) {
$_ = do { local $/; <$fh> };
close $fh;
} else {
warn $!;
}
return $_;
};
sub _write_file { # データをファイルに書き出す
my $file = shift;
my $data = shift;
open my $fh, '>', $file or die $!;
print $fh $data;
close $fh;
return;
}
1;
__END__
=head1 NAME
MAICO2010.pm - Multi Account Tweet Program support library
=head1 AUTHOR
L<http://mamesibori.net/maico2010_repeat/system/>
=head1 LICENCE
It is distributed under GPL and the Artistic License 2.0..
=cut
Filename: system/Next.pm
package Next;
use strict;
use warnings;
use utf8;
use MAICO2010;
sub new {
my $class = shift;
my $self = { @_, };
if ( -e $self->{ nextjson_file } ) {
$self->{ next } = MAICO2010::JSON_LoadFile( $self->{ nextjson_file } );
}
if ( exists $self->{m} ) {
$self->{m}
->update_location(
$self->{next}->{lat}, $self->{next}->{long}
);
}
return bless $self, $class;
}
sub update { # 位置情報更新
my $self = shift;
my $h = shift;
if ( ( exists $h->{lat } ) && ( $self->{next}->{lat } != $h->{lat } )
|| ( exists $h->{long} ) && ( $self->{next}->{long} != $h->{long} ) ) {
$self->{m}->update_location( $h->{lat}, $h->{long} ); # MAICOTwitter現在位置更新
}
if ( ( exists $h->{place} ) && ( $self->{next}->{place} ne $h->{place} ) ) {
$self->{m}->update_profile( location => $h->{place} ); # MAICOプロフ位置更新
}
foreach my $key ( keys %$h ) {
$self->{next}->{$key} = $h->{$key};
}
MAICO2010::JSON_DumpFile( $self->{nextjson_file}, $self->{next} );
return;
}
1;
__END__
=head1 NAME
Next.pm - Multi Account Tweet Program support plugin
=head1 AUTHOR
L<http://mamesibori.net/maico2010_repeat/system/>
=head1 LICENCE
It is distributed under GPL and the Artistic License 2.0..
=cut
架空のキャラクターが発言した時刻を特定するための手がかりをまとめました。
このページは、ラジオドラマ版に準拠します。ラジオドラマ版は、放送はゲルゲットショッキングセンターと同様、夜10時からのワイド3時間番組という設定です。よって、ラジオ版のタイムシーケンスは、生放送がある回はその前後の時間を考えて設定することになります。
アニメ版は、放送は夜8時10分から9時50時までの番組という設定です。次回予告のバックで描かれているMAICOの日記からすると、MAICOは放送があった日に日記を書いているようです。日記には、日付が書かれているので、それを参照することになります。
清水としみつ先生なので、お色気満載です。(時刻が特定しにくい)