#!/usr/bin/perl package LJ; use strict; # # name: LJ::get_sent_invites # des: Get a list of sent invitations from the past 30 days. # args: cuserid # des-cuserid: a userid or u object of the community to get sent invitations for # returns: hashref of arrayrefs with keys userid, maintid, recvtime, status, args (itself # a hashref if what abilities the user would be given) # sub get_sent_invites { my $cu = shift; $cu = LJ::want_user($cu); return undef unless $cu; # now hit the database for their recent invites my $dbcr = LJ::get_cluster_def_reader($cu); return LJ::error('db') unless $dbcr; my $data = $dbcr->selectall_arrayref('SELECT userid, maintid, recvtime, status, args FROM invitesent ' . 'WHERE commid = ? AND recvtime > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))', undef, $cu->{userid}); # now break data down into usable format for caller my @res; foreach my $row (@{$data || []}) { my $temp = {}; LJ::decode_url_string($row->[4], $temp); push @res, { userid => $row->[0]+0, maintid => $row->[1]+0, recvtime => $row->[2], status => $row->[3], args => $temp, }; } # all done return \@res; } # # name: LJ::send_comm_invite # des: Sends an invitation to a user to join a community with the passed abilities. # args: uuserid, cuserid, muserid, attrs # des-uuserid: a userid or u object of the user to invite # des-cuserid: a userid or u object of the community to invite the user to # des-muserid: a userid or u object of the maintainer doing the inviting # des-attrs: a hashref of abilities this user should have (e.g. member, post, unmoderated, ...) # returns: 1 for success, undef if failure # sub send_comm_invite { my ($u, $cu, $mu, $attrs) = @_; $u = LJ::want_user($u); $cu = LJ::want_user($cu); $mu = LJ::want_user($mu); return undef unless $u && $cu && $mu; # step 1: if the user has banned the community, don't accept the invite return LJ::error('comm_user_has_banned') if LJ::is_banned($cu, $u); # step 2: outstanding invite? my $dbcr = LJ::get_cluster_def_reader($u); return LJ::error('db') unless $dbcr; my $argstr = $dbcr->selectrow_array('SELECT args FROM inviterecv WHERE userid = ? AND commid = ? ' . 'AND recvtime > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))', undef, $u->{userid}, $cu->{userid}); # step 3: exceeded outstanding invitation limit? only if no outstanding invite unless ($argstr) { my $cdbcr = LJ::get_cluster_def_reader($cu); return LJ::error('db') unless $cdbcr; my $count = $cdbcr->selectrow_array("SELECT COUNT(*) FROM invitesent WHERE commid = ? AND userid <> ? AND status = 'outstanding'", undef, $cu->{userid}, $u->{userid}); my $fr = LJ::get_friends($cu) || {}; my $max = int(scalar(keys %$fr) / 10); # can invite up to 1/10th of the community $max = 50 if $max < 50; # or 50, whichever is greater return LJ::error('comm_invite_limit') if $count > $max; } # step 4: setup arg string as url-encoded string my $newargstr = join('=1&', map { LJ::eurl($_) } @$attrs) . '=1'; # step 5: delete old stuff (lazy cleaning of invite tables) return LJ::error('db') unless $u->writer; $u->do('DELETE FROM inviterecv WHERE userid = ? AND ' . 'recvtime < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))', undef, $u->{userid}); return LJ::error('db') unless $cu->writer; $cu->do('DELETE FROM invitesent WHERE commid = ? AND ' . 'recvtime < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))', undef, $cu->{userid}); # step 6: branch here to update or insert if ($argstr) { # merely an update, so just do it quietly $u->do("UPDATE inviterecv SET args = ? WHERE userid = ? AND commid = ?", undef, $newargstr, $u->{userid}, $cu->{userid}); $cu->do("UPDATE invitesent SET args = ?, status = 'outstanding' WHERE userid = ? AND commid = ?", undef, $newargstr, $cu->{userid}, $u->{userid}); } else { # insert new data, as this is a new invite $u->do("INSERT INTO inviterecv VALUES (?, ?, ?, UNIX_TIMESTAMP(), ?)", undef, $u->{userid}, $cu->{userid}, $mu->{userid}, $newargstr); $cu->do("REPLACE INTO invitesent VALUES (?, ?, ?, UNIX_TIMESTAMP(), 'outstanding', ?)", undef, $cu->{userid}, $u->{userid}, $mu->{userid}, $newargstr); } # step 7: error check database work return LJ::error('db') if $u->err || $cu->err; # success return 1; } # # name: LJ::accept_comm_invite # des: Accepts an invitation a user has received. This does all the work to make the # user join the community as well as sets up privileges. # args: uuserid, cuserid # des-uuserid: a userid or u object of the user to get pending invites for # des-cuserid: a userid or u object of the community to reject the invitation from # returns: 1 for success, undef if failure # sub accept_comm_invite { my ($u, $cu) = @_; $u = LJ::want_user($u); $cu = LJ::want_user($cu); return undef unless $u && $cu; # get their invite to make sure they have one my $dbcr = LJ::get_cluster_def_reader($u); return LJ::error('db') unless $dbcr; my $argstr = $dbcr->selectrow_array('SELECT args FROM inviterecv WHERE userid = ? AND commid = ? ' . 'AND recvtime > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))', undef, $u->{userid}, $cu->{userid}); return undef unless $argstr; # decode to find out what they get my $args = {}; LJ::decode_url_string($argstr, $args); # valid invite. let's accept it as far as the community listing us goes. # 0, 0 means don't add comm to user's friends list, and don't auto-add P edge. LJ::join_community($u, $cu, 0, 0) if $args->{member}; # now grant necessary abilities my %edgelist = ( post => 'P', preapprove => 'N', moderate => 'M', admin => 'A', ); foreach (keys %edgelist) { LJ::set_rel($cu->{userid}, $u->{userid}, $edgelist{$_}) if $args->{$_}; } # now we can delete the invite and update the status on the other side return LJ::error('db') unless $u->writer; $u->do("DELETE FROM inviterecv WHERE userid = ? AND commid = ?", undef, $u->{userid}, $cu->{userid}); return LJ::error('db') unless $cu->writer; $cu->do("UPDATE invitesent SET status = 'accepted' WHERE commid = ? AND userid = ?", undef, $cu->{userid}, $u->{userid}); # done return 1; } # # name: LJ::reject_comm_invite # des: Rejects an invitation a user has received. # args: uuserid, cuserid # des-uuserid: a userid or u object of the user to get pending invites for # des-cuserid: a userid or u object of the community to reject the invitation from # returns: 1 for success, undef if failure # sub reject_comm_invite { my ($u, $cu) = @_; $u = LJ::want_user($u); $cu = LJ::want_user($cu); return undef unless $u && $cu; # get their invite to make sure they have one my $dbcr = LJ::get_cluster_def_reader($u); return LJ::error('db') unless $dbcr; my $test = $dbcr->selectrow_array('SELECT userid FROM inviterecv WHERE userid = ? AND commid = ? ' . 'AND recvtime > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))', undef, $u->{userid}, $cu->{userid}); return undef unless $test; # now just reject it return LJ::error('db') unless $u->writer; $u->do("DELETE FROM inviterecv WHERE userid = ? AND commid = ?", undef, $u->{userid}, $cu->{userid}); return LJ::error('db') unless $cu->writer; $cu->do("UPDATE invitesent SET status = 'rejected' WHERE commid = ? AND userid = ?", undef, $cu->{userid}, $u->{userid}); # done return 1; } # # name: LJ::get_pending_invites # des: Gets a list of pending invitations for a user to join a community. # args: uuserid # des-uuserid: a userid or u object of the user to get pending invites for # returns: [ [ commid, maintainerid, time, args(url encoded) ], [ ... ], ... ] or # undef if failure # sub get_pending_invites { my $u = shift; $u = LJ::want_user($u); return undef unless $u; # hit up database for invites and return them my $dbcr = LJ::get_cluster_def_reader($u); return LJ::error('db') unless $dbcr; my $pending = $dbcr->selectall_arrayref('SELECT commid, maintid, recvtime, args FROM inviterecv WHERE userid = ? ' . 'AND recvtime > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))', undef, $u->{userid}); return undef if $dbcr->err; return $pending; } # # name: LJ::leave_community # des: Makes a user leave a community. Takes care of all reluser and friend stuff. # args: uuserid, ucommid, defriend # des-uuserid: a userid or u object of the user doing the leaving # des-ucommid: a userid or u object of the community being left # des-defriend: remove comm from user's friends list # returns: 1 if success, undef if error of some sort (ucommid not a comm, uuserid not in # comm, db error, etc) # sub leave_community { my ($uuid, $ucid, $defriend) = @_; my $u = LJ::want_user($uuid); my $cu = LJ::want_user($ucid); $defriend = $defriend ? 1 : 0; return LJ::error('comm_not_found') unless $u && $cu; # defriend comm -> user return LJ::error('comm_not_comm') unless $cu->{journaltype} =~ /[CS]/; my $ret = LJ::remove_friend($cu->{userid}, $u->{userid}); return LJ::error('comm_not_member') unless $ret; # $ret = number of rows deleted, should be 1 if the user was in the comm # clear edges that effect this relationship foreach my $edge (qw(P N A M)) { LJ::clear_rel($cu->{userid}, $u->{userid}, $edge); } # defriend user -> comm? return 1 unless $defriend; LJ::remove_friend($u, $cu); # don't care if we failed the removal of comm from user's friends list... return 1; } # # name: LJ::join_community # des: Makes a user join a community. Takes care of all reluser and friend stuff. # args: uuserid, ucommid, friend?, noauto? # des-uuserid: a userid or u object of the user doing the joining # des-ucommid: a userid or u object of the community being joined # des-friend: 1 to add this comm to user's friends list, else not # des-noauto: if defined, 1 adds P edge, 0 does not; else, base on community postlevel # returns: 1 if success, undef if error of some sort (ucommid not a comm, uuserid already in # comm, db error, etc) # sub join_community { my ($uuid, $ucid, $friend, $canpost) = @_; my $u = LJ::want_user($uuid); my $cu = LJ::want_user($ucid); $friend = $friend ? 1 : 0; return LJ::error('comm_not_found') unless $u && $cu; return LJ::error('comm_not_comm') unless $cu->{journaltype} eq 'C'; # friend comm -> user LJ::add_friend($cu->{userid}, $u->{userid}); # add edges that effect this relationship... if the user sent a fourth # argument, use that as a bool. else, load commrow and use the postlevel. my $addpostacc = 0; if (defined $canpost) { $addpostacc = $canpost ? 1 : 0; } else { my $crow = LJ::get_community_row($cu); $addpostacc = $crow->{postlevel} eq 'members' ? 1 : 0; } LJ::set_rel($cu->{userid}, $u->{userid}, 'P') if $addpostacc; # friend user -> comm? return 1 unless $friend; LJ::add_friend($u->{userid}, $cu->{userid}, { defaultview => 1 }); # done return 1; } # # name: LJ::get_community_row # des: Gets data relevant to a community such as their membership level and posting access. # args: ucommid # des-ucommid: a userid or u object of the community # returns: a hashref with user, userid, name, membership, and postlevel data from the # user and community tables; undef if error # sub get_community_row { my $ucid = shift; my $cu = LJ::want_user($ucid); return unless $cu; # hit up database my $dbr = LJ::get_db_reader(); my ($membership, $postlevel) = $dbr->selectrow_array('SELECT membership, postlevel FROM community WHERE userid=?', undef, $cu->{userid}); return if $dbr->err; return unless $membership && $postlevel; # return result hashref my $row = { user => $cu->{user}, userid => $cu->{userid}, name => $cu->{name}, membership => $membership, postlevel => $postlevel, }; return $row; } # # name: LJ::get_pending_members # des: Gets a list of userids for people that have requested to be added to a community # but haven't yet actually been approved or rejected. # args: comm # des-comm: a userid or u object of the community to get pending members of # returns: an arrayref of userids of people with pending membership requests # sub get_pending_members { my $comm = shift; my $cu = LJ::want_user($comm); # database request my $dbr = LJ::get_db_reader(); my $args = $dbr->selectcol_arrayref('SELECT arg1 FROM authactions WHERE userid = ? ' . "AND action = 'comm_join_request' AND used = 'N'", undef, $cu->{userid}) || []; # parse out the args my @list; foreach (@$args) { push @list, $1+0 if $_ =~ /^targetid=(\d+)$/; } return \@list; } # # name: LJ::approve_pending_member # des: Approves someone's request to join a community. This updates the authactions table # as appropriate as well as does the regular join logic. This also generates an email to # be sent to the user notifying them of the acceptance. # args: commid, userid # des-commid: userid of the community # des-userid: userid of the user doing the join # returns: 1 on success, 0/undef on error # sub approve_pending_member { my ($commid, $userid) = @_; my $cu = LJ::want_user($commid); my $u = LJ::want_user($userid); return unless $cu && $u; # step 1, update authactions table my $dbh = LJ::get_db_writer(); my $count = $dbh->do("UPDATE authactions SET used = 'Y' WHERE userid = ? AND arg1 = ?", undef, $cu->{userid}, "targetid=$u->{userid}"); return unless $count; # step 2, make user join the community return unless LJ::join_community($u->{userid}, $cu->{userid}); # step 3, email the user my $email = "Dear $u->{name},\n\n" . "Your request to join the \"$cu->{user}\" community has been approved. If you " . "wish to add this community to your friends page reading list, click the link below.\n\n" . "\t$LJ::SITEROOT/friends/add.bml?user=$cu->{user}\n\n" . "Regards,\n$LJ::SITENAME Team"; LJ::send_mail({ to => $u->{email}, from => $LJ::COMMUNITY_EMAIL, fromname => $LJ::SITENAME, charset => 'utf-8', subject => "Your Request to Join $cu->{user}", body => $email, }); return 1; } # # name: LJ::reject_pending_member # des: Rejects someone's request to join a community. Updates authactions and generates # an email to the user. # args: commid, userid # des-commid: userid of the community # des-userid: userid of the user doing the join # returns: 1 on success, 0/undef on error # sub reject_pending_member { my ($commid, $userid) = @_; my $cu = LJ::want_user($commid); my $u = LJ::want_user($userid); return unless $cu && $u; # step 1, update authactions table my $dbh = LJ::get_db_writer(); my $count = $dbh->do("UPDATE authactions SET used = 'Y' WHERE userid = ? AND arg1 = ?", undef, $cu->{userid}, "targetid=$u->{userid}"); return unless $count; # step 2, email the user my $email = "Dear $u->{name},\n\n" . "Your request to join the \"$cu->{user}\" community has been declined. You " . "may wish to contact the maintainer(s) of this community if you are still " . "interested in joining.\n\n" . "Regards,\n$LJ::SITENAME Team"; LJ::send_mail({ to => $u->{email}, from => $LJ::COMMUNITY_EMAIL, fromname => $LJ::SITENAME, charset => 'utf-8', subject => "Your Request to Join $cu->{user}", body => $email, }); return 1; } # # name: LJ::comm_join_request # des: Registers an authaction to add a user to a # community and sends an approval email to the maintainers # returns: Hashref; output of LJ::register_authaction() # includes datecreate of old row if no new row was created # args: comm, u # des-comm: Community user object # des-u: User object to add to community # sub comm_join_request { my ($comm, $u) = @_; return undef unless ref $comm && ref $u; my $arg = "targetid=$u->{userid}"; my $dbh = LJ::get_db_writer(); # check for duplicates within the same hour (to prevent spamming) my $oldaa = $dbh->selectrow_hashref("SELECT aaid, authcode, datecreate FROM authactions " . "WHERE userid=? AND arg1=? " . "AND action='comm_join_request' AND used='N' " . "AND NOW() < datecreate + INTERVAL 1 HOUR " . "ORDER BY 1 DESC LIMIT 1", undef, $comm->{'userid'}, $arg); return $oldaa if $oldaa; # insert authactions row my $aa = LJ::register_authaction($comm->{'userid'}, 'comm_join_request', $arg); return undef unless $aa; # if there are older duplicates, invalidate any existing unused authactions of this type $dbh->do("UPDATE authactions SET used='Y' WHERE userid=? AND aaid<>? AND arg1=? " . "AND action='comm_invite' AND used='N'", undef, $comm->{'userid'}, $aa->{'aaid'}, $arg); # get maintainers of community my $adminids = LJ::load_rel_user($comm->{userid}, 'A') || []; my $admins = LJ::load_userids(@$adminids); # now prepare the emails my %dests; my $cuser = $comm->{user}; foreach my $au (values %$admins) { next if $dests{$au->{email}}++; LJ::load_user_props($au, 'opt_communityjoinemail'); next if $au->{opt_communityjoinemail} =~ /[DN]/; # Daily, None my $body = "Dear $au->{name},\n\n" . "The user \"$u->{user}\" has requested to join the \"$cuser\" community. If you wish " . "to add this user to your community, please click this link:\n\n" . "\t$LJ::SITEROOT/approve/$aa->{aaid}.$aa->{authcode}\n\n" . "Alternately, to approve or reject all outstanding membership requests at the same time, " . "visit the community member management page:\n\n" . "\t$LJ::SITEROOT/community/pending.bml?comm=$cuser\n\n" . "You may also ignore this e-mail. The request to join will expire after a period of 30 days.\n\n" . "If you wish to no longer receive these e-mails, visit the community management page and " . "set the relevant options:\n\n\t$LJ::SITEROOT/community/manage.bml\n\n" . "Regards,\n$LJ::SITENAME Team\n"; LJ::send_mail({ to => $au->{email}, from => $LJ::COMMUNITY_EMAIL, fromname => $LJ::SITENAME, charset => 'utf-8', subject => "$cuser Membership Request by $u->{user}", body => $body, wrap => 76, }); } return $aa; } 1;