Sidebar

Combodo

iTop Extensions

Computer telephony integration

name:
Computer telephony integration
description:
Integrates telephony systems with iTop
version:
1.2.0
release:
2024-09-10
itop-version-min:
3.2
code:
combodo-telephony-integration
state:
stable
php-version-max:
PHP 8.3

This extension provides an easy integration between iTop and a CTI (Computer Telephony Integration) system.

Features

The job of support agents may be eased by integrating iTop with a CTI. This is precisely what the Telephony Integration extension provides.

  • Inbound phone calls received by a support agent may be transformed into an iTop WEB page on the agent's own browser: a dashboard containing information on the caller(s) and his(their) ongoing tickets is displayed and the creation of new tickets can be directly launched from there,
  • Outbound phone calls may be initiated with the caller of a given ticket, directly from the ticket's detailed display,
  • New Phone Call object tracks the phone calls that occurred and that led to a ticket modification,

Behaviour of the extension is highly configurable to fit phone system capabilities and to best answer support teams' requirements.

Revision History

Version Release Date Comments
1.2.0 2024-09-03 N°7550 - Propose the different Person's phone attributes for outbound calls
N°7736 - Interprete the “+” in front of phone numbers not as a space, but as a character
N°7747 - Check caller's attributes before displaying E-Call menu
1.1.0 2024-07-10 N°5723 - Adapt extension to php 8.1 → 8.3
N°5879 - Provide capability to pre-process phone numbers provided by CTI
N°7058 - Better handle rights on PhoneCall, namely for Support and Service Desk Agents
N°7492 - Adapt Computer Telephony Integration to iTop 3.1+
1.0.1 2022-06-13 First official version

Limitations

The extension doesn't bring any new feature to the CTI itself, like defining the behaviour of the call routing process based on information stored in iTop. Such integration is possible but should be done on the CTI.

Requirements

The extension only works with iTop 3.2.0 and above.

Installation

Use the Standard installation process for this extension.

Configuration

The default configuration of the extension is brought by its XML datamodel. This configuration can be altered through standard XML customization and / or completed by an administrator through the configuration file.

By default, as shown in the XML datamodel extract, below:

  • The attribute phone is expected in the URL,
  • Parameters to retrieve and display callers are set,
  • Direct ticket creation is present but disabled,
  • Indirect creation of User Requests and Persons are allowed,
  • Ongoing tickets of the found user(s) and its(their) organization(s) are displayed.
Default configuration defined in XML
    <module_parameters>
        <parameters id="combodo-telephony-integration" _delta="define">
            <!-- List here the parameters provided in the URL -->
            <url_parameters type="hash">
                <phone>phone</phone>
            </url_parameters>
            <!-- This section defines how to retrieve in iTop the contacts related to the given parameters -->
            <caller_information type="hash">
                <label>Contacts with phone number $phone$</label>
                <query>SELECT Person WHERE phone LIKE :phone OR mobile_phone LIKE :phone</query>
                <display_name>friendlyname, org_name</display_name><!-- Specify how to display them -->
            </caller_information>
            <!-- Should you wish to directly open a ticket when receiving a call, then this section is for you -->
            <direct_creation type="hash">
                <UserRequest type="hash">
                    <enabled type="bool">false</enabled><!-- enable or disable the function -->
                    <preset type="hash">
                        <org_id>SELECT Organization AS o JOIN Person AS p ON p.org_id = o.id WHERE p.id = :selected_caller</org_id>
                        <caller_id>:selected_caller</caller_id>
                        <origin>phone</origin>
                        <public_log>Call from customer dated $datetime$</public_log>
                    </preset>
                </UserRequest>
            </direct_creation>
            <!-- List here the class of objects that the agent could be creating from the listed contacts -->
            <indirect_creation type="hash">
                <UserRequest type="hash">
                   <enabled type="bool">true</enabled>
                   <preset type="hash">
                        <org_id>SELECT Organization AS o JOIN Person AS p ON p.org_id = o.id WHERE p.id = :selected_caller</org_id>
                        <caller_id>:selected_caller</caller_id>
                        <origin>phone</origin>
                        <public_log>Call from customer dated $datetime$</public_log>
                    </preset>
                </UserRequest>
                <Person type="hash">
                    <enabled type="bool">true</enabled>
                    <preset type="hash">
                        <phone>:phone</phone>
                    </preset>
                </Person>
            </indirect_creation>
            <!-- This section defines the different list of tickets that you'd like to see upon the reception of the call -->
            <display_tickets type="hash">
                <lists type="array">
                    <class id="0" type="hash">
                        <enabled type="bool">true</enabled>
                        <label>User Requests opened for this caller</label>
                        <query>SELECT UserRequest AS u WHERE u.caller_id IN (:callers_list) AND u.status != 'closed'</query>
                    </class>
                    <class id="1" type="hash">
                        <enabled type="bool">true</enabled>
                        <label>User Requests opened for the organization of the caller</label>
                        <query>SELECT UserRequest AS u JOIN Organization AS o ON u.org_id = o.id JOIN Person AS p ON p.org_id = o.id
                            WHERE p.id IN (:callers_list) AND u.status != 'closed'</query>
                    </class>
                </lists>
            </display_tickets>
        </parameters>
    </module_parameters>

Should an iTop administrator wishes to change these parameters, he'd just need to add specific lines into the configuration file, under the 'Modules specific settings' section, in a 'combodo-telephony-integration' array. For instance:

Configuration file
      'combodo-telephony-integration' => array (
                // List here the parameters provided in the URL
                'url_parameters' => array (
                  'phone' => 'phone',
                ),
                // This section defines how to retrieve in iTop the contacts related to the given parameters
                'caller_information' => array (
                  'label' => 'Contacts with phone number $phone$',
                  'label/FR FR' => 'Contacts ayant le numéro de téléphone $phone$',
                  'query' => 'SELECT Person WHERE phone LIKE :phone OR mobile_phone LIKE :phone',
                  'display_name' => 'friendlyname, org_name, location_name', // Specify how to display them
                ),
                // Should you wish to directly open a ticket when receiving a call, then this section is for you
                'direct_creation' => array (
                  'UserRequest' => array (
                    'enabled' => false,
                    'preset' => array (
                      'org_id' => 'SELECT Organization AS o JOIN Person AS p ON p.org_id = o.id WHERE p.id = :selected_caller',
                      'caller_id' => ':selected_caller',
                      'origin' => 'phone',
                      'public_log' => 'Call from customer dated $datetime$',
                    ),
                  ),
                ),
                // This section defines the different list of tickets that you'd like to see upon the reception of the call
                'indirect_creation' => array (
                  'UserRequest' => array (
                    'enabled' => false,
                    'preset' => array (
                      'org_id' => 'SELECT Organization AS o JOIN Person AS p ON p.org_id = o.id WHERE p.id = :selected_caller',
                      'caller_id' => ':selected_caller',
                      'origin' => 'phone',
                      'public_log' => 'Call from customer dated $datetime$',
                    ),
                  ),
                  'Incident' => array (
                    'enabled' => true,
                    'preset' => array (
                      'org_id' => 'SELECT Organization AS o JOIN Person AS p ON p.org_id = o.id WHERE p.id = :selected_caller',
                      'caller_id' => ':selected_caller',
                      'origin' => 'phone',
                      'public_log' => 'Call from customer dated $datetime$',
                    ),
                  ),
                  'Change' => array (
                    'enabled' => true,
                    'preset' => array (
                      'org_id' => 'SELECT Organization AS o JOIN Person AS p ON p.org_id = o.id WHERE p.id = :selected_caller',
                      'caller_id' => ':selected_caller',
                      'origin' => 'phone',
                      'private_log' => 'Call from customer dated $datetime$',
                    ),
                  ),
                  'Person' => array (
                    'enabled' => true,
                    'preset' => array (
                      'phone' => ':phone',
                    ),
                  ),
                ),
                // This section defines the different list of tickets that you'd like to see upon the reception of the call
                'display_tickets' => array (
                  'lists' => array (
                    0 => array (
                      'enabled' => true,
                      'label' => 'User Requests opened for this caller',
                      'query' => 'SELECT UserRequest AS u WHERE u.caller_id IN (:callers_list) AND u.status != \'closed\'',
                    ),
                    1 => array (
                      'enabled' => true,
                      'label' => 'User Requests opened for the organization of the caller',
                      'label/FR FR' => 'Demandes utilisateurs de l\'organisation de l\'appelant',
                      'query' => 'SELECT UserRequest AS u JOIN Organization AS o ON u.org_id = o.id JOIN Person AS p ON p.org_id = o.id WHERE p.id IN (:callers_list) AND u.status != \'closed\'',
                    ),
                    2 => array (
                      'enabled' => true,
                      'label' => 'Problems opened for this caller',
                      'label/FR FR' => 'Problèmes ouverts par l\'appelant',
                      'query' => 'SELECT Problem AS u WHERE u.caller_id IN (:callers_list) AND u.status != \'closed\'',
                    ),
                    3 => array (
                      'enabled' => true,
                      'label' => 'Incidents opened for this caller',
                      'label/FR FR' => 'Incidents ouverts par l\'appelant',
                      'query' => 'SELECT Incident AS u WHERE u.caller_id IN (:callers_list) AND u.status != \'closed\'',
                    ),
                  ),
                ),
                // Define here how to launch an outbound call from a ticket, using the caller's parameters
                'ecall_parameters' => array (
                  'url' => 'https://myctiserver/index.php',
                  'url_parameters' => array (
                    'telephone' => 'phone',
                    'mailaddress' => 'email',
                  ),
                ),
        ),

Inbound calls

The behaviour of the extension when processing inbound calls is fully driven by it's configuration. The following chapters will help you to understand how.

iTop URL to reach the telephony extension

When answering a phone call, a support agent may decide to visualize, in iTop, information related to the caller and his ongoing tickets. For that purpose, he launches an operation on his CTI that opens, on his own browser, a page containing the URL of the telephony extension. The called URL must have a specific format that holds both:

  • the path to the iTop server,
  • the parameters that will allow iTop to look for the caller in its data base.

This could be something like:

https://myitopserver/pages/exec.php?exec_module=combodo-telephony-integration&exec_page=index.php&phone=0987654321

The first (https://myitopserver/pages/exec.php?exec_module=combodo-telephony-integration&exec_page=index.php) part refers to the server and is static. The second (&phone=0987654321) one holds the caller's attributes, like here the 'phone' parameter that obviously corresponds to the phone number of the caller, and must correspond to what has been defined in the configuration file, under the 'url_parameters' section:

'url_parameters' => array (
  'phone' => 'phone',
),

That array specifies that the 'phone' parameter (on the left) used in the URL actually corresponds to the 'phone' attribute (right) of the class Person in iTop.

We could have defined another configuration, like:

'url_parameters' => array (
  'telephone' => 'phone',
  'mailaddress' => 'email',
),

With:

  • 'telephone' as the URL parameter that identifies the 'phone' attribute of the class Person in iTop,
  • 'mailaddress' as the URL parameter that identifies the 'email' attribute of the class Person in iTop.

Then, the URL would have been :

https://myitopserver/pages/exec.php?exec_module=combodo-telephony-integration&exec_page=index.php&telephone=0987654321&mailaddress=mymail@mycompany.com
Now, in order to activate the extension, CTI's administrators must configure their system to generate the correct URL.
  • 'myitopserver' should be replaced by iTop server's address or name and path to the iTop application,
  • each URL parameters should be set according to the caller's parameters

Caller(s) lookup

Once the CTI has launched the telephony page, iTop extracts the different parameters given in the URL as specified in the 'url_parameters' section. If any listed parameter is missing, an error is generated.

Wrong URLparameters

Should the URL contain additional parameters, these will be discarded.

Once all parameters are correctly extracted, iTop will search for the corresponding person(s) in its data base by executing the OQL query defined in the 'caller_information' section of the configuration.

'caller_information' => array (
  'label' => 'Contacts with phone number $phone$',
  'label/FR FR' => 'Contacts ayant le numéro de téléphone $phone$',
  'query' => 'SELECT Person WHERE phone LIKE :phone OR mobile_phone LIKE :phone',
  'display_name' => 'friendlyname, org_name, location_name',
),
  • The label parameter allows administrator to set the title of the block displaying the found persons,
  • This label can be localized,
  • Parameters extracted from the URL may be used in the OQL query through placeholders. These are iTop attributes preset with a semi-column ':',
  • The display_name parameter allow the administrator to select the attributes that will be used to identify a person when displayed in the telephony page.

From there, the OQL may return nobody, a unique person or several persons. That result is processed accordingly through the following steps.

Preprocessing of incoming parameters to match iTop attributes' format

The format of phone numbers provided by the CTI (like 0987654321) may not be aligned with the de facto format used to store phone numbers in iTop (like +33 9 87 65 43 21). In order to enable a match between these 2 numbers, a preprocessing may be activated on the CTI's phone number:

  • This processing is handled by the method ReformatPhoneNumberToMatchiTopUsage defined in the XML datamodel file of the extension, under the class Contact.
  • The method is called by the CTI code on all entry parameters but does nothing by default (parameters are left untouched).
  • It may be overloaded by an extension so that real preprocessing is made.
  • The method contains some inactive examples that may be activated if required.

Direct ticket creation

iTop administrator may decide that a call to the telephony extension directly opens the creation form of a ticket. This is driven by the 'direct_creation' chapter of the configuration.

'direct_creation' => array (
  'UserRequest' => array (
    'enabled' => false,
    'preset' => array (
      'org_id' => 'SELECT Organization AS o JOIN Person AS p ON p.org_id = o.id WHERE p.id = :selected_caller',
      'caller_id' => ':selected_caller',
      'origin' => 'phone',
      'public_log' => 'Call from customer dated $datetime$',
    ),
  ),
  'Incident' => array (
    'enabled' => true,
    'preset' => array (
      'org_id' => 'SELECT Organization AS o JOIN Person AS p ON p.org_id = o.id WHERE p.id = :selected_caller',
      'caller_id' => ':selected_caller',
      'origin' => 'phone',
      'public_log' => 'Call from customer dated $datetime$',
    ),
  ),
),

Direct creation is launched as soon as it exists one entry in the 'direct_creation' chapter with the 'enabled' status set to true.

  • Such entry must be named according to the name of an abstract or non abstract iTop class (UserRequest or Incident in the above example).
  • Should several such entries be enabled, only the first one will be processed.
  • Should no entry be enabled or should that chapter be missing, the telephony extension will skip that step and directly jump to the following one.
The 'enabled' status allow you to keep direct creation entries in the configuration file and to make sure these are not processed by the extension

In the above example, the Incident creation form is opened when the telephony extension URL is called.

To help the support agent capturing the caller's request, some attributes may be preset in the creation form. These are specified in the 'preset' chapter of each 'direct_creation' entry.

  • external keys may be defined by an OQL that uses the same type of placeholders as the one defined in the caller information query,
  • some external keys may be directly defined by a placeholder,
  • the value of an enum or a string can be set,
  • an entry can be added into a log attribute.

The caller attribute of the ticket is preset according to the result of the caller(s) lookup:

  • If nobody has been found, the attribute is left empty,
  • If a unique person has been found, the caller is preset with that person,
  • If several persons have been found, the caller is preset with the first one that the OQL returned.
When defining preset values, the administrator must make sure that all attributes are coherent. For instance, the org_id must comply with the organization of the selected person.

Indirect ticket creation

Should iTop administrator decide to not directly open the creation form of a ticket, he may select a set of objects (tickets, most of the time, but not necessary) that the support agent will be able to create once the telephony page is displayed. This is driven by the 'indirect_creation' chapter of the configuration.

'indirect_creation' => array (
  'UserRequest' => array (
    'enabled' => false,
    'preset' => array (
      'org_id' => 'SELECT Organization AS o JOIN Person AS p ON p.org_id = o.id WHERE p.id = :selected_caller',
      'caller_id' => ':selected_caller',
      'origin' => 'phone',
      'public_log' => 'Call from customer dated $datetime$',
    ),
  ),
  'Incident' => array (
    'enabled' => true,
    'preset' => array (
      'org_id' => 'SELECT Organization AS o JOIN Person AS p ON p.org_id = o.id WHERE p.id = :selected_caller',
      'caller_id' => ':selected_caller',
      'origin' => 'phone',
      'public_log' => 'Call from customer dated $datetime$',
    ),
  ),
  'Change' => array (
    'enabled' => true,
    'preset' => array (
      'org_id' => 'SELECT Organization AS o JOIN Person AS p ON p.org_id = o.id WHERE p.id = :selected_caller',
      'caller_id' => ':selected_caller',
      'origin' => 'phone',
      'private_log' => 'Call from customer dated $datetime$',
    ),
  ),
  'Person' => array (
    'enabled' => true,
    'preset' => array (
      'phone' => ':phone',
    ),
  ),
),

In the above example, the agent will be able to create an Incident, a Change or a Person through its telephony page. Choice for UserRequest creation will not be given because the 'enabled' parameter is set to false in the UserRequest section of the 'indirect_creation' chapter.

Indirect creations

Creation buttons only appear if the support agent has the right to create the associated class.

The list of callers is displayed in a combo box, according to who has been found previsouly. This can be:

Display Persons found Combo box choices Number of choices
Unknown contact No one Unknown Contact 1
One contact A unique one Contact #1
Unknown Contact
2
Many contacts Several All contacts found
Contact #1

Contact #n
Unknown Contact
n + 2

Selecting a person and clicking on a 'Create a new xxx' button opens the creation form of the xxx class, already preset according to what has been specified in the 'preset' section of the class, under the 'indirect_creation' chapter.

Tickets display

Next to the found person list and the object creation buttons, the telephony extension may display different groups of tickets, according to what is defined in the 'display_tickets' chapter of the extension configuration.

'display_tickets' => array (
  'lists' => array (
    0 => array (
      'enabled' => true,
      'label' => 'User Requests opened for this caller',
      'query' => 'SELECT UserRequest AS u WHERE u.caller_id IN (:callers_list) AND u.status != \'closed\'',
    ),
    1 => array (
      'enabled' => true,
      'label' => 'User Requests opened for the organization of the caller',
      'label/FR FR' => 'Demandes utilisateurs de l\'organisation de l\'appelant',
      'query' => 'SELECT UserRequest AS u JOIN Organization AS o ON u.org_id = o.id JOIN Person AS p ON p.org_id = o.id WHERE p.id IN (:callers_list) AND u.status != \'closed\'',
    ),
    2 => array (
      'enabled' => false,
      'label' => 'Problems opened for this caller',
      'label/FR FR' => 'Problèmes ouverts par l\'appelant',
      'query' => 'SELECT Problem AS u WHERE u.caller_id IN (:callers_list) AND u.status != \'closed\'',
    ),
    3 => array (
      'enabled' => true,
      'label' => 'Incidents opened for this caller',
      'label/FR FR' => 'Incidents ouverts par l\'appelant',
      'query' => 'SELECT Incident AS u WHERE u.caller_id IN (:callers_list) AND u.status != \'closed\'',
    ),
  ),
),

Each group may be enabled or disabled. Their description can be configured and localized through the label parameters and their content is defined by an OQL. Of course, a group will not be listed if the class defined by its OQL is not implemented in the data model. The above example would give:

List of tickets

  • Selecting another user will reload the different lists accordingly,
  • Selecting 'Unknow contact' will just remove the lists from the screen,
  • Selecting 'All contacts found' (In the case where multiple persons have been found) will reload the lists with the tickets of each persons.

From there, the support agent may select a given ticket and decide to just display its content or to open its modification form. No content is automatically added to the ticket at this stage.

Outbound calls

Nowadays, all Internet browsers offer a “click to call” feature that associates a phone call action (simply calling, usually) to specific attributes like phone numbers. Next to that standard capability, the Telephony extension allows support agents to directly launch a phone call from a ticket, where no phone attribute is usually displayed, to the caller of the ticket.

This behaviour is driven by the 'ecall_parameters' section of the Telephony configuration. Different flavours of the configuration are supported.

For version 1.1.0 and below:

'ecall_parameters' => array (
  'url' => 'https://myctiserver/index.php',
  'url_parameters' => array (
    'telephone' => 'phone',
    'mailaddress' => 'email',
  ),
),

Where:

  • 'url' defines the url where to reach the CTI server,
  • 'url_parameters' lists the parameters that need to be provided to the CTI and what they correspond to in iTop. Here:
    • 'telephone' will contain the 'phone' attribute of the caller,
    • 'mail' will contain the 'email' attribute of the caller.

For version 1.2.0 and above:

'ecall_parameters' => array (
  'url' => 'https://myctiserver/index.php',
  'url_parameters' => array (
    'phone' => array (
      'icon' => 'fas fa-phone',
      'attributes' => array (
        'telephone' => 'phone',
        'mailaddress' => 'email',
      ),
    ),  
    'mobile_phone' => array (
      'icon' => 'fa fa-mobile',
      'attributes' => array (
        'telephone' => 'mobile_phone', 
      ),
    ),
  ),
),

Where:

  • 'url' defines the url where to reach the CTI server,
  • 'url_parameters' lists different actions that may be selected to launch the outbound call:
    • 'phone' / 'mobile_phone':
      • 'icon': icon that will be displayed next to the given action
      • 'attributes' :parameters that need to be provided to the CTI for the selected action and what they correspond to in iTop.
Note that version 1.1.0 type of configuration is understood by version 1.2.0+ of the extension.

With the above 1.1.0 configuration example, the generated URL would be something like:

https://myctiserver/index.php?telephone=0123456789&mailaddress=mymail@mycompany.com

Setting up this 'ecall_parameters' configuration section will activate the feature. In such case, a phone icon will appear in the list of the actions associated to a ticket. Clicking on it will just open a new tab with the correct URL.

E-Call menu on a ticket

Should you wish to move the menu at the top right of the ticket, next to the “Create” action, just add its name in the 'shortcut_actions' parameter of iTop's configuration, as shown below:

// shortcut_actions: Actions that are available as direct buttons next to the "Actions" menu
//      default: 'UI:Menu:Modify,UI:Menu:New'
'shortcut_actions' => 'UI:Menu:Modify,UI:Menu:New,telephony_integration_ecall_legacy,telephony_integration_ecall_phone,telephony_integration_ecall_mobile_phone',
  • The telephony_integration_ecall_legacy term relates to configuration type 1.1.0
  • The telephony_integration_ecall_phone or telephony_integration_ecall_mobile_phone term relates to configuration type 1.2.0

Phone icon on a ticket

When activated, the feature is available on all tickets that inherit from the class Ticket.

Note on person's phone numbers with regards outbound calls.
  • Version 1.2.0+ of the extension accepts person's phone numbers that start with a '+' followed by a country code, as specified in E.164 standard. Such phone numbers are integrally transfered to the CTI, '+' included.
  • In the case where a phone number attribute is empty, the E-Call corresponding menu is not displayed on the ticket's detailed page.

Phone Calls

The Telephony extension brings a new object named Phone Call which purpose is to track inbound and outbound calls related to a ticket. Every time a ticket is created with the origin parameter set to phone (which is the case for tickets created from the “Telephony Control Panel”), a Phone Call is created and attached to the ticket. Similarly, every call launched from a ticket through the extension creates a Phone Call and attaches it to the ticket.

Phone Call Properties

Name Type Mandatory?
Contact Foreign key to a(n) Person No
Agent Foreign key to a(n) Person Yes
Ticket Foreign key to a(n) Ticket Yes
Date Date (year-month-day) No
Flow Possible values : Inbound / Outbound Yes
Note Multiline character string No
  • Modify / Bulk Modify as well as Delete rights on PhoneCall objects are granted to the Support and Service Desk Agents. Tickets, with a phone origin, created by other profiles won't have any PhoneCall created.
  • By default, Read / Bulk Read capabilities are given to all other profiles unless specifically denied by an extension or modification of the default data model.

Display a Phone Call

From the detailed display of a ticket, open the 'Phone Calls' tab and select one of the listed elements.

Ticket's tab

From a ticket perspective, a new tab called 'Phone Calls' is displayed on classes that do appear in the 'display_tickets' chapter of the Telephony configuration.

Integration use cases

The extension has been tested with different CTIs. This chapter provides information on how to configure both iTop and the CTI application to activate the features described here above.

3CX

See WEB site.

Inbound calls

In order to allow 3CX to open a page on your browser when a call is received, go to the “Settings / Integration” menu of your desktop application (web page or client app), then:

  • Select the option “Auto launch a Custom Contact URL using variables”
  • Set the contact URL to
    http://your_itop/pages/exec.php?exec_module=combodo-telephony-integration&exec_page=index.php&phone=%CallerNumber%
  • Decide when to be connected: when the phone rings or when connected with the caller.

Outbound calls

The 'ecall_parameters' section of the Telephony configuration should be set as follows:

'ecall_parameters' => array (
  'url' => 'https://your_3cx_server:5001/webclient/#/call',
  'url_parameters' => array (
    'phone' => 'phone',
  ),
),
extensions/combodo-telephony-integration.txt · Last modified: 2024/08/27 19:10 by 127.0.0.1
Back to top
Contact us