#!/usr/bin/perl
#
# DW::External::Site
#
# This is a base class used by other classes to define what kind of things an
# external site can do.  This class is actually responsible for instantiating
# the right kind of class.
#
# Authors:
#      Mark Smith <mark@dreamwidth.org>
#      Denise Paolucci <denise@dreamwidth.org>
#      Azure Lunatic <azurelunatic@dreamwidth.org>
#
# Copyright (c) 2009-2014 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself.  For a copy of the license, please reference
# 'perldoc perlartistic' or 'perldoc perlgpl'.
#

package DW::External::Site;

use strict;
use Carp qw/ croak /;
use DW::External::Userinfo;
use DW::External::XPostProtocol;

use LJ::ModuleLoader;
LJ::ModuleLoader->require_subclasses("DW::External::Site");

my %domaintosite;
my %idtosite;
my @all_sites_without_alias;

### static initializers
# with tld
$domaintosite{"livejournal.com"} =
    DW::External::Site->new( "2", "www.livejournal.com", "livejournal.com", "LiveJournal", "lj" );
$domaintosite{"insanejournal.com"} =
    DW::External::Site->new( "3", "www.insanejournal.com", "insanejournal.com", "InsaneJournal",
    "lj" );
$domaintosite{"deadjournal.com"} =
    DW::External::Site->new( "4", "www.deadjournal.com", "deadjournal.com", "DeadJournal", "lj" );
$domaintosite{"inksome.com"} =
    DW::External::Site->new( "5", "www.inksome.com", "inksome.com", "Inksome", "lj" );
$domaintosite{"journalfen.net"} =
    DW::External::Site->new( "6", "www.journalfen.net", "journalfen.net", "JournalFen", "lj" );
$domaintosite{"dreamwidth.org"} =
    DW::External::Site->new( "7", "www.dreamwidth.org", "dreamwidth.org", "Dreamwidth", "lj" );
$domaintosite{"archiveofourown.org"} =
    DW::External::Site->new( "8", "www.archiveofourown.org", "archiveofourown.org",
    "ArchiveofOurOwn", "AO3" );
$domaintosite{"twitter.com"} =
    DW::External::Site->new( "9", "twitter.com", "twitter.com", "Twitter", "Twitter" );
$domaintosite{"tumblr.com"} =
    DW::External::Site->new( "10", "tumblr.com", "tumblr.com", "Tumblr", "Tumblr" );
$domaintosite{"etsy.com"} =
    DW::External::Site->new( "11", "www.etsy.com", "etsy.com", "Etsy", "Etsy" );
$domaintosite{"diigo.com"} =
    DW::External::Site->new( "12", "www.diigo.com", "diigo.com", "Diigo", "Diigo" );
$domaintosite{"blogspot.com"} =
    DW::External::Site->new( "13", "blogspot.com", "blogspot.com", "Blogspot", "blogspot" );
$domaintosite{"delicious.com"} =
    DW::External::Site->new( "14", "delicious.com", "delicious.com", "Delicious", "delicious" );
$domaintosite{"deviantart.com"} =
    DW::External::Site->new( "15", "deviantart.com", "deviantart.com", "DeviantArt", "da" );
$domaintosite{"last.fm"} =
    DW::External::Site->new( "16", "last.fm", "last.fm", "LastFM", "lastfm" );
$domaintosite{"ravelry.com"} =
    DW::External::Site->new( "17", "www.ravelry.com", "ravelry.com", "Ravelry", "ravelry" );
$domaintosite{"wordpress.com"} =
    DW::External::Site->new( "18", "wordpress.com", "wordpress.com", "Wordpress", "WP" );
$domaintosite{"plurk.com"} =
    DW::External::Site->new( "19", "www.plurk.com", "plurk.com", "Plurk", "Plurk" );
$domaintosite{"pinboard.in"} =
    DW::External::Site->new( "20", "www.pinboard.in", "pinboard.in", "Pinboard", "Pinboard" );
$domaintosite{"fanfiction.net"} =
    DW::External::Site->new( "21", "www.fanfiction.net", "fanfiction.net", "FanFiction",
    "FanFiction" );
$domaintosite{"pinterest.com"} =
    DW::External::Site->new( "22", "www.pinterest.com", "pinterest.com", "Pinterest", "pinterest" );
$domaintosite{"youtube.com"} =
    DW::External::Site->new( "23", "www.youtube.com", "youtube.com", "YouTube", "yt" );
$domaintosite{"github.com"} =
    DW::External::Site->new( "24", "www.github.com", "github.com", "GitHub", "gh" );

# three-part domain name
$domaintosite{"lj.rossia.org"} =
    DW::External::Site->new( "25", "lj.rossia.org", "lj.rossia.org", "LJRossia", "lj" );

# more two-part sites
$domaintosite{"medium.com"} =
    DW::External::Site->new( "26", "medium.com", "medium.com", "Medium", "medium" );
$domaintosite{"imzy.com"} =
    DW::External::Site->new( "27", "www.imzy.com", "imzy.com", "Imzy", "imzy" );
$domaintosite{"facebook.com"} =
    DW::External::Site->new( "28", "www.facebook.com", "facebook.com", "Facebook", "FB" );
$domaintosite{"instagram.com"} =
    DW::External::Site->new( "29", "www.instagram.com", "instagram.com", "Instagram", "instagram" );
$domaintosite{"del.icio.us"} =
    DW::External::Site->new( "30", "del.icio.us", "del.icio.us", "Delicious", "delicious" );
$domaintosite{"substack.com"} =
    DW::External::Site->new( "31", "substack.com", "substack.com", "Substack", "substack" );

$domaintosite{"itch.io"} =
    DW::External::Site->new( "32", "www.itch.io", "itch.io", "Itch", "itch" );

$domaintosite{"furaffinity.com"} =
    DW::External::Site->new( "33", "www.furaffinity.com", "furaffinity.com", "FurAffinity", "fa" );
$domaintosite{"artstation.com"} =
    DW::External::Site->new( "33", "www.artstation.com", "artstation.com", "ArtStation",
    "artstation" );
$domaintosite{"ko-fi.com"} =
    DW::External::Site->new( "34", "www.ko-fi.com", "ko-fi.com", "Kofi", "kofi" );

$domaintosite{"bsky.app"} =
    DW::External::Site->new( "35", "bsky.app", "bsky.app", "Bluesky", "atproto" );
$domaintosite{"bsky.social"} =
    DW::External::Site->new( "36", "bsky.app", "bsky.social", "BlueskySocial", "atproto" );

@all_sites_without_alias = values %domaintosite;

# without tld
$domaintosite{"livejournal"}     = $domaintosite{"livejournal.com"};
$domaintosite{"lj"}              = $domaintosite{"livejournal.com"};
$domaintosite{"insanejournal"}   = $domaintosite{"insanejournal.com"};
$domaintosite{"ij"}              = $domaintosite{"insanejournal.com"};
$domaintosite{"deadjournal"}     = $domaintosite{"deadjournal.com"};
$domaintosite{"dj"}              = $domaintosite{"deadjournal.com"};
$domaintosite{"deviantart"}      = $domaintosite{"deviantart.com"};
$domaintosite{"da"}              = $domaintosite{"deviantart.com"};
$domaintosite{"inksome"}         = $domaintosite{"inksome.com"};
$domaintosite{"journalfen"}      = $domaintosite{"journalfen.net"};
$domaintosite{"jf"}              = $domaintosite{"journalfen.net"};
$domaintosite{"dreamwidth"}      = $domaintosite{"dreamwidth.org"};
$domaintosite{"dw"}              = $domaintosite{"dreamwidth.org"};
$domaintosite{"archiveofourown"} = $domaintosite{"archiveofourown.org"};
$domaintosite{"ao3.org"}         = $domaintosite{"archiveofourown.org"};
$domaintosite{"ao3"}             = $domaintosite{"archiveofourown.org"};
$domaintosite{"twitter"}         = $domaintosite{"twitter.com"};
$domaintosite{"tumblr"}          = $domaintosite{"tumblr.com"};
$domaintosite{"etsy"}            = $domaintosite{"etsy.com"};
$domaintosite{"diigo"}           = $domaintosite{"diigo.com"};
$domaintosite{"blogspot"}        = $domaintosite{"blogspot.com"};
$domaintosite{"blogger.com"}     = $domaintosite{"blogspot.com"};
$domaintosite{"blogger"}         = $domaintosite{"blogspot.com"};
$domaintosite{"delicious"}       = $domaintosite{"del.icio.us"};
$domaintosite{"delicious.com"}   = $domaintosite{"del.icio.us"};
$domaintosite{"deviantart"}      = $domaintosite{"deviantart.com"};
$domaintosite{"ravelry"}         = $domaintosite{"ravelry.com"};
$domaintosite{"wordpress"}       = $domaintosite{"wordpress.com"};
$domaintosite{"plurk"}           = $domaintosite{"plurk.com"};
$domaintosite{"pinboard"}        = $domaintosite{"pinboard.in"};
$domaintosite{"ffn"}             = $domaintosite{"fanfiction.net"};
$domaintosite{"pinterest"}       = $domaintosite{"pinterest.com"};
$domaintosite{"youtube"}         = $domaintosite{"youtube.com"};
$domaintosite{"github"}          = $domaintosite{"github.com"};
$domaintosite{"lj.rossia"}       = $domaintosite{"lj.rossia.org"};
$domaintosite{"ljr"}             = $domaintosite{"lj.rossia.org"};
$domaintosite{"medium"}          = $domaintosite{"medium.com"};
$domaintosite{"imzy"}            = $domaintosite{"imzy.com"};
$domaintosite{"facebook"}        = $domaintosite{"facebook.com"};
$domaintosite{"fb"}              = $domaintosite{"facebook.com"};
$domaintosite{"instagram"}       = $domaintosite{"instagram.com"};
$domaintosite{"ig"}              = $domaintosite{"instagram.com"};
$domaintosite{"fa"}              = $domaintosite{"furaffinity.com"};
$domaintosite{"artstation"}      = $domaintosite{"artstation.com"};
$domaintosite{"substack"}        = $domaintosite{"substack.com"};
$domaintosite{"itch"}            = $domaintosite{"itch.io"};
$domaintosite{"kofi"}            = $domaintosite{"ko-fi.com"};
$domaintosite{"bsky"}            = $domaintosite{"bsky.app"};

foreach my $value (@all_sites_without_alias) {
    $idtosite{ $value->{siteid} } = $value;
}

# now on to the class definition

# creates a new Site.  should only get called by the static initializer
sub new {
    my ( $class, $siteid, $hostname, $domain, $sitename, $servicetype ) = @_;

    return bless {
        siteid      => $siteid,
        hostname    => $hostname,
        domain      => $domain,
        sitename    => $sitename,
        servicetype => $servicetype
        },
        $class . "::" . $sitename;
}

# returns the appropriate site for this sitename.
sub get_site {
    my ( $class, %opts ) = @_;

    my $site = delete $opts{site}
        or croak 'site argument required';
    croak 'invalid extra parameters'
        if %opts;

    # cleanup
    $site =~ s/\r?\n//s;              # multiple lines is pain
    $site =~ s!^(?:.+)://(.*)!$1!;    # remove proto:// leading
    $site =~ s!^([^/]+)/.*$!$1!;      # remove /foo/bar.html trailing

    my $mapped;

    # if there are no dots in the site argument, do a straightforward hash check
    if ( $site !~ /\./ ) {
        $site   = lc $site;
        $mapped = $domaintosite{$site};

    }
    else {
        # validate each part of the domain based on RFC 1035
        my @parts = grep { /^[a-z][a-z0-9\-]*?[a-z0-9]$/ }
            map { lc $_ }
            split( /\./, $site );

        $mapped = $domaintosite{"$parts[-2].$parts[-1]"};

        # if two-part domain not matched, try three-part (for e.g. lj.rossia.org)
        $mapped ||= $domaintosite{"$parts[-3].$parts[-2].$parts[-1]"};
        $mapped ||= DW::External::Site::Unknown->accepts( \@parts );
    }

    return $mapped || undef;
}

sub get_deadsites {
    return (
        "del.icio.us"    => 1,
        "diigo.com"      => 1,
        "imzy.com"       => 1,
        "inksome.com"    => 1,
        "journalfen.net" => 1
    );
}

# returns a list of all supported sites for linking
sub get_sites { return @all_sites_without_alias; }

# returns a list of all supported sites for crossposting
sub get_xpost_sites {
    my %protocols = DW::External::XPostProtocol->get_all_protocols;
    return
        grep { exists $protocols{ $_->{servicetype} } && LJ::is_enabled( "external_sites", $_ ) }
        @all_sites_without_alias;
}

# returns the appropriate site by site_id
sub get_site_by_id {
    my ( $class, $siteid ) = @_;

    return $idtosite{$siteid};
}

# returns this object if we accept the given domain, or 0 if not.
sub accepts {
    my ( $self, $parts ) = @_;

    # allows anything at this sitename
    return 0
        unless $parts->[-1] eq $self->{tld}
        && $parts->[-2] eq $self->{domain};

    return $self;
}

# returns the account type for this user on this site.
sub journaltype {
    my $self = shift;
    return DW::External::Userinfo->lj_journaltype(@_)
        if $self->{servicetype} eq 'lj';
    return 'P';    # default
}

# returns the journal_url for this user on this site.
sub journal_url {
    my ( $self, $u ) = @_;
    croak 'need a DW::External::User'
        unless $u && ref $u eq 'DW::External::User';

    # IF YOU OVERRIDE THIS WITH CODE THAT CHECKS JOURNALTYPE,
    # YOU MUST PASS THE BASE URL TO CHECK EXPLICITLY.
    # OTHERWISE IT WILL CALL BACK HERE FOR THE URL,
    # AND YOU WILL SEE WHAT INFINITE RECURSION LOOKS LIKE.

    # override this on a site-by-site basis if needed
    return "https://$self->{hostname}/users/" . $u->user . '/';
}

# returns an entry link for this user on this site.
sub entry_url {
    my ( $self, $u, %opts ) = @_;
    croak 'need a DW::External::User'
        unless $u && ref $u eq 'DW::External::User';

    # override this on a site-by-site basis if needed
    my $base = $self->journal_url($u);

    # several possible options for specifying an entry;
    # for now we just support itemid + anum
    return unless exists $opts{itemid} && exists $opts{anum};

    my $pagenum = $opts{itemid} * 256 + $opts{anum};

    return $base . $pagenum . ".html";
}

# returns the profile_url for this user on this site.
sub profile_url {
    my ( $self, $u ) = @_;
    croak 'need a DW::External::User'
        unless $u && ref $u eq 'DW::External::User';

    # IF YOU OVERRIDE THIS WITH CODE THAT CHECKS JOURNALTYPE,
    # YOU MUST PASS THE BASE URL TO CHECK EXPLICITLY.
    # OTHERWISE IT WILL CALL BACK HERE FOR THE URL,
    # AND YOU WILL SEE WHAT INFINITE RECURSION LOOKS LIKE.

    # override this on a site-by-site basis if needed
    return $self->journal_url($u) . 'profile';

}

# returns the feed_url for this user on this site.
sub feed_url {
    my ( $self, $u ) = @_;
    croak 'need a DW::External::User'
        unless $u && ref $u eq 'DW::External::User';

    # IF YOU OVERRIDE THIS WITH CODE THAT CHECKS JOURNALTYPE,
    # YOU MUST PASS THE BASE URL TO CHECK EXPLICITLY.
    # OTHERWISE IT WILL CALL BACK HERE FOR THE URL,
    # AND YOU WILL SEE WHAT INFINITE RECURSION LOOKS LIKE.

    # override this on a site-by-site basis if needed
    return $self->journal_url($u) . 'data/atom';
}

# returns the badge_image info for this user on this site.
sub badge_image {
    my ( $self, $u ) = @_;

    croak 'need a DW::External::User'
        unless $u && ref $u eq 'DW::External::User';

    # override this on a site-by-site basis if needed
    my $type = $self->journaltype($u) || 'P';
    my $gif  = {

        #      URL,                   width, height
        P => [ '/img/userinfo.gif',   17, 17 ],
        C => [ '/img/community.gif',  16, 16 ],
        Y => [ '/img/syndicated.gif', 16, 16 ],
    };

    my $img = $gif->{$type};
    return {
        # this will do the right thing for an lj-based site,
        # but it's better to override this with cached images
        # to avoid hammering the remote site with image requests.

        url    => "http://$self->{hostname}$img->[0]",
        width  => $img->[1],
        height => $img->[2],
    };
}

# adjust the request for any per-site limitations
sub pre_crosspost_hook {
    return $_[1];
}

# returns the servicetype
sub servicetype {
    return $_[0]->{servicetype};
}

# returns a cleaned version of the username
sub canonical_username {
    my $input = $_[1];
    my $user  = "";

    if ( $input =~ /^\s*([a-zA-Z0-9_\-]+)\s*$/ ) {    # good username
        $user = $1;
    }
    return $user;
}

1;
