Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100755 510 lines (408 sloc) 13.984 kb
f1bbfe2 Andrew Fresh 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;
46a06a8 Andrew Fresh Better decode the ChangeLog
authored
19 use 5.012;
20 use Encode qw(encode decode);
f1bbfe2 Andrew Fresh Add the script to git
authored
21
22 use DB_File;
23 use File::Basename;
24 use Net::Twitter;
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
25 use Net::FTP;
f1bbfe2 Andrew Fresh Add the script to git
authored
26
27 my $seen_file = $ENV{HOME} . '/.tweeted_changes';
28 my $auth_file = $ENV{HOME} . '/.auth_tokens';
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
29 my $mirror = 'openbsd.cs.toronto.edu';
f1bbfe2 Andrew Fresh Add the script to git
authored
30
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
31 my ($changelog) = @ARGV;
32 die "Usage: $0 <path/to/ChangeLog>\n" unless $changelog;
33
f1bbfe2 Andrew Fresh Add the script to git
authored
34 my %accounts = (
35 cvs => 'openbsd_cvs',
36 src => 'openbsd_src',
37 ports => 'openbsd_ports',
38 xenocara => 'openbsd_xenocar',
39 www => 'openbsd_www',
a2c7505 Andrew Fresh Tweet changes to stable on OpenBSD_stable
authored
40 stable => 'openbsd_stable',
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
41
42 sets => 'openbsd_sets',
f1bbfe2 Andrew Fresh Add the script to git
authored
43 );
44
45 # Login to twitter
46 foreach my $key ( sort keys %accounts ) {
47 my $account = $accounts{$key};
48 get_twitter_account($account);
49 }
50
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
51 my @commits = parse_changelog($changelog);
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
52 my @sets = parse_sets($mirror);
53 foreach my $details (@commits, @sets) {
54 check_message( $details );
f1bbfe2 Andrew Fresh Add the script to git
authored
55 }
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
56 sleep 10;
57 retweet();
f1bbfe2 Andrew Fresh Add the script to git
authored
58
59 sub check_message {
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
60 my ($details) = @_;
f1bbfe2 Andrew Fresh Add the script to git
authored
61
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
62 return unless $details;
63 return unless $details->{id};
f1bbfe2 Andrew Fresh Add the script to git
authored
64
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
65 my $seen = seen();
f1bbfe2 Andrew Fresh Add the script to git
authored
66
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
67 my ( $message, $params ) = make_tweet($details);
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
68
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
69 if (!$seen->{ $details->{id} }) {
a2c7505 Andrew Fresh Tweet changes to stable on OpenBSD_stable
authored
70 if ( tweet( $message, $params ) ) {
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
71 $seen->{ $details->{id} } = time;
a2c7505 Andrew Fresh Tweet changes to stable on OpenBSD_stable
authored
72 }
73 }
74
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
75 if ($details->{Tag} && !$seen->{ 'stable_' . $details->{id} }) {
a2c7505 Andrew Fresh Tweet changes to stable on OpenBSD_stable
authored
76 $params->{who} = account_for( 'stable' );
70210cb Andrew Fresh Tweet the message, not the details
authored
77 if ( tweet( $message, $params ) ) {
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
78 $seen->{ 'stable_' . $details->{id} } = time;
a2c7505 Andrew Fresh Tweet changes to stable on OpenBSD_stable
authored
79 }
80 }
f1bbfe2 Andrew Fresh Add the script to git
authored
81
82 sync_seen();
83 }
84
85 sub account_for {
86 my ($module) = @_;
87 return $accounts{$module} || 'openbsd_cvs';
88 }
89
90 sub change_for {
91 my ($commit) = @_;
92 my %changes;
93 my @dirs;
94
4e8fa04 Andrew Fresh Better messages for files with regress tests
authored
95 my $has_regress = 0;
96 my $has_non_regress = 0;
f1bbfe2 Andrew Fresh Add the script to git
authored
97 foreach my $key ( keys %{$commit} ) {
98 if ( $key =~ /^(\w+)\s+files$/ ) {
99 $changes{ lc $1 }++;
4e8fa04 Andrew Fresh Better messages for files with regress tests
authored
100 foreach ( keys %{ $commit->{$key} } ) {
101 my $dir = $_;
102 my @files = @{ $commit->{$key}->{$dir} || [] };
103 @files = '' unless @files;
104
105 if ( $dir =~ s{^regress/}{} ) { $has_regress++ }
106 else { $has_non_regress++ }
107
108 push @dirs, map {"$dir/$_"} @files;
f1bbfe2 Andrew Fresh Add the script to git
authored
109 }
110 }
111 }
112
4e8fa04 Andrew Fresh Better messages for files with regress tests
authored
113 my @changes = keys %changes;
114 my $changed = @changes == 1 ? $changes[0] : 'changed';
115
116 unless (@dirs) {
117 if (@changes) {
118 return "$changed something";
119 }
120 return "did something the parser didn't understand";
121 }
122
f1bbfe2 Andrew Fresh Add the script to git
authored
123 # Put them shortest first
124 @dirs = sort { length $a <=> length $b } @dirs;
79a280d Andrew Fresh Friendlier wording when we don't know what was changed
authored
125 my $num_changed = @dirs;
f1bbfe2 Andrew Fresh Add the script to git
authored
126
127 my $match = shift @dirs;
4e8fa04 Andrew Fresh Better messages for files with regress tests
authored
128 $match //= '';
e869c04 Andrew Fresh Handle partial word chops better
authored
129
130 my $last = '/';
f1bbfe2 Andrew Fresh Add the script to git
authored
131 foreach my $dir (@dirs) {
e869c04 Andrew Fresh Handle partial word chops better
authored
132 $last = chop $match while $dir !~ /^\Q$match/;
f1bbfe2 Andrew Fresh Add the script to git
authored
133 }
fdea85f Andrew Fresh Make the logic of when to add a "wildcard" easier to read
authored
134 $match .= '*' if $match and $last ne '/' and $match !~ m{/$};
f1bbfe2 Andrew Fresh Add the script to git
authored
135
5902c80 Andrew Fresh Just match on files, easier that way
authored
136 $match =~ s{^[\.\/]+}{}; # No need for leading ./
137 $match =~ s{/+$}{}; # one less char most likely
f1bbfe2 Andrew Fresh Add the script to git
authored
138
4e8fa04 Andrew Fresh Better messages for files with regress tests
authored
139 my $message = $changed;
140 if ( !$match ) {
79a280d Andrew Fresh Friendlier wording when we don't know what was changed
authored
141 if ($has_non_regress) {
142 if ( $num_changed > 5 ) { $message .= ' many things' }
143 elsif ( $num_changed > 2 ) { $message .= ' a few things' }
144 elsif ( $num_changed > 1 ) { $message .= ' a couple things' }
145 else { $message .= ' something' }
146 }
147 $message .= ' including' if $has_regress and $has_non_regress;
4e8fa04 Andrew Fresh Better messages for files with regress tests
authored
148 $message .= ' regression tests' if $has_regress;
149 }
150 elsif ($has_regress) {
151 if ($has_non_regress) {
152 $message .= " $match and regression tests";
153 }
154 else {
155 $message .= " regress/$match";
156 }
157 }
158 else {
159 $message .= " $match";
160 }
f1bbfe2 Andrew Fresh Add the script to git
authored
161
4e8fa04 Andrew Fresh Better messages for files with regress tests
authored
162 return $message;
f1bbfe2 Andrew Fresh Add the script to git
authored
163 }
164
165 sub make_tweet {
166 my ($commit) = @_;
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
167 return make_tweet_for_sets($commit) if $commit->{type};
168
f1bbfe2 Andrew Fresh Add the script to git
authored
169 my %params = ( who => account_for( $commit->{'Module name'} ), );
170
171 my $by = $commit->{'Changes by'};
172 $by =~ s/\@.*$/\@/;
173
174 my $change = change_for($commit);
175
176 my $message = "$by $change: " . $commit->{'Log message'};
34b55d2 Andrew Fresh Add the "tag" if there is one, indicates a commit to -stable
authored
177 $message = $commit->{Tag} . ' ' . $message if $commit->{Tag};
74241c8 Andrew Fresh Don't show the import conflict message in the tweet
authored
178 $message =~ s/\s*\d+\s*conflicts created by this import.*//s;
c577da7 Andrew Fresh Only combine spaces or tabs, not newlines
authored
179 $message =~ s/[[:blank:]+]]/ /gms;
f1bbfe2 Andrew Fresh Add the script to git
authored
180
181 return shorten($message), \%params;
182 }
183
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
184 sub make_tweet_for_sets {
185 my ($set) = @_;
186 my %params = ( who => 'openbsd_sets' );
187
188 my $message = "New OpenBSD $set->{release} $set->{type} for $set->{arch}";
189
190 return shorten($message), \%params;
191 }
192
f1bbfe2 Andrew Fresh Add the script to git
authored
193 sub shorten {
55dfb2b Andrew Fresh Re-shorten tweets that fail because they are too long
authored
194 my ($message, $maxlen) = @_;
195 $maxlen ||= 140;
196 if ( length $message > $maxlen ) {
197 my $keep = $maxlen - 3;
198 $message =~ s/^(.{$keep}).*/$1/ms;
f1bbfe2 Andrew Fresh Add the script to git
authored
199 $message =~ s/\s+$//ms;
200 $message .= '...';
201 }
202 return $message;
203 }
204
205 sub tweet {
206 my ( $message, $params ) = @_;
207
46a06a8 Andrew Fresh Better decode the ChangeLog
authored
208 say encode('UTF-8', "Tweeting [$message]");
209 my $encoded = encode('UTF-8', $message);
210 eval { get_twitter_account( $params->{who} )->update($encoded) };
f1bbfe2 Andrew Fresh Add the script to git
authored
211 if ($@) {
55dfb2b Andrew Fresh Re-shorten tweets that fail because they are too long
authored
212
213 # If we have what Twitter thinks is a URL, they are going to
214 # "shorten" it. That might make it longer, too long.
215 # so, our best bet is to just keep chomping letters.
216 if ($@ =~ /tweet is too long|is over 140 characters/) {
217 $message =~ s/\.+$//; # strip the ellipse
218 return tweet( shorten($message, length($message) - 1), $params );
219 }
220 elsif ($@ =~ /Status is a duplicate/) {
221 warn "$@\n";
222 return 1;
223 }
224
f1bbfe2 Andrew Fresh Add the script to git
authored
225 warn $@;
226 return 0;
227 }
228 return 1;
229 }
230
8e85794 Andrew Fresh ReTweet other accounts on OpenBSD_cvs instead of real content
authored
231 sub retweet {
232
233 my $opts = { count => 100, trim_user => 1 };
234 my $since_id = seen()->{openbsd_cvs_last_retweet} || 0;
235 $opts->{since_id} = $since_id if $since_id;
236
237 my $nt = get_twitter_account('openbsd_cvs');
238 my $tokens = get_access_tokens('openbsd_cvs');
239 my $tweets = $nt->home_timeline($opts);
240
241 foreach my $tweet ( reverse @{$tweets} ) {
242 next if $tweet->{user}->{id_str} == $tokens->{user_id};
243 next if $tweet->{retweeted};
dd991cb Andrew Fresh Mention what we retweet
authored
244 print "Retweet $tweet->{id_str}\n";
8e85794 Andrew Fresh ReTweet other accounts on OpenBSD_cvs instead of real content
authored
245 $nt->retweet( $tweet->{id_str} );
246 seen()->{openbsd_cvs_last_retweet} = $tweet->{id_str};
247 }
248 sync_seen();
249 }
250
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
251 sub parse_changelog {
f1bbfe2 Andrew Fresh Add the script to git
authored
252 my ($file) = @_;
253 return {} unless -f $file;
254
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
255 my @commits;
f1bbfe2 Andrew Fresh Add the script to git
authored
256 my %commit;
257
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
258 my $finish_commit = sub {
259 if ( my $changes = $commit{'Changes by'} ) {
260 my ( $who, $when ) = split /\s+/, $changes, 2;
261 $commit{'Changes by'} = $who;
262 $commit{'Changes on'} = $when;
263 }
264
265 $commit{'Log message'} //= '';
266 $commit{'Log message'} =~ s/^\s+|\s+$//gms;
267
268 $commit{id} = join '|', grep {defined}
269 @commit{ 'Module name', 'Changes by', 'Changes on' };
270
271 push @commits, {%commit};
272 %commit = ();
273 };
274
f1bbfe2 Andrew Fresh Add the script to git
authored
275 open my $fh, '<', $file or die $!;
276 my $key = '';
277 my $dir = '';
46a06a8 Andrew Fresh Better decode the ChangeLog
authored
278 while (1) {
279 $_ = decode('UTF-8', readline $fh) || last;
f1bbfe2 Andrew Fresh Add the script to git
authored
280 chomp;
281
f808250 Andrew Fresh Notice that the import details are inside of the Log Message
authored
282 if (/^\s*(CVSROOT|Module name|Changes by):\s+(.*)$/) {
f1bbfe2 Andrew Fresh Add the script to git
authored
283 $commit{$1} = $2;
284 next;
285 }
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
286 next unless $commit{CVSROOT}; # first thing should be CVSROOT
f1bbfe2 Andrew Fresh Add the script to git
authored
287
e453c04 Andrew Fresh Match Update commit messages
authored
288 if (/^(Update of)\s+(.*)\/([^\/]+)$/) {
289 $commit{'Updated files'}{$2} = [$3];
290 next;
291 }
292
27cb5f2 Andrew Fresh Split handling of Log Message out
authored
293 if (/^(\w+ files):/) {
f1bbfe2 Andrew Fresh Add the script to git
authored
294 $key = $1;
295 next;
296 }
297
298 if ($key) {
27cb5f2 Andrew Fresh Split handling of Log Message out
authored
299 s/^\s+//;
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
300 unless ($_) { $key = ''; $dir = ''; next; }
27cb5f2 Andrew Fresh Split handling of Log Message out
authored
301
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
302 my @files;
303 if (/^\s*([^:]*?)\s*:\s*(.*)$/) {
304 $dir = $1;
305 @files = $2;
306 }
307 else { @files = $_ }
27cb5f2 Andrew Fresh Split handling of Log Message out
authored
308 @files = map {split} @files;
309 next unless $dir;
310
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
311 if (@files && $files[0] eq 'Tag:') {
312 my $k = shift @files;
313 my $v = shift @files;
314
315 $k =~ s/:$//;
316 $commit{$k} = $v;
317 }
318
27cb5f2 Andrew Fresh Split handling of Log Message out
authored
319 push @{ $commit{$key}{$dir} }, @files;
c3c4fc2 Andrew Fresh Handle Imports
authored
320 next;
27cb5f2 Andrew Fresh Split handling of Log Message out
authored
321 }
322
323 if (/^Log [Mm]essage:/) {
e86aafb Andrew Fresh Refactor parse_log_message
authored
324 my $cvsroot = parse_log_message( \%commit, $fh );
325 $finish_commit->();
326 $commit{CVSROOT} = $cvsroot;
f1bbfe2 Andrew Fresh Add the script to git
authored
327 }
328 }
329 close $fh;
330
31d294e Andrew Fresh Parse the ChangeLog instead of a Maildir
authored
331 $finish_commit->();
332 return @commits;
f1bbfe2 Andrew Fresh Add the script to git
authored
333 }
334
e86aafb Andrew Fresh Refactor parse_log_message
authored
335 sub parse_log_message {
336 my ( $commit, $fh ) = @_;
337
338 my $importing = 0;
339
340 while (<$fh>) {
341 if ( /^CVSROOT:\s+(.*)$/ ) {
342 return $1; # we've found the end of this message
343 }
344 elsif ( my ( $k, $v ) = /^\s*(Vendor Tag|Release Tags):\s+(.*)$/ ) {
345 $commit->{$k} = $v;
346 $commit->{'Log message'} =~ s/\s*Status:\s*$//ms;
347 $importing = 1;
348 }
349 elsif ( $importing && m{^\s*[UCN]\s+[^/]*/(.*)/([^/]+)\b$} ) {
350 push @{ $commit->{'Imported files'}{$1} }, $2;
351 }
352 else {
353 $commit->{'Log message'} .= $_;
354 }
355 }
356 return;
357 }
358
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
359 sub parse_sets {
360 my ($host) = @_;
361
362 my $ftp = Net::FTP->new( $host, Debug => 0 )
363 or die "Cannot connect to $host: $@";
364
365 $ftp->login( "anonymous", 'openbsd_sets@twitter' )
366 or die "Cannot login ", $ftp->message;
367
368 my @sets;
369 for ( $ftp->dir('/pub/OpenBSD/*/{,packages/}*/index.txt') ) {
2a69e56 Andrew Fresh Better ids for tweets
authored
370 my ( $perm, $links, $u, $g, $size, $mon, $day, $yort, $file ) = split;
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
371 my ( $release, $arch, $pkg_arch ) = ( split qr{/}, $file )[ 3, 4, 5 ];
372
373 next if $arch eq 'tools';
374
375 my $type = 'sets';
376 if ( $arch eq 'packages' ) {
377 $type = $arch;
378 $arch = $pkg_arch;
379 }
380
381 $release = 'snapshot' if $release eq 'snapshots';
382
2a69e56 Andrew Fresh Better ids for tweets
authored
383 my $id = "$type-$release-$arch-$mon-$day";
384 $id .= "-$yort" if $release eq 'snapshot'; # yort: YearOrTime
385
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
386 push @sets, {
2a69e56 Andrew Fresh Better ids for tweets
authored
387 id => $id,
44217b1 Andrew Fresh Add OpenBSD_sets twitter feed
authored
388 type => $type,
389 release => $release,
390 arch => $arch,
391 };
392 }
393
394 return @sets;
395 }
396
f1bbfe2 Andrew Fresh Add the script to git
authored
397 {
398 my $X;
7bfb42b Andrew Fresh Convert get_seen() to just seen()
authored
399 my %seen;
400
401 sub seen {
402 return \%seen if %seen;
403
404 $X = tie %seen, 'DB_File', $seen_file or die;
f1bbfe2 Andrew Fresh Add the script to git
authored
405
406 return \%seen;
407 }
408
409 sub sync_seen {
410 $X->sync;
411 }
412
413 }
414
415 {
416 my %tokens;
417
418 sub get_access_tokens {
419 my ( $account, $nt ) = @_;
420
421 return $tokens{$account} if exists $tokens{$account};
422
423 open my $fh, '<', $auth_file or die $!;
424 while (<$fh>) {
425 chomp;
426 my ($account_from_file, $access_token, $access_token_secret,
427 $user_id, $screen_name
428 ) = split /\s+/;
429
430 if ( $account_from_file eq 'consumer' ) {
431 $tokens{$account_from_file} = {
432 consumer_key => $access_token,
433 consumer_secret => $access_token_secret,
434 };
435 }
436 else {
437 $tokens{$account_from_file} = {
438 access_token => $access_token,
439 access_token_secret => $access_token_secret,
440 user_id => $user_id,
441 screen_name => $screen_name,
442 };
443 }
444 }
445 close $fh;
446 return $tokens{$account} if exists $tokens{$account};
447
448 return unless $nt;
449
450 my $auth_url = $nt->get_authorization_url;
451 print
452 " Authorize $account for this application at:\n $auth_url\nThen, enter the PIN# provided to continue ";
453
454 my $pin = <STDIN>; # wait for input
455 chomp $pin;
456
457 # request_access_token stores the tokens in $nt AND returns them
458 my ( $access_token, $access_token_secret, $user_id, $screen_name )
459 = $nt->request_access_token( verifier => $pin );
460
461 # save the access tokens
462 $tokens{$account} = {
463 access_token => $access_token,
464 access_token_secret => $access_token_secret,
465 user_id => $user_id,
466 screen_name => $screen_name,
467 };
468
469 save_access_tokens();
470
471 return $tokens{$account};
472 }
473
474 sub save_access_tokens {
9a27cde Andrew Fresh Slightly better disabling of saving new keys
authored
475 die "Saving is disabled, make sure you really want to";
476 open my $fh, '>', $auth_file or die $!;
f1bbfe2 Andrew Fresh Add the script to git
authored
477 foreach my $key ( sort keys %tokens ) {
478 my @keys
479 = $key eq 'consumer'
480 ? qw( consumer_key consumer_secret )
481 : qw( access_token access_token_secret user_id screen_name );
9a27cde Andrew Fresh Slightly better disabling of saving new keys
authored
482 say $fh join "\t", $key, @{ $tokens{$key} }{@keys};
f1bbfe2 Andrew Fresh Add the script to git
authored
483 }
484 close $fh;
485 }
486 }
487
488 sub get_twitter_account {
489 my ($account) = @_;
490
491 my $consumer_tokens = get_access_tokens('consumer');
492
493 my $nt = Net::Twitter->new(
4a79973 Andrew Fresh Switch to twitter's v1.1 API
authored
494 traits => [qw/API::RESTv1_1 OAuth/],
50c7daa Andrew Fresh Enable SSL to twitter
authored
495 ssl => 1,
f1bbfe2 Andrew Fresh Add the script to git
authored
496 %{$consumer_tokens}
497 );
498
499 my $tokens = get_access_tokens( $account, $nt );
500
501 $nt->access_token( $tokens->{access_token} );
502 $nt->access_token_secret( $tokens->{access_token_secret} );
503
504 #my $status = $nt->user_timeline( { count => 1 } );
505 #print Dumper $status;
506 #print Dumper $nt;
507
508 return $nt;
509 }
Something went wrong with that request. Please try again.