Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100755 327 lines (262 sloc) 8.624 kb
f1bbfe2 @afresh1 Add the script to git
authored
1 #!/usr/bin/perl
2 ########################################################################
3 # Copyright (c) 2012 Andrew Fresh <andrew@afresh1.com>
4 #
5 # Permission to use, copy, modify, and distribute this software for any
6 # purpose with or without fee is hereby granted, provided that the above
7 # copyright notice and this permission notice appear in all copies.
8 #
9 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 ########################################################################
17 use strict;
18 use warnings;
19 use 5.010;
20
21 use DB_File;
22 use File::Basename;
23 use File::ChangeNotify;
24 use File::Find;
25 use Net::Twitter;
26
27 my $seen_file = $ENV{HOME} . '/.tweeted_changes';
28 my $auth_file = $ENV{HOME} . '/.auth_tokens';
29
30 my %accounts = (
31 cvs => 'openbsd_cvs',
32 src => 'openbsd_src',
33 ports => 'openbsd_ports',
34 xenocara => 'openbsd_xenocar',
35 www => 'openbsd_www',
36 );
37
38 # Login to twitter
39 foreach my $key ( sort keys %accounts ) {
40 my $account = $accounts{$key};
41 get_twitter_account($account);
42 }
43
44 my @dirs = (
45 'Maildir/.lists.openbsd.source-changes/',
46 'Maildir/.lists.openbsd.ports-changes/',
47 );
48
49 find( sub { check_message($_) }, @dirs );
50
51 my $watcher
52 = File::ChangeNotify->instantiate_watcher( directories => \@dirs, );
53 while ( my @events = $watcher->wait_for_events() ) {
54 foreach my $event (@events) {
55 next unless $event->type eq 'create';
56 check_message( $event->path );
57 }
58 }
59
60 sub check_message {
61 my ($file) = @_;
62 state $seen = load_seen();
63
64 my $commit = parse_commit($file);
65 return unless $commit;
66 return unless $commit->{id};
67
68 return if $seen->{ $commit->{id} };
69
70 my ( $message, $params ) = make_tweet($commit);
71 tweet( $message, $params );
72
73 if ( $params->{who} ne 'openbsd_cvs' ) {
74 tweet( shorten( $commit->{'Module name'} . ': ' . $message ),
75 { %{$params}, who => 'openbsd_cvs' } );
76 }
77 $seen->{ $commit->{id} } = time;
78 sync_seen();
79 }
80
81 sub account_for {
82 my ($module) = @_;
83 return $accounts{$module} || 'openbsd_cvs';
84 }
85
86 sub change_for {
87 my ($commit) = @_;
88 my %changes;
89 my @dirs;
90
91 foreach my $key ( keys %{$commit} ) {
92 if ( $key =~ /^(\w+)\s+files$/ ) {
93 $changes{ lc $1 }++;
94 foreach my $dir ( keys %{ $commit->{$key} } ) {
5902c80 @afresh1 Just match on files, easier that way
authored
95 push @dirs, map {"$dir/$_"} @{ $commit->{$key}->{$dir} };
f1bbfe2 @afresh1 Add the script to git
authored
96 }
97 }
98 }
99
100 # Put them shortest first
101 @dirs = sort { length $a <=> length $b } @dirs;
102
103 my $match = shift @dirs;
104 foreach my $dir (@dirs) {
105 chop $match while $dir !~ /^\Q$match/;
106 }
107
5902c80 @afresh1 Just match on files, easier that way
authored
108 $match =~ s{^[\.\/]+}{}; # No need for leading ./
109 $match =~ s{/+$}{}; # one less char most likely
f1bbfe2 @afresh1 Add the script to git
authored
110 $match ||= 'many things';
111
112 my @changes = keys %changes;
113 my $change = @changes == 1 ? $changes[0] : 'changed';
114
115 return "$change $match";
116 }
117
118 sub make_tweet {
119 my ($commit) = @_;
120 my %params = ( who => account_for( $commit->{'Module name'} ), );
121
122 my $by = $commit->{'Changes by'};
123 $by =~ s/\@.*$/\@/;
124
125 my $change = change_for($commit);
126
127 my $message = "$by $change: " . $commit->{'Log message'};
4d86cc5 @afresh1 collapse whitespace
authored
128 $message =~ s/\s+/ /gms;
f1bbfe2 @afresh1 Add the script to git
authored
129
130 return shorten($message), \%params;
131 }
132
133 sub shorten {
134 my ($message) = @_;
135 if ( length $message > 140 ) {
136 $message =~ s/^(.{137}).*/$1/ms;
137 $message =~ s/\s+$//ms;
138 $message .= '...';
139 }
140 return $message;
141 }
142
143 sub tweet {
144 my ( $message, $params ) = @_;
145
146 say "Tweeting $message";
147 eval { get_twitter_account( $params->{who} )->update($message) };
148 if ($@) {
149 warn $@;
150 return 0;
151 }
152 return 1;
153 }
154
155 sub parse_commit {
156 my ($file) = @_;
157 return {} unless -f $file;
158
159 my %commit;
160
161 my $in = 'HEADER';
162 open my $fh, '<', $file or die $!;
163 my $key = '';
164 my $dir = '';
165 while (<$fh>) {
166 chomp;
167
168 if ( $in eq 'HEADER' ) {
169 if (/^Message-ID:\s+(.+?)\s*$/i) { $commit{id} = $1 }
170 unless ($_) { $in = 'BODY' }
171 next;
172 }
173
174 if (/(CVSROOT|Module name|Changes by):\s+(.*)$/) {
175 $commit{$1} = $2;
176 next;
177 }
178 return unless $commit{CVSROOT}; # first thing should be CVSROOT
179
180 if (/^(\w+ files|Log message):/) {
181 $key = $1;
182 next;
183 }
184
185 if ($key) {
186 if ( $key eq 'Log message' ) {
187 $commit{$key} = $_;
188 $commit{$key} .= $_ while <$fh>;
189 }
190 else {
191 chomp;
192 s/^\s+//;
193 unless ($_) {
194 $key = '';
195 next;
196 }
197
198 my (@files) = split /\s*:\s+/;
199 $dir = shift @files if @files > 1;
200 @files = map {split} @files;
201 next unless $dir;
202
203 push @{ $commit{$key}{$dir} }, @files;
204 }
205 }
206 }
207 close $fh;
208
209 if ( my $changes = $commit{'Changes by'} ) {
210 my ( $who, $when ) = split /\s+/, $changes, 2;
211 $commit{'Changes by'} = $who;
212 $commit{'Changes on'} = $when;
213 }
214
215 $commit{'Log message'} =~ s/\s+$//ms;
216
217 return \%commit;
218 }
219
220 {
221 my $X;
222
223 sub load_seen {
224 $X = tie my %seen, 'DB_File', $seen_file or die;
225 return \%seen;
226 }
227
228 sub sync_seen {
229 $X->sync;
230 }
231
232 }
233
234 {
235 my %tokens;
236
237 sub get_access_tokens {
238 my ( $account, $nt ) = @_;
239
240 return $tokens{$account} if exists $tokens{$account};
241
242 open my $fh, '<', $auth_file or die $!;
243 while (<$fh>) {
244 chomp;
245 my ($account_from_file, $access_token, $access_token_secret,
246 $user_id, $screen_name
247 ) = split /\s+/;
248
249 if ( $account_from_file eq 'consumer' ) {
250 $tokens{$account_from_file} = {
251 consumer_key => $access_token,
252 consumer_secret => $access_token_secret,
253 };
254 }
255 else {
256 $tokens{$account_from_file} = {
257 access_token => $access_token,
258 access_token_secret => $access_token_secret,
259 user_id => $user_id,
260 screen_name => $screen_name,
261 };
262 }
263 }
264 close $fh;
265 return $tokens{$account} if exists $tokens{$account};
266
267 return unless $nt;
268
269 my $auth_url = $nt->get_authorization_url;
270 print
271 " Authorize $account for this application at:\n $auth_url\nThen, enter the PIN# provided to continue ";
272
273 my $pin = <STDIN>; # wait for input
274 chomp $pin;
275
276 # request_access_token stores the tokens in $nt AND returns them
277 my ( $access_token, $access_token_secret, $user_id, $screen_name )
278 = $nt->request_access_token( verifier => $pin );
279
280 # save the access tokens
281 $tokens{$account} = {
282 access_token => $access_token,
283 access_token_secret => $access_token_secret,
284 user_id => $user_id,
285 screen_name => $screen_name,
286 };
287
288 save_access_tokens();
289
290 return $tokens{$account};
291 }
292
293 sub save_access_tokens {
294 open my $fh;
295 foreach my $key ( sort keys %tokens ) {
296 my @keys
297 = $key eq 'consumer'
298 ? qw( consumer_key consumer_secret )
299 : qw( access_token access_token_secret user_id screen_name );
300 say join "\t", $key, @{ $tokens{$key} }{@keys};
301 }
302 close $fh;
303 }
304 }
305
306 sub get_twitter_account {
307 my ($account) = @_;
308
309 my $consumer_tokens = get_access_tokens('consumer');
310
311 my $nt = Net::Twitter->new(
312 traits => [qw/API::REST OAuth/],
313 %{$consumer_tokens}
314 );
315
316 my $tokens = get_access_tokens( $account, $nt );
317
318 $nt->access_token( $tokens->{access_token} );
319 $nt->access_token_secret( $tokens->{access_token_secret} );
320
321 #my $status = $nt->user_timeline( { count => 1 } );
322 #print Dumper $status;
323 #print Dumper $nt;
324
325 return $nt;
326 }
Something went wrong with that request. Please try again.