OSDN Git Service

Merge branch 'master' into splitfb
[mubot4fb/mubot4fb.git] / mubot4fb.pl
1 #!/usr/bin/perl
2 #
3 # Copyright (c) 2012 ISHIKAWA Mutsumi <ishikawa@hanzubon.jp>
4 # This program is covered by the GNU General Public License 2
5 #
6 package Mubot4FB::FB;
7 use strict;
8 use utf8;
9
10 use Facebook::Graph;
11 use LWP::UserAgent;
12 use HTTP::Request::Common;
13
14 sub new {
15         my $proto = shift;
16         my $class = ref $proto || $proto;
17         my $self = {cfg => shift};
18         bless $self, $class;
19
20         $self->init();
21         return $self;
22 }
23
24 sub init {
25         my ($me) = @_;
26         my $fb = Facebook::Graph->new(app_id   => $me->{cfg}->{fb_app_id},
27                                       secret   => $me->{cfg}->{fb_app_secret},
28                                       postback => $me->{cfg}->{fb_postback_url});
29
30         my $res_token = $fb->request_access_token($me->{cfg}->{fb_access_code});
31         die 'token get error' if (!defined $res_token || !$res_token->response->is_success);
32
33         my $acts = $fb->fetch('me/accounts');
34         die 'can not get account list' if(!defined $acts || !$acts);
35
36         my $page_access_token = '';
37         foreach my $d (@{$acts->{data}}) {
38                 if ($d->{id} eq $me->{cfg}->{fb_page_id}) {
39                         $page_access_token = $d->{'access_token'};
40                 }
41         }
42         die 'can not get access tokenfor page_id=' . $me->{cfg}->{fb_page_id} if ($page_access_token eq '');
43
44         return $me->{fbo} = Facebook::Graph->new(access_token => $page_access_token);
45 }
46
47 sub publish {
48         my ($me, $text, $uri) = @_;
49
50         return $me->{fbo}->add_post($me->{cfg}->{fb_page_id})
51             ->set_message($text)
52             ->set_link_uri($uri)
53             ->publish()
54             ->as_hashref();
55 }
56
57 sub post_uri {
58         my ($me, $post_id) = @_;
59
60         return $me->{cfg}->{fb_page_url} . 'posts/' . $post_id;
61 }
62
63 sub remove {
64         my ($me, $post_id) = @_;
65
66         my $uri = $fb->query->find($post_id)->uri_as_string;
67         my $req = HTTP::Request::Common::DELETE($uri);
68         $req->header('Content-Length', 0);
69         my $resp;
70         eval{$resp = LWP::UserAgent->new->request($req)};
71         return !$@;
72 }
73
74 1;
75 package Mubot4FB;
76
77 use strict;
78 use utf8;
79
80 use base 'Bot::BasicBot';
81 use Facebook::Graph;
82 use LWP::UserAgent;
83 use HTTP::Request::Common;
84 use DBI qw/:sql_types/;
85 use POSIX 'strftime';
86
87 use Data::Dumper;
88
89 my $mu_re = qr/^([^\s]+)\s+((?:https?|ftps?):\/\/[^\s]+)\s+(.+)$/i;
90 my $irc_type = 1;
91
92 sub db_init {
93         my ($me) = @_;
94         $me->{dbh} = DBI->connect('DBI:mysql:'.$me->{cfg}->{database}, $me->{cfg}->{db_user}, $me->{cfg}->{db_pass},{mysql_enable_utf8 => 1}) || die $DBI::errstr;
95 }
96
97 sub misc_init {
98         my ($me) = @_;
99
100         $me->{last_search} = {};
101 }
102
103 sub init {
104         my ($me) = @_;
105         $me->{fb} = Mubot4FB::FB->new($me->{cfg});
106         $me->db_init();
107 }
108
109 sub _check_dup {
110         my ($me, $args, $uri) = @_;
111
112         my $found = 0;
113
114         my $sth = $me->{dbh}->prepare('select * from posts where uri = ? order by post_time desc limit 1');
115         my $rv = $sth->execute($uri);
116         my $res = $sth->fetchrow_hashref;
117         if ($res) {
118                 if ($res->{post_time} < time() - 7 * 24 * 60 * 60) {
119                         $me->_response($args, 'だいぶ前 '.$me->_format_submit($res).'にいってたにゃー '.$me->_fb_post_uri($res->{fb_post_id}));
120                 } else {
121                         $me->_response($args, '既に '.$me->_format_submit($res).'に言ってますよ? '.$me->{cfg}->{fb_page_url}.'posts/'.$res->{fb_post_id});
122                         $found = 1;
123                 }
124         }
125         $sth->finish;
126
127         return $found;
128 };
129
130 sub _db_insert {
131         my ($me, $db_args) = @_;
132
133         my ($scheme, $path) = split(/:\/\//, $db_args->{uri});
134         my $sth = $me->{dbh}->prepare("insert into posts (submitter, fb_post_id, uri, prefix, comment, scheme, path, post_time) values (?, ?, ?, ?, ?, ?, ?, ?)");
135         $sth->bind_param(1, $db_args->{submitter}, SQL_VARCHAR);
136         $sth->bind_param(2, $db_args->{fb_post_id}, SQL_BIGINT);
137         $sth->bind_param(3, $db_args->{uri}, SQL_VARCHAR);
138         $sth->bind_param(4, $db_args->{prefix}, SQL_VARCHAR);
139         $sth->bind_param(5, $db_args->{comment}, SQL_VARCHAR);
140         $sth->bind_param(6, $scheme, SQL_VARCHAR);
141         $sth->bind_param(7, $path, SQL_VARCHAR);
142         $sth->bind_param(8, time, SQL_BIGINT);
143         my $rv = $sth->execute();
144         $sth->finish;
145
146         return $rv;
147 }
148
149 sub _db_delete {
150         my ($me, $db_args) = @_;
151         $db_args->{submitter_type} ||= 1;
152
153         my $sth = $me->{dbh}->prepare("delete from posts where fb_post_id = ? and submitter = ? and submitter_type = ?");
154
155         $sth->bind_param(1, $db_args->{fb_post_id}, SQL_BIGINT);
156         $sth->bind_param(2, $db_args->{submitter}, SQL_VARCHAR);
157         $sth->bind_param(3, $db_args->{submitter_type}, SQL_INTEGER);
158         my $rv = $sth->execute();
159         my $ret = $rv ? $sth->rows : 0;
160
161         $sth->finish;
162
163         return $ret;
164 }
165
166 sub _db_search {
167         my ($me, $word) = @_;
168
169         my $column = $word =~ /:\/\// ? 'uri' : 'path';
170         my $w = '%' . $word . '%';
171         my $sth = $me->{dbh}->prepare('select * from posts where prefix like ? or '.$column.' like ? or comment like ? order by post_time desc limit 1000');
172         $sth->bind_param(1, $w, SQL_VARCHAR);
173         $sth->bind_param(2, $w, SQL_VARCHAR);
174         $sth->bind_param(3, $w, SQL_VARCHAR);
175         $sth->execute();
176
177         my $ret = $sth->fetchall_arrayref({});
178         $sth->finish;
179
180         return $ret;
181 }
182
183 sub _db_search_lastpost {
184         my ($me, $who) = @_;
185
186         my $sth = $me->{dbh}->prepare('select * from posts where submitter = ? order by post_time desc limit 1');
187         $sth->bind_param(1, $who, SQL_VARCHAR);
188         $sth->execute();
189
190         my $ret = $sth->fetchrow_hashref();
191         $sth->finish;
192
193         return $ret;
194 }
195
196 sub _format_submit {
197         my ($me, $e) = @_;
198
199         return $e->{submitter}.'が『'.$e->{prefix}.' '.$e->{uri}.' '.$e->{comment}.'』と'.strftime('%Y-%m-%d %H:%M:%S', localtime($e->{post_time}));
200 }
201
202 sub _response {
203         my ($me, $args, $msg) = @_;
204
205         $me->say(channel => $args->{channel},
206                  body => $msg);
207 }
208
209 sub _add {
210         my ($me, $args)  =@_;
211         my $post_ok = 1;
212         my ($resp, $resp_msg);
213
214         if ($args->{body} =~ /$mu_re/) {
215                 my $prefix = $1;
216                 my $uri = $2;
217                 my $comment = $3;
218                 my $text = $args->{who} . '曰く、'.$prefix.' '.$comment;
219
220                 return 0 if ($me->_check_dup($args, $uri));
221
222                 eval{$resp = $me->{fb}->publish($text, $uri)};
223                 if ($@) {
224                         $me->fb_init();
225                         eval{$resp = $me->{fb}->publish($text, $uri)};
226                         $post_ok = 0 if ($@);
227                 }
228
229                 if ($post_ok) {
230                         my (undef, $post_id) = split(/_/, $resp->{id});
231                         $me->_db_insert({submitter => $args->{who},
232                                          fb_post_id => $post_id,
233                                          uri => $uri,
234                                          prefix => $prefix,
235                                          comment => $comment});
236                         $resp_msg = $args->{who} . ': うい  '.$me->{fb}->post_uri($post_id).' で登録';
237                 } else {
238                         $resp_msg = 'can not post to facebook';
239                 }
240
241                 return $resp_msg;
242         }
243         return 0;
244 }
245
246 sub _delete_prev {
247         my ($me, $args) = @_;
248
249         my $last_post = $me->_db_search_lastpost($args->{who});
250
251         if (!defined $last_post) {
252                 return $args->{who}.': いまのっていつの? というか ないし';
253         } elsif ($last_post->{post_time} < time() - 3600) {
254                 return $args->{who}.': いまのっていつの? 最後のはこれだけど古いんだにゃ ' . $me->{fb}->post_uri($last_post->{fb_post_id});
255         } else {
256                 return $me->_delete($args, $last_post->{'fb_post_id'});
257         }
258 }
259
260 sub _delete {
261         my ($me, $args, $post_id)  =@_;
262         my ($resp_msg, $resp);
263
264         $me->{dbh}->begin_work;
265         if ($resp = $me->_db_delete({fb_post_id => $post_id, submitter => $args->{who}})) {
266                 # fb 側のエントリを削除しないといけない
267                 if ($me->{fb}->remove($post_id)) {
268                         $me->{dbh}->commit;
269                         $resp_msg = $args->{who} . ': 削除しました ' . $me->{fb}->post_uri($post_id);
270                 } else {
271                         $resp_msg = $args->{who} . ': 削除に失敗しましたよ? ' . $me->{fb}->post_uri($post_id);
272                         $me->{dbh}->rollback;
273                 }
274         } else {
275                 $resp_msg = $args->{who} . ': そんな投稿ないよ? ' . $me->{fb}->post_uri($post_id);
276                 $me->{dbh}->rollback;
277         }
278         return $resp_msg;
279 }
280
281 sub _search_start {
282         my ($me, $args)  = @_;
283
284         if ($args->{body} =~ /^ふみくん\s+(.+)\?\s*$/) {
285                 $me->{last_search}->{$args->{who}} = undef;
286                 $me->{last_search}->{$args->{who}} = $me->_db_search($1);
287                 return $me->_search_next($args);
288         }
289 }
290
291 sub _search_next {
292         my ($me, $args)  = @_;
293
294         my $resp_msg = 'ないっす';
295         if (defined $me->{last_search}->{$args->{who}}) {
296                 my $ent = pop($me->{last_search}->{$args->{who}});
297                 if ($ent) {
298                         my $count = @{$me->{last_search}->{$args->{who}}};
299                         if ($count) {
300                         }
301                         $resp_msg = $args->{who} . ': ' . $me->_format_submit($ent).'に言ってた '.($count ? '[ほか'.$count.'件] ' : '[ほかにはもうないよ] ').$me->{fb}->post_uri($ent->{fb_post_id});
302                 }
303         }
304         return $resp_msg;
305 }
306
307 sub _not_yet {
308         return 'まだ実装してないです';
309 }
310
311 sub said {
312         my ($me, $args) = @_;
313         my $resp_msg;
314
315         if ($args->{body} =~ /$mu_re/) {
316                 $resp_msg = $me->_add($args) unless ($1 eq 'deb');
317         } elsif ($args->{body} =~ /^ふみくん\s+(.+)\s*$/) {
318                 my $cmd = $1;
319                 if ($cmd eq 'いまのなし') {
320                         $resp_msg = $me->_delete_prev($args);
321                 } elsif ($cmd =~ /削除\s+(?:$me->{cfg}->{fb_page_url}posts\/)?([0-9]+)$/) {
322                         $resp_msg = $me->_delete($args, $1);
323                 } elsif ($cmd =~ /\?$/) {
324                         $resp_msg = $me->_search_start($args);
325                 } elsif ($cmd =~ /つぎ/) {
326                         $resp_msg = $me->_search_next($args);
327                 }
328         }
329
330         $me->_response($args, $resp_msg) if ($resp_msg);
331 }
332
333 package main;
334 use strict;
335 use utf8;
336
337 use Config::Any;
338 use Hash::Merge::Simple;
339 use Data::Recursive::Encode;
340
341 use Data::Dumper;
342
343 my $config_name = $ARGV[0] || 'not_found';
344
345 my $cfg = {};
346 my $config_path = ['/etc/mubot4fb/'.$config_name,
347                    $ENV{HOME} . '/.mubot4fb/'. $config_name,
348                    $ENV{PWD} . '/mubot4fb_' . $config_name];
349
350 my $c = Config::Any->load_stems({stems => $config_path,  use_ext => 1, flatten_to_hash => 1});
351 foreach my $i (keys %$c) {
352         $cfg = Hash::Merge::Simple->merge($cfg, $c->{$i});
353 }
354 die 'missing config file' unless (keys %$cfg);
355
356 die 'missing some config parameters should be defined (irc_server, fb_app_id, fb_app_secret, fb_access_code, fb_page_id fb_postback_url)'
357   if (!defined $cfg->{'irc_server'}
358       || !defined $cfg->{'fb_app_id'}
359       || !defined $cfg->{'fb_app_secret'}
360       || !defined $cfg->{'fb_access_code'}
361       || !defined $cfg->{'fb_page_id'}
362       || !defined $cfg->{'fb_postback_url'}
363       || !defined $cfg->{'db_user'}
364       || !defined $cfg->{'db_pass'}
365     );
366 $cfg = Data::Recursive::Encode->decode('utf8', $cfg);
367
368 $cfg->{irc_port} ||= 6667;
369 $cfg->{irc_channels} ||= ['#mubot4fb'];
370 $cfg->{irc_nick} ||= 'mubot4fb';
371 $cfg->{irc_name}||= $cfg->{irc_nick};
372 $cfg->{irc_charset} ||= 'utf8';
373 $cfg->{database} ||= 'mubot4fb';
374
375 my $bot = Mubot4FB->new(server => $cfg->{'irc_server'},
376                         port => $cfg->{'irc_port'},
377                         channels => $cfg->{'irc_channels'},
378                         nick => $cfg->{'irc_nick'},
379                         username => $cfg->{'irc_name'},
380                         name => $cfg->{'irc_name'},
381                         charset => $cfg->{'irc_charset'},
382                         cfg => $cfg)->run();
383
384 1;