Chapter 6. Scrips

Scrips, often used together with templates, are the quickest and simplest way to customize RT’s behavior. A scrip consists of a condition, an action, and a template. RT comes with a number of pre-defined conditions, actions, and templates, but you also can write your own. Scrips give you the freedom to implement whatever business logic you need.

Custom conditions and actions can be created via RT’s web interface, or you can create a Perl module for each custom condition and action.

Additionally, a scrip can have custom cleanup code that will run after all other code, but before the scrip exits. Custom scrip code is always written in standard Perl, and templates are created using the Text::Template module.

Scrips can be applied across all queues or to individual queues. With the current version of RT, if you want to apply the same scrip to a subset of queues, you will have to go into each queue’s configuration and create the scrip for each one.

The scrips system is the last major part of RT that works exactly how it did when RT 2.0 came out in 2001.

In a future version of RT, the scrips system will be overhauled to make it easier to specify which scrips apply to which queues and to build more complex workflow.

RT comes with a set of standard conditions for scrips , as shown in Table 6-1.

Additionally, you can create a new scrip with a user-defined action. The following example is a very simple user-defined condition:

    $self->TicketObj->status eq 'deleted';

This condition is true whenever a ticket’s status is changed to deleted. This code is suitable for pasting into RT’s web interface.

The equivalent Perl module would look like this:

    package RT::Condition::OnDelete;
     
    use strict;
    use base 'RT::Condition::Generic';
     
    sub IsApplicable {
        my $self = shift;
        return ($self->TicketObj->status eq 'deleted');
    }
     
    1;

If your RT base directory is /opt/rt3, this code could be installed as /opt/rt3/local/lib/RT/Condition/OnDelete.pm. You can automate this process using the Module::Install::RTx module, which is covered in Chapter 10.

You also need to make RT aware of this module, which means adding some information to the database. Mucking around with the database directly is not a good idea, but there is a simple way to write command-line scrips to manipulate RT’s database.

Use the RT::Interface::CLI module to talk to RT:

    #!/usr/bin/perl
     
    use strict;
    use lib "/opt/rt3/lib";
     
    use RT;
    use RT::Interface::CLI qw( CleanEnv GetCurrentUser );
    use RT::ScripCondition;
     
    CleanEnv();
    RT::LoadConfig();
    RT::Init();
     
    my $user = GetCurrentUser();
    unless( $user->Id ) {
        print "No RT user found. Please consult your RT administrator.\n";
        exit 1;
    }

This first part is the voodoo needed to begin doing anything with RT from a command-line script. When GetCurrentUser() is called, it will look for a user in the RT database matching the username running the script. It does this by checking the Unix login field for the user, which by default is empty. So you will need to set this for at least one user via the web interface.

Now that you have RT initialized and the current user loaded, you can add the following code to make RT aware of this new condition:

    my $sc = RT::ScripCondition->new($user);
     
    $sc->Create( Name                 => 'On Delete',
                 Description          => 'When a ticket is deleted',
                 ExecModule           => 'OnDelete',
                 ApplicableTransTypes => 'Status',
               );

The Name is a short description for the condition and will be seen in the web interface.

After you run this script, when you go to create a new scrip in the web interface, you’ll see a new condition available in the condition select list.

Later in this chapter we’ll explore the possibilities for custom conditions in more detail.

An action is what the scrip does if its condition is true. RT comes with a number of actions built in, as shown in Table 6-2.

Of course, just as with conditions, you can write your own custom actions. And also like conditions, these actions can be defined either through a bit of Perl code pasted into the web interface, or as a separate module.

Let’s assume that you have an LDAP directory that lets you look up the department someone belongs to, based on their email address. You’d like to include that department as a custom field for all tickets.

To do that, you can create a custom action that sets this field for all newly created tickets. That means you would have this action run when the On Create condition was triggered, as part of the action preparation.

Your action code would look something like this:

    my $email = ($self->TicketObj->RequestorAddresses)[0];
     
    my $ldap = Net::LDAP->new( 'ldap.example.com' );
    $ldap->bind;
     
    my $msg = $ldap->search( base   => 'dc=ldap,dc=example,dc=com',
                             filter => "(email=$email)",
                           );
     
    my $entry = $msg->entry(0);
     
    my $dept = $entry->get_value('ou');
     
    my $cf = RT::CustomField->new( $RT::SystemUser );
    $cf->LoadByName( Name => 'Department' );
     
    $self->TicketObj->AddCustomFieldValue( Field => $cf, Value => $dept );
     
    return 1;

This same code as a Perl module looks like this:

    package RT::Action::AddDepartment;
     
    use strict;
     
    use base 'RT::Action::Generic';
     
    sub Prepare {
        my $self = shift;
     
        my $email = ($self->TicketObj->RequestorAddresses)[0];
     
        my $ldap = Net::LDAP->new( 'ldap.example.com' );
        $ldap->bind;
     
        my $msg = $ldap->search( base   => 'dc=ldap,dc=example,dc=com',
                                 filter => "(email=$email)",
                               );
     
        my $entry = $msg->entry(0);
     
        my $dept = $entry->get_value('ou');
     
        my $cf = RT::CustomField->new( $RT::SystemUser );
        $cf->LoadByName( Name => 'Department' );
     
        $self->TicketObj->AddCustomFieldValue( Field => $cf, Value => $dept );
     
        return 1;
    }
     
    1;

Again, if you add a new module, you need to make RT aware of it by adding it to the database. This can be done with a script just like the one for adding new conditions, except with the following code at the end:

    my $sc = RT::ScripAction->new($user);
     
    $sc->Create( Name        => 'Add Department',
                 Description => 'set department custom field for new tickets',
                 ExecModule  => 'AddDepartment',
               );

After creating this action, you can use the web interface to add a new scrip with the condition set to On Create and the action set to Add Department. You also need to make sure that there is a custom field named Department.

Each scrip can have one associated template. These usually generate email, but they can be used for any purpose you like. They allow you to generate arbitrary text based on the content of a transaction. For example, a scrip with the Create Tickets action will use the output of its template to generate new tickets.

Templates are written using the Text::Template templating language, which is a very simple templating language allowing you to embed Perl in text. Perl code is placed in curly braces ({ and }). Everything else is plain text.

RT installs the base templates shown in Table 6-3.

You can make your own custom templates. If you’re using a template to send email, you also can set the email headers from the template.

Let’s assume that you created a custom scrip to fire whenever someone steals a ticket. You want to send email to the former owner letting them know this.

The following example is a template for that email:

    To: { my $old_owner = RT::User->new( $self->CurrentUser );
          $old_owner->Load( $Transaction->OldValue );
          $old_owner->EmailAddress() }
    Subject: Ticket #{ $Ticket->Id() } taken by { $Ticket->OwnerObj->Name() }
     
    A ticket you owned:
     
      { $Ticket->Subject() }
     
    has been taken by { $Ticket->OwnerObj->Name() }.
     
    { $RT::WebURL }Ticket/Display.html?id={ $Ticket->Id() }

If the template is being used to send an email, then you can set headers simply by specifying them as name/value pairs at the beginning of the template. In the code just shown the To and Subject header is set from the template.

To actually send this message only when a ticket is stolen also requires a custom condition, which we will see later.

If a transaction adds attachments to a ticket, and you want to include those attachments with the outgoing email, add this line to the top of your template:

    RT-Attach-Message: Yes

Now that you have seen a number of simple examples of custom scrip code, let’s get into details of how these work and what objects are available for your custom code.

Besides the ticket and transaction, several other pieces of the RT API often are useful when creating custom scrips.

$object->CurrentUser()

Most objects in the RT class hierarchy inherit from RT::Base, which provides a CurrentUser() method. This represents the user currently operating on the object. In the web interface, this is the logged-in user who is triggering the scrip.

RT::Nobody()

The RT::Nobody() function returns an RT::User object for the Nobody user. You can compare this user’s ID to another user ID to check if that user is a real user. For example, you might have a condition that is true whenever the owner changes, as long as the previous owner wasn’t Nobody. Or you might have an action that is triggered only when a ticket is assigned from a real user to Nobody.

RT::SystemUser()

RT::SystemUser() returns an RT::User object for RT’s internal superuser, RT_SystemUser. RT uses this user internally to do things that need to happen without access control, such as performing internal consistency checks. Generally, you shouldn’t ever do anything as the system user, but it’s ok to use to look at things if you want to avoid ACL checks.

$RT::Logger

This is a Log::Dispatch object with outputs based on your RT config. You can call methods on this object like debug(), warn(), or error() to log output from your code. This is useful both for debugging as well as for logging serious errors.

Everything else

Of course, RT is a large system with a complex API. There are many other objects that you may want to make use of, such as queues, users, or custom fields. See Chapter 9 and the documentation for the relevant modules for more details on the API.

The best way to understand what custom scrips can do is to look at some complete examples.

The custom template we looked at earlier notified a user when another user stole their ticket. Let’s examine that example in more detail. First, we’ll implement it solely via the web interface, and then we’ll reimplement it as custom modules.

From the web interface, we can create a new scrip. Let’s call it Ticket Stolen. The condition should be set to User Defined. The action will be Notify Other Recipients. We will create a custom template to generate an email to the right recipient. If you created that template earlier, you can set the Template field to use that one now. Otherwise you can leave it empty for now.

The stage for this scrip will be TransactionCreate.

When someone emails RT for the first time, RT will add them as a user to the database. However, by default this new user will not be able to log in to the system to view their tickets. If you want them to be able to log in, they need to have a password.

You can customize the existing AutoReply template to create a new password for them and include it in the response. For existing users, you can include their current password.

The customized template might look like this:

    Subject: AutoReply: { $Ticket->Subject() }
     
     
    Greetings,
     
    This message has been automatically generated in response to the
    creation of a trouble ticket regarding:
            "{ $Ticket->Subject() }",
    a summary of which appears below.
     
    There is no need to reply to this message right now.  Your ticket
    has been assigned an ID of [{ $rtname } #{ $Ticket->Id() }].
     
    Please include the string:
     
             [{ $rtname } #{ $Ticket->Id() }]
     
    in the subject line of all future correspondence about this
    issue. To do so, you may reply to this message.
     
    You may log into the Request Tracker system to view your past and
    current tickets at:
     
              { $RT::WebURL }
     
    {
      if ( $Transaction->CreatorObj->id != $RT::Nobody->id
           && ! $Transaction->CreatorObj->Privileged
           && ! $Transaction->CreatorObj->HasPassword ) {
     
           my $user = RT::User->new( $RT::SystemUser );
           $user->Load( $Transaction->CreatorObj->Id );
     
           my ($stat, $password) = $user->SetRandomPassword();
     
           if ($stat) {
               my $username = $user->Name;
     
               $OUT .= "
    When prompted to log in, please use the following credentials:
     
                Username: $username
                Password: $password
    ";
           } else {
               $RT::Logger->error( 'Error trying to set random password for '
                                   . $user->Name . ": $password" );
     
               $OUT .= "
    There was an error when trying to assign you a new password.
    Please contact your local RT administrator at for assistance.
    ";
           }
      }
    }
                            Thank you,
                            { $Ticket->QueueObj->CorrespondAddress() }
     
    -------------------------------------------------------------------------
    {$Transaction->Content()}

This template is the same as the one that ships with RT, except for the section in the middle, which automatically assigns a new password to the user if they do not already have one.

Several pieces are worth noting in this template. First, there is the check to see if the user has a password:

    unless ($Transaction->CreatorObj->id == $RT::Nobody->id
            && $Transaction->CreatorObj->Privileged
            && $Transaction->CreatorObj->HasPassword ) { ...

We want to make sure that we don’t try to give the Nobody user a password, as this user is solely for internal use. We also do not want to auto-generate a password for privileged users, because we assume that the RT administrator manually manages these users. Finally, we need to make sure that the user doesn’t already have a password.

When we call $user->SetRandomPassword() we check the return value. This method returns a two item list. The first is a return code indicating success (true) or failure (false). If creating the password succeeded, the second item is the new password, which we include in the email. If the password could not be created for some reason, the second item is the error message. We make sure to log this error so that the RT administrator can follow up on it later.

If you are using RT to handle a support or sysadmin center, it might be useful to send a message to a pager for certain types of requests.

Let’s take a look at how to set up RT to send pages for new tickets with a subject matching /^Emergency/i, but only from 6 p.m. to 8 a.m. These could come from users or from system monitoring software.

This can be done by creating a custom condition and template. The condition checks the subject and time. The template creates the SMS message. To send email, we can use RT’s existing Notify action.

    package RT::Condition::OnAfterHoursEmergency;
     
    use strict;
    use base 'RT::Condition::Generic';
     
    sub IsApplicable {
        my $self = shift;
     
        return 0 unless $self->TicketObj->Subject =~ /^Emergency/i;
     
        my $hour = (localtime)[2];
     
        return 0 unless $hour >= 18 || $hour <= 8;
     
        return 1;
    }
     
    1;

First, we check the ticket subject to make sure that it indicates an emergency. Then we check the time. This will work properly only if the server RT runs on has the correct time. If all of the checks pass, we return a true value. We also will want to make sure that this condition is checked only when creating a ticket, but that can be done when we register the condition with the system.

Now let’s create our template:

    From:    Yoyodyne RT <rt@yoyodyne.example.com>
    To:      6125559912@pager.example.com
    Subject: Yoyodyne RT 911
     
    { $Ticket->Subject() }

We want to keep the message short, so we use only the ticket’s subject as the email body. The template can be created in the system with RT’s web interface.

To add our custom condition, we need two steps. First, we need to save the code to our RT installation’s local lib directory. This would be /opt/rt3/local/lib/RT/Condition/OnAfterHoursEmergency.pm in this example.

Then we can use a script like the one we saw before to register the condition with RT:

    #!/usr/bin/perl
     
    use strict;
    use lib "/opt/rt3/lib";
     
    use RT;
    use RT::Interface::CLI qw( CleanEnv GetCurrentUser );
    use RT::ScripCondition;
     
    CleanEnv();
    RT::LoadConfig();
    RT::Init();
     
    my $user = GetCurrentUser();
    unless( $user->Id ) {
        print "No RT user found. Please consult your RT administrator.\n";
        exit 1;
    }
     
    my $sc = RT::ScripCondition->new($user);
     
    $sc->Create( Name                 => 'After Hours Emergency',
                 Description          => 'An emergency ticket is created after hours',
                 ExecModule           => 'OnAfterHoursEmergency',
                 ApplicableTransTypes => 'Create',
               );

Note that ApplicableTransTypes field is set to Create, ensuring that this condition is checked only when a new ticket is created. We could have done this in the condition module’s IsApplicable() method, but this is more efficient.

To create a new scrip for this condition, we would pick After Hours Emergency from the condition drop down in the new scrip form. The action will be Notify Other Recipients. The actual recipients will be picked up from the template. For the template, we use the one we just created.

If we had different pager numbers for different queues, we could create several templates. Then we would set up scrips for each queue, all using the same condition and action, each with a different template.

As we mentioned earlier, one of the uses for custom scrips is to implement a workflow system in RT. Let’s look at a simple example of this concept, which uses the ability to have a scrip create a new ticket.

For our example, let’s assume that we have two groups of people using RT, each with their own queue. Both of these groups are working in some sort of agency that does creative work, like a graphic design firm.

The first group is the designers. Their queue has tickets like “create mockup of poster for Faye Wong’s spring tour.” The other group is the account representatives. They use RT to track the designer’s work, so they know when things are ready for review with the clients.

It would be possible to simply transfer tickets from one queue to the other, so that when a designer wanted a representative to review their work, they would transfer the ticket to the Review queue and assign ownership to Nobody. But if we wanted to use RT for time tracking, by updating the time worked field for tickets, this could get awkward. In this case, it’s better to create a new ticket for each piece of work. And of course, ideally, every time a ticket in the Design queue is closed, a new ticket in the Review queue is opened. If a given project needs additional design work, the account representative can create a new ticket in the Design queue.

To make the automatic creation of Review tickets happen, we can create a new scrip where the condition is On Resolve, and the action is Create Tickets. We just need to create a custom template that creates the correct ticket in the Review queue. That template would look something like this:

    =  =  =Create-Ticket: design-review
    Queue:     Review
    Subject:   Review of { $Tickets{'TOP'}->Subject() }
    Owner:     { $Tickets{'TOP'}->Requestors->UserMembersObj->Next->Id() }
    Requestor: { $Tickets{'TOP'}->OwnerObj->EmailAddress() }
    RefersTo:  { $Tickets{'TOP'}->Id() }
    Content:   A ticket in the Design queue has been resolved and requires review.

A template used by the Create Tickets action has a special format. It can create one or more tickets. Each ticket to be created has its own header, which in our example is the first line. The header is = = =Create-Ticket: followed by a name.

If you were creating multiple tickets, then each ticket could access the tickets already created via the %Tickets hash. This is handy if you want to create several tickets and link them together. The ticket that triggered the Create Tickets action is always available as $Tickets{'TOP'}.

In our example, we create only one ticket. The subject of the new ticket is based on the ticket being resolved. The owner of the ticket is the first requestor of the original ticket. The requestor is the ticket’s owner. We want the new ticket to have a link back to the original ticket, so we set the RefersTo field.

Finally, the Content field is the body of the new ticket. This could be combined with a custom script to send an email whenever a new ticket is created in the Review queue with an owner (as opposed to being owned by Nobody). This way the owner of the new ticket would know that it was their responsibility to review the work just completed.