505 lines
11 KiB
Perl
505 lines
11 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Afuna <coder.dw@afunamatata.com>
|
||
|
|
#
|
||
|
|
# Copyright (c) 2013 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::Entry::Moderated;
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
|
||
|
|
=head1 NAME
|
||
|
|
|
||
|
|
DW::Entry::Moderated - Entry in the moderation queue
|
||
|
|
|
||
|
|
=head1 SYNOPSIS
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub new {
|
||
|
|
my ( $class, $cu, $modid ) = @_;
|
||
|
|
|
||
|
|
# use dbcm to read to minimize collisions between moderators due to replication lag
|
||
|
|
my $dbcm = LJ::get_cluster_master($cu);
|
||
|
|
|
||
|
|
my ( $posterid, $frozen ) = $dbcm->selectrow_array(
|
||
|
|
"SELECT l.posterid, b.request_stor FROM modlog l, modblob b "
|
||
|
|
. "WHERE l.journalid=? AND l.modid=? AND l.journalid=b.journalid AND l.modid=b.modid",
|
||
|
|
undef, $cu->userid, $modid
|
||
|
|
);
|
||
|
|
|
||
|
|
# not in modlog
|
||
|
|
return unless $posterid;
|
||
|
|
|
||
|
|
# there's no entry. maybe there was a modlog row, but not a modblob row
|
||
|
|
# for whatever reason. let's lazy-clean. don't care if it returns a value
|
||
|
|
# or not, because they may have legitimately just raced another moderator
|
||
|
|
unless ($frozen) {
|
||
|
|
my $sth = $dbcm->prepare("DELETE FROM modlog WHERE journalid=? AND modid=?");
|
||
|
|
$sth->execute( $cu->userid, $modid );
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $raw_data = Storable::thaw($frozen);
|
||
|
|
my $self = bless {
|
||
|
|
_raw_data => $raw_data,
|
||
|
|
|
||
|
|
id => $modid,
|
||
|
|
journal => $cu,
|
||
|
|
poster => LJ::load_userid($posterid),
|
||
|
|
};
|
||
|
|
|
||
|
|
return $self;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->id >>
|
||
|
|
|
||
|
|
Returns the modid.
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub id {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
return $self->{id};
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->event >>
|
||
|
|
|
||
|
|
Returns the event text cleaned, with anything that should be expanded also processed (e.g., polls)
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub event {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
my $raw_data = $self->{_raw_data};
|
||
|
|
|
||
|
|
# cleaning
|
||
|
|
my $event = $raw_data->{event};
|
||
|
|
$event =~ s/^\s+//;
|
||
|
|
$event =~ s/\s+$//;
|
||
|
|
|
||
|
|
if ( ( $raw_data->{lineendings} || "" ) eq "mac" ) {
|
||
|
|
$event =~ s/\r/\n/g;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
$event =~ s/\r//g;
|
||
|
|
}
|
||
|
|
|
||
|
|
my $error;
|
||
|
|
my @polls = LJ::Poll->new_from_html(
|
||
|
|
\$event,
|
||
|
|
\$error,
|
||
|
|
{
|
||
|
|
journalid => $self->journal->userid,
|
||
|
|
posterid => $self->poster->userid,
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
my $poll_preview = sub {
|
||
|
|
my $poll = shift @polls;
|
||
|
|
return '' unless $poll;
|
||
|
|
return $poll->preview;
|
||
|
|
};
|
||
|
|
|
||
|
|
$event =~ s/<poll-placeholder>/$poll_preview->()/eg;
|
||
|
|
LJ::CleanHTML::clean_event(
|
||
|
|
\$event,
|
||
|
|
{
|
||
|
|
preformatted => $raw_data->{props}->{opt_preformatted},
|
||
|
|
editor => $raw_data->{props}->{editor},
|
||
|
|
cutpreview => 1,
|
||
|
|
cuturl => '#',
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
# create iframe from <lj-embed> tag
|
||
|
|
LJ::EmbedModule->expand_entry( $self->journal, \$event );
|
||
|
|
|
||
|
|
return $event;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->subject >>
|
||
|
|
|
||
|
|
Returns the cleaned subject for this moderated entry.
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub subject {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
my $subject = $self->{_raw_data}->{subject};
|
||
|
|
LJ::CleanHTML::clean_subject( \$subject );
|
||
|
|
return $subject;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->time >>
|
||
|
|
|
||
|
|
Returns the date / time, already formatted
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub time {
|
||
|
|
my ( $self, %opts ) = @_;
|
||
|
|
|
||
|
|
my $raw_data = $self->{_raw_data};
|
||
|
|
my $datestr =
|
||
|
|
sprintf( "%04d-%02d-%02d", $raw_data->{year}, $raw_data->{mon}, $raw_data->{day} );
|
||
|
|
my $etime = sprintf( "%s %02d:%02d",
|
||
|
|
$opts{linkify} ? LJ::date_to_view_links( $self->journal, $datestr ) : $datestr,
|
||
|
|
$raw_data->{hour}, $raw_data->{min} );
|
||
|
|
return $etime;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->journal >>
|
||
|
|
|
||
|
|
The journal this entry was posted in. LJ::User object.
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub journal {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
return $self->{journal};
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->poster >>
|
||
|
|
|
||
|
|
The poster of this entry. LJ::User object.
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub poster {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
return $self->{poster};
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->props >>
|
||
|
|
|
||
|
|
Returns all props
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub props {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
return $self->{_raw_data}->{props};
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->icon >>
|
||
|
|
|
||
|
|
Returns the icon used for this entry
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub icon {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
my $kw = $self->{props}->{picture_keyword};
|
||
|
|
my $icon = LJ::Userpic->new_from_keyword( $self->poster, $kw );
|
||
|
|
|
||
|
|
return $icon;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->currents_html >>
|
||
|
|
|
||
|
|
Return HTML for the currents (should see if we can move this view-related code out of here)
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub currents_html {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
my $props = $self->props;
|
||
|
|
my %current = LJ::currents( $props, $self->poster );
|
||
|
|
|
||
|
|
$current{Tags} = join( ", ", sort split( /\s*,\s*/, $props->{taglist} ) )
|
||
|
|
if $props->{taglist};
|
||
|
|
|
||
|
|
return LJ::currents_table(%current);
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->security_html >>
|
||
|
|
|
||
|
|
Return HTML for the security icon (should see if we can move this view-related code out of here)
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub security_html {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
my $security = $self->{_raw_data}->{security};
|
||
|
|
|
||
|
|
return LJ::img("security-private") if $security eq "private";
|
||
|
|
return LJ::img("security-protected") if $security eq "usemask";
|
||
|
|
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->age_restriction_html >>
|
||
|
|
|
||
|
|
Returns HTML for age restrictions icon (should see if we can move this view-related code out of here)
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub age_restriction_html {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
my $age_restriction = $self->props->{adult_content};
|
||
|
|
|
||
|
|
return LJ::img("adult-18") if $age_restriction eq "explicit";
|
||
|
|
return LJ::img("adult-nsfw") if $age_restriction eq "concepts";
|
||
|
|
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->age_restriction_reason >>
|
||
|
|
|
||
|
|
Returns the reason provided for the age restriction status
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub age_restriction_reason {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
return $self->props->{adult_content_reason};
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->authcode >>
|
||
|
|
|
||
|
|
Return the authcode for this moderated entry
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub auth {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
return $self->{_raw_data}->{_moderate}->{authcode};
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->request_data >>
|
||
|
|
|
||
|
|
Hash of the entry text / metadata. Can be used with postevent
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub request_data {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
my %req = %{ $self->{_raw_data} };
|
||
|
|
my $poster = $self->poster;
|
||
|
|
|
||
|
|
# in case the user renamed while the submission was in the queue
|
||
|
|
# we need to fix up the username based on the userid we stored
|
||
|
|
$req{user} = $poster->user;
|
||
|
|
$req{username} = $poster->user;
|
||
|
|
|
||
|
|
return %req;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->approve >>
|
||
|
|
|
||
|
|
Approves the moderated entry and posts it to the community.
|
||
|
|
Returns ( 1, $entry_url ) on success, ( 0, $error ) on failure.
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub approve {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
my $req = { $self->request_data };
|
||
|
|
|
||
|
|
# allow all system logprops
|
||
|
|
# we've already made sure that the original user didn't provide any system ones
|
||
|
|
my $protocol_error;
|
||
|
|
my $res = LJ::Protocol::do_request(
|
||
|
|
'postevent',
|
||
|
|
$req,
|
||
|
|
\$protocol_error,
|
||
|
|
{
|
||
|
|
nomod => 1,
|
||
|
|
noauth => 1,
|
||
|
|
allow_system => 1,
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
if ($res) {
|
||
|
|
$self->delete_from_queue;
|
||
|
|
my $entry =
|
||
|
|
LJ::Entry->new( $self->journal, jitemid => $res->{itemid}, anum => $res->{anum} );
|
||
|
|
return ( 1, $entry->url );
|
||
|
|
}
|
||
|
|
|
||
|
|
my $error = "";
|
||
|
|
$error = LJ::Protocol::error_message($protocol_error) if $protocol_error;
|
||
|
|
return ( 0, $error );
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->reject >>
|
||
|
|
|
||
|
|
Reject the moderated entry. Returns 1 on success, 0 on failure.
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub reject {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
$self->delete_from_queue;
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->reject_as_spam >>
|
||
|
|
|
||
|
|
Reject the moderated entry as spam. Returns 1 on success, 0 on failure.
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub reject_as_spam {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
my $did_reject = LJ::reject_entry_as_spam( $self->journal->userid, $self->id ) ? 1 : 0;
|
||
|
|
$self->delete_from_queue if $did_reject;
|
||
|
|
|
||
|
|
return $did_reject;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->notify_poster( $msg_ml ) >>
|
||
|
|
|
||
|
|
Sends an email to the poster of the moderated entry. Returns 1 if it was sent.
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub notify_poster {
|
||
|
|
my ( $self, $status, %opts ) = @_;
|
||
|
|
|
||
|
|
my $poster = $self->poster;
|
||
|
|
return unless $poster->is_validated && $poster->is_visible;
|
||
|
|
|
||
|
|
my $journal = $self->journal;
|
||
|
|
|
||
|
|
my $props = $self->props;
|
||
|
|
my $raw_data = $self->{_raw_data};
|
||
|
|
my $ml_scope = "/communities/queue/entries/edit.tt";
|
||
|
|
|
||
|
|
my @metadata;
|
||
|
|
push @metadata,
|
||
|
|
LJ::Lang::ml( "$ml_scope.email.submission.music", { music => $props->{current_music} } )
|
||
|
|
if $props->{current_music};
|
||
|
|
push @metadata,
|
||
|
|
LJ::Lang::ml( "$ml_scope.email.submission.mood", { mood => $props->{current_mood} } )
|
||
|
|
if $props->{current_mood};
|
||
|
|
push @metadata,
|
||
|
|
LJ::Lang::ml( "$ml_scope.email.submission.icon", { icon => $props->{picture_keyword} } )
|
||
|
|
if $props->{picture_keyword};
|
||
|
|
|
||
|
|
my $subject =
|
||
|
|
LJ::Lang::ml( "$ml_scope.email.submission.subject", { subject => $raw_data->{subject} } );
|
||
|
|
my $time = LJ::Lang::ml( "$ml_scope.email.submission.time", { time => $self->time } );
|
||
|
|
|
||
|
|
# this is all ugly because we only conditionally include metadata if it's available. So we can't just do this as one string
|
||
|
|
# the trailing spaces are unfortunately necessary to introduce line breaks...
|
||
|
|
my $submission_text = LJ::Lang::ml(
|
||
|
|
"$ml_scope.email.submission",
|
||
|
|
{
|
||
|
|
time => "> $time ",
|
||
|
|
subject => "> $subject ",
|
||
|
|
metadata => join( "\n", map { "> $_ " } @metadata ),
|
||
|
|
text => LJ::markdown_blockquote( $raw_data->{event} ),
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
my $email_body = "";
|
||
|
|
if ( $status eq 'approved' ) {
|
||
|
|
$email_body = LJ::Lang::ml(
|
||
|
|
"$ml_scope.email.body.approved",
|
||
|
|
{
|
||
|
|
comm => "@" . $journal->user,
|
||
|
|
entry_url => $opts{entry_url},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
$email_body .= "\n\n"
|
||
|
|
. LJ::Lang::ml(
|
||
|
|
"$ml_scope.email.body.approved.message",
|
||
|
|
{
|
||
|
|
message => $opts{message}
|
||
|
|
}
|
||
|
|
) if $opts{message};
|
||
|
|
|
||
|
|
}
|
||
|
|
elsif ( $status eq 'error' ) {
|
||
|
|
$email_body = LJ::Lang::ml(
|
||
|
|
"$ml_scope.email.body.error",
|
||
|
|
{
|
||
|
|
comm => "@" . $journal->user,
|
||
|
|
error => $opts{error},
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
elsif ( $status eq 'rejected' ) {
|
||
|
|
$email_body =
|
||
|
|
$opts{message}
|
||
|
|
? LJ::Lang::ml(
|
||
|
|
"$ml_scope.email.body.rejected_with_message",
|
||
|
|
{
|
||
|
|
comm => "@" . $journal->user,
|
||
|
|
message => $opts{message}
|
||
|
|
}
|
||
|
|
)
|
||
|
|
: LJ::Lang::ml(
|
||
|
|
"$ml_scope.email.body.rejected",
|
||
|
|
{
|
||
|
|
comm => "@" . $journal->user,
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
$email_body .= "\n\n" . $submission_text;
|
||
|
|
|
||
|
|
LJ::send_formatted_mail(
|
||
|
|
to => $poster->email_raw,
|
||
|
|
greeting_user => $poster->user,
|
||
|
|
|
||
|
|
from => $LJ::BOGUS_EMAIL,
|
||
|
|
fromname => qq{"$LJ::SITENAME"},
|
||
|
|
|
||
|
|
subject => LJ::Lang::ml("$ml_scope.email.subject"),
|
||
|
|
body => $email_body,
|
||
|
|
charset => 'utf-8',
|
||
|
|
);
|
||
|
|
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
=head2 C<< $self->delete_from_queue >>
|
||
|
|
|
||
|
|
Delete from the moderation queue. It's been handled now
|
||
|
|
|
||
|
|
=cut
|
||
|
|
|
||
|
|
sub delete_from_queue {
|
||
|
|
my ($self) = @_;
|
||
|
|
|
||
|
|
my $modid = $self->id;
|
||
|
|
my $journal = $self->journal;
|
||
|
|
|
||
|
|
# Delete this moderated entry from the list
|
||
|
|
$journal->do( "DELETE FROM modlog WHERE journalid=? AND modid=?",
|
||
|
|
undef, $journal->userid, $modid );
|
||
|
|
$journal->do( "DELETE FROM modblob WHERE journalid=? AND modid=?",
|
||
|
|
undef, $journal->userid, $modid );
|
||
|
|
|
||
|
|
# expire mod_queue_count memcache
|
||
|
|
$journal->memc_delete('mqcount');
|
||
|
|
}
|
||
|
|
|
||
|
|
1;
|