LJSUP-17669: Login.bml form refactoring
[livejournal.git] / cgi-bin / communitylib.pl
blob4c09bf052bd717ff7d5d27163483543bb21c2542
1 #!/usr/bin/perl
3 package LJ;
5 use strict;
6 use warnings;
8 use Class::Autouse qw(
9 LJ::Event::CommunityInvite
10 LJ::Event::CommunityJoinReject
11 LJ::Event::CommunityJoinRequest
12 LJ::Event::CommunityJoinApprove
15 # External modules
16 use Readonly;
18 # Internal modules
19 use LJ::MemCacheProxy;
20 use LJ::RelationService;
22 use LJ::User::FriendInvites;
24 Readonly my $COMMUNITY_ROW_CACHE_KEY => 'community:';
26 # Possible membership:
27 # - open
28 # - closed
29 # - moderated
30 # Possible postlevel
31 # - all ?
32 # - select
33 # - members
35 ## Create supermaintainer poll
36 ## Args:
37 ## comm_id = community id
38 ## alive_maintainers = array ref of alive maintainers (visible, active, is maintainers, etc)
39 ## no_job = nothing to do. only logging.
40 ## textref = where save log info
41 ## to_journal = which journal save polls to?
42 ## Return:
43 ## pollid = id of new created poll
44 sub create_supermaintainer_election_poll {
45 my %args = @_;
46 my $comm_id = $args{'comm_id'};
47 my $alive_maintainers = $args{'maint_list'};
48 my $textref = $args{'log'};
49 my $no_job = $args{'no_job'} || 0;
50 my $check_active = $args{'check_active'} || 0;
51 my $to_journal = $args{'to_journal'} || LJ::load_user('lj_elections');
53 my $comm = LJ::load_userid($comm_id);
54 my $comm_username = $comm->{user};
55 if ($comm_username eq 'cheaptrip' ||
56 $comm_username eq 'cheaptrip_spb' ||
57 $comm_username eq 'cheaptrip_ua')
59 die "Can't create supermaintainer election poll for $comm_username: the community doesn't participate in voting";
62 my $entry = undef;
63 unless ($no_job) {
64 $entry = _create_post (to => $to_journal, comm => $comm);
65 die "Entry for Poll does not created\n" unless $entry;
66 $$textref .= "Entry url: " . $entry->url . "\n";
69 my @items = ();
70 foreach my $u (@$alive_maintainers) {
71 $$textref .= "\tAdd ".$u->user." as item to poll\n";
72 push @items, {
73 item => "<lj user='".$u->user."'>",
78 my @q = (
80 qtext => LJ::Lang::ml('poll.election.subject'),
81 type => 'radio',
82 items => \@items,
86 my $poll = undef;
87 unless ($no_job) {
88 $poll = LJ::Poll->create (entry => $entry, whovote => 'all', whoview => 'all', questions => \@q)
89 or die "Poll was not created";
91 eval {
92 $poll->set_prop ('createdate' => $entry->eventtime_mysql)
93 or die "Can't set prop 'createdate'";
95 $poll->set_prop ('supermaintainer' => $comm->userid)
96 or die "Can't set prop 'supermaintainer'";
98 _edit_post (to => $to_journal, comm => $comm, entry => $entry, poll => $poll)
99 or die "Can't edit post";
102 ## ugly, but reliable
103 if ($@) {
104 print $$textref;
105 use Data::Dumper;
106 warn Dumper($poll);
107 warn Dumper($to_journal);
108 die $@;
112 ## We need to remove all previous owners from community because election poll is started.
113 my $s_maints = LJ::load_rel_user($comm_id, 'S');
114 foreach my $user_id (@$s_maints) {
115 LJ::clear_rel($comm_id, $user_id, 'S');
118 ## All are ok. Emailing to all maintainers about election.
119 my $subject = LJ::Lang::ml('poll.election.email.subject');
120 $$textref .= "Sending emails to all maintainers for community " . $comm->user . "\n";
121 foreach my $u (@$alive_maintainers) {
122 next unless $u && $u->is_visible && $u->can_manage($comm);
123 next if !$check_active && $u->check_activity(90);
124 $$textref .= "\tSend email to maintainer ".$u->user."\n";
125 LJ::send_mail({ 'to' => $u->email_raw,
126 'from' => $LJ::ACCOUNTS_EMAIL,
127 'fromname' => $LJ::SITENAMESHORT,
128 'wrap' => 1,
129 'charset' => $u->mailencoding || 'utf-8',
130 'subject' => $subject,
131 'html' => (LJ::Lang::ml('poll.election.start.email', {
132 username => LJ::ljuser($u),
133 communityname => LJ::ljuser($comm),
134 faqlink => '#',
135 shortsite => $LJ::SITENAMESHORT,
136 authas => $comm->{user},
137 siteroot => $LJ::SITEROOT,
140 'body' => (LJ::Lang::ml('poll.election.start.email.plain', {
141 username => LJ::ljuser($u),
142 communityname => LJ::ljuser($comm),
143 faqlink => '#',
144 shortsite => $LJ::SITENAMESHORT,
145 authas => $comm->{user},
146 siteroot => $LJ::SITEROOT,
149 }) unless ($no_job);
152 return $no_job ? undef : $poll->pollid;
155 sub _edit_post {
156 my %opts = @_;
158 my $u = $opts{to};
159 my $comm = $opts{comm};
160 my $entry = $opts{entry};
161 my $poll = $opts{poll};
163 my $security = delete $opts{security} || 'private';
164 my $proto_sec = $security;
165 if ($security eq "friends") {
166 $proto_sec = "usemask";
169 my $subject = delete $opts{subject} || LJ::Lang::ml('poll.election.post_subject');
170 my $body = delete $opts{body} || LJ::Lang::ml('poll.election.post_body', { comm => $comm->user });
172 my %req = (
173 mode => 'editevent',
174 ver => $LJ::PROTOCOL_VER,
175 user => $u->{user},
176 password => '',
177 event => $body . "<br/>" . "<lj-poll-".$poll->pollid.">",
178 subject => $subject,
179 tz => 'guess',
180 security => $proto_sec,
181 itemid => $entry->jitemid,
184 $req{allowmask} = 1 if $security eq 'friends';
186 my %res;
187 my $flags = { noauth => 1, nomod => 1 };
189 LJ::do_request(\%req, \%res, $flags);
191 die "Error posting: $res{errmsg}" unless $res{'success'} eq "OK";
192 my $jitemid = $res{itemid} or die "No itemid";
194 return LJ::Entry->new($u, jitemid => $jitemid);
197 sub _create_post {
198 my %opts = @_;
200 my $u = $opts{to};
201 my $comm = $opts{comm};
203 my $security = delete $opts{security} || 'private';
204 my $proto_sec = $security;
205 if ($security eq "friends") {
206 $proto_sec = "usemask";
209 my $subject = delete $opts{subject} || LJ::Lang::ml('poll.election.post_subject');
210 my $body = delete $opts{body} || LJ::Lang::ml('poll.election.post_body', { comm => $comm->user });
212 my %req = (
213 mode => 'postevent',
214 ver => $LJ::PROTOCOL_VER,
215 user => $u->{user},
216 password => '',
217 event => $body,
218 subject => $subject,
219 tz => 'guess',
220 security => $proto_sec,
223 $req{allowmask} = 1 if $security eq 'friends';
225 my %res;
226 my $flags = { noauth => 1, nomod => 1 };
228 LJ::do_request(\%req, \%res, $flags);
230 die "Error posting: $res{errmsg}" unless $res{'success'} eq "OK";
231 my $jitemid = $res{itemid} or die "No itemid";
233 return LJ::Entry->new($u, jitemid => $jitemid);
236 # <LJFUNC>
237 # name: LJ::get_sent_invites
238 # des: Get a list of sent invitations from the past 30 days.
239 # args: cuserid
240 # des-cuserid: a userid or u object of the community to get sent invitations for
241 # returns: hashref of arrayrefs with keys userid, maintid, recvtime, status, args (itself
242 # a hashref of what abilities the user would be given)
243 # </LJFUNC>
244 sub get_sent_invites {
245 my $cu = shift;
246 $cu = LJ::want_user($cu);
247 return undef unless $cu;
249 if (LJ::is_enabled('new_friends_and_subscriptions')) {
250 my @invites = LJ::User::FriendInvites->list_sent_invites($cu);
252 return [ map {
253 my $temp = {};
254 LJ::decode_url_string($_->{args}, $temp);
256 userid => $_->{userid},
257 maintid => $_->{maintid},
258 recvtime => $_->{recvtime},
259 status => $_->{status},
260 args => $temp,
262 } @invites ];
265 # now hit the database for their recent invites
266 my $dbcr = LJ::get_cluster_def_reader($cu);
267 return LJ::error('db') unless $dbcr;
268 my $data = $dbcr->selectall_arrayref('SELECT userid, maintid, recvtime, status, args FROM invitesent ' .
269 'WHERE commid = ? AND recvtime > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))',
270 undef, $cu->{userid});
272 # now break data down into usable format for caller
273 my @res;
274 foreach my $row (@{$data || []}) {
275 my $temp = {};
276 LJ::decode_url_string($row->[4], $temp);
277 push @res, {
278 userid => $row->[0]+0,
279 maintid => $row->[1]+0,
280 recvtime => $row->[2],
281 status => $row->[3],
282 args => $temp,
286 # all done
287 return \@res;
290 # <LJFUNC>
291 # name: LJ::send_comm_invite
292 # des: Sends an invitation to a user to join a community with the passed abilities.
293 # args: uuserid, cuserid, muserid, attrs
294 # des-uuserid: a userid or u object of the user to invite.
295 # des-cuserid: a userid or u object of the community to invite the user to.
296 # des-muserid: a userid or u object of the maintainer doing the inviting.
297 # des-attrs: a hashref of abilities this user should have (e.g. member, post, unmoderated, ...)
298 # returns: 1 for success, undef if failure
299 # </LJFUNC>
300 sub send_comm_invite {
301 my ($u, $cu, $mu, $attrs) = @_;
303 if (LJ::is_enabled('new_friends_and_subscriptions')) {
304 return LJ::User::FriendInvites->send($u, $cu, $mu, $attrs);
307 $u = LJ::want_user($u);
308 $cu = LJ::want_user($cu);
309 $mu = LJ::want_user($mu);
310 return undef unless $u && $cu && $mu;
312 # step 1: if the user has banned the community, don't accept the invite
313 return LJ::error('comm_user_has_banned') if LJ::is_banned($cu, $u);
315 # step 2: lazily clean out old community invites.
316 return LJ::error('db') unless $u->writer;
317 $u->do('DELETE FROM inviterecv WHERE userid = ? AND ' .
318 'recvtime < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))',
319 undef, $u->{userid});
321 return LJ::error('db') unless $cu->writer;
322 $cu->do('DELETE FROM invitesent WHERE commid = ? AND ' .
323 'recvtime < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))',
324 undef, $cu->{userid});
326 my $dbcr = LJ::get_cluster_def_reader($u);
327 return LJ::error('db') unless $dbcr;
328 my $argstr = $dbcr->selectrow_array('SELECT args FROM inviterecv WHERE userid = ? AND commid = ?',
329 undef, $u->{userid}, $cu->{userid});
331 # step 4: exceeded outstanding invitation limit?
332 # should be checked when
333 # - there is no outstanding invite for this user AND
334 # - maintainer has no unlimited invites ability
335 if (!$argstr && !$LJ::UNLIMITED_INVITES_TO_COMMUNITIES{ $mu->user }) {
336 my $cdbcr = LJ::get_cluster_def_reader($cu);
337 return LJ::error('db') unless $cdbcr;
338 my $count = $cdbcr->selectrow_array("SELECT COUNT(*) FROM invitesent WHERE commid = ? " .
339 "AND userid <> ? AND status = 'outstanding'",
340 undef, $cu->{userid}, $u->{userid});
341 my $fr = LJ::get_friends($cu) || {};
342 my $max = int(scalar(keys %$fr) / 10); # can invite up to 1/10th of the community
343 $max = 50 if $max < 50; # or 50, whichever is greater
344 return LJ::error('comm_invite_limit') if $count > $max;
347 # step 5: setup arg string as url-encoded string
348 my $newargstr = join('=1&', map { LJ::eurl($_) } @$attrs) . '=1';
350 # step 6: branch here to update or insert
351 if ($argstr) {
352 # merely an update, so just do it quietly
353 $u->do("UPDATE inviterecv SET args = ? WHERE userid = ? AND commid = ?",
354 undef, $newargstr, $u->{userid}, $cu->{userid});
356 $cu->do("UPDATE invitesent SET args = ?, status = 'outstanding' WHERE userid = ? AND commid = ?",
357 undef, $newargstr, $cu->{userid}, $u->{userid});
358 } else {
359 # insert new data, as this is a new invite
360 $u->do("INSERT INTO inviterecv VALUES (?, ?, ?, UNIX_TIMESTAMP(), ?)",
361 undef, $u->{userid}, $cu->{userid}, $mu->{userid}, $newargstr);
363 $cu->do("REPLACE INTO invitesent VALUES (?, ?, ?, UNIX_TIMESTAMP(), 'outstanding', ?)",
364 undef, $cu->{userid}, $u->{userid}, $mu->{userid}, $newargstr);
367 # Fire community invite event
368 LJ::Event::CommunityInvite->new($u, $mu, $cu)->fire unless $LJ::DISABLED{esn};
370 # step 7: error check database work
371 return LJ::error('db') if $u->err || $cu->err;
373 _clear_invite_cache($cu, $u);
375 # success
376 return 1;
379 # <LJFUNC>
380 # name: LJ::accept_comm_invite
381 # des: Accepts an invitation a user has received. This does all the work to make the
382 # user join the community as well as sets up privileges.
383 # args: uuserid, cuserid
384 # des-uuserid: a userid or u object of the user to get pending invites for
385 # des-cuserid: a userid or u object of the community to reject the invitation from
386 # returns: 1 for success, undef if failure
387 # </LJFUNC>
388 sub accept_comm_invite {
389 my ($u, $cu) = @_;
391 if (LJ::is_enabled('new_friends_and_subscriptions')) {
392 return LJ::User::FriendInvites->accept($u, $cu);
395 $u = LJ::want_user($u);
396 $cu = LJ::want_user($cu);
397 return undef unless $u && $cu;
399 # get their invite to make sure they have one
400 my $dbcr = LJ::get_cluster_def_reader($u);
401 return LJ::error('db') unless $dbcr;
402 my ($argstr, $maintid) = $dbcr->selectrow_array('SELECT args, maintid FROM inviterecv WHERE userid = ? AND commid = ? ' .
403 'AND recvtime > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))',
404 undef, $u->{userid}, $cu->{userid});
405 return undef unless $argstr;
407 # decode to find out what they get
408 my $args = {};
409 LJ::decode_url_string($argstr, $args);
411 # valid invite. let's accept it as far as the community listing us goes.
412 # 1, 0 means add comm to user's friends list, but don't auto-add P edge.
413 if ($args->{'member'}) {
414 my ($code, $error) = LJ::do_join_community($u, $cu, 1, 0);
416 unless ($code) {
417 return LJ::error(
418 "Can't call LJ::join_community($u->{user}, $cu->{user}): $error"
423 # now grant necessary abilities
424 my %edgelist = (
425 post => 'P',
426 preapprove => 'N',
427 moderate => 'M',
428 admin => 'A',
430 my ($is_super, $poll) = (undef, undef);
431 my $poll_id = $cu->prop('election_poll_id');
432 if ($poll_id) {
433 $poll = LJ::Poll->new ($poll_id);
434 $is_super = $poll->prop('supermaintainer');
436 my $flag_set_owner_error = 0;
437 foreach (keys %edgelist) {
438 if ($poll && $is_super && !$poll->is_closed && $_ eq 'admin' && $args->{$_}) {
439 $flag_set_owner_error = 1;
440 } else {
441 LJ::set_rel($cu->{userid}, $u->{userid}, $edgelist{$_}) if $args->{$_};
443 $cu->clear_cache_friends($u);
445 if ( $_ eq 'admin' && $args->{$_} ) {
446 LJ::User::UserlogRecord::MaintainerAdd->create( $cu,
447 'maintid' => $u->userid,
448 'remote' => LJ::load_userid($maintid) || $u,
454 # now we can delete the invite and update the status on the other side
455 return LJ::error('db') unless $u->writer;
456 $u->do("DELETE FROM inviterecv WHERE userid = ? AND commid = ?",
457 undef, $u->{userid}, $cu->{userid});
459 return LJ::error('db') unless $cu->writer;
460 $cu->do("UPDATE invitesent SET status = 'accepted' WHERE commid = ? AND userid = ?",
461 undef, $cu->{userid}, $u->{userid});
463 if ($flag_set_owner_error) {
464 ## Save for later acceptance after the elections will be closed
465 $u->do("INSERT INTO inviterecv VALUES (?, ?, ?, UNIX_TIMESTAMP(), ?)",
466 undef, $u->{userid}, $cu->{userid}, $maintid, 'A');
467 $cu->do("REPLACE INTO invitesent VALUES (?, ?, ?, UNIX_TIMESTAMP(), 'outstanding', ?)",
468 undef, $cu->{userid}, $u->{userid}, $maintid, 'A');
469 return LJ::error("Can't set user $u->{user} as maintainer for $cu->{user}")
472 $cu->clear_cache_friends($u);
474 _clear_invite_cache($cu, $u);
476 # done
477 return 1;
480 # <LJFUNC>
481 # name: LJ::reject_comm_invite
482 # des: Rejects an invitation a user has received.
483 # args: uuserid, cuserid
484 # des-uuserid: a userid or u object of the user to get pending invites for.
485 # des-cuserid: a userid or u object of the community to reject the invitation from
486 # returns: 1 for success, undef if failure
487 # </LJFUNC>
488 sub reject_comm_invite {
489 my ($u, $cu) = @_;
491 if (LJ::is_enabled('new_friends_and_subscriptions')) {
492 return LJ::User::FriendInvites->reject($u, $cu);
495 $u = LJ::want_user($u);
496 $cu = LJ::want_user($cu);
497 return undef unless $u && $cu;
499 # get their invite to make sure they have one
500 my $dbcr = LJ::get_cluster_def_reader($u);
501 return LJ::error('db') unless $dbcr;
502 my $test = $dbcr->selectrow_array('SELECT userid FROM inviterecv WHERE userid = ? AND commid = ? ' .
503 'AND recvtime > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))',
504 undef, $u->{userid}, $cu->{userid});
505 return undef unless $test;
507 # now just reject it
508 return LJ::error('db') unless $u->writer;
509 $u->do("DELETE FROM inviterecv WHERE userid = ? AND commid = ?",
510 undef, $u->{userid}, $cu->{userid});
512 return LJ::error('db') unless $cu->writer;
513 $cu->do("UPDATE invitesent SET status = 'rejected' WHERE commid = ? AND userid = ?",
514 undef, $cu->{userid}, $u->{userid});
516 $cu->clear_cache_friends($u);
518 _clear_invite_cache($cu, $u);
520 # done
521 return 1;
524 # <LJFUNC>
525 # name: LJ::get_pending_invites
526 # des: Gets a list of pending invitations for a user to join a community.
527 # args: uuserid
528 # des-uuserid: a userid or u object of the user to get pending invites for.
529 # returns: [ [ commid, maintainerid, time, args(url encoded) ], [ ... ], ... ] or
530 # undef if failure
531 # </LJFUNC>
532 sub get_pending_invites {
533 my $u = shift;
534 $u = LJ::want_user($u);
535 return undef unless $u;
537 if (LJ::is_enabled('new_friends_and_subscriptions')) {
538 my @invites = LJ::User::FriendInvites->list_recv_invites($u);
539 return [ map { [$_->{commid}, $_->{maintid}, $_->{recvtime}, $_->{args}] } @invites ];
542 # hit up database for invites and return them
543 my $dbcr = LJ::get_cluster_def_reader($u);
544 return LJ::error('db') unless $dbcr;
545 my $pending = $dbcr->selectall_arrayref('SELECT commid, maintid, recvtime, args FROM inviterecv WHERE userid = ? ' .
546 'AND recvtime > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY))',
547 undef, $u->{userid});
548 return undef if $dbcr->err;
549 return $pending;
552 # <LJFUNC>
553 # name: LJ::revoke_invites
554 # des: Revokes a list of outstanding invitations to a community.
555 # args: cuserid, userids
556 # des-cuserid: a userid or u object of the community.
557 # des-ruserids: userids to revoke invitations from.
558 # returns: 1 if success, undef if error
559 # </LJFUNC>
560 sub revoke_invites {
561 my $cu = shift;
562 my @uids = @_;
563 $cu = LJ::want_user($cu);
564 return undef unless ($cu && @uids);
566 foreach my $uid (@uids) {
567 return undef unless int($uid) > 0;
569 my $in = join(',', @uids);
571 return LJ::error('db') unless $cu->writer;
572 $cu->do("DELETE FROM invitesent WHERE commid = ? AND " .
573 "userid IN ($in)", undef, $cu->{userid});
574 return LJ::error('db') if $cu->err;
576 my $stats = {
577 journalid => $cu->userid,
578 journalcaps => $cu->caps,
579 users => []
582 # remove from inviterecv also,
583 # otherwise invite cannot be resent for over 30 days
584 foreach my $uid (@uids) {
585 my $u = LJ::want_user($uid);
586 my $res = $u->do("DELETE FROM inviterecv WHERE userid = ? AND " .
587 "commid = ?", undef, $uid, $cu->{userid});
589 push @{$stats->{users}}, { id => $u->userid, caps => $u->caps } if $res;
591 if (LJ::is_enabled('new_friends_and_subscriptions')) {
592 my $invite = LJ::User::FriendInvites->new({ commid => $cu->{userid}, userid => $uid });
593 $invite->clear_cache;
594 } else {
595 _clear_invite_cache($cu, $u);
599 LJ::run_hooks('revoke_invite', $stats);
601 # success
602 return 1;
605 # temporary method to handle beta/prod with enabled/disabled new scheme
606 sub _clear_invite_cache {
607 my ($cu, $u) = @_;
608 my $keys = LJ::User::FriendInvites->_memkey(undef, {fromjournal => $cu, recipient => $u});
609 my @keys = values %$keys;
610 map { $_ && LJ::MemCache::delete($_) } @keys;
614 # <LJFUNC>
615 # name: LJ::leave_community
616 # des: Makes a user leave a community. Takes care of all [special[reluserdefs]] and friend stuff.
617 # args: uuserid, ucommid, defriend
618 # des-uuserid: a userid or u object of the user doing the leaving.
619 # des-ucommid: a userid or u object of the community being left.
620 # des-defriend: remove comm from user's friends list.
621 # returns: 1 if success, 0 if error, and error/message if need
622 # </LJFUNC>
623 sub leave_community {
624 my ($uid, $cid, $defriend) = @_;
625 my $u = LJ::want_user($uid);
626 my $c = LJ::want_user($cid);
628 die 'Expected parameter $u in LJ::leave_community not found' unless $u;
629 die 'Expected parameter $c in LJ::leave_community not found' unless $c;
631 unless ($c->is_community) {
632 return (0, LJ::Lang::ml('error.code.comm_not_comm'));
635 if (LJ::is_maintainer($u, $c)) {
636 if (LJ::count_maintainers($c) <= 1) {
637 return (0, LJ::Lang::ml('/community/leave.bml.label.lastmaintainer'));
641 # defriend comm -> user
642 unless ($c->remove_friend($u)) {
643 return;
646 # clear edges that effect this relationship
647 foreach my $edge (qw(P N A M)) {
648 LJ::clear_rel($c, $u, $edge);
651 # defriend user -> comm?
652 if ($defriend) {
653 $u->remove_friend($c);
654 $c->clear_cache_friends($u);
657 if (LJ::is_maintainer($u, $c)) {
658 LJ::User::UserlogRecord::MaintainerRemove->create(
659 $c, maintid => $u->id,
663 # don't care if we failed the removal of comm from user's friends list...
664 return 1;
667 sub leave_all_communities {
668 my ($u, %args) = @_;
669 my $friendsof = $u->friendsof();
671 foreach my $c (values %$friendsof) {
672 next unless $c;
673 next unless $c->is_community;
675 if (LJ::is_maintainer($u, $c)) {
676 if (LJ::count_maintainers($c) <= 1) {
677 next;
681 # defriend comm -> user
682 unless ($c->remove_friend($u, {nonotify => 1})) {
683 return;
686 # clear edges that effect this relationship
687 foreach my $edge (qw(P N A M)) {
688 LJ::clear_rel($c, $u, $edge);
691 if (LJ::is_maintainer($u, $c)) {
692 LJ::User::UserlogRecord::MaintainerRemove->create(
693 $c, maintid => $u->id,
697 $c->clear_cache_friends($u);
700 return;
703 # <LJFUNC>
704 # name: LJ::join_community
705 # des: Makes a user join a community. Takes care of all [special[reluserdefs]] and friend stuff.
706 # args: uuserid, ucommid, friend?, noauto?
707 # des-uuserid: a userid or u object of the user doing the joining
708 # des-ucommid: a userid or u object of the community being joined
709 # des-friend: 1 to add this comm to user's friends list, else not
710 # des-noauto: if defined, 1 adds P edge, 0 does not; else, base on community postlevel
711 # returns: 1 if success, undef if error of some sort (ucommid not a comm, uuserid already in
712 # comm, db error, etc)
713 # </LJFUNC>
714 sub join_community {
715 my ($uid, $cid, $friend, $canpost) = @_;
716 my $u = LJ::want_user($uid);
717 my $c = LJ::want_user($cid);
719 die 'Expected parameter $u in LJ::leave_community not found' unless $u;
720 die 'Expected parameter $c in LJ::leave_community not found' unless $c;
722 if ($c->is_banned($u)) {
723 return (0, LJ::Lang::ml('/community/join.bml.label.banned'));
726 unless ($c->is_community) {
727 return (0, LJ::Lang::ml('error.code.comm_not_comm'));
730 unless ($u->is_validated) {
731 return (0, qq|Sorry, you aren't allowed to join communities until your email address "
732 . "has been validated. If you've lost the confirmation email to do this, "
733 . "you can <a href="$LJ::SITEROOT/register.bml">have it re-sent.</a>|);
736 unless ($u->can_join_adult_comm(comm => $c)) {
737 return (0, LJ::Lang::ml(
738 '/community/join.bml.error.isminor', {
739 comm => $c->ljuser_display
744 my $row = LJ::get_community_row($c);
746 unless ($row) {
747 warn "Cant load community row [" . $c->user . "]";
750 # Check mebership
752 last unless $row;
753 last unless $row->{membership};
754 last unless $row->{membership} ne 'open';
756 # get maintainers
757 my $maintainers = LJ::load_userids(@{
758 LJ::load_rel_user($c->id, 'A') || []
761 my $maints = join(', ', map {
762 LJ::ljuser($_)
763 } values %$maintainers
766 if ($row->{membership} eq 'closed') {
767 return (0, LJ::Lang::ml(
768 '/community/join.bml.error.closed', {
769 admins => $maints
774 if ($row->{membership} eq 'moderated') {
775 # submit request
776 if (LJ::comm_join_request($u, $c)) {
777 return (1, LJ::Lang::ml('/community/join.bml.reqsubmitted.body') . $maints);
778 } else {
779 return;
783 return;
786 return LJ::do_join_community($u, $c, $friend, $canpost);
789 sub do_join_community {
790 my ($u, $c, $friend, $canpost) = @_;
791 die 'Expected parameter $u in LJ::leave_community not found' unless $u;
792 die 'Expected parameter $c in LJ::leave_community not found' unless $c;
794 my $row = LJ::get_community_row($c);
796 unless ($row) {
797 warn "Cant load community row [" . $c->user . "]";
801 my $err = '';
802 unless ($c->can_join_community(\$err, { friend => $u })) {
803 return (0, $err);
806 # friend comm -> user
807 unless ($c->add_friend($u)) {
808 return;
811 # add edges that effect this relationship... if the user sent a fourth
812 # argument, use that as a bool. else, load commrow and use the postlevel.
813 my $addpostacc = 0;
815 if (defined $canpost) {
816 $addpostacc = $canpost ? 1 : 0;
817 } elsif ($row) {
818 if ($row->{postlevel}) {
819 if ($row->{postlevel} eq 'members') {
820 $addpostacc = 1;
825 if ($addpostacc) {
826 LJ::set_rel($c->id, $u->id, 'P');
829 # friend user -> comm
830 if ($friend) {
831 if (LJ::is_enabled('new_friends_and_subscriptions')) {
832 $u->subscribe_to_user($c, nonotify => 1)
833 } else {
834 # don't do the work if they already friended the comm
835 unless ($u->has_friend($c)) {
836 my $err = '';
838 unless ($u->can_add_friends(\$err, { friend => $c })) {
839 return (1, "You have joined the community, but it has not been added to "
840 . "your Friends list. $err");
843 $u->add_friend($c);
848 # done
849 return 1;
852 # <LJFUNC>
853 # name: LJ::get_community_row
854 # des: Gets data relevant to a community such as their membership level and posting access.
855 # args: ucommid
856 # des-ucommid: a userid or u object of the community
857 # returns: a hashref with user, userid, name, membership, and postlevel data from the
858 # user and community tables; undef if error.
859 # </LJFUNC>
860 sub get_community_row {
861 my ($uid) = @_;
862 my $c = LJ::want_user($uid);
864 unless ($c) {
865 return;
868 # hit up database
869 my $cid = $c->id;
870 my $dbh = LJ::get_db_reader();
872 unless ($dbh) {
873 return;
876 my $row = LJ::MemCacheProxy::get(
877 [$uid, $COMMUNITY_ROW_CACHE_KEY . $uid]
880 unless ($row) {
881 $row = $dbh->selectrow_hashref(qq[
882 SELECT
883 membership, postlevel
884 FROM
885 community
886 WHERE
887 userid = ?
889 undef,
890 $cid
893 if ($dbh->err) {
894 return;
897 unless ($row) {
898 return;
901 LJ::MemCacheProxy::set(
902 [$uid, $COMMUNITY_ROW_CACHE_KEY . $uid], $row, 86400
906 # return result hashref
907 return {
908 %$row,
909 user => $c->user,
910 name => $c->name,
911 userid => $c->userid,
915 # <LJFUNC>
916 # name: LJ::get_community_moderation_queue
917 # des: Gets a list of hashrefs for posts that people have requested to be posted to a community
918 # but have not yet actually been approved or rejected.
919 # args: comm
920 # des-comm: a userid or u object of the community to get pending members of
921 # returns: an array of requests as it is in modlog db table
922 # </LJFUNC>
923 sub get_community_moderation_queue {
924 my $comm = shift;
925 my $c = LJ::want_user($comm);
927 my $dbcr = LJ::get_cluster_reader($c);
928 my @e; # fetched entries
929 my @entries; # entries to show
930 my @entries2del; # entries to delete
931 my $sth = $dbcr->prepare("SELECT * FROM modlog WHERE journalid=$c->{'userid'}");
932 $sth->execute;
933 while ($_ = $sth->fetchrow_hashref) {
934 push @e, $_;
937 my %users;
938 my $suspend_time = $LJ::SUSPENDED_REQUESTS_TIMEOUT || 60; # days.
939 if (@e) {
940 LJ::load_userids_multiple([ map { $_->{'posterid'}, \$users{$_->{'posterid'}} } @e ]);
941 foreach my $e (@e) {
942 next unless keys %$e;
943 my $e_poster = $users{$e->{'posterid'}};
944 if (LJ::isu($e_poster)) {
945 if ($e_poster->is_suspended()) {
946 if (time - $e_poster->statusvisdate_unix() > $suspend_time * 24 * 3600) {
947 push @entries2del, $e;
949 } else {
950 push @entries, $e;
952 } else {
953 push @entries2del, $e;
958 if (@entries2del) {
959 # Users has been suspended more then 60 days ago.
960 # Delete entries of this user(s) from modlog and modblob.
961 my $count = scalar @entries2del;
962 my $max_count = $count > 50 ? 50 : $count;
963 while($count) {
964 my $lst = join(',', map {$_->{modid}} splice(@entries2del, 0, $max_count));
965 $c->do("DELETE FROM modlog WHERE modid in ($lst)");
966 $c->do("DELETE FROM modblob WHERE modid in ($lst)");
967 $count -= $max_count;
971 return @entries;
974 # Requests
976 # <LJFUNC>
977 # name: LJ::comm_join_request
978 # des: Registers an authaction to add a user to a
979 # community and sends an approval email to the maintainers
980 # returns: Hashref; output of LJ::register_authaction()
981 # includes datecreate of old row if no new row was created
982 # args: comm, u
983 # des-comm: Community user object
984 # des-u: User object to add to community
985 # </LJFUNC>
986 sub comm_join_request {
987 my ($u, $c) = @_;
988 die 'Expected parameter $u in LJ::leave_community not found' unless $u;
989 die 'Expected parameter $c in LJ::leave_community not found' unless $c;
991 my $cid = $c->id;
992 my $uid = $u->id;
994 return unless $cid;
995 return unless $uid;
997 my $arg = "targetid=$uid";
998 my $dbh = LJ::get_db_writer();
1000 return unless $dbh;
1002 # check for duplicates within the same hour (to prevent spamming)
1003 my $oldaa = $dbh->selectrow_hashref(qq[
1004 SELECT
1005 aaid, authcode, datecreate
1006 FROM
1007 authactions
1008 WHERE
1009 userid = ?
1011 arg1 = ?
1013 action = 'comm_join_request'
1015 used = 'N'
1017 NOW() < datecreate + INTERVAL 1 HOUR
1018 ORDER BY
1020 DESC LIMIT
1023 undef,
1024 $cid,
1025 $arg
1028 if ($dbh->err) {
1029 return;
1032 return $oldaa if $oldaa;
1034 # insert authactions row
1035 my $aa = LJ::register_authaction(
1036 $cid, 'comm_join_request', $arg
1039 return unless $aa;
1041 LJ::User::FriendInvites->send($c, $u, $u);
1043 # if there are older duplicates, invalidate any existing unused authactions of this type
1044 $dbh->do(qq[
1045 UPDATE
1046 authactions
1048 used = 'Y'
1049 WHERE
1050 userid = ?
1052 aaid <> ?
1054 arg1 = ?
1056 action = 'comm_invite'
1058 used = 'N'
1060 undef,
1061 $cid,
1062 $aa->{aaid},
1063 $arg
1066 if ($dbh->err) {
1067 return;
1070 # get maintainers of community
1071 my $admins = $c->maintainers();
1073 # now prepare the emails
1074 foreach my $au (values %$admins) {
1075 next unless $au && !$au->is_expunged;
1077 # unless it's a hyphen, we need to migrate
1078 my $prop = $au->prop("opt_communityjoinemail");
1080 if ($prop && $prop ne "-") {
1081 if ($prop ne "N") {
1082 my %params = (
1083 event => 'CommunityJoinRequest',
1084 journal => $au
1087 unless ($au->has_subscription(%params)) {
1088 foreach (qw(Inbox Email)) {
1089 $au->subscribe(%params, method => $_);
1094 $au->set_prop("opt_communityjoinemail", "-");
1097 LJ::Event::CommunityJoinRequest->new($au, $u, $c)->fire;
1100 LJ::MemCacheProxy::delete([$cid, "community:request:$cid:$uid"]);
1102 return $aa;
1105 # <LJFUNC>
1106 # name: LJ::get_pending_members
1107 # des: Gets a list of userids for people that have requested to be added to a community
1108 # but have not yet actually been approved or rejected.
1109 # args: comm
1110 # des-comm: a userid or u object of the community to get pending members of
1111 # returns: an arrayref of userids of people with pending membership requests
1112 # </LJFUNC>
1113 sub get_pending_members {
1114 my $comm = shift;
1115 my $cu = LJ::want_user($comm);
1117 # database request
1118 my $dbr = LJ::get_db_reader();
1120 my $sth = $dbr->prepare('SELECT aaid, arg1 FROM authactions' .
1121 ' WHERE userid = ' . $cu->{userid} .
1122 " AND action = 'comm_join_request' AND used = 'N'");
1123 # parse out the args
1124 my @list;
1125 my @delete;
1126 $sth->execute;
1127 my $suspend_time = $LJ::SUSPENDED_REQUESTS_TIMEOUT || 60; # days.
1128 while (my $row = $sth->fetchrow_hashref) {
1129 if ($row->{arg1} =~ /^targetid=(\d+)$/) {
1130 my ($uid, $u) = ($1, LJ::want_user($1));
1131 if (LJ::isu($u)) {
1132 if ($u->is_suspended()) {
1133 if (time - $u->statusvisdate_unix() > $suspend_time * 24 * 3600) {
1134 push @delete, $row->{aaid};
1136 } else {
1137 push @list, $uid;
1139 } else {
1140 push @delete, $row->{aaid};
1145 if (@delete) {
1146 my $count = scalar @delete;
1147 my $max_count = $count > 50 ? 50 : $count;
1148 while($count) {
1149 my $lst = join(',', splice(@delete, 0, $max_count));
1150 my $dbh = LJ::get_db_writer();
1151 $dbh->do("DELETE FROM authactions WHERE aaid in ($lst)");
1152 $count -= $max_count;
1156 return \@list;
1159 # <LJFUNC>
1160 # name: LJ::approve_pending_member
1161 # des: Approves someone's request to join a community. This updates the [dbtable[authactions]] table
1162 # as appropriate as well as does the regular join logic. This also generates an e-mail to
1163 # be sent to the user notifying them of the acceptance.
1164 # args: commid, userid
1165 # des-commid: userid of the community
1166 # des-userid: userid of the user doing the join
1167 # returns: 1 on success, 0/undef on error
1168 # </LJFUNC>
1169 sub approve_pending_member {
1170 my ($cid, $uid) = @_;
1171 my $c = LJ::want_user($cid);
1172 my $u = LJ::want_user($uid);
1174 return unless $c;
1175 return unless $u;
1177 my $arg = "targetid=$uid";
1178 my $dbh = LJ::get_db_writer();
1180 return unless $dbh;
1182 my $cnt = $dbh->do(qq[
1183 UPDATE
1184 authactions
1186 used = 'Y'
1187 WHERE
1188 userid = ?
1190 arg1 = ?
1192 undef,
1193 $cid,
1194 $arg
1197 if ($dbh->err) {
1198 return;
1201 return unless $cnt;
1203 LJ::User::FriendInvites->accept($c, $u);
1205 LJ::run_hooks('approve_member',{
1206 users => [{
1207 id => $uid,
1208 caps => $u->caps
1210 journalid => $cid,
1211 journalcaps => $c->caps,
1214 LJ::MemCacheProxy::delete([$cid, "community:request:$cid:$uid"]);
1216 my ($code, $error) = LJ::do_join_community($u, $c, 1);
1218 return unless $code;
1220 my %params = (event => 'CommunityJoinApprove', journal => $u);
1222 unless ($u->has_subscription(%params)) {
1223 $u->subscribe(%params, method => 'Email');
1226 unless ($LJ::DISABLED{esn}) {
1227 LJ::Event::CommunityJoinApprove->new($u, $c)->fire;
1230 return 1;
1233 # <LJFUNC>
1234 # name: LJ::reject_pending_member
1235 # des: Rejects someone's request to join a community.
1236 # Updates [dbtable[authactions]] and generates an e-mail to the user.
1237 # args: commid, userid
1238 # des-commid: userid of the community
1239 # des-userid: userid of the user doing the join
1240 # returns: 1 on success, 0/undef on error
1241 # </LJFUNC>
1242 ## LJ::reject_pending_member($cid, $id, $remote->{userid}, $POST{'reason'});
1243 sub reject_pending_member {
1244 my ($cid, $uid, $mid, $reason) = @_;
1245 my $c = LJ::want_user($cid);
1246 my $u = LJ::want_user($uid);
1247 my $m = LJ::want_user($mid);
1249 return unless $c;
1250 return unless $u;
1251 return unless $m;
1253 if ($reason eq '0') {
1254 $reason = LJ::Lang::ml('/community/pending.bml.reason.default.text');
1257 # step 1, update authactions table
1258 my $arg = "targetid=$uid";
1259 my $dbh = LJ::get_db_writer();
1261 return unless $dbh;
1263 my $cnt = $dbh->do(qq[
1264 UPDATE
1265 authactions
1267 used = 'Y'
1268 WHERE
1269 userid = ?
1271 arg1 = ?
1273 undef,
1274 $cid, $arg
1277 if ($dbh->err) {
1278 return;
1281 return unless $cnt;
1283 LJ::User::FriendInvites->reject($c, $u);
1285 LJ::run_hooks('reject_member', {
1286 users => [{
1287 id => $uid,
1288 caps => $u->caps
1290 journalid => $cid,
1291 journalcaps => $c->caps,
1294 LJ::MemCacheProxy::delete([$cid, "community:request:$cid:$uid"]);
1296 # step 2, email the user
1297 my %params = (event => 'CommunityJoinReject', journal => $c);
1299 unless ($u->has_subscription(%params)) {
1300 $u->subscribe(%params, method => 'Email');
1303 # Email to user about rejecting
1304 unless ($LJ::DISABLED{esn}) {
1305 LJ::Event::CommunityJoinReject->new($c, $u, undef, undef, $reason)->fire;
1308 # Email to maints about user rejecting
1309 my $maintainers = $c->maintainers();
1311 foreach my $mu (values %$maintainers) {
1312 next if $mu->id == $mid;
1314 my %params = (event => 'CommunityJoinReject', journal => $c);
1316 if ($mu && $mu->has_subscription(%params)) {
1317 unless ($LJ::DISABLED{esn}) {
1318 LJ::Event::CommunityJoinReject->new($c, $mu, $m, $u, $reason)->fire;
1323 return 1;
1326 sub is_request_sent {
1327 my ($c, $u) = @_;
1329 return unless $c;
1330 return unless $u;
1332 my $cid = $c->id;
1333 my $uid = $u->id;
1335 return unless $cid;
1336 return unless $uid;
1338 my $key = [$cid, "community:request:$cid:$uid"];
1339 my $val = LJ::MemCacheProxy::get([$cid, "community:request:$cid:$uid"]);
1341 if (defined $val) {
1342 return $val;
1345 my $dbh = LJ::get_db_writer();
1346 my $arg = "targetid=$uid";
1348 return unless $dbh;
1350 my $row = $dbh->selectrow_hashref(qq[
1351 SELECT
1353 FROM
1354 authactions
1355 WHERE
1356 userid = ?
1358 arg1 = ?
1360 action = 'comm_join_request'
1362 used = 'N'
1364 undef,
1365 $cid,
1366 $arg
1369 if ($dbh->err) {
1370 return;
1373 LJ::MemCacheProxy::set([$cid, "community:request:$cid:$uid"], $row, 86400);
1375 return $row;
1378 sub maintainer_linkbar {
1379 my $comm = shift;
1380 my $page = shift;
1382 unless ($page) {
1383 $page = '';
1386 my $username = $comm->user;
1387 my @links;
1389 my %manage_link_info = LJ::run_hook('community_manage_link_info', $username);
1390 if (keys %manage_link_info) {
1391 push @links, $page eq "account" ?
1392 "<strong>$manage_link_info{text}</strong>" :
1393 "<a href='$manage_link_info{url}'>$manage_link_info{text}</a>";
1396 push @links, (
1397 $page eq "profile" ?
1398 "<strong>" . LJ::Lang::ml('/community/manage.bml.commlist.actinfo2') . "</strong>" :
1399 "<a href='$LJ::SITEROOT/manage/profile/?authas=$username'>" . LJ::Lang::ml('/community/manage.bml.commlist.actinfo2') . "</a>",
1400 $page eq "customize" ?
1401 "<strong>" . LJ::Lang::ml('/community/manage.bml.commlist.customize2') . "</strong>" :
1402 "<a href='$LJ::SITEROOT/customize/?authas=$username'>" . LJ::Lang::ml('/community/manage.bml.commlist.customize2') . "</a>",
1403 $page eq "settings" ?
1404 "<strong>" . LJ::Lang::ml('/community/manage.bml.commlist.actsettings2') . "</strong>" :
1405 "<a href='$LJ::SITEROOT/community/settings.bml?authas=$username'>" . LJ::Lang::ml('/community/manage.bml.commlist.actsettings2') . "</a>",
1406 $page eq "invites" ?
1407 "<strong>" . LJ::Lang::ml('/community/manage.bml.commlist.actinvites') . "</strong>" :
1408 "<a href='$LJ::SITEROOT/community/sentinvites.bml?authas=$username'>" . LJ::Lang::ml('/community/manage.bml.commlist.actinvites') . "</a>",
1409 $page eq "members" ?
1410 "<strong>" . LJ::Lang::ml('/community/manage.bml.commlist.actmembers2') . "</strong>" :
1411 "<a href='$LJ::SITEROOT/community/members.bml?authas=$username'>" . LJ::Lang::ml('/community/manage.bml.commlist.actmembers2') . "</a>",
1414 if (LJ::SUP->is_sup_enabled($comm) && LJ::is_enabled('wishlist_v2')) {
1415 push @links, $page eq "wishlist" ?
1416 "<strong>" . LJ::Lang::ml('/community/manage.bml.commlist.wishlist') . "</strong>" :
1417 "<a href='".$comm->wishlist_url."'>" . LJ::Lang::ml('/community/manage.bml.commlist.wishlist') . "</a>";
1420 push @links, $page eq 'massmailing'
1421 ? '<strong>' . LJ::Lang::ml('/community/manage.bml.commlist.massmailing') . '</strong>'
1422 : "<a href='$LJ::SITEROOT/community/mailing.bml?authas=$username'>" . LJ::Lang::ml('/community/manage.bml.commlist.massmailing') . "</a>";
1424 if (LJ::is_enabled('lj_art') && ($comm->prop('ljart_event') || $comm->prop('ljart_institut'))) {
1425 push @links, $page eq "ljart" ?
1426 "<strong>" . LJ::Lang::ml('/community/manage.bml.commlist.ljart') . "</strong>" :
1427 "<a href='$LJ::SITEROOT/community/ljart.bml?authas=$username'>" . LJ::Lang::ml('/community/manage.bml.commlist.ljart') . "</a>",
1430 my $ret .= "<strong>" . LJ::Lang::ml('/community/manage.bml.managelinks', { user => $comm->ljuser_display }) . "</strong> ";
1431 $ret .= join(" | ", @links);
1433 return "<p style='margin-bottom: 20px;'>$ret</p>";
1436 # Get membership and posting level settings for a community
1437 sub get_comm_settings {
1438 my $c = shift;
1440 my $cid = $c->{userid};
1441 my ($membership, $postlevel);
1442 my $memkey = [ $cid, "commsettings:$cid" ];
1444 my $memval = LJ::MemCache::get($memkey);
1445 ($membership, $postlevel) = @$memval if ($memval);
1446 return ($membership, $postlevel)
1447 if ( $membership && $postlevel );
1449 my $dbr = LJ::get_db_reader();
1450 ($membership, $postlevel) =
1451 $dbr->selectrow_array("SELECT membership, postlevel FROM community WHERE userid=?", undef, $cid);
1453 LJ::MemCache::set($memkey, [$membership,$postlevel] ) if ( $membership && $postlevel );
1455 return ($membership, $postlevel);
1458 # Set membership and posting level settings for a community
1459 sub set_comm_settings {
1460 my ($c, $u, $opts) = @_;
1462 die "User cannot modify this community"
1463 unless (LJ::can_manage_other($u, $c));
1465 die "Membership and posting levels are not available"
1466 unless ($opts->{membership} && $opts->{postlevel});
1468 my $cid = $c->{userid};
1470 my $dbh = LJ::get_db_writer();
1471 $dbh->do("REPLACE INTO community (userid, membership, postlevel) VALUES (?,?,?)" , undef, $cid, $opts->{membership}, $opts->{postlevel});
1473 my $memkey = [ $cid, "commsettings:$cid" ];
1474 LJ::MemCache::delete($memkey);
1476 return;
1479 sub is_maintainer {
1480 my ($u, $c) = @_;
1481 die 'Expected parameter $u in LJ::is_maintainer' unless $u;
1482 die 'Expected parameter $c in LJ::is_maintainer' unless $c;
1484 return LJ::RelationService->is_relation_to($c->id, $u->id, 'A');
1487 sub count_maintainers {
1488 my ($c) = @_;
1489 my $ids = LJ::load_rel_user($c->id, 'A');
1491 $ids ||= [];
1493 return scalar @$ids;
1497 sub set_community_user_edge {
1498 my ($c, $u, $edge, $remote) = @_;
1500 unless ($edge =~ /^[A-Z]$/) {
1501 $edge = {
1502 post => 'P',
1503 preapprove => 'N',
1504 moderate => 'M',
1505 admin => 'A',
1506 }->{$edge};
1509 if ($edge eq 'A') {
1510 my ( $is_super, $poll ) = ( undef, undef );
1511 my $poll_id = $c->prop('election_poll_id');
1512 if ($poll_id) {
1513 $poll = LJ::Poll->new($poll_id);
1514 $is_super = $poll->prop('supermaintainer');
1517 if ( $poll && $is_super && !$poll->is_closed ) {
1518 return LJ::error("Can't set user $u->{user} as maintainer for $c->{user}");
1521 LJ::User::UserlogRecord::MaintainerAdd->create(
1523 'maintid' => $u->userid,
1524 'remote' => $remote || $u,
1528 LJ::set_rel( $c->userid, $u->userid, $edge );