ダメ人間オンライン

あまり信用しないほうがいい技術メモとか備忘録とかその他雑記

OAuthを使ったtwitterのbotを作ってみた

ちょっと前やけどtwitterで自分のbot(ほぼ分身)を作るというのが一部で流行っていたみたいだったので便乗して作ってみた。

botを作るのは3体目だったけど今までOAuthを使ったbotを作ったことがなかったのでいい機会ということでOAuthを利用。

twitterでのOAuth利用については詳しく書いてくれてるサイトがいっぱいあるのでググってみればいいんじゃないかな!!

流れとしては、

1.twitterのwebページからOAuth利用アプリケーションを登録

2.登録後に発行されるconsumer_keyとconsumer_secretを取得

3.consumer_keyとconsumer_secretからauthorization_urlを取得

4.authorization_urlをブラウザで開きそこに書いてあるPINコードを取得

5.PINコードからaccess_tokenとconsumer_key_secretを取得

6.consumer_key、consumer_secret、access_token、consumer_key_secretを使ってOAuthでなんやかんや。


こんな感じでIDとパスワードを入力することなくtwitterにpostしたりできます。

# 自分用botなのでOAuthでやる必要全くないけどな!!


それでどんなPOSTをさせようか考えたところ、丸々コピーだと面白みが無いので自分の過去のPOST履歴からランダムでマルコフ連鎖文字列を生成してPOSTしようと。

自分の過去のPOSTはRSSから取得するようにした。そこはOAuthではやらない。うん。

そんなこんなでなんとなーく作って稼働中。

まさに俺得bot!!

@dameninngennbot


仕様

twitterRSSを指定するとそれからマルコフ連鎖文字列を生成してPOSTします。
・"@","#","http"を含むPOSTはマルコフ連鎖の対象にしない。
・過去100POSTを元データとして使用。
形態素解析はyahooのAPIを利用。(mecabと仲違いしたためw)
・replyを投げると投げた人のPOST履歴からマルコフ連鎖文字列を生成してreplyし返します。
・起動したら確実にPOSTするのではなく確率でPOSTするようにしてます。
 replyには起動ごとに反応します。


ソースコード

githubにも上げてます。

dameninngennbot.pl


  1 #!/usr/bin/perl
  2
  3 # マルコフ連鎖POST
  4 # OAuth ver
  5
  6 use strict;
  7 use Net::Twitter;
  8 use LWP::Simple;
  9 use XML::Simple;
 10 use File::Basename;
 11 use Cwd 'abs_path';
 12 use Dumpvalue;
 13
 14 my $d = Dumpvalue->new(); # デバッグ
 15
 16 # OAuth settings
 17 use constant CONSUMER_KEY => 'CONSUMER_KEY';
 18 use constant CONSUMER_KEY_SECRET => 'CONSUMER_KEY_SECRET';
 19 use constant ACCESS_TOKEN => 'ACCESS_TOKEN';
 20 use constant ACCESS_TOKEN_SECRET => 'ACCESS_TOKEN_SECRET';
 21
 22 # XML settings
 23 use constant TWITTER_XML_URL => 'http://twitter.com/statuses/user_timeline/--USER_ID--.xml?count=100';
 24 use constant OWNER_USER_ID => 'OWNER_USER_ID';
 25 use constant APPID => 'APPID';
 26 use constant API_BASE_URL => 'http://jlp.yahooapis.jp/MAService/V1/parse';
 27 use constant API_FIX_PARAM => '&result=ma';
 28
 29 # other settings
 30 use constant SINCE_ID_FILENAME => '.since_id';
 31 use constant ROOP_COUNT => 50;
 32 use constant RAND_PROB => 10; # 確率の分子
 33 use constant RAND_PARAM => 100; # 確率の分母
 34
 35 # OAuthの準備(global)
 36 my $twitter = Net::Twitter->new(
 37   traits => ['API::REST', 'OAuth'],
 38   consumer_key => CONSUMER_KEY,
 39   consumer_secret => CONSUMER_KEY_SECRET,
 40 );
 41 $twitter->access_token(ACCESS_TOKEN);
 42 $twitter->access_token_secret(ACCESS_TOKEN_SECRET);
 43
 44 # main
 45 {
 46     # 前回のmentionsの最大のIDをファイルから読み込み
 47     my $since_id = &read_since_id(SINCE_ID_FILENAME);
 48
 49     # 新規mentionsに対してreplyを返す
 50     my $recent_since_id = &reaction_to_mentions($since_id);
 51
 52     # 今回のmentionの最大のIDをファイルに書き込み
 53     &write_since_id(SINCE_ID_FILENAME,$recent_since_id);
 54
 55     # 確率でPOSTする
 56     my @rand_val = 1 .. RAND_PARAM;
 57     if(rand @rand_val < RAND_PROB){
 58         my $owner_xml_url = TWITTER_XML_URL;
 59         my $owner_user_id = OWNER_USER_ID;
 60         $owner_xml_url =~ s/--USER_ID--/$owner_user_id/;
 61
 62         # XMLを取得しマルコフ連鎖文字列を生成
 63         my $tweet_str = &make_markov_chain_str($owner_xml_url);
 64
 65         # 返ってきた文字列がundefでなければPOSTする
 66         if(defined $tweet_str){
 67             my $res = $twitter->update({ status => "$tweet_str" });
 68         }
 69     }
 70 }
 71
 72 # 新着mentionに対して相手のPOST履歴からマルコフ連鎖文字列を生成しreplyを返す
 73 #
 74 # ARGV:
 75 # $since_id : 前回のmentionsの最大のID
 76 #
 77 # return:
 78 # $recent_since_id : 今回のmentionの最大のID
 79 #
 80 sub reaction_to_mentions{
 81     my $since_id = my $recent_since_id = shift;
 82     my %uniq_user;
 83
 84     # mentions取得
 85     my $mentions = $twitter->mentions();
 86
 87     foreach(@{$mentions}){
 88         my $user_xml_url = TWITTER_XML_URL;
 89
 90         # 今回のmentionの最大のIDが$recent_since_idに入るようにする
 91         $recent_since_id = &max($recent_since_id,$_->{'id'});
 92
 93         # 以下の条件に一致するものにはreplyしない
 94         # ・前回のmentionsの最大のIDより前のIDのもの
 95         # ・先頭以外に@があるもの
 96         # ・プロテクトアカウント
 97         # ・今回の起動で既に1回replyしたユーザー
 98         if($_->{'id'} <= $since_id or $_->{'text'} =~ m/^(.)(.*)\@/ or $_->{'user'}->{'protected'} == 1 or exists $uniq_user{$_->{'user'}->{'id'}}){
 99             next;
100         }
101
102         # $since_idが0だった場合今回のIDを入れておく
103         unless($since_id){
104             $since_id = $_->{'id'};
105         }
106
107         # 今回replyしたユーザーのリスト
108         $uniq_user{$_->{'user'}->{'id'}} = 1;
109
110         # 取得するXMLのURLにuser_idをセット
111         $user_xml_url =~ s/--USER_ID--/$_->{'user'}->{'id'}/;
112
113         # XMLを取得しマルコフ連鎖文字列を生成
114         my $tweet_str = &make_markov_chain_str($user_xml_url);
115
116         # 返ってきた文字列がundefでなければ@、in_reply_toをつけてPOSTする
117         if(defined $tweet_str){
118             $tweet_str = '@'.$_->{'user'}->{'screen_name'}.' '.$tweet_str;
119             my $res = $twitter->update({ status => "$tweet_str", in_reply_to_status_id => $_->{'id'} });
120         }
121         sleep(10);
122     }
123     return $recent_since_id;
124 }
125
126 # 指定XMLからマルコフ連鎖文字列を生成
127 #
128 # ARGV:
129 # $xml_url : 取得対象XMLのURL
130 #
131 # return:
132 # undef or $tweet_str : 生成したマルコフ連鎖文字列
133 #
134 sub make_markov_chain_str{
135     my $xml_url = shift;
136     my $markov_table;
137     my $tweet_list;
138     my @head_word;
139
140     # XML取得
141     my $xml_result = get($xml_url);
142     my $xs = new XML::Simple();
143     my $ref = $xs->XMLin($xml_result);
144
145     # textキーのみハッシュへ
146     foreach my $key (sort keys %{$ref->{'status'}}){
147         $tweet_list->{$key} = $ref->{'status'}->{$key}->{'text'};
148     }
149
150     # 1POSTごとに形態素解析しマルコフテーブルを作成
151     foreach my $key (keys %{$tweet_list}){
152         my @markov_list;
153
154         # # @ http を含むPOSTの場合はスキップ
155         if($tweet_list->{$key} =~ m/http/ or $tweet_list->{$key} =~ m/\@/ or $tweet_list->{$key} =~ m// or $tweet_list->{$key} =~ m/\#/ or $tweet_list->{$key} =~ m//){
156             next;
157         }
158
159         # apiを叩いて形態素解析
160         my $api_param = '?appid='.APPID.'&sentence='.$tweet_list->{$key}.API_FIX_PARAM;
161         my $api_url = API_BASE_URL.$api_param;
162         my $api_res = get($api_url);
163         chomp($api_res);
164
165         # 切り出された単語をリストにpushする
166         while($api_res =~ m/<surface>(.+?)<\/surface>/g){
167             unless(scalar(@markov_list)){
168                 push (@head_word,$1);
169             }
170             push (@markov_list,$1);
171         }
172
173         # マルコフテーブルを作成
174         foreach my $num (0 .. $#markov_list){
175             push (@{ $markov_table->{$markov_list[$num]} },$markov_list[$num+1]);
176         }
177     }
178
179     # 要素数が0の場合何もしない
180     unless(scalar(keys(%{$markov_table}))){
181         return;
182     }
183
184     # 最初の単語を決定
185     my $tweet_str = my $chain_word = $head_word[rand @head_word];
186
187     # 無限ループしないよう最大回数を決めてマルコフ連鎖
188     foreach (0 .. ROOP_COUNT){
189         my $add_word = $markov_table->{$chain_word}->[rand @{$markov_table->{$chain_word}}];
190         $tweet_str .= $chain_word = $add_word;
191     }
192     return $tweet_str;
193 }
194
195 # IDをファイルに書き込み
196 #
197 # ARGV:
198 # $filename : 書き込み対象ファイル名
199 # $id : 書き込みID
200 #
201 # return:
202 # undef
203 #
204 sub write_since_id{
205     my $filename = shift;
206     my $id = shift;
207     my $since_id_file = dirname(abs_path($0)) . '/' . $filename;
208
209     open (OUT, ">$since_id_file");
210     print OUT $id;
211     close(OUT);
212
213     return;
214 }
215
216 # IDをファイルから読み込み
217 #
218 # ARGV:
219 # $filename : 読み込み対象ファイル名
220 #
221 # return:
222 # $id : ファイルから読み込んだID
223 #
224 sub read_since_id{
225     my $filename = shift;
226     my $since_id_file = dirname(abs_path($0)) . '/' . $filename;
227     my $id = 0;
228
229     if(open IN,$since_id_file){
230         $id = <IN>;
231         chomp($id);
232     }
233     close(IN);
234
235     return $id;
236 }
237
238 # 最大値判定処理
239 #
240 # ARGV:
241 # $val1 : 比較する値
242 # $val2 : 比較する値
243 #
244 # return:
245 # $val1 or $val2 : $val1と$val2を比較し大きい方の値を返す
246 #
247 sub max{
248     my $val1 = shift;
249     my $val2 = shift;
250
251     if($val1 >= $val2){
252         return $val1;
253     }
254     else{
255         return $val2;
256     }
257 }