#!/usr/bin/perl
#
# DW::User::Edges::WatchTrust
#
# Implements the watch and trust edges between accounts.  These are closely
# related edges that serve a lot of core functionality of the site.  Without
# these edges, the site will probably not work.
#
# Authors:
#      Mark Smith <mark@dreamwidth.org>
#
# Copyright (c) 2008-2019 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::User::Edges::WatchTrust;

use strict;
use v5.10;
use Log::Log4perl;
my $log = Log::Log4perl->get_logger(__PACKAGE__);

use Carp qw/ confess /;

use DW::User::Edges;
use DW::User::Edges::WatchTrust::Loader;
use DW::User::Edges::WatchTrust::UserHelper;
use LJ::Global::Constants;

# the watch edge simply adds one user's content to another user's watch page
# and has no security implications whatsoever
DW::User::Edges::define_edge(
    watch => {
        type    => 'hashref',
        db_edge => 'W',
        options => {
            fgcolor  => { required => 0, type => 'int' },
            bgcolor  => { required => 0, type => 'int' },
            nonotify => { required => 0, type => 'bool', default => 0 },
        },
        add_sub => \&_add_wt_edge,
        del_sub => \&_del_wt_edge,
    }
);

# the trust edge is what provides authorization for one user to see another
# user's protected posts
DW::User::Edges::define_edge(
    trust => {
        type    => 'hashref',
        db_edge => 'T',
        options => {
            mask     => { required => 0, type => 'int' },
            nonotify => { required => 0, type => 'bool', default => 0 },
        },
        add_sub => \&_add_wt_edge,
        del_sub => \&_del_wt_edge,
    }
);

# internal method used to add a watch/trust edge to an account
sub _add_wt_edge {
    my ( $from_u, $to_u, $edges ) = @_;

    # bail unless there are watch/trust edges
    my ( $trust_edge, $watch_edge ) = ( delete $edges->{trust}, delete $edges->{watch} );
    return unless $trust_edge || $watch_edge;

    # now setup some helper variables
    my $do_watch = $watch_edge ? 1 : 0;
    $watch_edge ||= {};
    my $do_trust = $trust_edge ? 1 : 0;
    $trust_edge ||= {};

    # throw errors if we're not allowed
    return 0 if $do_watch && !$from_u->can_watch($to_u);
    return 0 if $do_trust && !$from_u->can_trust($to_u);

    # get current record, so we know what to modify
    my $dbh = LJ::get_db_writer();
    my $row = $dbh->selectrow_hashref(
        'SELECT fgcolor, bgcolor, groupmask FROM wt_edges WHERE from_userid = ? AND to_userid = ?',
        undef, $from_u->id, $to_u->id
    );
    confess $dbh->errstr if $dbh->err;
    $row ||= { groupmask => 0 };

    # store some existing trust/watch values for use later
    my $existing_watch = $row->{groupmask} & ( 1 << 61 );
    my $existing_trust = $row->{groupmask} & ~( 7 << 61 );

    # only matters in the read case, but ...
    my ( $fgcol, $bgcol ) = (
        $row->{fgcolor} || LJ::color_todb('#000000'),
        exists $row->{bgcolor} ? $row->{bgcolor} : LJ::color_todb('#ffffff')
    );
    $fgcol = $watch_edge->{fgcolor} if exists $watch_edge->{fgcolor};
    $bgcol = $watch_edge->{bgcolor} if exists $watch_edge->{bgcolor};

    # calculate the mask we're going to apply to the user; this is somewhat complicated
    # as we have to account for situations where we're updating only one of the edges, as
    # well as the situation where we are just updating the trust edge without changing
    # the user's group memberships.  lots of comments.  start with a mask of 0.
    my $mask = 0;

    # if they are adding a watch edge, simply turn that bit on
    $mask |= ( 1 << 61 ) if $do_watch;

    # if they are not adding a watch edge, import the existing watch edge
    $mask |= $existing_watch unless $do_watch;

    # if they are adding a trust edge, we need to turn bit 1 on
    $mask |= 1 if $do_trust;

    # now, we have to copy some trustmask, depending on some factors
    if (
        ( $do_watch && !$do_trust ) ||                 # 1) if we're only updating watch
        ( $do_trust && !exists $trust_edge->{mask} )
        )    # 2) they're adding a trust edge but don't set a mask
    {
        # in these two cases, we want to copy up the trustmask from the database
        $mask |= $existing_trust;
    }

    # the final case, they are trusting and have specified a mask; but note we cannot allow
    # them to set the high bits
    if ( $do_trust && exists $trust_edge->{mask} ) {
        $mask |= ( $trust_edge->{mask} + 0 & ~( 7 << 61 ) );
    }

    # now add the row
    $dbh->do(
'REPLACE INTO wt_edges (from_userid, to_userid, fgcolor, bgcolor, groupmask) VALUES (?, ?, ?, ?, ?)',
        undef, $from_u->id, $to_u->id, $fgcol, $bgcol, $mask
    );
    confess $dbh->errstr if $dbh->err;

    # delete friend-of memcache keys for anyone who was added
    my ( $from_userid, $to_userid ) = ( $from_u->id, $to_u->id );
    LJ::MemCache::delete( [ $from_userid, "trustmask:$from_userid:$to_userid" ] );
    LJ::memcache_kill( $to_userid,   'wt_edges_rev' );
    LJ::memcache_kill( $from_userid, 'wt_edges' );
    LJ::memcache_kill( $from_userid, 'wt_list' );
    LJ::memcache_kill( $from_userid, 'watched' );
    LJ::memcache_kill( $from_userid, 'trusted' );
    LJ::memcache_kill( $to_userid,   'watched_by' );
    LJ::memcache_kill( $to_userid,   'trusted_by' );

    # fire notifications if we have theschwartz
    my $notify =
          !$from_u->equals($to_u)
        && $from_u->is_visible
        && ( $from_u->is_personal || $from_u->is_identity )
        && ( $to_u->is_personal   || $to_u->is_identity )
        && !$to_u->has_banned($from_u) ? 1 : 0;
    my $trust_notify = $notify && !$trust_edge->{nonotify} ? 1 : 0;
    my $watch_notify = $notify && !$watch_edge->{nonotify} ? 1 : 0;

    my @jobs;

    push @jobs, LJ::Event::AddedToCircle->new( $to_u, $from_u, 1 )
        if $do_trust && $trust_notify;
    push @jobs, LJ::Event::AddedToCircle->new( $to_u, $from_u, 2 )
        if $do_watch && $watch_notify;

    DW::TaskQueue->dispatch(@jobs) if @jobs;

    return 1;
}

# internal method to delete an edge
sub _del_wt_edge {
    my ( $from_u, $to_u, $edges ) = @_;
    $from_u = LJ::want_user($from_u) or return 0;
    $to_u   = LJ::want_user($to_u)   or return 0;

    # determine if we're doing an update or a delete
    my $de_watch = delete $edges->{watch};
    my $de_trust = delete $edges->{trust};
    return 1 unless $de_watch || $de_trust;

    # now setup some helper variables
    my $do_watch = $de_watch ? 1 : 0;
    my $do_trust = $de_trust ? 1 : 0;

    # get what we know
    my $does_watch = $from_u->watches($to_u);
    my $does_trust = $from_u->trusts($to_u);
    return 1 unless $does_watch || $does_trust;

    # make sure we have a valid edge to remove
    return 1
        unless ( $de_watch && $does_watch )
        || ( $de_trust && $does_trust );

    my $dbh = LJ::get_db_writer()
        or return 0;

    # deletes are easy, these are cases where we're removing both edges,
    # or removing the only remaining edge
    if (   ( $de_watch && $de_trust )
        || ( $de_watch && $does_watch && !$does_trust )
        || ( $de_trust && $does_trust && !$does_watch ) )
    {

        $dbh->do( 'DELETE FROM wt_edges WHERE from_userid = ? AND to_userid = ?',
            undef, $from_u->id, $to_u->id );
        return 0 if $dbh->err;

        # at this point, we're guaranteed to have only the other edge remaining
    }
    else {

        my $mask = $de_trust ? 1 << 61 : $from_u->trustmask($to_u);

        $dbh->do( 'UPDATE wt_edges SET groupmask = ? WHERE from_userid = ? AND to_userid = ?',
            undef, $mask, $from_u->id, $to_u->id );
        return 0 if $dbh->err;
    }

    # kill memcaches
    LJ::memcache_kill( $from_u, 'wt_edges' );
    LJ::memcache_kill( $to_u,   'wt_edges_rev' );
    LJ::memcache_kill( $from_u, 'wt_list' );
    LJ::memcache_kill( $from_u, 'watched' );
    LJ::memcache_kill( $from_u, 'trusted' );
    LJ::memcache_kill( $to_u,   'watched_by' );
    LJ::memcache_kill( $to_u,   'trusted_by' );
    LJ::MemCache::delete( [ $from_u->id, "trustmask:" . $from_u->id . ":" . $to_u->id ] );

    # fire notifications if we have theschwartz
    my $notify =
          !$from_u->equals($to_u)
        && $from_u->is_visible
        && ( $from_u->is_personal || $from_u->is_identity )
        && ( $to_u->is_personal   || $to_u->is_identity )
        && !$to_u->has_banned($from_u) ? 1 : 0;
    my $trust_notify = $notify && !$de_trust->{nonotify} ? 1 : 0;
    my $watch_notify = $notify && !$de_watch->{nonotify} ? 1 : 0;

    my @jobs;
    push @jobs, LJ::Event::RemovedFromCircle->new( $to_u, $from_u, 1 )
        if $do_trust && $trust_notify;
    push @jobs, LJ::Event::RemovedFromCircle->new( $to_u, $from_u, 2 )
        if $do_watch && $watch_notify;
    DW::TaskQueue->dispatch(@jobs) if @jobs;

    return 1;
}

# returns the valid version of a group name
sub valid_group_name {
    my $name = $_[0];

    # strip off trailing slash(es)
    $name =~ s!/+\s*$!!;

    # conform to maxes
    $name = LJ::text_trim( $name, LJ::BMAX_GRPNAME, LJ::CMAX_GRPNAME );

    return $name;
}

###############################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
###############################################################################

# push methods up into the DW::User namespace
package DW::User;
use strict;

use Carp qw/ confess /;

# returns 1 if the given user watches the given account
# returns 0 otherwise
sub watches {
    my ( $from_u, $to_u ) = @_;
    $from_u = LJ::want_user($from_u) or return 0;
    $to_u   = LJ::want_user($to_u)   or return 0;

    # now get the mask; note we have to use the internal method so we
    # can get the real mask - without the top-bit masking that $u->trustmask
    # does...
    my $mask = DW::User::Edges::WatchTrust::Loader::_trustmask( $from_u->id, $to_u->id );
    return ( $mask & ( 1 << 61 ) ) ? 1 : 0;
}
*LJ::User::watches = \&watches;

# returns 1 if you generally trust the target user
# returns 0 otherwise
sub trusts {
    my ( $from_u, $to_u ) = @_;
    $from_u = LJ::want_user($from_u) or return 0;
    $to_u   = LJ::want_user($to_u)   or return 0;

    # you always trust yourself...
    return 1 if $from_u->id == $to_u->id;

    # now get the mask; again with the internal mask method
    my $mask = DW::User::Edges::WatchTrust::Loader::_trustmask( $from_u->id, $to_u->id );
    return ( $mask & 1 ) ? 1 : 0;
}
*LJ::User::trusts = \&trusts;

# return 1/0 if the given user is mutually trusted
sub mutually_trusts {
    my ( $from_u, $to_u ) = @_;
    $from_u = LJ::want_user($from_u) or return 0;
    $to_u   = LJ::want_user($to_u)   or return 0;

    return 1
        if $from_u->trusts($to_u)
        && $to_u->trusts($from_u);
    return 0;
}
*LJ::User::mutually_trusts = \&mutually_trusts;

# returns a numeric trustmask; can also be used as a setter if you specify a numeric
# as the third argument.  in which case it returns the newly updated trustmask.
sub trustmask {
    my ( $from_u, $to_u ) = @_;
    $from_u = LJ::want_user($from_u) or return 0;
    $to_u   = LJ::want_user($to_u)   or return 0;

    # if we still have an argument, we need to set someone's mask
    if ( scalar(@_) == 3 ) {

        # make sure we trust them... we have to do this here because otherwise we could
        # implicitly create a trust relationship when one doesn't exist
        confess 'attempted to set trustmask on non-trusted edge'
            unless $from_u->trusts($to_u);

        # we update the mask by re-adding the trust edge; this is the simplest way
        # that ensures we do everything "properly"
        $from_u->add_edge( $to_u, trust => { mask => $_[2], nonotify => 1 } );
    }

    # note: we mask out the top three bits (i.e., the reserved bits and the watch bit)
    # so external callers never see them.
    return DW::User::Edges::WatchTrust::Loader::_trustmask( $from_u->id, $to_u->id ) & ~( 7 << 61 );
}
*LJ::User::trustmask = \&trustmask;

# name: LJ::User::get_birthdays
# des: get the upcoming birthdays for friends of a user. shows birthdays 3 months away by default
#      pass in full => 1 to get all friends' birthdays.
# returns: arrayref of [ month, day, username ] arrayrefs
sub get_birthdays {
    my ( $u, %opts ) = @_;
    $u = LJ::want_user($u) or return undef;

    my $months_ahead = $opts{months_ahead} || 3;
    my $full         = $opts{full};

    # try to get the cached birthday
    my $memkey = [ $u->userid, 'bdays:' . $u->userid . ':' . ( $full ? 'full' : $months_ahead ) ];
    my $cached_bdays = LJ::MemCache::get($memkey);
    return @$cached_bdays if $cached_bdays;

    my @circle = $u->is_community ? $u->member_userids : $u->circle_userids;
    my $nb     = LJ::User->next_birthdays(@circle) or return undef;

    # now map it to an array with DateTime objects
    my @bdays = map { [ $_, DateTime->from_epoch( epoch => $nb->{$_} ) ] }
        keys %$nb;

    # now push a second set of objects, for each object, one year ago
    push @bdays, [ $bdays[$_]->[0], $bdays[$_]->[1]->clone->subtract( years => 1 ) ]
        for 0 .. $#bdays;

    # sort the list... will end up nicely sorted by time
    @bdays = sort { DateTime->compare( $a->[1], $b->[1] ) } @bdays;

    # remove anything that is actually in the past
    my $now = $u->time_now;
    shift @bdays while @bdays && $bdays[0]->[1]->epoch < $now->epoch;

    # remove anything that is too far in the future
    my $months  = $full ? 12 : $months_ahead;
    my $compare = $now->add( months => $months )->epoch;
    pop @bdays while @bdays && $bdays[-1]->[1]->epoch > $compare;

    # now load the userids
    my $uids = LJ::load_userids( map { $_->[0] } @bdays );

    my @results;
    foreach my $ub (@bdays) {
        my $u = $uids->{ $ub->[0] };
        next unless $u->is_personal && $u->can_notify_bday;

        push @results, [ $ub->[1]->month, $ub->[1]->day, $u->user ];
    }

    # set birthdays in memcache for later
    LJ::MemCache::set( $memkey, \@results, 86400 );

    return @results;
}
*LJ::User::get_birthdays = \&get_birthdays;

# return users you trust
sub trusted_users {
    my $u        = shift;
    my @trustids = $u->trusted_userids;
    my $users    = LJ::load_userids(@trustids);
    return values %$users if wantarray;
    return $users;
}
*LJ::User::trusted_users = \&trusted_users;

# return users you watch
sub watched_users {
    my $u        = shift;
    my @watchids = $u->watched_userids;
    my $users    = LJ::load_userids(@watchids);
    return values %$users if wantarray;
    return $users;
}
*LJ::User::watched_users = \&watched_users;

# return users you watch and/or trust
sub circle_users {
    my $u         = shift;
    my @circleids = $u->circle_userids;
    my $users     = LJ::load_userids(@circleids);
    return values %$users if wantarray;
    return $users;
}
*LJ::User::circle_users = \&circle_users;

# return users who trust you
sub trusted_by_users {
    my $u            = shift;
    my @trustedbyids = $u->trusted_by_userids;
    my $users        = LJ::load_userids(@trustedbyids);
    return values %$users if wantarray;
    return $users;
}
*LJ::User::trusted_by_users = \&trusted_by_users;

# return users who watch you
sub watched_by_users {
    my $u            = shift;
    my @watchedbyids = $u->watched_by_userids;
    my $users        = LJ::load_userids(@watchedbyids);
    return values %$users if wantarray;
    return $users;
}
*LJ::User::watched_by_users = \&watched_by_users;

# returns array of trusted by uids.  by default, limited at 50,000 items.
sub trusted_by_userids {
    my ( $u, %args ) = @_;
    $u = LJ::want_user($u) or confess 'not a valid user object';
    my $limit = delete $args{limit} || 0;
    $limit = int($limit) || $LJ::MAX_WT_EDGES_LOAD;
    confess 'unknown option' if %args;

    return DW::User::Edges::WatchTrust::Loader::_wt_userids(
        $u,
        limit   => $limit,
        mode    => 'trust',
        reverse => 1
    );
}
*LJ::User::trusted_by_userids = \&trusted_by_userids;

# returns array of trusted uids.  by default, limited at 50,000 items.
sub trusted_userids {
    my ( $u, %args ) = @_;
    $u = LJ::want_user($u) or confess 'not a valid user object';
    my $limit = delete $args{limit} || 0;
    $limit = int($limit) || $LJ::MAX_WT_EDGES_LOAD;
    confess 'unknown option' if %args;

    return DW::User::Edges::WatchTrust::Loader::_wt_userids(
        $u,
        limit => $limit,
        mode  => 'trust'
    );
}
*LJ::User::trusted_userids = \&trusted_userids;

# returns array of watched by uids.  by default, limited at 50,000 items.
sub watched_by_userids {
    my ( $u, %args ) = @_;
    $u = LJ::want_user($u) or confess 'not a valid user object';
    my $limit = delete $args{limit} || 0;
    $limit = int($limit) || $LJ::MAX_WT_EDGES_LOAD;
    confess 'unknown option' if %args;

    return DW::User::Edges::WatchTrust::Loader::_wt_userids(
        $u,
        limit   => $limit,
        mode    => 'watch',
        reverse => 1
    );
}
*LJ::User::watched_by_userids = \&watched_by_userids;

# returns array of watched uids.  by default, limited at 50,000 items.
sub watched_userids {
    my ( $u, %args ) = @_;
    $u = LJ::want_user($u) or confess 'not a valid user object';
    my $limit = delete $args{limit} || 0;
    $limit = int($limit) || $LJ::MAX_WT_EDGES_LOAD;
    confess 'unknown option' if %args;

    return DW::User::Edges::WatchTrust::Loader::_wt_userids(
        $u,
        limit => $limit,
        mode  => 'watch'
    );
}
*LJ::User::watched_userids = \&watched_userids;

# returns array of watched and/or trusted uids.
sub circle_userids {
    my ( $u, %args ) = @_;
    $u = LJ::want_user($u) or confess 'not a valid user object';
    my %circle;    # using a hash avoids duplicate elements
    map { $circle{$_}++ } ( $u->watched_userids(%args), $u->trusted_userids(%args) );
    return keys %circle;
}
*LJ::User::circle_userids = \&circle_userids;

# returns array of mutually watched userids.  by default, limit at 50k.
# note that this function will be wildly inaccurate in any situation where
# an account actually has more than 50k of either direction.  but we'll
# cross that bridge when we come to it...
sub mutually_watched_userids {
    my ( $u, %args ) = @_;
    $u = LJ::want_user($u) or confess 'not a valid user object';

    my %mutual;
    my %watched_fwd = map { $_ => 1 } $u->watched_userids(%args);
    foreach my $uid ( $u->watched_by_userids(%args) ) {
        $mutual{$uid} = 1
            if exists $watched_fwd{$uid};
    }

    return keys %mutual;
}
*LJ::User::mutually_watched_userids = \&mutually_watched_userids;

# returns array of mutually trusted userids.  by default, limit at 50k.
# same limitations as above.
sub mutually_trusted_userids {
    my ( $u, %args ) = @_;
    $u = LJ::want_user($u) or confess 'not a valid user object';

    my %mutual;
    my %trusted_fwd = map { $_ => 1 } $u->trusted_userids(%args);
    foreach my $uid ( $u->trusted_by_userids(%args) ) {
        $mutual{$uid} = 1
            if exists $trusted_fwd{$uid};
    }

    return keys %mutual;
}
*LJ::User::mutually_trusted_userids = \&mutually_trusted_userids;

# returns hashref;
#
#   { userid =>
#      { groupmask => NNN, fgcolor => '#xxx', bgcolor => '#xxx', showbydefault => NNN }
#   }
#
# one entry in the hashref for everything the given user trusts.  note that fgcolor/bgcolor
# are only really useful for watched users, so these will be default/empty if the user
# is only trusted.
#
# arguments is a hash of options
#    memcache_only => 1,     if set, never hit database
#    force_database => 1,    if set, ALWAYS hit database (DANGER)
#
sub trust_list {
    my ( $u, %args ) = @_;
    $u = LJ::want_user($u) or confess 'invalid user object';
    my $memc_only = delete $args{memcache_only}  || 0;
    my $db_only   = delete $args{force_database} || 0;
    confess 'extra/invalid arguments' if %args;

    # special case, we can disable loading friends for a user if there is a site
    # problem or some other issue with this codebranch
    return undef if $LJ::FORCE_EMPTY_SUBSCRIPTIONS{ $u->id };

    # attempt memcache if allowed
    unless ($db_only) {
        my $memc = DW::User::Edges::WatchTrust::Loader::_trust_list_memc($u);
        return $memc if $memc;
    }

    # bail early if we are supposed to only hit memcache, this saves us from a
    # potentially expensive database call in codepaths that are best-effort
    return {} if $memc_only;

    # damn you memcache for not having our data
    return DW::User::Edges::WatchTrust::Loader::_trust_list_db( $u, force_database => $db_only );
}
*LJ::User::trust_list = \&trust_list;

# returns hashref;
#
#   { userid =>
#      { groupmask => NNN, fgcolor => '#xxx', bgcolor => '#xxx', showbydefault => NNN }
#   }
#
# one entry in the hashref for everything the given user has in a particular trust
# group.  you can specify the group by id or name.
#
# arguments is a hash of options
#    id => 1,                if set, get members of trust group id 1
#    name => "Foo Group",    if set, get members of trust group "Foo Group"
#    memcache_only => 1,     if set, never hit database
#    force_database => 1,    if set, ALWAYS hit database (DANGER)
#
sub trust_group_members {
    my ( $u, %args ) = @_;
    $u = LJ::want_user($u) or confess 'invalid user object';
    my $memc_only = delete $args{memcache_only}  || 0;
    my $db_only   = delete $args{force_database} || 0;
    my $name      = delete $args{name};
    my $id        = delete $args{id};
    confess 'extra/invalid arguments' if %args;
    confess 'need one of: id, name' unless $id || $name;
    confess 'do not need both: id, name' if $id && $name;

    # special case, we can disable loading friends for a user if there is a site
    # problem or some other issue with this codebranch
    return undef if $LJ::FORCE_EMPTY_SUBSCRIPTIONS{ $u->id };

    # load the user's groups
    my $grp = $u->trust_groups( id => $id, name => $name );
    return {} unless $grp;

    # calculate mask to use later
    my $mask = 1 << $grp->{groupnum};

    # attempt memcache if allowed
    unless ($db_only) {
        my $memc = DW::User::Edges::WatchTrust::Loader::_trust_group_members_memc( $mask, $u );
        return $memc if $memc;
    }

    # bail early if we are supposed to only hit memcache, this saves us from a
    # potentially expensive database call in codepaths that are best-effort
    return {} if $memc_only;

    # damn you memcache for not having our data
    return DW::User::Edges::WatchTrust::Loader::_trust_group_members_db( $mask, $u,
        force_database => $db_only );
}
*LJ::User::trust_group_members = \&trust_group_members;

# returns hashref;
#
#   { userid =>
#      { groupmask => NNN, fgcolor => '#xxx', bgcolor => '#xxx', showbydefault => NNN }
#   }
#
# one entry in the hashref for everything the given user is watching.
#
# arguments is a hash of options
#    memcache_only => 1,     if set, never hit database
#    force_database => 1,    if set, ALWAYS hit database (DANGER)
#
sub watch_list {
    my ( $u, %args ) = @_;
    $u = LJ::want_user($u) or confess 'invalid user object';
    my $memc_only = delete $args{memcache_only}  || 0;
    my $db_only   = delete $args{force_database} || 0;
    my $comm_okay = delete $args{community_okay} || 0;
    confess 'extra/invalid arguments' if %args;

    # special case, we can disable loading friends for a user if there is a site
    # problem or some other issue with this codebranch
    return undef if $LJ::FORCE_EMPTY_SUBSCRIPTIONS{ $u->id };

    # attempt memcache if allowed
    unless ($db_only) {
        my $memc = DW::User::Edges::WatchTrust::Loader::_watch_list_memc( $u,
            community_okay => $comm_okay );
        return $memc if $memc;
    }

    # bail early if we are supposed to only hit memcache, this saves us from a
    # potentially expensive database call in codepaths that are best-effort
    return {} if $memc_only;

    # damn you memcache for not having our data
    return DW::User::Edges::WatchTrust::Loader::_watch_list_db(
        $u,
        force_database => $db_only,
        community_okay => $comm_okay
    );
}
*LJ::User::watch_list = \&watch_list;

# gets a hashref of trust group requested.  arguments is a hash of options
#   id => NNN,      id of group to get
#   name => "ZZZ",  name of group to get
#
# returns undef if group not found
#
sub trust_groups {
    my ( $u, %opts ) = @_;
    $u = LJ::want_user($u)
        or confess 'invalid user object';
    my $id  = delete $opts{id};
    my $bit = defined $id ? $id + 0 : 0;
    confess 'invalid bit number' if $bit < 0 || $bit > 60;
    my $name = delete( $opts{name} );
    $name = lc $name if defined $name;
    confess 'invalid arguments' if %opts;

    return DW::User::Edges::WatchTrust::Loader::_trust_groups( $u, $bit, $name );
}
*LJ::User::trust_groups = \&trust_groups;

# edits a new trust_group, arguments is a hash of options
#   id => NNN,           (optional) bit/ID of the group to edit (1..60)
#   groupname => "ZZZ",  name of this group
#   sortorder => NNN,    (optional) sort order (0..255)
#   is_public => 1/0,    (optional) defaults to 0
#
# arguments are used to create the group.  if you don't specify an id then one
# will be automatically created for you.
#
# returns id of new group.
#
sub create_trust_group {
    my ( $u, %opts ) = @_;
    $u = LJ::want_user($u)
        or confess 'invalid user object';
    my $grp = $u->trust_groups;

    # calculate an id to use
    my $id = delete( $opts{id} ) + 0;
    confess 'group with that id already exists'
        if $id > 0 && exists $grp->{$id};
    ($id) ||= ( grep { !exists $grp->{$_} } 1 .. 60 )[0];
    confess 'id invalid'
        if $id < 1 || $id > 60;

    # validate other parameters
    confess 'invalid sortorder (not in range 0..255)'
        if exists $opts{sortorder} && $opts{sortorder} !~ /^\d+$/;
    confess 'invalid is_public (not 1/0)'
        if exists $opts{is_public} && $opts{is_public} !~ /^(?:0|1)$/;

    # need a name
    $opts{groupname} = DW::User::Edges::WatchTrust::valid_group_name( $opts{groupname} );
    confess 'name not provided'
        unless $opts{groupname};

    # now perform an edit with our chosen id
    return $id
        if $u->edit_trust_group( id => $id, _force_create => 1, %opts );
    return 0;
}
*LJ::User::create_trust_group = \&create_trust_group;

# edits a new trust_group, arguments is a hash of options
#   id => NNN,           bit/ID of the group to edit (1..60)
#   groupname => "ZZZ",  (optional) name of this group
#   sortorder => NNN,    (optional) sort order (0..255)
#   is_public => 1/0,    (optional) defaults to 0
#
# arguments are used to update the group, if you don't specify a particular
# parameter then we won't update that column.
#
# returns 1/0.
#
sub edit_trust_group {
    my ( $u, %opts ) = @_;
    $u = LJ::want_user($u)
        or confess 'invalid user object';
    my $id = delete( $opts{id} ) + 0;
    confess 'invalid id number' if $id < 0 || $id > 60;

    # and just in case they didn't tell us to change anything...
    return 1 unless %opts;

    # get current trust groups
    my $grps = $u->trust_groups;
    return 0 unless exists $grps->{$id} || $opts{_force_create};

    # now calculate what to change
    my %change = (
        sortorder => $grps->{$id}->{sortorder},
        groupname => $grps->{$id}->{groupname},
        is_public => $grps->{$id}->{is_public},
    );
    $change{sortorder} = $opts{sortorder}
        if exists $opts{sortorder} && $opts{sortorder} =~ /^\d+$/;
    $change{groupname} = DW::User::Edges::WatchTrust::valid_group_name( $opts{groupname} )
        if exists $opts{groupname};
    $change{is_public} = $opts{is_public}
        if exists $opts{is_public} && $opts{is_public} =~ /^(?:0|1)$/;

    # update the database
    $u->do(
'REPLACE INTO trust_groups (userid, groupnum, groupname, sortorder, is_public) VALUES (?, ?, ?, ?, ?)',
        undef,
        $u->id,
        $id,
        $change{groupname},
        $change{sortorder} || 50,
        $change{is_public} || 0
    );
    confess $u->errstr if $u->err;

    # kill memcache and return
    LJ::memcache_kill( $u, 'trust_group' );
    return 1;
}
*LJ::User::edit_trust_group = \&edit_trust_group;

# deletes a trust_group, arguments is a hash of options
#   id => NNN,       delete by id (1..60)
#   name => "ZZZ",   or delete by name
#
# specify either the id or the name.  note that deletion is a rather permanent
# option that will remove this group from all entries that are secured to it
# as well as remove this bit from all trustmasks.
#
# returns 1/0.
#
sub delete_trust_group {
    my ( $u, %opts ) = @_;
    $u = LJ::want_user($u) or confess 'invalid user object';

    # use existing accessor to figure out what group they mean
    my $grp = $u->trust_groups( id => $opts{id}, name => $opts{name} );
    return 0 unless $grp;

    # set bit to remove
    my $bit = $grp->{groupnum} + 0;
    return 0 unless $bit >= 1 && $bit <= 60;

    # remove all posts from allowing that group:
    my @posts_to_clean = @{
        $u->selectcol_arrayref(
            q{SELECT jitemid FROM logsec2 WHERE journalid = ? AND allowmask & (1 << ?)},
            undef, $u->id, $bit )
            || []
    };

    # now clean the posts while we can, this is a loop so we can do it in blocks of twenty
    # as it's somewhat hard on the database to do this enmasse
    my $userid = $u->id;    # convenience
    while (@posts_to_clean) {
        my @batch = splice( @posts_to_clean, 0, 50 );

        # actually updates the entries.  note that we do not return an error here because
        # it's not the end of the world if one of these fails...
        my $in = join ',', @batch;
        $u->do(   "UPDATE log2 SET allowmask=allowmask & ~(1 << $bit) "
                . "WHERE journalid=$userid AND jitemid IN ($in) AND security='usemask'" );
        $u->do(   "UPDATE logsec2 SET allowmask=allowmask & ~(1 << $bit) "
                . "WHERE journalid=$userid AND jitemid IN ($in)" );

        foreach my $id (@batch) {
            LJ::MemCache::delete( [ $userid, "log2:$userid:$id" ] );
        }
        LJ::MemCache::delete( [ $userid, "log2lt:$userid" ] );
    }

    # notify the tags system so it can do its thing
    LJ::Tags::deleted_trust_group( $u, $bit );

    # used here down
    my $dbh = LJ::get_db_writer()
        or return 0;

    # iterate over everybody in this group and remove the bit
    my $tglist = $u->trust_group_members( id => $bit, force_database => 1 );
    foreach my $tid ( keys %{ $tglist || {} } ) {
        $dbh->do(
q{UPDATE wt_edges SET groupmask = groupmask & ~(1 << ?) WHERE from_userid = ? AND to_userid = ?},
            undef, $bit, $u->id, $tid
        );

        # don't forget memcache
        LJ::MemCache::delete( [ $userid, "trustmask:$userid:$tid" ] );
    }

    # finally remove the trust group, huzzah
    $u->do( q{DELETE FROM trust_groups WHERE userid = ? AND groupnum = ?}, undef, $u->id, $bit );
    return 0 if $u->err;

    # invalidate memcache of friends/groups
    LJ::memcache_kill( $u->id, "trust_group" );
    LJ::memcache_kill( $u->id, "wt_list" );

    # sister mary of the holy hand grenade says hi and apologies if any of the
    # above failed.  we think it worked by this point, though.
    return 1;
}
*LJ::User::delete_trust_group = \&delete_trust_group;

# alters a trustmask to munge someone's group membership
#
#   $u->edit_trustmask( $otheru, ARGUMENTS )
#
# where ARGUMENTS can be one or more of:
#
#   set => [ 1, 3 ]        put $otheru in groups 1 and 3 only, remove from others
#   add => [ 1, 3 ]        add $otheru to groups 1 and 3
#   remove => [ 1, 3 ]     remove $otheru from groups 1 and 3
#
# if you are only adding/removing/setting a single group, you may pass the argument
# as a single number, not an arrayref.  e.g.,
#
#   $u->edit_trustmask( $otheru, add => 5 )
#
# adds $otheru to group 5.
#
# NOTE: passing the 'set' argument will override 'add' and 'remove' so they have no
# effect in the same call.  (so use either set or add/remove.  not both.)
#
# returns 1 on success, 0 on error.
#
sub edit_trustmask {
    my ( $u, $tu, %opts ) = @_;
    $u  = LJ::want_user($u)  or confess 'invalid user object';
    $tu = LJ::want_user($tu) or confess 'invalid target user object';
    return 0 unless $u->trusts($tu);

    # there's got to be a better way of doing this... but we want our arrays to only
    # contain valid group ids
    my @add = grep { $_ >= 1 && $_ <= 60 }
        map { $_ + 0 } @{ ref $opts{add} ? $opts{add} : [ $opts{add} ] };
    my @del = grep { $_ >= 1 && $_ <= 60 }
        map { $_ + 0 } @{ ref $opts{remove} ? $opts{remove} : [ $opts{remove} ] };
    my @set = grep { $_ >= 1 && $_ <= 60 }
        map { $_ + 0 } @{ ref $opts{set} ? $opts{set} : [ $opts{set} ] };
    my $do_clear = ( ref $opts{set} eq 'ARRAY' && scalar(@set) == 0 ) ? 1 : 0;
    return 1 unless @add || @del || @set || $do_clear;

    # this is a special case, they said "set => []" with an empty arrayref,
    # so we remove this person's membership from all groups
    if ($do_clear) {
        $u->trustmask( $tu, 0 );
        return 1;
    }

    # if we're only doing a set, we can do that easily too
    if (@set) {
        my $mask = 0;
        $mask += ( 1 << $_ ) foreach @set;
        $u->trustmask( $tu, $mask );
        return 1;
    }

    # hard path, we need to break down a user's mask and then update it
    # and send out a new one
    my $mask   = $u->trustmask($tu);
    my %groups = map { $_ => 1 } grep { $mask & ( 1 << $_ ) } 1 .. 60;

    # now process adds/deletes
    $groups{$_} = 1 foreach @add;
    delete $groups{$_} foreach @del;

    # now set it back and we're done
    $mask = 0;
    $mask += ( 1 << $_ ) foreach keys %groups;
    $u->trustmask( $tu, $mask );
    return 1;
}
*LJ::User::edit_trustmask = \&edit_trustmask;

# give a user and a group, returns if they are in that group
sub trust_group_contains {
    my ( $u, $tu, $gid ) = @_;
    $u  = LJ::want_user($u)  or confess 'invalid user object';
    $tu = LJ::want_user($tu) or confess 'invalid target user object';
    $gid = $gid + 0;

    return 0 unless $gid >= 1 && $gid <= 60;
    return 1 if $u->trustmask($tu) & ( 1 << $gid );
    return 0;
}
*LJ::User::trust_group_contains = \&trust_group_contains;

# returns 1/0 depending on if the source is allowed to add a trust edge
# to the target.  note: if you don't pass a target user, then we return
# a generic 1/0 meaning "this account is allowed to have a trust edge".
sub can_trust {
    my ( $u, $tu, %opts ) = @_;
    $u  = LJ::want_user($u) or confess 'invalid user object';
    $tu = LJ::want_user($tu);

    my $errref = $opts{errref};

    # the user must be an individual
    unless ( $u->is_individual ) {
        $$errref = LJ::Lang::ml('edges.trust.error.usernotindividual');
        return 0;
    }

    # the user must be visible
    unless ( $u->is_visible ) {
        $$errref = LJ::Lang::ml('edges.trust.error.usernotvisible');
        return 0;
    }

    if ($tu) {

        # the user cannot be the same as the target
        if ( $u->equals($tu) ) {
            $$errref = LJ::Lang::ml('edges.trust.error.userequalstarget');
            return 0;
        }

        # the target must be an individual
        unless ( $tu->is_individual ) {
            $$errref = LJ::Lang::ml('edges.trust.error.targetnotindividual');
            return 0;
        }

        # the target must not be purged/suspended/locked/deleted
        if ( $tu->is_expunged || $tu->is_suspended || $tu->is_locked || $tu->is_deleted ) {
            $$errref = LJ::Lang::ml('edges.trust.error.targetinvalidstatusvis');
            return 0;
        }

        # the target must not be banned by the user
        if ( $u->has_banned($tu) ) {
            $$errref = LJ::Lang::ml('edges.trust.error.userbannedtarget');
            return 0;
        }
    }

    # check limits
    return 0 unless _can_add_wt_edge( $u, $errref, { target => $tu } );

    # okay, good to go!
    return 1;
}
*LJ::User::can_trust = \&can_trust;

# returns 1/0 depending on if the source is allowed to add a watch edge
# to the target.  note: if you don't pass a target user, then we return
# a generic 1/0 meaning "this account is allowed to have a watch edge".
sub can_watch {
    my ( $u, $tu, %opts ) = @_;
    $u  = LJ::want_user($u) or confess 'invalid user object';
    $tu = LJ::want_user($tu);

    my $errref = $opts{errref};

    # the user must be an individual
    unless ( $u->is_individual ) {
        $$errref = LJ::Lang::ml('edges.watch.error.usernotindividual');
        return 0;
    }

    # the user must be visible
    unless ( $u->is_visible ) {
        $$errref = LJ::Lang::ml('edges.watch.error.usernotvisible');
        return 0;
    }

    if ($tu) {

        # the target must not be purged/suspended/locked/deleted
        if ( $tu->is_expunged || $tu->is_suspended || $tu->is_locked || $tu->is_deleted ) {
            $$errref = LJ::Lang::ml('edges.watch.error.targetinvalidstatusvis');
            return 0;
        }
    }

    # check limits
    return 0 unless _can_add_wt_edge( $u, $errref, { target => $tu } );

    # okay, good to go!
    return 1;
}
*LJ::User::can_watch = \&can_watch;

# internal helper sub to determine if we're at the rate limit
sub _can_add_wt_edge {
    my ( $u, $err, $opts ) = @_;

    if ( $u->is_suspended ) {
        $$err = LJ::Lang::ml("error.adduser.suspended");
        return 0;
    }

    # have they reached their friend limit?
    my $fr_count   = $opts->{'numfriends'} || $u->circle_userids;
    my $maxfriends = $u->count_maxfriends;
    if ( $fr_count >= $maxfriends ) {
        $$err = LJ::Lang::ml( "error.adduser.limit", { maxnum => $maxfriends } );
        return 0;
    }

    # are they trying to add friends too quickly?

    # don't count mutual friends
    if ( defined $opts->{target} ) {
        my $fr_user = $opts->{target};

        # we needed LJ::User object, not just a hash.
        if ( ref($fr_user) eq 'HASH' ) {
            $fr_user = LJ::load_user( $fr_user->{username} );
        }
        else {
            $fr_user = LJ::want_user($fr_user);
        }

        return 1
            if $fr_user
            && ( $fr_user->watches($u) || $fr_user->trusts($u) );
    }

    unless ( $u->rate_log( 'addfriend', 1 ) ) {
        $$err = LJ::Lang::ml("error.adduser.rate");
        return 0;
    }

    return 1;
}

1;
