diff --git a/docs/overlays/custom-widget.md b/docs/overlays/custom-widget.md index 7c2812f..7ab18cb 100644 --- a/docs/overlays/custom-widget.md +++ b/docs/overlays/custom-widget.md @@ -1,7 +1,7 @@ --- id: custom-widget -title: Custom Widget -sidebar_label: Custom Widget +title: Custom Widget & SE_API +sidebar_label: Custom Widget & SE_API description: Learn how to use the Custom Widget, the most powerful tool in StreamElements Overlay Editor, with HTML, CSS, JavaScript, and API access. tags: - custom widget @@ -15,6 +15,7 @@ keywords: - SE API - Widget customization - Stream overlay development +- SE_API sidebar_position: 2 --- @@ -23,7 +24,7 @@ sidebar_position: 2 This is the most powerful tool in SE Overlay editor. You can do a lot of things within this widget using HTML/CSS/JavaScript and accessing variables **Note:** -> You cannot access `document.cookie` nor `IndexedDB` via it (security reasons), so you need to keep your data elsewhere (accessible via HTTP API) or [SE_API](#se-api) store. +> You cannot access `document.cookie` nor `IndexedDB` via it (security reasons), so you need to keep your data elsewhere (accessible via HTTP API) or [SE_API](#se_api) store. ## Before Starting @@ -35,59 +36,160 @@ This article requires you to have an Overlay created with a Custom Widget added The Custom Code Editor is a powerful tool that allows you to write custom code in the Overlay Editor. For more information about the Custom Code Editor, please refer to the [Widget Structure](widget-structure.md) article. -## SE API +## SE_API -A global object is provided to access basic API functionality. The overlay's API token is also provided (via the `onWidgetLoad` event below) for more direct REST API calls to be used as authorization header. +SE_API is a set of functions that provide access to various personalized features for custom widgets. You can check the ones available below: -```javascript -SE_API.store.set('keyName', obj); // stores an object into our database under this keyName (multiple widgets using the same keyName will share the same data. keyName can be an alphanumeric string only). +- **.store** - A key-value store where objects can be saved globally in a permanent way. The stored data is accessible to other custom widgets under the same account. + +- **.cheerFilter** - Removes cheer emotes from a message. + +- **.counters** - Retrieves the value of a specific counter. + +- **.getOverlayStatus** - Gets the status of the overlay. + +- **.resumeQueue** - Resumes the queue immediately. + +- **.sanitize** - Sanitizes a message by removing bad words. + +- **.setField** - Sets a value for `fieldData[key]`. This does not save the value and works only in the Overlay Editor. + + + +### store.set + +Stores an object in the database under the specified keyName (`keyName` can be any alphanumeric string). + +**Note 1**: Setting a value will completely overwrite any existing data stored under the same key. Ensure you send the full object, as partial updates are not supported. + +**Note 2**: Keys cannot be removed once created. You can update their values as often as needed, but deletion is not possible so far. + +```js +const object = { "item1":"value1", "item2":"value2" } + +SE_API.store.set("keyName", object); +``` + +It emits an `onEventReceived` event for every custom widget. Example payload: + +```json +{ + "detail": { + "listener": "kvstore:update", + "event": { + "data": { + "key": "customWidget.keyName", + "value": { + "item1": "value1", + "item2": "value2" + } + } + } + } +} +``` + + +### .store.get + +Retrieves the contents stored under the specified "keyName" key. +The returned obj is the full object stored in the database. + +```js SE_API.store.get('keyName').then(obj => { - // obj is the object stored in the db, must be a simple object + console.log(obj); + /* + The result would look like below: + { + "item1": "value1", + "item2": "value2" + } + */ }); -SE_API.counters.get('counterName').then(counter => { - // counter is of the format { counter, value } +``` + +### .counters.get + +Gets the value of a counter. Your counters can be found at https://streamelements.com/dashboard/bot/counters + +For example, if you have a counter named "test" with a value of "15": + +```js +SE_API.counters.get('test').then(counter => { + console.log(counter); + /* + The result would look like below: + { + "id": "test", + "count":15 + } + */ }); +``` -SE_API.sanitize({ message: "Hello SomeVulgarWorld"}).then(sanityResult => { -/* - sanityResult={ - "result":{ - "message":"Hello Kreygasm" //Message after validation - }, - "skip":false // Should it be skipped according to rules - } -*/ + +### .sanitize +Sanitizes a message by filtering vulgar words. + +For example, if the word "vulgarWord" is marked as a bad word (configured in Tipping Settings) and the filter is set to "Replace bad words with emotes": + +```js +const vulgarSentence = "This is a sentence with vulgarWord"; + +SE_API.sanitize({ message: vulgarSentence }).then(sanityResult => { + console.log(sanityResult); + /* + The result would look like this: + { + "skip": false, // Indicates whether the message should be skipped according to the rules. + "result":{ + "message":"This is a sentence with 4Head" // Message after filtering + } + } + */ }); +``` + + +### .cheerFilter +Clears cheer emotes from message. + +```js +const message = "shamrock100 This is a cheer message cheer100 cheer100 cheerwhal100" SE_API.cheerFilter(message).then(cheerResult => { - // cheerResult = "message but without any cheers in it"; + console.log(cheerResult); + + /* + The result would look like below: + This is a cheer message + */ }); - -SE_API.setField('key', 'value'); // Sets the fieldData[key] = value. This does not save, so should be used with Editor Mode so the user can save. -SE_API.getOverlayStatus(); // { isEditorMode: true/false, muted: true/false } ``` -`SE_API.store.set` method emits an event received by every Custom Widget. Example payload: +### .getOverlayStatus + +Gets overlay Status. +```js +SE_API.getOverlayStatus().then(status => { + console.log(status); + + /* + Result would look like below: + { + "isEditorMode":true, + "muted":false + } + */ +}); +``` -```json -{ - "detail": { - "listener": "kvstore:update", - "event": { - "data": { - "key": "customWidget.keyName", - "value": { - "array": [ - 33, - "foobar" - ], - "date": "2021-03-15T08:46:10.919Z", - "test": 15 - } - } - } - } -} +### .setField +Sets a value for `fieldData[key]`. +It works only in the Overlay Editor itself and does not save the value. +The third parameter is an optional boolean that indicates whether the overlay should be reloaded after the value is set. The default is true. +```js +SE_API.setField('key', 'value', false); ``` ### resumeQueue method and widgetDuration property diff --git a/docs/overlays/events.md b/docs/overlays/events.md index 4ad6d89..3d41b13 100644 --- a/docs/overlays/events.md +++ b/docs/overlays/events.md @@ -18,26 +18,40 @@ sidebar_position: 3 # Events -Widget receives few types of native events during lifetime. +Widget receives few types of native events during lifetime. They are: +- **onWidgetLoad** - Triggers when the widget is loaded or refreshed. -## On Widget load +- **onEventReceived** - Triggers on every event like a chat message, new tip, new follower, subscriber, etc. + +- **onSessionUpdate** - Similar to `onEventReceived`, but it handles session events, like latest subscriber, goals, etc. Does not include chat messages. + +Example: + +```javascript +window.addEventListener('onWidgetLoad', function (obj) { + console.log(obj); // You can check obj value in the browser console. +}); +``` + + +## On Widget Load Event received upon widget is loaded. Contains information about fieldData (fields values), channel information (including apiKey) and session data. ```javascript window.addEventListener('onWidgetLoad', function (obj) { - //fancy stuff here + //fancy stuff here }); ``` In this scope `obj` has every information you could need to use. For better readibility, Let’s assign it: ```javascript window.addEventListener('onWidgetLoad', function (obj) { - let data=obj["detail"]["session"]["data"]; - let recents=obj["detail"]["recents"]; - let currency=obj["detail"]["currency"]; - let channelName=obj["detail"]["channel"]["username"]; - let apiToken=obj["detail"]["channel"]["apiToken"]; - let fieldData=obj["detail"]["fieldData"]; + let data = obj["detail"]["session"]["data"]; + let recents = obj["detail"]["recents"]; + let currency = obj["detail"]["currency"]; + let channelName = obj["detail"]["channel"]["username"]; + let apiToken = obj["detail"]["channel"]["apiToken"]; + let fieldData = obj["detail"]["fieldData"]; }); ``` @@ -145,7 +159,7 @@ window.addEventListener('onWidgetLoad', function (obj) { * `data["cheer-alltime-top-donation"]` - An array of top cheer all time * `data["cheer-alltime-top-donation"]["name"]` - Username * `data["cheer-alltime-top-donation"]["amount"]` - Cheer amount -* `data["cheer-session-top-donator"]` - Aan array of top cheerer since session start +* `data["cheer-session-top-donator"]` - An array of top cheerer since session start * `data["cheer-session-top-donator"]["name"]` - Username * `data["cheer-session-top-donator"]["amount"]` - Sum of the cheer amounts * `data["cheer-weekly-top-donator"]` - An array of top cheerer in past week @@ -158,70 +172,21 @@ window.addEventListener('onWidgetLoad', function (obj) { * `data["cheer-alltime-top-donator"]["name"]` - Username * `data["cheer-alltime-top-donator"]["amount"]` - Sum of the cheer amounts -#### Facebook -* `data["fan-latest"]["name"]` - Name of latest fan -* `data["fan-session"]["count"]` - Fans since session start -* `data["fan-week"]["count"]` - Fans this week -* `data["fan-month"]["count"]` - Fans this month -* `data["fan-total"]["count"]` - Total count of fans -* `data["fan-latest"]` - An array containing latest fan event -* `data["follower-latest"]["name"]` - Name of latest follower -* `data["follower-session"]["count"]` - Followers since session start -* `data["follower-week"]["count"]` - Followers this week -* `data["follower-month"]["count"]` - Followers this month -* `data["follower-goal"]["amount"]` - Followers goal -* `data["follower-total"]["count"]` - Total count of followers -* `data["share-goal"]["amount"]` - Amount of share goal -* `data["share-session"]["count"]` - Shares since session start -* `data["share-week"]["count"]` - Shares this week -* `data["share-month"]["count"]` - Shares this month -* `data["share-total"]["count"]` - Total count of shares -* `data["share-latest"]` - An array containing latest share event - * `data["share-latest"]["name"]` - Username - * `data["share-latest"]["amount"]` - amount -* `data["share-recent"]` - An array of latest share events with each element structure as in `share-latest` -* `data["stars-goal"]["amount"]` - Amount of stars goal -* `data["stars-session"]["count"]` - Stars since session start -* `data["stars-week"]["count"]` - Stars this week -* `data["stars-month"]["count"]` - Stars this month -* `data["stars-total"]["count"]` - Total count of stars -* `data["stars-latest"]` - An array containing latest stars event - * `data["stars-latest"]["name"]` - Username - * `data["stars-latest"]["amount"]` - amount -* `data["stars-recent"]` - An array of latest stars events with each element structure as in `stars-latest` -* `data["supporter-goal"]["amount"]` - Amount of supporter goal -* `data["supporter-session"]["count"]` - Supporters since session start -* `data["supporter-week"]["count"]` - Supporters this week -* `data["supporter-month"]["count"]` - Supporters this month -* `data["supporter-total"]["count"]` - Total count of supporters -* `data["supporter-latest"]` - An array containing latest supporter event - * `data["supporter-latest"]["name"]` - Username - * `data["supporter-latest"]["amount"]` - Amount -* `data["supporter-recent"]` - An array of latest supporter events with each element structure as in `supporter-latest` -* `data["videolike-goal"]["amount"]` - Amount of videolike goal -* `data["videolike-session"]["count"]` - Videolikes since session start -* `data["videolike-week"]["count"]` - Videolikes this week -* `data["videolike-month"]["count"]` - Videolikes this month -* `data["videolike-total"]["count"]` - Total count of videolikes -* `data["videolike-latest"]` - An array containing latest videolike event - * `data["videolike-latest"]["name"]` - Username - * `data["videolike-latest"]["amount"]` - Amount -* `data["videolike-recent"]` - An array of latest videolike events with each element structure as in `videolike-latest` - #### YouTube -* `data["sponsor-goal"]["amount"]` - Amount of sponsor goal -* `data["sponsor-session"]["count"]` - Sponsors since session start -* `data["sponsor-week"]["count"]` - Sponsors this week -* `data["sponsor-month"]["count"]` - Sponsors this month -* `data["sponsor-total"]["count"]` - Total count of sponsors -* `data["sponsor-latest"]` - An array containing latest sponsor event +OBS: Youtube member is used to be called sponsor, that's the reason the key is called "sponsor" +* `data["sponsor-goal"]["amount"]` - Amount of members goal +* `data["sponsor-session"]["count"]` - Members since session start +* `data["sponsor-week"]["count"]` - Members this week +* `data["sponsor-month"]["count"]` - Members this month +* `data["sponsor-total"]["count"]` - Total count of members +* `data["sponsor-latest"]` - An array containing latest member event * `data["sponsor-latest"]["name"]` - Username * `data["sponsor-latest"]["amount"]` - amount -* `data["sponsor-recent"]` - An array of latest sponsor events with each element structure as in `sponsor-latest` -* `data["sponsor-gifted-latest"]`- An Array of a gifted sponsors +* `data["sponsor-recent"]` - An array of latest member events with each element structure as in `sponsor-latest` +* `data["sponsor-gifted-latest"]`- An Array of a gifted members * `data["sponsor-gifted-latest"]["name"]`- Username getting the gift - * `data["sponsor-gifted-latest"]["amount"]` - Amount of sponsor gifts - * `data["sponsor-gifted-latest"]["tier"]` - Tier of sponsor gifts + * `data["sponsor-gifted-latest"]["amount"]` - Amount of member gifts + * `data["sponsor-gifted-latest"]["tier"]` - Tier of member gifts * `data["sponsor-gifted-latest"]["message"]` - Message from gifter * `data["sponsor-gifted-latest"]["sender"]` - username giving the gift * `data["subscriber-latest"]["name"]` - Name of latest subscriber @@ -314,8 +279,8 @@ The last element of `obj` is currency, which contains: * `symbol` - currency symbol (for example “$”) -## On event: -Live event for alerts, chat messages etc. +## On Event Received: +Live event for alerts, chat messages, SE_API store updates, etc. ```javascript window.addEventListener('onEventReceived', function (obj) { // fancy stuff here @@ -335,7 +300,7 @@ In the example above you have obj forwarded to that function, which has two inte * `event:skip` - User clicked "skip alert" button in activity feed * `alertService:toggleSound` - User clicked "mute/unmute alerts" button in activity feed * `bot:counter` - Update of bot counter - * `kvstore:update` - Update of [SE_API](#se-api) store value. + * `kvstore:update` - Update of [SE_API](custom-widget.md#se_api) store value. * `widget-button` - User clicked custom field button in widget properties * `obj.detail.event`: Will provide you information about event details. It contains few keys. For `-latest` events it is: @@ -356,20 +321,24 @@ So expanding our sample code above you can have ```javascript window.addEventListener('onEventReceived', function (obj) { - const listener = obj.detail.listener; - const data = obj["detail"]["event"]; - // Assigned new const value, for easier handling. You can do it with .property or ["property"]. - // I personally recommend using [""] as some of keys can have "-" within, so you won't be able - // to call them (JS will try to do math operation on it). - // jQuery is included by default, so you can use following - $("#usernameContainer").html(data["name"]); - $("#actionContainer").html(listener); - //You can use vanilla JS as well - document.getElementById("amount").innerHTML=data["amount"] + const listener = obj.detail.listener; + const data = obj["detail"]["event"]; + // Assigned new const value, for easier handling. You can do it with .property or ["property"]. + // I personally recommend using [""] as some of keys can have "-" within, + // so you won't be able to call them (JS will try to do math operation on it). + + // jQuery is included by default, so you can use following + $("#usernameContainer").html(data["name"]); + $("#actionContainer").html(listener); + + // You can use vanilla JS as well + document.getElementById("amount").innerHTML = data["amount"] }); ``` -### Message +### Chat Message For message events, there is an additional object that's accessible at `obj.detail.event.data`, which looks like this: + +#### Twitch: ```json { "time": 1552400352142, @@ -424,6 +393,54 @@ For message events, there is an additional object that's accessible at `obj.deta Every emote displayed on chat is within array of objects `emotes` with start/end index of `text` you can replace with image NOTE: if you are creating chat widget, remember to store `msgId` and `userId` of each message (for example `
`) for message deletion events handling. + +#### Youtube: +```json +{ + "kind": "youtube#liveChatMessage", + "etag": "joNqfOov4YhUTK1ly5QnEw2O_04", + "id": "LCC.EhwKGkNQakd1YS0tZ293REZWa1AxZ0FkMFU0VzNn", + "snippet": { + "type": "textMessageEvent", + "liveChatId": "KicKGFVDT2p0eVFPaTNNUjhYT3lzMFdOZmtKZxILMXFObko0UHU5bTA", + "authorChannelId": "noRfodywm2MXfIUCp9nm2MX", + "publishedAt": "2025-03-11T16:51:20.385245+00:00", + "hasDisplayContent": true, + "displayMessage": ":face-blue-smiling: Testing message", + "textMessageDetails": { + "messageText": ":face-blue-smiling: Testing message" + } + }, + "authorDetails": { + "channelId": "noRfodywm2MXfIUCp9nm2MX", + "channelUrl": "http://www.youtube.com/channel/noRfodywm2MXfIUCp9nm2MX", + "displayName": "userName", + "profileImageUrl": "https://yt3.ggpht.com/MkVId1UPvXogOyUqSyaFRZD2eFHqNaThFYO4-YNWAA4sQGvCPdoZKDkN5qgPUQ9BRiajmtcU=s88-c-k-c0x00ffffff-no-rj", + "isVerified": false, + "isChatOwner": false, + "isChatSponsor": false, + "isChatModerator": true + }, + "msgId": "LCC.EhwKGkNQakd1YS0tZ293REZWa1AxZ0FkMFU0VzNn", + "userId": "noRfodywm2MXfIUCp9nm2MX", + "nick": "username", + "badges": [ + ], + "displayName": "userName", + "isAction": false, + "time": 1741711893915, + "tags": [ + ], + "displayColor": null, + "channel": "noRfodywm2MXfIUCp9nm2MX", + "text": ":face-blue-smiling: Testing message", + "emotes": [ + ], + "avatar": "https://yt3.ggpht.com/MkVId1UPvXogOyUqSyaFRZD2eFHqNaThFYO4-YNWAA4sQGvCPdoZKDkN5qgPUQ9BRiajmtcU=s88-c-k-c0x00ffffff-no-rj" +} +``` + + ### Message deletion When user message is removed by channel moderator there is an event emited either: - `delete-message` - with msgId of message to be removed @@ -439,7 +456,7 @@ window.addEventListener('onEventReceived', function (obj) { const data = obj.detail.event; if (listener === 'bot:counter' && data.counter === counter) { - document.getElementById("mycounter").innerHTML=data.value; + document.getElementById("mycounter").innerHTML = data.value; } }); ``` @@ -448,24 +465,23 @@ window.addEventListener('onEventReceived', function (obj) { Contains two elements - field name (`field`) and value (`value`). Example below will send simplified event to test your chat widget ```javascript window.addEventListener('onEventReceived', function (obj) { - const data = obj.detail.event; - if (data.listener === 'widget-button') { - if (data.field==='chat' && data.value==='First Message'){ - const emulated = new CustomEvent("onEventReceived", { - detail: { - "listener": "message", - event: { - data: { - text: "Example message!", - displayName: "StreamElements" - } - } - } - - }); - window.dispatchEvent(emulated); + const data = obj.detail.event; + if (data.listener === 'widget-button') { + if (data.field === 'chat' && data.value === 'First Message') { + const emulated = new CustomEvent("onEventReceived", { + detail: { + "listener": "message", + event: { + data: { + text: "Example message!", + displayName: "StreamElements" } + } } + }); + window.dispatchEvent(emulated); + } + } }); ``` @@ -476,15 +492,18 @@ window.addEventListener('onSessionUpdate', function (obj) { //fancy stuff here }); ``` -This event is triggered every time a session data is updated (new tip/cheer/follower), basically most of the scenarios can be covered by `onEventReceived`, but `onSessionUpdate` provides a lot of more data you can use. The biggest advantage of using this is that you can check if top donator (not donation) changed. -> Note: Due to complexity of object in `onSessionUpdate` a `onEventReceived` event listener will be the best way to use for most of scenarios (easier to implement and better performance). +This event is triggered every time a session data is updated (new tip/cheer/follower), basically most of the scenarios can be covered by `onEventReceived`, but `onSessionUpdate` provides a lot of more data you can use. The biggest advantage of using this is that you can check if top donator (not donation) changed. Also it is the recommended option for goal widgets. +> Note: Due to complexity of object in `onSessionUpdate`, a `onEventReceived` event listener will be the best way to use for most of scenarios (easier to implement and better performance). Example: ```javascript window.addEventListener('onSessionUpdate', function (obj) { - const data = obj.detail.session; - $("#top-donator").text(data["tip-session-top-donator"]["name"]+" - "+data["tip-session-top-donator"]["amount"]); - $("#top-cheerer").text(data["cheer-session-top-donator"]["name"]+" - " +data["cheer-session-top-donator"]["amount"]); + const data = obj.detail.session; + const tipTopDonator = data["tip-session-top-donator"]; + const cheerTopDonator = data["cheer-session-top-donator"]; + + document.getElementById("top-donator").innerHTML = `${tipTopDonator["name"]} - ${tipTopDonator["amount"]}`; + document.getElementById("top-cheerer").innerHTML = `${cheerTopDonator["name"]} - ${cheerTopDonator["amount"]}`; }); ``` `data` is the same as in `onWidgetLoad` so every property is listed in section above. diff --git a/docs/overlays/widget-structure.md b/docs/overlays/widget-structure.md index e4305ad..46b1635 100644 --- a/docs/overlays/widget-structure.md +++ b/docs/overlays/widget-structure.md @@ -25,7 +25,7 @@ You can create custom variables, so end user doesn't have to interact with code, This data can be also called by `{{variableName}}` or `{variableName}` within HTML/CSS/JS code (however for better readibility we suggest using those calls only in HTML/CSS). -At this point we support all of HTML5 input types (except of file - use library inputs such as `video-input` instead), as well as a handful of custom inputs: `colorpicker`, `audio-input`, `sound-input`, `video-input`, `googleFont`, `dropdown`, and `slider`. +At this point we support most of HTML5 input types (except of file - use library inputs such as `video-input` instead), as well as a handful of custom inputs: `colorpicker`, `audio-input`, `sound-input`, `video-input`, `googleFont`, `dropdown`, and `slider`. There are some reserved field names (all future reserved words will start with `widget`): * `widgetName` - Used to set the display name of the widget @@ -112,6 +112,28 @@ Fields of type `image-input`, `video-input`, `sound-input` may use additional pa If you want to group some fields into a collapsible menu in the left panel, you can add to them the same parameter `"group": "Some group name"`. +#### Local font + +You can use a local font installed on your computer. +```json +{ + "customFont": { + "label": "Custom Font Name", + "type": "text", + "value": "Comic Sans MS" + } +} +``` +And add it to your CSS +```css +* { + font-family: {{customFont}}; +} +``` + +OBS: Restart the browser if you have just installed the font. + + #### Input on left panel construction Input field on left panel will look like: ```html