Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

merge #349 scripting hooks #374

Merged
merged 31 commits into from
Jan 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6d127dc
#349 stubs/initial work
syjer Dec 27, 2017
5dd972a
#349 add support for inserting/updating/deleting scripts
syjer Jan 2, 2018
c95fe35
suppress unchecked warnings, prevent NPE
cbellone Jan 3, 2018
7c487c7
#349 refactor script: remove configuration column, add extension mana…
syjer Jan 3, 2018
4f8f850
#349 extension manager: call the async script service
syjer Jan 3, 2018
29cda57
#349 wire up async script execution
syjer Jan 3, 2018
1bf85f1
#349 unused import
syjer Jan 3, 2018
bd88745
#349 mysql/pgsql scripts
syjer Jan 3, 2018
6fd3cf5
#349 mysql: fix sql scripts
syjer Jan 3, 2018
02df411
use JSON.stringify/GSON.fromJSON to simplify the java<->js interop
syjer Jan 3, 2018
57ad6cd
#349 add missing fk
syjer Jan 4, 2018
016c05b
#349 add hook for handling invoice generation scripts
syjer Jan 4, 2018
5000ecf
#349 add hook for handling invoice generation scripts, use optional
syjer Jan 4, 2018
fb567ee
#349 fix test
syjer Jan 4, 2018
5593e94
#349 add sample
syjer Jan 4, 2018
6864a61
#349 keep the order of active scripts always the same when loading them
syjer Jan 4, 2018
2526c22
#349 - UI refinements - first draft
cbellone Jan 4, 2018
1409ba0
#349 refactor script -> extension
syjer Jan 5, 2018
75fd9b3
#349 support global script too
syjer Jan 5, 2018
a3b0448
#349 refactor: deleteReservations -> deleteReservation, add new event…
syjer Jan 5, 2018
9f906e2
#349 - UI refinements
cbellone Jan 5, 2018
ffdcff0
#349 - UI refinements
cbellone Jan 5, 2018
8fe25e4
#349 - UI refinements
cbellone Jan 5, 2018
f58811f
#349 - split
cbellone Jan 5, 2018
20b62c9
#349 fix ui
syjer Jan 5, 2018
18a9af8
#349 switch from . to - as a path separator, handle correctly update,…
syjer Jan 5, 2018
9088ddd
#349 wire up RESERVATION_EXPIRED and RESERVATION_CANCELLED events
syjer Jan 6, 2018
ba05cc7
#349 - display event name
cbellone Jan 6, 2018
663ecc9
#349 - rename events
cbellone Jan 7, 2018
c4d1eeb
#349 - basic documentation
cbellone Jan 7, 2018
5b45644
#349 - fix typo
cbellone Jan 7, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions docs/extensions-howto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# How to write an extension

Extensions allows you to link alf.io with your existing tools, such as:

* Billing/Accounting systems
* CRMs
* Additional Email marketing services (Mailjet, ...)
* Custom notifications (Slack, Telegram, etc.)

## how it works

Extensions can be added and modified only by the Administrator.
For security and stability reasons, it is not possible to do that with less privileged users.

Each extension consists of a JavaScript script, you can find a sample below:

```javascript
/**
* The script metadata object describes whether or not your extension should be invoked asynchronously, and which events it supports
* @returns {{ async: boolean, events: string[] }}
*/
function getScriptMetadata() {
return {
async: false,
events: [
//supported values:
//'RESERVATION_CONFIRMED', //fired on reservation confirmation. No results expected.
//'RESERVATION_EXPIRED', //fired when reservation(s) expired
//'RESERVATION_CANCELLED', //fired when reservation(s) are cancelled
//'TICKET_ASSIGNED', //fired on ticket assignment. No results expected.
//'WAITING_QUEUE_SUBSCRIPTION', //fired on waiting queue subscription. No results expected.
'INVOICE_GENERATION' //fired on invoice generation. Returns the invoice model.
]
};
}

/**
* Executes the extension.
* @param scriptEvent
* @returns Object
*/
function executeScript(scriptEvent) {
log.warn('hello from script with event: ' + scriptEvent);
//this sample calls the https://csrng.net/ website and generates a random invoice number
var randomNumber = restTemplate.getForObject('https://csrng.net/csrng/csrng.php?min=0&max=100', Java.type('java.util.ArrayList').class)[0].random;
log.warn('the invoice number will be ' + randomNumber)
return {
invoiceNumber: randomNumber
};
}
```

each extension is registered to one or more Application Events, and is fired as soon as the Application Event occurs.

## Scope Variables

alf.io provides some objects and properties to the script in the script scope:

* **log** Log4j logger
* **restTemplate** Spring Framework's [RestTemplate](https://docs.spring.io/spring/docs/4.3.13.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html)
* **GSON** Google's [JSON parser/generator](http://static.javadoc.io/com.google.code.gson/gson/2.8.2/com/google/gson/Gson.html)
* **returnClass** `java.lang.Class<?>` the expected result type

other event-related variables are also injected in the scope

## Supported Application Events

#### RESERVATION_CONFIRMED

extensions will be invoked **asynchronously** once a reservation has been confirmed.

##### params

* **reservation**: [TicketReservation](https://github.com/alfio-event/alf.io/blob/master/src/main/java/alfio/model/TicketReservation.java)


#### RESERVATION_EXPIRED

extensions will be invoked **synchronously** once one or more reservations have expired.

##### params
* **event**: [Event](https://github.com/alfio-event/alf.io/blob/master/src/main/java/alfio/model/Event.java)
* **reservationIds**: String[] - the reservation IDs

##### expected result type
boolean

#### RESERVATION_CANCELLED

extensions will be invoked **synchronously** once one or more reservations have been cancelled.

##### params
* **event**: [Event](https://github.com/alfio-event/alf.io/blob/master/src/main/java/alfio/model/Event.java)
* **reservationIds**: String[] - the reservation IDs

##### expected result type
boolean

#### TICKET_ASSIGNED

extensions will be invoked **asynchronously** once a ticket has been assigned.

##### params

* **ticket**: [Ticket](https://github.com/alfio-event/alf.io/blob/master/src/main/java/alfio/model/Ticket.java)

#### WAITING_QUEUE_SUBSCRIBED

extensions will be invoked **asynchronously** once someone subscribes to the waiting queue.

##### params

* **waitingQueueSubscription**: [WaitingQueueSubscription](https://github.com/alfio-event/alf.io/blob/master/src/main/java/alfio/model/WaitingQueueSubscription.java)

#### INVOICE_GENERATION

extensions will be invoked **synchronously** while generating an invoice.

##### params
* **event**: [Event](https://github.com/alfio-event/alf.io/blob/master/src/main/java/alfio/model/Event.java)
* **reservationId**: String - the reservation ID
* **email**: String - contact email
* **customerName**: String
* **userLanguage**: String - ISO 639-1 2-letters language code
* **billingAddress**: String - the billing Address
* **reservationCost**: [TotalPrice](https://github.com/alfio-event/alf.io/blob/master/src/main/java/alfio/model/TotalPrice.java)
* **invoiceRequested**: boolean - whether or not the user has requested an invoice or just a receipt
* **vatCountryCode**: String - the EU country of business of the customer, if any
* **vatNr**: String - Customer's VAT Number
* **vatStatus**: [VatStatus](https://github.com/alfio-event/alf.io/blob/master/src/main/java/alfio/model/PriceContainer.java#L37), see [#278](https://github.com/alfio-event/alf.io/issues/278)

##### expected result type

[InvoiceGeneration](https://github.com/alfio-event/alf.io/blob/master/src/main/java/alfio/model/extension/InvoiceGeneration.java) - The invoice content, currently limited to the invoice number.

## Methods

#### getScriptMetadata

This methods returns the actual configuration options and capabilities of the extension.
It **must** return a JSON object with the following properties:

* async *boolean*: whether or not the script should be invoked asynchronously.
* events *string[]*: list of supported events
* configuration *{(key: string): string}*: the extension configuration (WIP)

#### executeScript

the actual event handling. Parameters and return types are event-dependent.
2 changes: 1 addition & 1 deletion src/main/java/alfio/config/DataSourceConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
@EnableTransactionManagement
@EnableScheduling
@EnableAsync
@ComponentScan(basePackages = {"alfio.manager"})
@ComponentScan(basePackages = {"alfio.manager", "alfio.extension"})
@Log4j2
public class DataSourceConfiguration implements ResourceLoaderAware {

Expand Down
102 changes: 102 additions & 0 deletions src/main/java/alfio/controller/api/admin/ExtensionApiController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/

package alfio.controller.api.admin;

import alfio.manager.user.UserManager;
import alfio.model.ExtensionSupport;
import alfio.extension.Extension;
import alfio.extension.ExtensionService;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.Validate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.security.Principal;
import java.util.List;

@RestController
@AllArgsConstructor
@RequestMapping("/admin/api/extensions")
public class ExtensionApiController {

private static final String SAMPLE_JS;

static {
try {
SAMPLE_JS = new String(Files.readAllBytes(new File(ExtensionApiController.class.getResource("/alfio/extension/sample.js").toURI()).toPath()));
} catch (URISyntaxException | IOException e) {
throw new IllegalStateException("cannot read sample file", e);
}
}

private final ExtensionService extensionService;
private final UserManager userManager;


@RequestMapping(value = "", method = RequestMethod.GET)
public List<ExtensionSupport> listAll(Principal principal) {
ensureAdmin(principal);
return extensionService.listAll();
}

@RequestMapping(value = "/sample", method = RequestMethod.GET)
public ExtensionSupport getSample() {
return new ExtensionSupport("-", "", null, true, true, SAMPLE_JS);
}

@RequestMapping(value = "", method = RequestMethod.POST)
public void create(@RequestBody Extension script, Principal principal) {
ensureAdmin(principal);
extensionService.createOrUpdate(null, null, script);
}

@RequestMapping(value = "{path}/{name}", method = RequestMethod.POST)
public void update(@PathVariable("path") String path, @PathVariable("name") String name, @RequestBody Extension script, Principal principal) {
ensureAdmin(principal);
extensionService.createOrUpdate(path, name, script);
}

@RequestMapping(value = "{path}/{name}", method = RequestMethod.GET)
public ResponseEntity<ExtensionSupport> loadSingle(@PathVariable("path") String path, @PathVariable("name") String name, Principal principal) throws UnsupportedEncodingException {
ensureAdmin(principal);
return extensionService.getSingle(URLDecoder.decode(path, "UTF-8"), name).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
}


@RequestMapping(value = "{path}/{name}", method = RequestMethod.DELETE)
public void delete(@PathVariable("path") String path, @PathVariable("name") String name, Principal principal) {
ensureAdmin(principal);
extensionService.delete(path, name);
}

@RequestMapping(value = "/{path}/{name}/toggle/{enable}", method = RequestMethod.POST)
public void toggle(@PathVariable("path") String path, @PathVariable("name") String name, @PathVariable("enable") boolean enable, Principal principal) {
ensureAdmin(principal);
extensionService.toggle(path, name, enable);
}

private void ensureAdmin(Principal principal) {
Validate.isTrue(userManager.isAdmin(userManager.findUserByUsername(principal.getName())));
}
}
31 changes: 31 additions & 0 deletions src/main/java/alfio/extension/Extension.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/

package alfio.extension;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class Extension {

private final String path;
private final String name;
private final String script;
private final boolean enabled;
}
30 changes: 30 additions & 0 deletions src/main/java/alfio/extension/ExtensionMetadata.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* This file is part of alf.io.
*
* alf.io is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* alf.io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with alf.io. If not, see <http://www.gnu.org/licenses/>.
*/

package alfio.extension;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

@Getter
@AllArgsConstructor
public class ExtensionMetadata {
boolean async;
List<String> events;
}
Loading