package App::SD::Replica::lighthouse::PullEncoder;
use Any::Moose;
extends 'App::SD::ForeignReplica::PullEncoder';
use Params::Validate qw(:all);
use Memoize;
use Time::Progress;
use DateTime;
use Net::Lighthouse::User;
has sync_source => (
isa => 'App::SD::Replica::lighthouse',
is => 'rw',
);
my %PROP_MAP = %App::SD::Replica::lighthouse::PROP_MAP;
sub ticket_id {
my $self = shift;
return shift->number;
}
=head2 translate_ticket_state
=cut
sub translate_ticket_state {
my $self = shift;
my $ticket = shift;
return $ticket;
}
=head2 find_matching_tickets QUERY
Returns a array of all tickets found matching your QUERY hash.
=cut
sub find_matching_tickets {
my $self = shift;
my %args = (@_);
my $last_changeset_seen_dt = $self->_only_pull_tickets_modified_after()
|| DateTime->from_epoch( epoch => 0 );
my @tickets =
$self->sync_source->lighthouse->tickets( query => $args{query} );
my @updated = map { $_->load( $_->number ); $_ }
grep { $_->{updated_at} ge $last_changeset_seen_dt } @tickets;
return \@updated;
}
sub _only_pull_tickets_modified_after {
my $self = shift;
my $last_pull = $self->sync_source->upstream_last_modified_date();
return unless $last_pull;
my $before = App::SD::Util::string_to_datetime($last_pull);
$self->log_debug( "Failed to parse '" . $self->sync_source->upstream_last_modified_date() . "' as a timestamp. That means we have to sync ALL history") unless ($before);
return $before;
}
=head2 find_matching_transactions { ticket => $id, starting_transaction => $num }
Returns a reference to an array of all transactions (as hashes) on ticket $id after transaction $num.
=cut
sub find_matching_transactions {
my $self = shift;
my %args = validate( @_, { ticket => 1, starting_transaction => 1 } );
my $sequence = 0;
# hack, let's add sequence for comments
my @raw_versions =
map { $_->{sequence} = $sequence++; $_ } $args{ticket}->versions;
my @raw_attachments = $args{ticket}->attachments;
my @raw_txns = ( @raw_versions, @raw_attachments );
my @txns;
for my $txn ( @raw_txns ) {
my $txn_date = $txn->created_at->epoch;
# Skip things we know we've already pulled
next if $txn_date < ( $args{'starting_transaction'} || 0 );
# Skip things we've pushed
next
if (
$self->sync_source->foreign_transaction_originated_locally(
( defined $txn->{sequence} ? $txn->{sequence} : $txn->id ),
$args{'ticket'}->number
)
);
push @txns,
{
timestamp => $txn->created_at,
object => $txn,
serial => defined $txn->{sequence} ? $txn->{sequence} : $txn->id,
};
}
$self->sync_source->log_debug('Done looking at pulled txns');
return \@txns;
}
sub transcode_create_txn {
my $self = shift;
my $txn = shift;
my $ticket = $txn->{object};
my $ticket_uuid =
$self->sync_source->uuid_for_remote_id($ticket->number);
my $creator = $self->resolve_user_id_to( undef, $ticket->creator_name );
my $created = $ticket->created_at;
my $changeset = Prophet::ChangeSet->new(
{
original_source_uuid => $ticket_uuid,
original_sequence_no => $ticket->{sequence},
creator => $creator,
created => $created->ymd . " " . $created->hms
}
);
my $change = Prophet::Change->new(
{
record_type => 'ticket',
record_uuid => $ticket_uuid,
change_type => 'add_file',
}
);
for my $prop (qw/title body state /) {
next unless $ticket->$prop;
$change->add_prop_change(
name => $PROP_MAP{$prop} || $prop,
new => $ticket->$prop,
);
}
if ( $ticket->assigned_user_id ) {
$change->add_prop_change(
name => 'owner',
new => $ticket->assigned_user_name . '('
. $ticket->assigned_user_id . ')',
);
}
if ( $ticket->milestone_id ) {
$change->add_prop_change(
name => 'milestone',
new => $ticket->milestone_title,
);
}
$change->add_prop_change(
name => $self->sync_source->uuid . '-id',
new => $ticket->number,
);
$changeset->add_change( { change => $change } );
return $changeset;
}
# we might get return:
# 0 changesets if it was a null txn
# 1 changeset if it was a normal txn
# 2 changesets if we needed to to some magic fixups.
sub transcode_one_txn {
my $self = shift;
my $txn_wrapper = shift;
my $ticket = shift;
my $txn = $txn_wrapper->{object};
if ( defined $txn->{sequence} && $txn->{sequence} == 0 ) {
return $self->transcode_create_txn($txn_wrapper);
}
my $ticket_uuid =
$self->sync_source->uuid_for_remote_id( $ticket->number );
my $changeset;
if ( $txn->can('filename') ) {
$changeset = Prophet::ChangeSet->new(
{
original_source_uuid => $ticket_uuid,
original_sequence_no => $txn->id,
creator =>
$self->resolve_user_id_to( undef, $txn->uploader_id ),
created => $txn->created_at->ymd . " " . $txn->created_at->hms
}
);
# it's an attachment
$self->_recode_attachment_create(
ticket_uuid => $ticket_uuid,
changeset => $changeset,
attachment => $txn,
);
}
else {
$changeset = Prophet::ChangeSet->new(
{
original_source_uuid => $ticket_uuid,
original_sequence_no => $txn->{sequence},
creator =>
$self->resolve_user_id_to( undef, $txn->creator_name ),
created => $txn->created_at->ymd . " " . $txn->created_at->hms
}
);
my $change = Prophet::Change->new(
{
record_type => 'ticket',
record_uuid => $ticket_uuid,
change_type => 'update_file'
}
);
my $diffable_attrs = $txn->diffable_attributes;
if ( keys %$diffable_attrs ) {
my %hash = (
':tag' => 'tag',
':milestone' => 'milestone_id',
':assigned_user' => 'assigned_user_id',
':state' => 'state',
);
for my $attr ( keys %$diffable_attrs ) {
next unless $hash{$attr};
my $method = $hash{$attr};
if ( $attr eq ':milestone' ) {
my $old = $diffable_attrs->{$attr};
my $old_title;
if ($old) {
# find milestone title
my $milestone =
$self->sync_source->lighthouse->milestone;
$milestone->load($old);
$old_title = $milestone->title;
}
$change->add_prop_change(
name => 'milestone',
new => $ticket->milestone_id
? $ticket->milestone_title
: undef,
old => $old_title || $old,
);
}
elsif ( $attr eq ':assigned_user' ) {
my $old = $diffable_attrs->{$attr};
my $old_with_name;
if ($old) {
require Net::Lighthouse::User;
my $user = Net::Lighthouse::User->new(
map { $_ => $self->sync_source->lighthouse->$_ }
grep { $self->sync_source->lighthouse->$_ }
qw/account auth/
);
eval { $user->load($old) };
if ($@) {
warn "can't load user $old on lighthouse";
}
else {
$old_with_name =
$user->name . '(' . $user->id . ')';
}
}
$change->add_prop_change(
name => 'owner',
new => $ticket->assigned_user_id
? ( $ticket->assigned_user_name . '('
. $ticket->assigned_user_id
. ')' )
: undef,
$old_with_name ? ( old => $old_with_name ) : (),
);
}
else {
$change->add_prop_change(
name => $PROP_MAP{ $hash{$attr} } || $hash{$attr},
new => $txn->$method,
old => $diffable_attrs->{$attr},
);
}
}
}
$changeset->add_change( { change => $change } )
if $change->has_prop_changes;
$self->_include_change_comment( $changeset, $ticket_uuid, $txn );
}
return unless $changeset->has_changes;
return $changeset;
}
sub _include_change_comment {
my $self = shift;
my $changeset = shift;
my $ticket_uuid = shift;
my $txn = shift;
my $comment = $self->new_comment_creation_change();
if ( my $content = $txn->body ) {
if ( $content !~ /^\s*$/s ) {
$comment->add_prop_change(
name => 'created',
new => $txn->created_at->ymd . ' ' . $txn->created_at->hms,
);
$comment->add_prop_change(
name => 'creator',
new => $self->resolve_user_id_to( undef => $txn->creator_name ),
);
$comment->add_prop_change( name => 'content', new => $content );
$comment->add_prop_change(
name => 'content_type',
new => 'text/plain',
);
$comment->add_prop_change( name => 'ticket', new => $ticket_uuid, );
$changeset->add_change( { change => $comment } );
}
}
}
sub _recode_attachment_create {
my $self = shift;
my %args =
validate( @_,
{ ticket_uuid => 1, changeset => 1, attachment => 1 } );
my $change = Prophet::Change->new(
{
record_type => 'attachment',
record_uuid => $self->sync_source->uuid_for_url(
$self->sync_source->remote_url
. "/attachment/"
. $args{'attachment'}->id,
),
change_type => 'add_file',
}
);
$change->add_prop_change(
name => 'content_type',
old => undef,
new => $args{'attachment'}->content_type,
);
$change->add_prop_change(
name => 'created',
old => undef,
new => $args{attachment}->created_at->ymd . ' '
. $args{attachment}->created_at->hms,
);
$change->add_prop_change(
name => 'creator',
old => undef,
new =>
$self->resolve_user_id_to( email_address =>
$args{'attachment'}->uploader_id )
);
$change->add_prop_change(
name => 'content',
old => undef,
new => $args{'attachment'}->content,
);
$change->add_prop_change(
name => 'name',
old => undef,
new => $args{'attachment'}->filename,
);
$change->add_prop_change(
name => 'ticket',
old => undef,
new => $args{ticket_uuid},
);
$args{'changeset'}->add_change( { change => $change } );
}
sub translate_prop_status {
my $self = shift;
my $status = shift;
return lc($status);
}
sub resolve_user_id_to {
my $self = shift;
shift;
my $id = shift;
if ( $id =~ /^\d+$/ ) {
my $user = Net::Lighthouse::User->new(
map { $_ => $self->sync_source->lighthouse->$_ }
grep { $self->sync_source->lighthouse->$_ }
qw/account auth/
);
$user->load( $id );
return $user->name;
}
else {
return $id;
}
}
__PACKAGE__->meta->make_immutable;
no Any::Moose;
1;