On this page
SiteGUI's Standard App is a standard PHP class with a namespace, the class should contain some configuration settings for the app and several methods for handling app records. Methods should always return an array containing a key name "result" with the value "success" to indicate successful execution.
Standard App must be submitted to SiteGUI Appstore for reviewing before it can be deployed. Once your app has been developed, simply use the button next to your app name to submit it to the Appstore.
App Settings (visibility, category) can be defined in a static method named config(), these values will determine whether the app is included in the app menu or not and whether the app records are accessible by different user groups. App can also create Per Site Settings by adding custom fields to allow Site Managers to input their own configuration values for the app. To create a custom field, just define field id (name), type, display name (label) and description for it. You can also set field visibility (visibility), a default value (value), whether the field is required or optional (is => required/optional/multiple) or accepting multiple values (select and lookup field only), and specify options if field supports them. The following field types are supported:
Below is an example of the static method config()
public static function config($property = null) {
$config['app_category'] = 'Bot';
$config['app_permissions'] = [
'staff_read' => 1,
'staff_write' => 1,
'staff_manage' => 1,
'client_read' => 1,
'client_write' => 1,
'client_delete' => 0,
'staff_read_permission' => '',
'staff_write_permission' => '',
'staff_manage_permission' => '',
'client_read_permission' => '',
'client_write_permission' => '',
'client_delete_permission' => '',
];
$config['app_configs'] = [
'client_id' => [
'label' => 'Client ID',
'type' => 'text',
'visibility' => 'editable',
'size' => '6',
'value' => '',
'description' => 'Enter the Client ID for the App created at https://api.slack.com/apps',
],
'client_secret' => [
'label' => 'Client Secret',
'type' => 'password',
'visibility' => 'editable',
'size' => '6',
'value' => '',
'description' => 'Enter the Client Secret provided by Slack',
],
'oauth' => [
'label' => 'Connect Slack',
'type' => 'oauth',
'value' => '',
'options' => [
0 => 'https://slack.com/oauth/v2/authorize?scope=chat%3Awrite%2Cchat%3Awrite.public%2Capp_mentions%3Aread%2Cchannels%3Aread%2Cchannels%3Ahistory%2Cgroups%3Ahistory%2Cim%3Ahistory%2Cmpim%3Ahistory&redirect_uri={redirect_uri}&state={state}&client_id={client_id}',
1 => 'https://slack.com/api/oauth.v2.access',
2 => 'client_id={client_id}&client_secret={client_secret}&code={code}',
3 => '',
4 => '',
5 => '',
6 => '',
],
'description' => 'Authorize to retrieve access token automatically',
],
'access_token' => [
'label' => 'Access Token',
'type' => 'password',
'visibility' => 'hidden',
'size' => '6',
'value' => '',
'description' => 'Enter the Access Token provided by Slack',
],
'refresh_token' => [
'label' => 'Refresh Token',
'type' => 'password',
'visibility' => 'hidden',
'size' => '6',
'value' => '',
'description' => 'Enter the Refresh Token provided by Slack',
],
'bot_user_id' => [
'label' => 'Bot User ID',
'type' => 'text',
'visibility' => 'hidden',
'size' => '6',
'value' => '{oauth::bot_user_id}',
'description' => '',
],
'callback' => [
'label' => 'Webhook Listener',
'type' => 'text',
'description' => 'Enter any code to enable Webhook Listener for Slack',
'value' => substr($_SESSION['token'], 8, 8),
],
'fieldset1' => [
'type' => 'fieldset',
'fields' => [
'channel' => [
'label' => 'Channel',
'visibility' => 'editable',
'is' => 'optional',
'type' => 'text',
'value' => '',
],
'ai_agent' => [
'label' => 'Agent',
'description' => 'Agent to react to events in the channel',
'visibility' => 'editable',
'is' => 'optional',
'type' => 'lookup',
'options' => [
'From::lookup' => 'From::lookup',
'activation', 'Scope::Agent'
],
],
],
],
];
$config['app_fields'] = [
'title' => [
'label' => 'Channel',
'type' => 'text',
'size' => '6',
'value' => '',
'description' => 'Enter the Slack public/private or IM channel',
],
'content' => [
'label' => 'Text Message',
'type' => 'textarea',
'size' => '6',
'value' => '',
'description' => '',
],
];
$config['app_columns']['title'] = 'Channel';
if ($property AND isset($config[ $property ]) ) {
$response[ $property ] = $config[ $property ];
} else {
$response['config'] = $config;
}
$response['result'] = 'success';
return $response;
}
When the app is initialized, it should receive the configuration values for the running site and for the app itself (per site settings), these values are stored in $this->config (sample below). The View object is also available for the app to use at $this->view.
{
"site": {
"id": 1000,
"name": "SiteGUI",
"url": "sitegui.com",
"account_url": "my.sitegui.com",
"template": "supersite",
"status": "Active",
"owner": 123456,
"language": "en",
"timezone": "America/New_York",
"editor": "wysiwyg",
"logo": "https://cdn.sitegui.com/public/templates/admin/bootstrap5/assets/img/logo.png",
"locales": {
"en": "english"
}
},
"app": {
"api_token": "xxxxxxx",
"mode": "advance",
"row_count": 25
}
}
SiteGUI renders content using a Layout and ViewBlocks. Layouts are important for creating visually appealing and functional user interfaces. They allow developers to organize the components of an application in a logical and intuitive way. A layout is an arrangement of BlockHolders that can be displayed at a relative position of a view, such as main, top, bottom, left or right. SiteGUI supports the following BlockHolders by default:
A layout may add more custom BlockHolders (should start with block_) but should support as many default BlockHolders as it could to make the layout usable with different applications which may attach ViewBlocks to those default BlockHolders.
A ViewBlock is actually an array containing the following keys:
One or more ViewBlocks can be attached to a BlockHolder. When attaching to one of the default BlockHolders, just remove the prefix block_ and use the position directly (i.e: top, left or main instead of block_top, block_left or block_main). For custom BlockHolders, please use the full block name. Any Smarty variable assigned/appended in each ViewBlock are available for all other ViewBlocks to use (and may be overwritten by other ViewBlocks). Any ViewBlock attached to a BlockHolder will have their rendered HTML output appended to a Smarty variable {$block_name} (block_name is the name of the block i.e: block_main, block_top, block_1, block_2, content_top) and displayed at the specified position in the layout.
The method edit() and clientView() are called when the record editor is opened to create a new or edit an existing record by staff or client respectively. These methods should return an array of SiteGUI ViewBlocks attached to supported BlockHolders to alter the look and feel of the editor. The main block may specify a template name to be rendered or use the default one (app_edit.tpl). If the default template is used, the main block should indicate which default inputs should be shown and may add custom fields to the editor.
The method update($page) and clientUpdate($page) are called whenever the app receives a submission from a staff or client respectively. These methods process the submitted data and then return them back to the main handler to store the data into the database. It is possible to bypass the saving of the data by returning a response with _abort set to true, in this case no record will be created/updated.
The method render() is called when a record is displayed. It receives the record data and returns an array of SiteGUI ViewBlocks that contain all variables and settings (layout, template name) to help the View object to render the record properly.
The method renderCollection() is called when a record category is displayed. It receives the category data and returns an array of SiteGUI ViewBlocks that contain all variables and settings (layout, template name) to help the View object to render the category properly.
The method callback($data) is triggered when a callback from an external site is received. While this method does not have access to the database, it can use an AI agent object at $this->config['agentObj'] to interact with any available AI agent using justRun($agent, $input, $thread) method. This method should provide a response code/message to the caller, process the received request and optionally prepare the data to send to the App. The data to send to the App should include the record ID or slug if present, the slug should not be duplicated. The data will be processed asynchronously in the background under the site owner account. Please check the sample app below for more details.
In order to enable the webhook listener, a config field 'callback' should be present, the value of this field will be appended to the webhook URL and served as the verifier code i.e: https://my.sitegui.com/account/cart/callback.json?app=App/Slack&verifier=215f0e99. App developer may use an optional, static method setCallback($config) to register the webhook URL with the external site if it supports setting up this URL programmatically.
<?php
namespace SiteGUI\App;
class Slack {
use \LiteGUI\Traits\Http;
protected $config;
protected $view;
public function __construct($site_config, $view = null){
$this->config = $site_config;
$this->view = $view;
}
public static function config($property = null) {
$config['app_permissions'] = [
'staff_write' => 1,
'client_write' => 1,
];
$config['app_configs'] = [
'client_id' => [
'label' => 'Client ID',
'type' => 'text',
'visibility' => 'editable',
'size' => '6',
'value' => '',
'description' => 'Enter the Client ID for the App created at https://api.slack.com/apps',
],
'client_secret' => [
'label' => 'Client Secret',
'type' => 'password',
'visibility' => 'editable',
'size' => '6',
'value' => '',
'description' => 'Enter the Client Secret provided by Slack',
],
'oauth' => [
'label' => 'Connect Slack',
'type' => 'oauth',
'value' => '',
'options' => [
0 => 'https://slack.com/oauth/v2/authorize?scope=chat%3Awrite%2Cchat%3Awrite.public%2Capp_mentions%3Aread%2Cchannels%3Aread%2Cchannels%3Ahistory%2Cgroups%3Ahistory%2Cim%3Ahistory%2Cmpim%3Ahistory&redirect_uri={redirect_uri}&state={state}&client_id={client_id}',
1 => 'https://slack.com/api/oauth.v2.access',
2 => 'client_id={client_id}&client_secret={client_secret}&code={code}',
3 => '',
4 => '',
5 => '',
6 => '',
],
'description' => 'Authorize to retrieve access token automatically',
],
'access_token' => [
'label' => 'Access Token',
'type' => 'password',
'visibility' => 'hidden',
'size' => '6',
'value' => '',
'description' => 'Enter the Access Token provided by Slack',
],
'refresh_token' => [
'label' => 'Refresh Token',
'type' => 'password',
'visibility' => 'hidden',
'size' => '6',
'value' => '',
'description' => 'Enter the Refresh Token provided by Slack',
],
'bot_user_id' => [
'label' => 'Bot User ID',
'type' => 'text',
'visibility' => 'hidden',
'size' => '6',
'value' => '{oauth::bot_user_id}',
'description' => '',
],
'callback' => [
'label' => 'Webhook Listener',
'type' => 'text',
'description' => 'Enter any code to enable Webhook Listener for Slack',
'value' => substr($_SESSION['token'], mt_rand(0,24), 8),
],
'fieldset1' => [
'type' => 'fieldset',
'fields' => [
'channel' => [
'label' => 'Channel',
'visibility' => 'editable',
'is' => 'optional',
'type' => 'text',
'value' => '',
],
'ai_agent' => [
'label' => 'Agent',
'description' => 'Agent to react to events in the channel',
'visibility' => 'editable',
'is' => 'optional',
'type' => 'lookup',
'options' => [
'From::lookup' => 'From::lookup',
'activation', 'Scope::Agent'
],
],
],
],
];
$config['app_fields'] = [
'title' => [
'label' => 'Channel',
'type' => 'text',
'size' => '6',
'value' => '',
'description' => 'Enter the Slack public/private or IM channel',
],
'content' => [
'label' => 'Text Message',
'type' => 'textarea',
'size' => '6',
'value' => '',
'description' => '',
],
];
$config['app_columns']['title'] = 'Channel';
if ($property AND isset($config[ $property ]) ) {
$response[ $property ] = $config[ $property ];
} else {
$response['config'] = $config;
}
$response['result'] = 'success';
return $response;
}
public function edit($page) {
if ( !empty($page) ){
$response['blocks']['main']['api']['page'] = $page;
}
$response['result'] = 'success';
return $response;
}
public function clientView($page) {
return $this->edit($page);
}
public function update($page){
$response['result'] = 'error';
$params['channel'] = $page['title'][ $this->config['site']['language']??'en' ]??$page['title']??'';
$params['text'] = strip_tags($page['content'][ $this->config['site']['language']??'en' ]??$page['content']??'', '<a>');
if ($params['channel'] AND $params['text']){
$slack_url = 'https://slack.com/api/';
$headers[] = 'Content-Type:application/json; charset=utf-8';
$headers[] = 'Authorization: Bearer '. $this->config['app']['access_token'];
$slack = $this->httpPost($slack_url . 'chat.postMessage', json_encode($params), $headers);
$slack = json_decode($slack, true);
if (!empty($slack['ok'])){
$response['result'] = 'success';
if (empty($page['id'])){
$slack2 = $this->httpGet($slack_url . 'conversations.info', $params, $headers);
$slack2 = json_decode($slack2, true);
if ( !empty($slack2['ok']) AND !empty($slack2['channel']['name']) ){
$page['name'] = $slack2['channel']['name'];
} else {
$page['name'] = $params['channel'];
}
}
unset($page['content']); //we dont keep content
$response['page'] = $page;
} else {
$response['message'] = 'Slack Error: '. $slack['error'];
$response['_abort'] = 1; //abort, dont save to db
}
}
return $response;
}
public function clientUpdate($page){
return $this->update($page);
}
public function callback($data) {
$data = json_decode($data??'', true);
$response['status']['result'] = 'error'; //default
if (empty($this->config['app']['callback']) OR empty($_GET['verifier']) OR $this->config['app']['callback'] != $_GET['verifier'] ){
$response['status']['code'] = 401;
$response['status']['message'] = 'Unauthorized';
} elseif ( array_key_exists('challenge', $data??[]) ){
header('Content-Type: text/plain');
echo $data['challenge'];
exit;
} elseif ( !empty($data['event']['text']??$data['event']['blocks']??$data['event']['attachments']) AND $data['event']['type'] == 'app_mention' ){
foreach ($this->config['app']['fieldset1']??[] as $handler) {
$handlers[ $handler['channel'] ] = current($handler['ai_agent']);
}
$agent = $handlers[ $data['event']['channel'] ]??
$handlers['Default']?? //default
$handlers['*']??null; //wildcard
if ( !empty($this->config['agentObj']) AND $agent ){
$slack_url = 'https://slack.com/api/';
$headers[] = 'Content-Type:application/json; charset=utf-8';
$headers[] = 'Authorization: Bearer '. $this->config['app']['access_token'];
$params['channel'] = $data['event']['channel'];
$params['latest'] = $data['event']['ts'];
$params['limit'] = 10;
$params['inclusive'] = true;
$slack = $this->httpPost($slack_url . 'conversations.history', json_encode($params), $headers);
$slack = json_decode($slack, true);
if (!empty($slack['ok']) AND !empty($slack['messages'])){
$input = '';
foreach (array_reverse($slack['messages']) as $message) {
if (!empty($message['subtype']) AND
in_array($message['subtype'], [
'bot_message',
'channel_join',
'channel_leave',
'channel_topic',
'channel_purpose',
'channel_name',
'message_deleted',
'channel_archive',
'channel_unarchive',
'group_join',
'group_leave',
'group_topic',
'group_purpose',
'group_name',
'tombstone',
])
){
continue;
}
$input .= $message['user'] .': '. ($message['text']?: json_encode($message['blocks']??$message['attachments']??'')) .'. ';
}
} else {
$input = $data['event']['text']?: json_encode($data['event']['blocks']??$data['event']['attachments']??'');
}
if ($input){
$data['agent'] = $this->config['agentObj']->justRun($agent, $input, $data['event']['channel']);
if ( !empty($data['agent']['status']['result']) AND
$data['agent']['status']['result'] == 'success' AND
!empty($data['agent']['output']['content'])
){
$content = $data['agent']['output']['content'];
} else {
$content = "I'm sorry. I couldn't process your query";
}
} else {
$content = "I'm sorry. I couldn't process empty query";
}
} else {
$content = "I'm sorry. The agent is on vacation and couldn't be located";
}
if ( !empty($content) ){
$this->update([
'title' => $data['event']['channel'],
'content' => $content,
]);
}
$response['status']['result'] = 'success';
} else {
$response['status']['result'] = 'success';
}
//set data to be sent to the App here
//ID (if available) or slug will be used to look for the record to update it, if no record exists, new one will be created
//$response['data']['id'] = 12345;
//$response['data']['slug'] = 'unique_slug';
//$response['data']['name'] = 'Record name';
//$response['data']['fields']['custom_field] = 'Custom';
//$response['data']['sub']['Comment]['content']['en'] = 'A comment';
return $response;
}
public static function setCallback($config) {
if (!empty($config['bot_token']) AND !empty($config['callback_url']) ){ //callback_url is provided at runtime
$slack_url = 'https://slack.com/api/';
$headers[] = 'Content-Type:application/json; charset=utf-8';
$headers[] = 'Authorization: Bearer '. $config['app']['access_token'];
$params['url'] = $config['callback_url'];
$params['secret_token'] = $config['callback'];
$slack = self::httpPostStatic($slack_url . '/setWebhook', json_encode($params), $headers);
$slack = json_decode($slack, true);
if (!empty($slack['ok'])){
return true;
}
}
return false;
}
}