The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package WWW::Shorten::Yourls;

use strict;
use warnings;
use Carp ();
use File::HomeDir ();
use File::Spec ();
use JSON::MaybeXS;
use Path::Tiny qw(path);
use Scalar::Util qw(blessed);
use URI ();

use base qw( WWW::Shorten::generic Exporter );

our $VERSION = '1.001';
$VERSION = eval $VERSION;

our @EXPORT = qw(new);

# _attr (private)
sub _attr {
    my $self = shift;
    my $attr = lc(_trim(shift) || '');
    # attribute list is small enough to just grep each time. meh.
    Carp::croak("Invalid attribute") unless grep {$attr eq $_} @{_attrs()};
    return $self->{$attr} unless @_;

    my $val = shift;
    unless (defined($val)) {
        $self->{$attr} = undef;
        return $self;
    }
    $self->{$attr} = $val;
    return $self;
}

# _attrs (static, private)
{
    my $attrs; # mimic the state keyword
    sub _attrs {
        return [@{$attrs}] if $attrs;
        $attrs = [
            qw(username password server signature),
        ];
        return [@{$attrs}];
    }
}

# _json_request (static, private)
sub _json_request {
    my $url = shift;
    Carp::croak("Invalid URI object") unless $url && blessed($url) && $url->isa('URI');
    my $ua = __PACKAGE__->ua();
    my $res = $ua->get($url);
    Carp::croak("Invalid response") unless $res;
    unless ($res->is_success) {
        Carp::croak($res->status_line);
    }

    my $content_type = $res->header('Content-Type');
    my $content = $res->decoded_content();
    unless ($content_type && $content_type =~ m{application/json}) {
        Carp::croak("Unexpected response: $content");
    }
    my $json = decode_json($content);
    Carp::croak("Invalid data returned: $content") unless $json;
    return $json;
}

# _parse_args (static, private)
sub _parse_args {
    my $args;
    if ( @_ == 1 && ref $_[0] ) {
        my %copy = eval { %{ $_[0] } }; # try shallow copy
        Carp::croak("Argument to method could not be dereferenced as a hash") if $@;
        $args = \%copy;
    }
    elsif (@_==1 && !ref($_[0])) {
        $args = {single_arg => $_[0]};
    }
    elsif ( @_ % 2 == 0 ) {
        $args = {@_};
    }
    else {
        Carp::croak("Method got an odd number of elements");
    }
    return $args;
}

# _parse_config (static, private)
{
    my $config; # mimic the state keyword
    sub _parse_config {
        # always give back a shallow copy
        return {%{$config}} if $config;
        # only parse the file once, please.
        $config = {};
        my $file = $^O eq 'MSWin32'? '_yourls': '.yourls';
        $file .= '_test' if $ENV{YOURLS_TEST_CONFIG};
        my $path = path(File::Spec->catfile(File::HomeDir->my_home(), $file));

        if ($path && $path->is_file) {
            my @lines = $path->lines_utf8({chomp => 1});
            my $attrs = _attrs();

            for my $line (@lines) {
                $line = _trim($line) || '';
                next if $line =~ /^\s*[;#]/; # skip comments
                $line =~ s/\s+[;#].*$//gm; # trim off comments
                next unless $line && $line =~ /=/; # make sure we have a =

                my ($key, $val) = split(/(?<![^\\]\\)=/, $line, 2);
                $key = lc(_trim($key) || '');
                $val = _trim($val);
                next unless $key && $val;
                $key = 'username' if $key eq 'user';
                $key = 'server' if $key eq 'base';
                next unless grep {$key eq $_} @{$attrs};
                $config->{$key} = $val;
            }
        }
        return {%{$config}};
    }
}

# _trim (static, private)
sub _trim {
    my $input = shift;
    return $input unless defined $input && !ref($input) && length($input);
    $input =~ s/\A\s*//;
    $input =~ s/\s*\z//;
    return $input;
}

sub new {
    my $class = shift;
    my $args;
    if ( @_ == 1 && ref $_[0] ) {
        my %copy = eval { %{ $_[0] } }; # try shallow copy
        Carp::croak("Argument to $class->new() could not be dereferenced as a hash") if $@;
        $args = \%copy;
    }
    elsif ( @_ % 2 == 0 ) {
        $args = {@_};
    }
    else {
        Carp::croak("$class->new() got an odd number of elements");
    }

    my $attrs = _attrs();
    # start with what's in our config file (if anything)
    my $href = _parse_config();

    # override with anything passed in
    for my $key (keys %{$args}) {
        my $lc_key = lc($key);
        $lc_key = 'username' if $lc_key eq 'user';
        $lc_key = 'server' if $lc_key eq 'base';
        next unless grep {$lc_key eq $_} @{$attrs};
        $href->{$lc_key} = $args->{$key};
    }
    my $server = $href->{server} ? $href->{server} : 'https://yourls.org/yourls-api.php';
    my $self = bless $href, $class;
    return $self->server($server);
}

sub clicks {
    my $self = shift;
    Carp::croak("You must tell us which server to use.") unless my $server = $self->server();

    my $args = _parse_args(@_);
    my $short_url = $args->{shortUrl} || $args->{single_arg} || $args->{URL} || $args->{url} || '';
    Carp::croak("A shortUrl parameter is required.\n") unless $short_url;

    my $url = $server->clone();
    my $params = {
        shorturl => $short_url,
        format => 'json',
        action => 'url-stats',
    };
    if (my $sig = $self->signature()) {
        $params->{signature} = $sig;
    }
    else {
        my $user = $self->username();
        my $pass = $self->password();
        unless ($user && $pass) {
            Carp::croak("Username and password required when not using a signature");
        }
        $params->{username} = $user;
        $params->{password} = $pass;
    }
    $url->query_form(%$params);
    return _json_request($url);
}

sub expand {
    my $self = shift;
    Carp::croak("You must tell us which server to use.") unless my $server = $self->server();

    my $args = _parse_args(@_);
    my $short_url = $args->{shortUrl} || $args->{single_arg} || $args->{URL} || $args->{url} || '';
    Carp::croak("A shortUrl parameter is required.\n") unless $short_url;

    my $url = $server->clone();
    my $params = {
        shorturl => $short_url,
        format => 'json',
        action => 'expand',
    };
    if (my $sig = $self->signature()) {
        $params->{signature} = $sig;
    }
    else {
        my $user = $self->username();
        my $pass = $self->password();
        unless ($user && $pass) {
            Carp::croak("Username and password required when not using a signature");
        }
        $params->{username} = $user;
        $params->{password} = $pass;
    }
    $url->query_form(%$params);
    return _json_request($url);
}

sub makealongerlink {
    my $self;
    if ($_[0] && blessed($_[0]) && $_[0]->isa('WWW::Shorten::Yourls')) {
        $self = shift;
    }
    my $url = shift or Carp::croak('No URL passed to makealongerlink');
    $self ||= __PACKAGE__->new(@_);
    my $res = $self->expand(shortUrl=>$url);
    return '' unless ref($res) eq 'HASH' and $res->{longurl};
    return $res->{longurl};
}

sub makeashorterlink {
    my $self;
    if ($_[0] && blessed($_[0]) && $_[0]->isa('WWW::Shorten::Yourls')) {
        $self = shift;
    }
    my $url = shift or Carp::croak('No URL passed to makeashorterlink');
    $self ||= __PACKAGE__->new(@_);
    my $res = $self->shorten(longUrl=>$url, @_);
    return $res->{shorturl};
}

sub password { return shift->_attr('password', @_); }

sub server {
    my $self = shift;
    return $self->{server} unless @_;
    my $val = shift;
    if (!defined($val) || $val eq '') {
        $self->{server} = undef;
        return $self;
    }
    elsif (blessed($val) && $val->isa('URI')) {
        $self->{server} = $val->clone();
        return $self;
    }
    elsif ($val && !ref($val)) {
        $self->{server} = URI->new(_trim($val));
        return $self;
    }

    Carp::croak("The server attribute must be set to a URI object");
}

sub shorten {
    my $self = shift;
    Carp::croak("You must tell us which server to use.") unless my $server = $self->server();

    my $args = _parse_args(@_);
    my $long_url = $args->{longUrl} || $args->{single_arg} || $args->{URL} || $args->{url} || '';
    Carp::croak("A longUrl parameter is required.\n") unless $long_url;

    my $url = $server->clone();
    my $params = {
        url => $long_url,
        format => 'json',
        action => 'shorturl',
    };
    if (my $sig = $self->signature()) {
        $params->{signature} = $sig;
    }
    else {
        my $user = $self->username();
        my $pass = $self->password();
        unless ($user && $pass) {
            Carp::croak("Username and password required when not using a signature");
        }
        $params->{username} = $user;
        $params->{password} = $pass;
    }
    $url->query_form(%$params);
    return _json_request($url);
}

sub signature { return shift->_attr('signature', @_); }

sub username { return shift->_attr('username', @_); }

1;   # End of WWW::Shorten::Yourls

__END__

=head1 NAME

WWW::Shorten::Yourls - Interface to shortening URLs using L<http://yourls.org>

=head1 SYNOPSIS

The traditional way, using the L<WWW::Shorten> interface:

    use strict;
    use warnings;

    use WWW::Shorten::Yourls;
    # use WWW::Shorten 'Yourls';  # or, this way

    # if you have a config file with your credentials:
    my $short_url = makeashorterlink('http://www.foo.com/some/long/url');
    my $long_url  = makealongerlink($short_url);
    # otherwise
    my $short = makeashorterlink('http://www.foo.com/some/long/url', {
        username => 'username',
        password => 'password',
        server => 'https://yourls.org/yourls-api.php',
        ...
    });

Or, the Object-Oriented way:

    use strict;
    use warnings;
    use Data::Dumper;
    use Try::Tiny qw(try catch);
    use WWW::Shorten::Yourls;

    my $yourls = WWW::Shorten::Yourls->new(
        username => 'username',
        password => 'password',
        signature => 'adflkdga234252lgka',
        server => 'https://yourls.org/yourls-api.php', # default
    );
    try {
        my $res = $yourls->shorten(longUrl => 'http://google.com/');
        say Dumper $res;
        # {
        #    message => "http://google.com/ added to database",
        #    shorturl => "https://yourls.org/4",
        #    status => "success",
        #    statusCode => 200,
        #    title => "Google",
        #    url => {
        #        date => "2017-02-08 02:34:37",
        #        ip => "192.168.0.1",
        #        keyword => 4,
        #        title => "Google",
        #        url => "http://google.com/"
        #    }
        # }
    }
    catch {
        die("Oh, no! $_");
    };

=head1 DESCRIPTION

A Perl interface to the L<Yourls.org API|http://yourls.org/#API>.

You can either use the traditional (non-OO) interface provided by L<WWW::Shorten>.
Or, you can use the OO interface that provides you with more functionality.

=head1 FUNCTIONS

In the non-OO form, L<WWW::Shorten::Yourls> makes the following functions available.

=head2 makeashorterlink

    my $short_url = makeashorterlink('https://some_long_link.com');
    # OR
    my $short_url = makeashorterlink('https://some_long_link.com', {
        username => 'foo',
        password => 'bar',
        # any other attribute can be set as well.
    });

The function C<makeashorterlink> will call the L<Yourls Server|http://yourls.org> web site,
passing it your long URL and will return the shorter version.

L<http://yourls.org> requires the use of a user account to shorten links.

=head2 makealongerlink

    my $long_url = makealongerlink('http://yourls.org/22');
    # OR
    my $long_url = makealongerlink('http://yourls.org/22', {
        username => 'foo',
        password => 'bar',
        # any other attribute can be set as well.
    });

The function C<makealongerlink> does the reverse. C<makealongerlink>
will accept as an argument either the full URL or just the identifier.

If anything goes wrong, either function will die.

=head1 ATTRIBUTES

In the OO form, each L<WWW::Shorten::Yourls> instance makes the following
attributes available.

=head2 password

    my $password = $yourls->password;
    $yourls = $yourls->password('some_secret'); # method chaining

Gets or sets the C<password>. This is used along with the
L<WWW::Shorten::Yourls/username> attribute.  Credentials are sent to the server
upon each and every request.

=head2 server

    my $server = $yourls->server;
    $yourls = $yourls->server(
        URI->new('https://yourls.org/yourls-api.php')
    ); # method chaining

Gets or sets the C<server>. This is full and absolute path to the server and
C<yourls-api.php> endpoint.

=head2 signature

    my $signature = $yourls->signature;
    $signature = $yourls->signature('abcdef123'); # method chaining

Gets or sets the C<signature>. If the C<signature> attribute is set, the
L<WWW::Shorten::Yourls/userna,e> and L<WWW::Shorten::Yourls/password> attributes
are ignored on each request and instead the C<signature> is sent.
See the L<Password-less API|https://github.com/YOURLS/YOURLS/wiki/PasswordlessAPI>
documentation for more details.

=head2 username

    my $username = $yourls->username;
    $yourls = $yourls->username('my_username'); # method chaining

Gets or sets the C<username>. This is used along with the
L<WWW::Shorten::Yourls/password> attribute.  Credentials are sent to the server
upon each and every request.

=head1 METHODS

In the OO form, L<WWW::Shorten::Yourls> makes the following methods available.

=head2 new

    my $yourls = WWW::Shorten::Yourls->new(
        username => 'username',
        password => 'password',
        signature => 'adflkdga234252lgka',
        server => 'https://yourls.org/yourls-api.php', # default
    );

The constructor can take any of the attributes above as parameters.

Any or all of the attributes can be set in your configuration file. If you have
a configuration file and you pass parameters to C<new>, the parameters passed
in will take precedence.

=head2 clicks

    my $clicks = $yourls->clicks(shorturl => "https://yourls.org/5");
    say Dumper $clicks;
    # {
    #    link => {
    #        clicks => 0,
    #        ip => "192.168.0.1",
    #        shorturl => "http://yourls.org/5",
    #        timestamp => "2017-02-08 02:37:24",
    #        title => "Google",
    #        url => "http://www.google.com"
    #    },
    #    message => "success",
    #    statusCode => 200
    # }

Get the C<url-stats> or number of C<clicks> for a given URL made shorter using
the L<Yourls API|http://yourls.org/#API>.
Returns a hash reference or dies. Make use of L<Try::Tiny>.

=head2 expand

    my $long = $yourls->expand(shorturl => "https://yourls.org/5");
    say $long->{longurl};
    # http://www.google.com
    say Dumper $long;
    # {
    #    keyword => 4,
    #    longurl => "http://www.google.com",
    #    message => "success",
    #    shorturl => "http://jupiter/yourls/5",
    #    statusCode => 200,
    #    title => "Google"
    # }

Expand a URL using the L<Yourls API|http://yourls.org/#API>.
Returns a hash reference or dies. Make use of L<Try::Tiny>.

=head2 shorten

    my $short = $yourls->shorten(
        url => "http://google.com/", # required.
    );
    say $short->{shorturl};
    # https://yourls.org/4
    say Dumper $short;
    # {
    #    message => "http://google.com/ added to database",
    #    shorturl => "https://yourls.org/4",
    #    status => "success",
    #    statusCode => 200,
    #    title => "Google",
    #    url => {
    #        date => "2017-02-08 02:34:37",
    #        ip => "192.168.0.1",
    #        keyword => 4,
    #        title => "Google",
    #        url => "http://google.com/"
    #    }
    # }

Shorten a URL using the L<Yourls API|http://yourls.org/#API>.
Returns a hash reference or dies. Make use of L<Try::Tiny>.

=head1 CONFIG FILES

C<$HOME/.yourls> or C<_yourls> on Windows Systems.

You may omit C<username> and C<password> in the constructor if you set them in the
C<.yourls> config file on separate lines using the syntax:

  username=username
  password=password
  server=https://yourls.org/yourls-api.php
  signature=foobarbaz123

Set any or all L<WWW::Shorten::Yourls/ATTRIBUTES> in your config file in your
home directory. Each C<key=val> setting should be on its own line. If any
parameters are then passed to the L<WWW::Shorten::Yourls/new> constructor, those
parameter values will take precedence over these.

=head1 AUTHOR

Pankaj Jain, <F<pjain@cpan.org>>

=head1 CONTRIBUTORS

=over

=item *

Chase Whitener <F<capoeirab@cpan.org>>

=item *

Michiel Beijen <F<michielb@cpan.org>>

=back

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2009 Pankaj Jain, All Rights Reserved L<http://blog.linosx.com>.

Copyright (c) 2009 Teknatus Solutions LLC, All Rights Reserved L<http://www.teknatus.com>.


This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut