Skip to content

Instantly share code, notes, and snippets.

@ClementRoyer
Last active August 2, 2024 12:34
Show Gist options
  • Save ClementRoyer/e223f2adf80882de2f4384f1aaa4cfdd to your computer and use it in GitHub Desktop.
Save ClementRoyer/e223f2adf80882de2f4384f1aaa4cfdd to your computer and use it in GitHub Desktop.
Sample of WebSocket connexion from FIORI (sapui5) to SAP using Abap Push Channel and Abap Messaging Channel (APC/AMC)
<core:FragmentDefinition
xmlns:core="sap.ui.core"
xmlns="sap.m">
<VBox width="100%" height="100%"
renderType="Bare">
<!-- Toolbar spacer -->
<VBox class="sapUiTinyMarginTop"
height="2.75rem"
width="100%">
<Text text=" "/>
</VBox>
<!-- Header -->
<HBox class="sapUiSmallMarginTop sapUiSmallMarginBottom">
<Button
icon="sap-icon://decline"
press=".ChatController.onCloseChat"/>
<VBox width="100%"
alignItems="Center">
<Title text="{i18n>chat.title}" titleStyle="H3"/>
</VBox>
</HBox>
<!-- Seperator -->
<VBox class="sapMTBStandard"/>
<!-- Message -->
<ScrollContainer id="ScrollContainer"
class="sapUiSmallMarginEnd"
width="100%"
height="100%"
vertical="true">
<VBox
width="100%"
alignItems="Center"
visible="{DisplayConfig>/Chat/EnableLoadMore}">
<Button class="sapUiSmallMarginTopBottom"
icon="sap-icon://drill-up"
press=".ChatController.onPressLoadMore"/>
</VBox>
<VBox items="{Chat>/}" >
<VBox>
<VBox visible="{= ${Chat>SenderName} !== 'System' }">
<FeedListItem
sender="{Chat>SenderName}"
iconDensityAware="false"
iconInitials="{= ${Chat>SenderName}.split(' ')[0][0] + ${Chat>SenderName}.split(' ')[1][0] }"
iconSize="XS"
timestamp="{Chat>TimeStamp}"
text="{Chat>Content}"
convertLinksToAnchorTags="All"/>
</VBox>
<!-- Systeme Text -->
<VBox visible="{= ${Chat>SenderName} === 'System' }">
<Label
class="sapUiTinyMarginTop"
text="{i18n>chat.wss.info} {Chat>Content}"
visible="{= ${Chat>SenderName} === 'System' }"/>
</VBox>
</VBox>
</VBox>
</ScrollContainer>
<!-- Input Message -->
<FeedInput
post=".ChatController.onSendPress"
class="sapUiSmallMarginTopBottom" />
</VBox>
</core:FragmentDefinition>
/* eslint-disable no-console */
sap.ui.define([
"xx/xxx/xx/investmenthub/detail/controller/extension/popup/ErrorMessagePopover",
"sap/ui/core/mvc/ControllerExtension",
"sap/ui/core/ws/SapPcpWebSocket",
"sap/ui/model/json/JSONModel",
"sap/ca/ui/model/type/Time"
], function (
ErrorMessagePopover,
ControllerExtension,
SapPcpWebSocket,
JSONModel,
Time
) {
"use strict";
return ControllerExtension.extend("xx.xxx.xx.investmenthub.detail.controller.extension.popup.ChatController", {
/** @private **/
ErrorHandler: ErrorMessagePopover,
/**
* @private
**/
ownerController: undefined,
/** @private **/
_chatMessagePageSize: 25,
/**
* ! Warning, WebSocket don't work in BAS, deploy to test/use it
*
* @private
* @type {sap.ui.core.ws.SapPcpWebSocket}
**/
webSocket: undefined,
override: {
onInit: function () {
this.ownerController = this.base.getView().getController();
this.ErrorHandler = this.ownerController.ErrorHandler;
}
},
/* * * * * *\
* PUBLIC *
\* * * * * */
/**
* Start fetching messages and conntect to WebSocket
*
* @public
**/
start: function() {
this.loadMessages().then(() => {
this.connectToLiveChat();
});
},
/**
* Load Chat Messages
*
* @public
* @param {string} [top] - Number of messages to load
* @param {boolean} [keepPrevious] - Keep previously fetch messages
* @returns {Promise} - Http response
**/
loadMessages: function(top = this._chatMessagePageSize, keepPrevious = false) {
/** @type {sap.ui.model.odata.v2.ODataModel} */
const model = this.getModel();
const chatModel = this.getChatModel();
const project = this.getProject().Project;
const offset = keepPrevious ? chatModel.getProperty("/").length : 0;
return new Promise((resolve, reject) => {
model.metadataLoaded().then(() => {
model.read("/ZC_FIO_InvestmentHubChat", {
urlParameters: {
"$top": top,
"$skip": offset,
"$orderby": "TimeStamp desc" /** Use DESC to get last messages first **/
},
filters: [ new sap.ui.model.Filter("ProjectGUID", sap.ui.model.FilterOperator.EQ, project) ],
success: (oData) => {
var messages = oData.results;
if (messages.length < top) {
/** All messages haves been loaded */
this.getDisplayConfigModel().setProperty("/Chat/EnableLoadMore", false);
}
if (keepPrevious) {
const currentMessages = chatModel.getProperty("/") || [];
messages = messages.concat(currentMessages);
}
/** Sort back to ascending **/
messages.sort((a, b) => a.TimeStamp - b.TimeStamp);
chatModel.setProperty("/", messages);
resolve(messages);
},
error: (oError) => {
this.ErrorHandler.showError(oError);
reject(oError);
}
});
});
});
},
/**
* Connect to Live Chat (WebSocket)
*
* @public
**/
connectToLiveChat: function() {
this._connectWebSocket();
},
/* * * * * * *\
* Formatter *
\* * * * * * */
/* * * * *\
* Event *
\* * * * */
onSenderPress() {
},
/**
* Event: Press send on a chat message
*
* @listens press
* @param {sap.ui.base.Event} oEvent - Event object
**/
onSendPress: function(oEvent) {
const messageValue = oEvent.getParameter("value");
this._sendMessage(messageValue);
},
/**
* Event: Open Chat
*
* @listens press
* @param {sap.ui.base.Event} oEvent - Event object
**/
onChatOpen: function (oEvent) {
var chatPanelId = this.ownerController.getChatId();
var chatPanel = this.byId(chatPanelId);
var isVisible = chatPanel.getVisible();
if (!isVisible) {
chatPanel.setVisible(true);
chatPanel.addStyleClass("open");
}
this._scrollToBottom();
},
/**
* Event: Press close chat
*
* @listens press
* @param {sap.ui.base.Event} oEvent - Event object
*/
onCloseChat: function (oEvent) {
var chatPanelId = this.ownerController.getChatId();
var chatPanel = this.byId(chatPanelId);
var isVisible = chatPanel.getVisible();
if (isVisible) {
chatPanel.setVisible(false);
chatPanel.removeStyleClass("open");
}
},
/**
* Event: On Press Load More in chat
*
* @listens press
* @param {sap.ui.base.Event} oEvent - Event object
**/
onPressLoadMore: function(oEvent) {
this.loadMessages(this._chatMessagePageSize, true);
},
/* * * * * *\
* Private *
\* * * * * */
/**
* Scroll chat to first element (down)
*
* @private
**/
_scrollToBottom: function() {
// Ensure changes are applied before scrolling
sap.ui.getCore().applyChanges();
// Scroll to the bottom after a delay to ensure rendering is complete
setTimeout(function() {
const scrollContainer = this.byId("ScrollContainer");
scrollContainer.scrollTo(0, 9999999999999);
}.bind(this), 0);
},
/**
* Start WebSocket Connection
*
* @private
**/
_connectWebSocket: function() {
const project = this.getProject().Project;
/** SapPcpWebSocket doesn't work in BAS so change URL to use local sap URL **/
const host = location.host.includes(".cloud.sap") ? "xxxxxx.xxx.com:xxxxx" : location.host;
const protocol = location.protocol === "https:" ? "wss" : "ws";
const url = `${protocol}://${host}/sap/bc/apc/sap/zps_investment_hub_chat?Project=${project}`;
this.webSocket = new SapPcpWebSocket(url);
this.webSocket.attachOpen((oEvent) => {
console.info("WebSocket connection opened");
});
this.webSocket.attachClose((oEvent) => {
this.addSystemMessage(this.i18n("chat.wss.connection.closed"));
console.info("WebSocket connection closed");
});
this.webSocket.attachError((oEvent) => {
this.addSystemMessage(this.i18n("chat.wss.connection.error"));
console.error("WebSocket connection error", oEvent);
});
this.webSocket.attachMessage((oEvent) => {
console.info("WebSocket message received", oEvent);
const params = oEvent.getParameters();
// const user = params.pcpFields.User;
const userName = params.pcpFields.UserName;
const content = params.data;
if (!userName || userName === "") {
this.addSystemMessage(content);
} else {
this.addMessageToModel(userName, content);
}
});
},
/**
* Send a chat message
*
* @private
* @param {string} message - Message to send
**/
_sendMessage: function(message) {
if (!message) {
return;
}
this.webSocket.send(message);
},
/**
* Add a system message to message queue
*
* @private
* @param {string} message - Message to display
**/
addSystemMessage: function(message) {
this.addMessageToModel("System", message);
},
/**
* Add a message to the chat model
*
* @private
* @param {string} userName - UserName
* @param {string} message - Message Content
**/
addMessageToModel: function(userName, message) {
const chatModel = this.getChatModel();
const messages = chatModel.getProperty("/");
messages.push({SenderName: userName, Content: message, TimeStamp: new Date()});
chatModel.setProperty("/", messages);
this._scrollToBottom();
},
/* * * * *\
* Utils *
\* * * * */
/**
* Select by ID
*
* @private
* @param {string} objectID - The object you are looking for
* @return {Object} - your object
**/
byId: function(objectID) {
return this.ownerController.byId(objectID);
},
/**
* Create a new model
*
* @private
* @param {any} content - Your object
* @param {string} name - Your new model name
* @return {sap.ui.model.Model} - The new model
**/
_createModel: function(content, name) {
const view = this.getView();
view.setModel(new JSONModel(content), name);
return view.getModel(name);
},
/**
* Create/update a model
*
* @private
* @param {any} oContent - the content of the new model
* @param {string} [sModel] - name of the model
* @return {sap.ui.model.odata.v2.ODataModel} - the model
**/
setModel: function (oContent, sModel) {
return this.getView().setModel(oContent, sModel);
},
/**
* Get a model
*
* @private
* @param {string} [sModel] - name of the model
* @return {sap.ui.model.odata.v2.ODataModel} - the model
**/
getModel: function (sModel) {
return this.getView().getModel(sModel);
},
/**
* Get Project Model
*
* @private
* @return {any} - Project model
**/
getProject: function () {
return this.ownerController.getProject();
},
/**
* Get Chat Model
*
* @private
* @return {sap.ui.model.json.JSONModel} - the model
**/
getChatModel: function () {
return this.getView().getModel("Chat");
},
/**
* Get Chat Model
*
* @private
* @return {sap.ui.model.json.JSONModel} - the model
**/
getDisplayConfigModel: function () {
return this.ownerController.getDisplayConfigModel();
},
/**
* Get view from owner controller
*
* @private
* @return {sap.ui.core.mvc.View} View
**/
getView() {
return this.ownerController.getView();
},
/**
* i18n base method encapsulation
*
* @private
* @param {string} sKey - key
* @param {string[]} [args] - parameters
* @returns {string} - i18n result
**/
i18n: function (sKey, args = []) {
let _i18n = this.getModel("i18n").getResourceBundle();
return _i18n ? _i18n.getText(sKey, args) : sKey;
}
});
});
CLASS zcl_apc_wsp_ext_zps_investment DEFINITION
PUBLIC
INHERITING FROM cl_apc_wsp_ext_stateless_base
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
METHODS:
if_apc_wsp_extension~on_message REDEFINITION,
if_apc_wsp_extension~on_start REDEFINITION.
PROTECTED SECTION.
PRIVATE SECTION.
CONSTANTS:
c_application_id TYPE amc_application_id VALUE 'ZPS_INVESTMENT_HUB_CHAT',
c_channel_id TYPE amc_channel_id VALUE '/chat'.
DATA: message_manager TYPE REF TO if_apc_wsp_message_manager.
METHODS:
"! Subscribe to AMC sub channel
"!
"! @parameter i_context | Request Context
"! @parameter channel | Sub Channel to subscribe
subscribe_to_channel
IMPORTING VALUE(i_context) TYPE REF TO if_apc_wsp_server_context
VALUE(channel) TYPE if_abap_channel_types=>ty_amc_channel_extension_id,
"! Send message to any connected sessions
"!
"! @parameter message_text | Text Message to send
send_message_to_channel
IMPORTING VALUE(channel) TYPE if_abap_channel_types=>ty_amc_channel_extension_id
VALUE(message_text) TYPE string
VALUE(user) TYPE syst_uname DEFAULT sy-uname,
"! Send message back to single user
"!
"! @parameter message_text | Text Message to send
send_message_back
IMPORTING VALUE(message_text) TYPE string,
"! Retrieve username from user sap id
"!
"! @parameter user | User SAP ID
"! @parameter r_username | User full name
get_username
IMPORTING VALUE(user) TYPE syst_uname
RETURNING VALUE(r_username) TYPE string,
"! Save message to chat table
"!
"! @parameter raw_project_guid | Raw Project GUID (Fiori format)
"! @parameter message | Message content
save_message_to_db
IMPORTING VALUE(raw_project_guid) TYPE string
VALUE(message) TYPE string.
ENDCLASS.
CLASS zcl_apc_wsp_ext_zps_investment IMPLEMENTATION.
METHOD if_apc_wsp_extension~on_message.
"" Receive a new message ""
CONSTANTS: separator TYPE string VALUE ':'.
DATA(username) = me->get_username( sy-uname ).
me->message_manager = i_message_manager.
DATA(project) = i_context->get_initial_request( )->get_form_field( 'Project' ).
IF project IS INITIAL.
"" Message can't be processed without a project ID ""
RETURN.
ENDIF.
TRY.
DATA(received_message) = i_message->get_text( ).
DATA(pcp_message) = cl_amc_message_type_pcp=>deserialize_text( received_message ).
DATA(message_text) = pcp_message->get_text( ).
me->send_message_to_channel( channel = CONV #( project )
message_text = |{ message_text }| ).
me->save_message_to_db( raw_project_guid = project
message = message_text ).
CATCH cx_apc_error.
"handle exception
ENDTRY.
ENDMETHOD.
METHOD if_apc_wsp_extension~on_start.
"" New Connection ""
DATA(username) = me->get_username( sy-uname ).
me->message_manager = i_message_manager.
DATA(project) = i_context->get_initial_request( )->get_form_field( 'Project' ).
me->send_message_to_channel( channel = CONV #( project )
message_text = |{ username } a rejoint le chat.|
user = 'System' ).
me->subscribe_to_channel( i_context = i_context channel = CONV #( project ) ).
me->send_message_back( |Connexion au chat réussi.| ).
ENDMETHOD.
METHOD subscribe_to_channel.
"" Subscribe to sub channel ""
"" Bind the WebSocket connection to the AMC channel
TRY.
DATA(binding_manager) = i_context->get_binding_manager( ).
binding_manager->bind_amc_message_consumer( i_application_id = c_application_id
i_channel_id = c_channel_id
i_channel_extension_id = channel ).
CATCH cx_apc_error INTO DATA(lx_apc_error).
"" Error
ENDTRY.
ENDMETHOD.
METHOD send_message_to_channel.
"" Send Message to any connected session ""
DATA(user_name) = COND #( WHEN user <> 'System' THEN get_username( user ) ELSE 'System' ).
TRY.
DATA(message_channel) = CAST if_amc_message_producer_pcp( cl_amc_channel_manager=>create_message_producer(
i_application_id = c_application_id
i_channel_id = c_channel_id
i_channel_extension_id = channel ) ).
DATA(pcp_message) = cl_ac_message_type_pcp=>create( ).
"" Add header technical data
pcp_message->set_field( i_name = 'UserName' i_value = user_name ).
pcp_message->set_field( i_name = 'User' i_value = CONV #( user ) ).
"" Add Text
pcp_message->set_text( message_text ).
"" Send Message
message_channel->send( pcp_message ).
CATCH cx_amc_error.
"" Error
CATCH cx_ac_message_type_pcp_error.
"" Error
ENDTRY.
ENDMETHOD.
METHOD send_message_back.
"" Send Message to any connected session ""
CHECK message_manager IS BOUND.
CHECK strlen( message_text ) > 0.
TRY.
DATA(message) = message_manager->create_message( ).
message->set_text( message_text ).
message_manager->send( message ).
CATCH cx_apc_error.
"" Error
ENDTRY.
ENDMETHOD.
METHOD get_username.
"" Retrieve user full name ""
CHECK user IS NOT INITIAL.
SELECT SINGLE UserName
FROM ZI_User
WHERE UserID = @user
INTO @r_username.
ENDMETHOD.
METHOD save_message_to_db.
"" Save current message to DB ""
CHECK raw_project_guid IS NOT INITIAL
AND message IS NOT INITIAL.
DATA(project) = cl_soap_wsrmb_helper=>convert_uuid_hyphened_to_raw( raw_project_guid ).
DATA(chat_api) = NEW zcl_ps_investment_hub_chat( project ).
chat_api->add_message( CONV #( message ) ).
ENDMETHOD.
ENDCLASS.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment