Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100755 338 lines (271 sloc) 8.86 kb
f1bbfe25 »
2012-07-08 Add the script to git
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 my @files;
91
92 foreach my $key ( keys %{$commit} ) {
93 if ( $key =~ /^(\w+)\s+files$/ ) {
94 $changes{ lc $1 }++;
95 foreach my $dir ( keys %{ $commit->{$key} } ) {
96 push @dirs, $dir;
97 push @files, @{ $commit->{$key}->{$dir} };
98 }
99 }
100 }
101
102 # Put them shortest first
103 @dirs = sort { length $a <=> length $b } @dirs;
104
105 my $match = shift @dirs;
106
107 unless (@dirs) {
108 my $base = $match;
109 if ( $base eq '.' ) { $base = '' }
110 else { $base .= '/' }
111
112 @dirs = sort { length $a <=> length $b }
113 map { $base . $_ } @files;
114 $match = shift @dirs;
115 }
116
117 foreach my $dir (@dirs) {
118 chop $match while $dir !~ /^\Q$match/;
119 }
120
121 $match =~ s{/+$}{}; # one less char most likely
122 $match ||= 'many things';
123
124 my @changes = keys %changes;
125 my $change = @changes == 1 ? $changes[0] : 'changed';
126
127 return "$change $match";
128 }
129
130 sub make_tweet {
131 my ($commit) = @_;
132 my %params = ( who => account_for( $commit->{'Module name'} ), );
133
134 my $by = $commit->{'Changes by'};
135 $by =~ s/\@.*$/\@/;
136
137 my $change = change_for($commit);
138
139 my $message = "$by $change: " . $commit->{'Log message'};
4d86cc5a »
2012-07-08 collapse whitespace
140 $message =~ s/\s+/ /gms;
f1bbfe25 »
2012-07-08 Add the script to git
141
142 return shorten($message), \%params;
143 }
144
145 sub shorten {
146 my ($message) = @_;
147 if ( length $message > 140 ) {
148 $message =~ s/^(.{137}).*/$1/ms;
149 $message =~ s/\s+$//ms;
150 $message .= '...';
151 }
152 return $message;
153 }
154
155 sub tweet {
156 my ( $message, $params ) = @_;
157
158 say "Tweeting $message";
159 eval { get_twitter_account( $params->{who} )->update($message) };
160 if ($@) {
161 warn $@;
162 return 0;
163 }
164 return 1;
165 }
166
167 sub parse_commit {
168 my ($file) = @_;
169 return {} unless -f $file;
170
171 my %commit;
172
173 my $in = 'HEADER';
174 open my $fh, '<', $file or die $!;
175 my $key = '';
176 my $dir = '';
177 while (<$fh>) {
178 chomp;
179
180 if ( $in eq 'HEADER' ) {
181 if (/^Message-ID:\s+(.+?)\s*$/i) { $commit{id} = $1 }
182 unless ($_) { $in = 'BODY' }
183 next;
184 }
185
186 if (/(CVSROOT|Module name|Changes by):\s+(.*)$/) {
187 $commit{$1} = $2;
188 next;
189 }
190 return unless $commit{CVSROOT}; # first thing should be CVSROOT
191
192 if (/^(\w+ files|Log message):/) {
193 $key = $1;
194 next;
195 }
196
197 if ($key) {
198 if ( $key eq 'Log message' ) {
199 $commit{$key} = $_;
200 $commit{$key} .= $_ while <$fh>;
201 }
202 else {
203 chomp;
204 s/^\s+//;
205 unless ($_) {
206 $key = '';
207 next;
208 }
209
210 my (@files) = split /\s*:\s+/;
211 $dir = shift @files if @files > 1;
212 @files = map {split} @files;
213 next unless $dir;
214
215 push @{ $commit{$key}{$dir} }, @files;
216 }
217 }
218 }
219 close $fh;
220
221 if ( my $changes = $commit{'Changes by'} ) {
222 my ( $who, $when ) = split /\s+/, $changes, 2;
223 $commit{'Changes by'} = $who;
224 $commit{'Changes on'} = $when;
225 }
226
227 $commit{'Log message'} =~ s/\s+$//ms;
228
229 return \%commit;
230 }
231
232 {
233 my $X;
234
235 sub load_seen {
236 $X = tie my %seen, 'DB_File', $seen_file or die;
237 return \%seen;
238 }
239
240 sub sync_seen {
241 $X->sync;
242 }
243
244 }
245
246 {
247 my %tokens;
248
249 sub get_access_tokens {
250 my ( $account, $nt ) = @_;
251
252 return $tokens{$account} if exists $tokens{$account};
253
254 open my $fh, '<', $auth_file or die $!;
255 while (<$fh>) {
256 chomp;
257 my ($account_from_file, $access_token, $access_token_secret,
258 $user_id, $screen_name
259 ) = split /\s+/;
260
261 if ( $account_from_file eq 'consumer' ) {
262 $tokens{$account_from_file} = {
263 consumer_key => $access_token,
264 consumer_secret => $access_token_secret,
265 };
266 }
267 else {
268 $tokens{$account_from_file} = {
269 access_token => $access_token,
270 access_token_secret => $access_token_secret,
271 user_id => $user_id,
272 screen_name => $screen_name,
273 };
274 }
275 }
276 close $fh;
277 return $tokens{$account} if exists $tokens{$account};
278
279 return unless $nt;
280
281 my $auth_url = $nt->get_authorization_url;
282 print
283 " Authorize $account for this application at:\n $auth_url\nThen, enter the PIN# provided to continue ";
284
285 my $pin = <STDIN>; # wait for input
286 chomp $pin;
287
288 # request_access_token stores the tokens in $nt AND returns them
289 my ( $access_token, $access_token_secret, $user_id, $screen_name )
290 = $nt->request_access_token( verifier => $pin );
291
292 # save the access tokens
293 $tokens{$account} = {
294 access_token => $access_token,
295 access_token_secret => $access_token_secret,
296 user_id => $user_id,
297 screen_name => $screen_name,
298 };
299
300 save_access_tokens();
301
302 return $tokens{$account};
303 }
304
305 sub save_access_tokens {
306 open my $fh;
307 foreach my $key ( sort keys %tokens ) {
308 my @keys
309 = $key eq 'consumer'
310 ? qw( consumer_key consumer_secret )
311 : qw( access_token access_token_secret user_id screen_name );
312 say join "\t", $key, @{ $tokens{$key} }{@keys};
313 }
314 close $fh;
315 }
316 }
317
318 sub get_twitter_account {
319 my ($account) = @_;
320
321 my $consumer_tokens = get_access_tokens('consumer');
322
323 my $nt = Net::Twitter->new(
324 traits => [qw/API::REST OAuth/],
325 %{$consumer_tokens}
326 );
327
328 my $tokens = get_access_tokens( $account, $nt );
329
330 $nt->access_token( $tokens->{access_token} );
331 $nt->access_token_secret( $tokens->{access_token_secret} );
332
333 #my $status = $nt->user_timeline( { count => 1 } );
334 #print Dumper $status;
335 #print Dumper $nt;
336
337 return $nt;
338 }
Something went wrong with that request. Please try again.