summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--bin/update_twitter46
-rw-r--r--dist.ini10
-rw-r--r--lib/Linkulator/Twitter.pm163
-rw-r--r--lib/Linkulator/Twitter/Link.pm25
5 files changed, 245 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index e004fc9..b44c88f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ MANIFEST.bak
Linkulator-Twitter-*
*.bs
*.o
+state.json
diff --git a/bin/update_twitter b/bin/update_twitter
new file mode 100644
index 0000000..c5eb888
--- /dev/null
+++ b/bin/update_twitter
@@ -0,0 +1,46 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+# PODNAME: update_twitter
+
+use JSON;
+use Linkulator::Twitter;
+use Path::Class;
+use Try::Tiny;
+
+my $state_file = 'state.json';
+if (!-e $state_file) {
+ file($state_file)->openw->print(encode_json({last_tweeted => 0}));
+}
+
+my $conf = decode_json(file($state_file)->slurp);
+
+my $lt = Linkulator::Twitter->new(
+ exists $conf->{token} && exists $conf->{token_secret}
+ ? (twitter_access_token => $conf->{token},
+ twitter_access_token_secret => $conf->{token_secret})
+ : (),
+);
+
+if (my $url = $lt->authenticate_twitter) {
+ print "Authorize this app at $url and enter the pin here: ";
+
+ my $pin = <STDIN>;
+ chomp $pin;
+
+ my ($token, $secret) = $lt->authenticate_twitter($pin);
+ $conf->{token} = $token;
+ $conf->{token_secret} = $secret;
+}
+
+try {
+ $lt->update;
+ for my $link (sort { $a->id <=> $b->id } $lt->links) {
+ next if $link->id <= $conf->{last_tweeted};
+ $lt->tweet($link);
+ $conf->{last_tweeted} = $link->id;
+ }
+}
+finally {
+ file($state_file)->openw->print(encode_json($conf));
+};
diff --git a/dist.ini b/dist.ini
index 7b6fa35..fee4b26 100644
--- a/dist.ini
+++ b/dist.ini
@@ -7,3 +7,13 @@ copyright_holder = Jesse Luehrs
dist = Linkulator-Twitter
[Prereqs]
+JSON = 0
+Moose = 2.0200
+namespace::autoclean = 0
+Net::Twitter = 3.16000
+Path::Class = 0
+String::Truncate = 0
+Try::Tiny = 0
+WWW::Shorten = 0
+WWW::Shorten::VGd = 0
+XML::RAI = 0
diff --git a/lib/Linkulator/Twitter.pm b/lib/Linkulator/Twitter.pm
index e69de29..84134ce 100644
--- a/lib/Linkulator/Twitter.pm
+++ b/lib/Linkulator/Twitter.pm
@@ -0,0 +1,163 @@
+package Linkulator::Twitter;
+use Moose;
+use namespace::autoclean;
+# ABSTRACT: scrape linkulator urls and turn them into tweets
+
+use LWP::UserAgent;
+use Net::Twitter;
+use String::Truncate 'elide';
+use WWW::Shorten 'VGd';
+use XML::RAI;
+
+use Linkulator::Twitter::Link;
+
+has feed_url => (
+ is => 'ro',
+ isa => 'Str',
+ default => 'http://offtopic.akrasiac.org/?feed=sfw',
+);
+
+has twitter_user => (
+ is => 'ro',
+ isa => 'Str',
+ default => 'crawl_offtopic',
+);
+
+has twitter_access_token => (
+ is => 'ro',
+ isa => 'Str',
+ predicate => 'has_twitter_access_token',
+);
+
+has twitter_access_token_secret => (
+ is => 'ro',
+ isa => 'Str',
+ predicate => 'has_twitter_access_token_secret',
+);
+
+has links => (
+ traits => ['Array'],
+ isa => 'ArrayRef[Linkulator::Twitter::Link]',
+ default => sub { [] },
+ handles => {
+ links => 'elements',
+ add_link => 'push',
+ },
+);
+
+has twitter_consumer_key => (
+ is => 'ro',
+ isa => 'Str',
+ default => '6mW3vek1Edty1NGJe7yPFg',
+);
+
+has twitter_consumer_secret => (
+ is => 'ro',
+ isa => 'Str',
+ default => 'OzQuhNQ4HlO2eQw9tdo8R4QDLYqORwSEIzkF6ZBCAY',
+);
+
+has ua => (
+ is => 'ro',
+ isa => 'LWP::UserAgent',
+ default => sub { LWP::UserAgent->new },
+);
+
+has twitter => (
+ is => 'ro',
+ isa => 'Net::Twitter',
+ lazy => 1,
+ default => sub {
+ my $self = shift;
+ Net::Twitter->new(
+ traits => ['API::REST', 'OAuth'],
+ consumer_key => $self->twitter_consumer_key,
+ consumer_secret => $self->twitter_consumer_secret,
+ ($self->has_twitter_access_token
+ ? (access_token => $self->twitter_access_token)
+ : ()),
+ ($self->has_twitter_access_token_secret
+ ? (access_token_secret => $self->twitter_access_token_secret)
+ : ()),
+ );
+ },
+);
+
+sub authenticate_twitter {
+ my $self = shift;
+ my ($pin) = @_;
+
+ return if $self->twitter->authorized;
+
+ return $self->twitter->get_authorization_url
+ if !defined $pin;
+
+ my ($token, $secret, $id, $user) = $self->twitter->request_access_token(
+ verifier => $pin,
+ );
+ die "Authenticated the wrong user: $user (should be " . $self->twitter_user . ')'
+ unless $self->twitter_user eq $user;
+
+ return ($token, $secret);
+}
+
+sub update {
+ my $self = shift;
+
+ my $res = $self->ua->get($self->feed_url);
+ die "couldn't get " . $self->feed_url . " : " . $res->status_line
+ unless $res->is_success;
+
+ my $rss = $self->_munge_xml($res->content);
+
+ my $feed = XML::RAI->parse_string($rss);
+ die "got no items!" unless $feed->item_count;
+
+ for my $item (@{ $feed->items }) {
+ my ($link_num) = ($item->identifier =~ /(\d+)$/);
+ my $uri = $item->link;
+ $uri =~ s/^\s+|\s+$//g;
+ my $desc = $item->title;
+ $desc =~ s/^\s+|\s+$//g;
+ $self->add_link(
+ Linkulator::Twitter::Link->new(
+ id => $link_num,
+ uri => $uri,
+ desc => $desc,
+ )
+ );
+ }
+}
+
+sub tweet {
+ my $self = shift;
+ my ($link) = @_;
+
+ die "not a link"
+ unless blessed($link) && $link->isa('Linkulator::Twitter::Link');
+
+ my $uri = makeashorterlink($link->uri);
+ my $desc = elide($link->desc, 140 - length($uri) - 1, { at_space => 1 });
+
+ $self->twitter->update("$desc $uri");
+}
+
+# the feed produces invalid xml - the <link> and <guid> tags don't escape
+# ampersands in urls, and the xml parser chokes on this. need to fix it up
+# here.
+sub _munge_xml {
+ my $self = shift;
+ my ($xml) = @_;
+
+ $xml =~ s#<(link|guid)(.*)>([^<]*)</\1>#
+ my ($tag, $attrs, $text) = ($1, $2, $3);
+ $text =~ s+&+&amp;+g;
+ "<${tag}${attrs}>$text</$tag>"
+ #eg;
+
+ return $xml;
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/lib/Linkulator/Twitter/Link.pm b/lib/Linkulator/Twitter/Link.pm
new file mode 100644
index 0000000..9d8ae68
--- /dev/null
+++ b/lib/Linkulator/Twitter/Link.pm
@@ -0,0 +1,25 @@
+package Linkulator::Twitter::Link;
+use Moose;
+
+has id => (
+ is => 'ro',
+ isa => 'Int',
+ required => 1,
+);
+
+has uri => (
+ is => 'ro',
+ isa => 'Str',
+ required => 1,
+);
+
+has desc => (
+ is => 'ro',
+ isa => 'Str',
+ required => 1,
+);
+
+__PACKAGE__->meta->make_immutable;
+no Moose;
+
+1;