Professional Documents
Culture Documents
Default
Default
<head>
<meta charset="utf-8">
<meta name="title" content="SAMMI Bridge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-
fit=no">
<meta name="description"
content="SAMMI component which allows SAMMI to connect to its extensions.">
<meta name="keywords" content="SAMMI, Bridge, Twitch, Stream">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="language" content="English">
<title>SAMMI Bridge</title>
<link rel="shortcut icon" type="image/x-icon"
href="https://raw.githubusercontent.com/SAMMISolutions/SAMMI-Bridge/main/
favicon.ico"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/SAMMISolutions/SAMMI-
Bridge@main/lib/bootstrap.min.css">
<link href='https://fonts.googleapis.com/css?family=Lato:400,700'
rel='stylesheet' type='text/css'>
<style>
body {
font-family: 'Lato', sans-serif;
background-color: #040b15;
}
h1 {
font-size: calc(1.2em + 1vw);
}
a {
color: #ffac7c;
text-decoration: underline;
}
a:hover {
color: #ff6810;
text-decoration: underline;
}
.tslCollapse, .tslCollapse:hover, .tslCollapse a {
text-decoration: none;
color: #FFFFFF;
}
.tslCollapse.collapsed:before {
content:'Show Options \01F847' ;
width:15px;
}
.tslCollapse:before {
content:'Hide Options \01F845' ;
width:15px;
}
.SAMMITestTriggers .form-check-input[type=checkbox] {
vertical-align: text-bottom;
}
#footer .tslCollapse.collapsed:before {
content:'Show Installed Extensions \01F847' ;
width:15px;
}
#footer .tslCollapse:before {
content:'Hide Installed Extensions \01F845' ;
width:15px;
}
#SAMMIcorelog {
background:#040b15;
padding:5px 15px;
display:block;
position: relative;
float: left;
text-align: left;
max-height: 50%;
overflow-y: auto;
}
#debugLogContent, #debugLog {
border:none;
background-color:rgba(0,0,0,0);
box-shadow:none;
}
#debugLog .nav-link {
padding: .2rem 1rem;
}
#SAMMIBridge {
overflow: hidden;
padding:5px;
margin:2px;
padding: 1px
}
samp {
width: 200px;
word-break: break-all;
white-space: normal;
}
input{
padding:0px;
margin:1px 1px
}
button{
padding:2px 5px;
margin:3px 0;
box-shadow:2 2px #c5c5c5
}
button:active{
background-color:#797979;
color:#fff;
box-shadow:0 0 rgb(223, 223, 223);
transform:translateY(1px)
}
.nav {
padding-left: 0;
margin-bottom: 0;
}
.nav-pills .nav-link {
font-family: Arial;
padding: .4em .6em .1em .6em;
margin: 2px 1px 0px 1px!important;
background: rgb(175,177,184);
background-color: linear-gradient(0deg, rgba(175,177,184,1) 15%,
rgba(203,203,213,1) 61%);
font-weight: bold;
color:rgb(31, 32, 54);
border-radius: 5px 5px 0px 0px;
text-shadow: 0px 1px 2px rgb(195, 195, 195);
transition: 0.01s;
}
.nav-pills > li > .nav-link.active {
/*background-image: linear-gradient(to bottom, #E28B3B, #B96C23);*/
background-image: linear-gradient(to bottom, #761a2c , #761a2c );
border: 1px solid #761a2c;
color: rgb(255, 255, 255);
border: 1px solid #761a2c;
text-shadow: 1px 1px 3px rgb(0 0 0 / 100%);
/*
/*
background-image: linear-gradient(to bottom, #76511A, #76511A);
color: rgb(255, 255, 255);
border: 1px solid #76511A;
*/
*/
}
.tab-content {
/*background-color: rgba(39, 55, 110, 0.5);*/
background-color: rgba(22, 53, 100, 0.769);
box-shadow: 0.5rem 0.5rem 0.5rem #00000080;
min-height:50px;
width:100%;
border-radius:3px;
overflow:hidden;
padding:20px;
}
.draggable-source--is-dragging {
opacity: 0;
}
.btn-primary:hover {
background-color: #60646ad9;
border-color: #60646ad9
.notabs>.tab-pane {
display: block !important;
opacity: 1 !important;
}
.notabs button, .notabs input{
color:#fff;
background-color:#4c4c4c;
border-color:#464546
}
.connected{
color:#4ad84a
}
.disconnected{
color:#fb4848
}
</style>
</head>
<body>
<div class="container">
<h1 class="text-center">SAMMI Bridge </h1>
<!-- Connection Info -->
<div class="row justify-content-center">
<div class="col col-auto">
<svg id="toclient_circle" xmlns="http://www.w3.org/2000/svg" width="16"
height="16" fill="red" class="bi bi-circle-fill d-md-none me-1" viewBox="0 0 16
16">
<circle cx="8" cy="8" r="8"/>
</svg>
<span>SAMMI Core</span><span class="d-none d-md-inline-flex me-1">:
</span><span id="toclient" class="disconnected d-none d-md-inline-flex">Not
connected.</span>
</div>
</div> <br>
<!--Tabs -->
<div class="row justify-content-center g-0">
<ul class="nav nav-pills mb-0" id="extensions-tab" role="tablist">
</div>
<!-- Tab Content -->
<div class="tab-content" id="extensions-tabContent">
<div class="tab-pane" id="content-basic" role="tabpanel" title="Status" data-
type="default">
<div class='row pt-3'>
<h5>Bridge Connection</h5>
<div class="col">
<div class="row">
<label for="inputPassword" class="col-sm-5 col-form-label">IP
Address:</label>
<div class="col-sm-4 w-auto">
<input type="text" id="nIPbox" name="nIPbox" value='127.0.0.1'
class="form-control form-control-sm">
</div>
</div>
<div class="row">
<label for="inputPassword" class="col-sm-5 col-form-label">Port:</label>
<div class="col-sm-4 w-auto">
<input type="number" min="0" max="65535" id="nPortBox" name="nPortBox"
value=9425
class="form-control form-control-sm">
</div>
</div>
<div class="row">
<label for="inputPassword" class="col-sm-5 col-form-label">Password
(optional):</label>
<div class="col-sm-4 w-auto">
<input id="nPassBox" type="password" name="nPassBox" size="20" value=''
class="form-control form-control-sm">
</div>
</div>
<div class="mt-1 row">
<button type="button" id="cnctbutton" class="btn btn-primary btn-sm mb-2 w-
auto ms-3"
onclick="connectbutton()">Connect</button>
</div>
</div>
</div>
<div class='row pt-3 d-block'>
<h5>Message Logging</h5>
<div class='pl-5 ml-5'>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="dbgBridge"
onclick="SAMMIDebugLog(this)" >
<label class="form-check-label"for="flexSwitchCheckDefault">SAMMI</label>
</div>
<div class="tab-content p-0 mt-2" id="debugLogContent">
<!--SAMMI Core Log-->
<div class="tab-pane fade show active" id="SAMMIBridge" role="tabpanel"
aria-labelledby="SAMMIBridge-tab" >
<div id="SAMMIcorelog" class="col col-10 text-wrap">Logging is
disabled.</div>
</div>
</div>
</div>
</div>
</div>
<!--Your external script will be inserted here-->
<!--<script src="example.js"></script>-->
<!--INSERT PART 1-->
<!--Twitch Triggers-->
<div class="tab-pane" id="content-triggers" role="tabpanel" title="Twitch Triggers"
data-type="default">
<form id="SAMMITestTwitchFollow" class="SAMMITestTriggers">
<button type="submit" class="btn btn-primary btn-sm me-1">Test
Follower</button>
</form>
<form id="SAMMITestTwitchSubs" class="SAMMITestTriggers">
<button type="submit" class="btn btn-primary btn-sm me-1">Test Sub</button>
<a class="tslCollapse collapsed" data-bs-toggle="collapse" href="#hidesubs"
role="button" aria-expanded="false"
aria-controls="hidesubs"></a>
<div class="collapse" id="hidesubs">
<input type="radio" class="form-check-input" id="tier1" name="tier"
value="Tier 1" checked>
Tier 1
<input type="radio" class="form-check-input" id="tier2" name="tier"
value="Tier 2">
Tier 2
<input type="radio" class="form-check-input" id="tier3" name="tier"
value="Tier 3">
Tier 3
<input type="radio" class="form-check-input" id="prime" name="tier"
value="Prime">
Prime <br>
<input type="checkbox" class="form-check-input" id="subgift"
value="subgift">
SubGift
<input type="checkbox" class="form-check-input" id="anongift"
value="anongift">
AnonGift
<input type="number" min="1" max="999" id="submonths" value=1> Months <br>
Message:
<input type="text" id="submessage" size="20">
</div>
<div>
</form>
<form id="SAMMITestTwitchSubGift" class="SAMMITestTriggers">
<button type="submit" class="btn btn-primary btn-sm me-1">Test Subs Gift
Amount</button>
Amount:
<input type="number" min="1" max="999" id="subGiftAmount" value=1>
</div>
</form>
<form id="SAMMITestTwitchBits" class="SAMMITestTriggers">
<button type="submit" class="btn btn-primary btn-sm me-1">Test Bits</button>
<a class="tslCollapse collapsed" data-bs-toggle="collapse" href="#hidebits"
role="button" aria-expanded="false"
aria-controls="hidebits"></a>
<div class='collapse' id="hidebits">
Amount of bits:
<input type="number" min="1" max="99999" id="bitsamount" value=10>
Total bits:
<input type="number" min="1" max="99999" id="bitstotal" value=100><br />
Message:
<input type="text" id="bitsmessage" size="20" style="margin:5px 0px;"
value='Hello World!'>
</div>
</form>
<form id="SAMMITestTwitchPoints" class="SAMMITestTriggers">
<div><button type="submit" class="btn btn-primary btn-sm me-1">Test Channel
Points</button>
<a class="tslCollapse collapsed" data-bs-toggle="collapse" href="#hidepoints"
role="button" aria-expanded="false"
aria-controls="hidebits"></a>
</div>
<div class="collapse" id="hidepoints">
Redeem Name:
<input type="text" id="channelPointsName" size="10" style="margin:5px 0px"
value="Test Reward"> <input
type="checkbox" class="form-check-input" id="channelPointsInput"> User
Input Required <br>
Redeem Message:
<input type="text" id="channelPointsMsg" size="20"> <br>
Redeem Cost:
<input type="number" min="1" max="9999" id="channelPointsCost" size="5"
value=50>
</div>
</form>
<form id="SAMMITestTwitchRaid" class="SAMMITestTriggers">
<div><button type="submit" class="btn btn-primary btn-sm me-1">Test
Raid</button>
Amount:
<input type="number" min="1" max="999" id="raidAmount" value=5>
</div>
</form>
<form id="SAMMITestTwitchHost" class="SAMMITestTriggers">
<div><button type="submit" class="btn btn-primary btn-sm me-1">Test
Host</button>
Amount:
<input type="number" min="1" max="999" id="hostamount" value=5> <br>
</div>
</form>
</div>
</body>
<script>
/** SAMMI Core Helper Functions
* You can call them with SAMMI.{helperfunction}
* Use promises if you want to get a reply back from SAMMI
* No promise example: SAMMI.setVariable(myVariable, 'some value', 'someButtonID')
* Promise example: SAMMI.getVariable(myVariable,
'someButtonID').then(reply=>console.log(reply))
*/
function SAMMICommands() {
const SendCommand = {
/**
* Get a variable from SAMMI
* @param {string} name - name of the variable
* @param {string} buttonId - button ID for local variable, default = global
variable
*/
async getVariable(name, buttonId = 'global') {
return sendToSAMMI('GetVariable', {
Variable: name,
ButtonId: buttonId,
});
},
/**
* Set a variable in SAMMI
* @param {string} name - name of the variable
* @param {(string|number|object|array|null)} value - new value of the variable
* @param {string} buttonId - button ID for local variable, default = global
variable
*/
async setVariable(name, value, buttonId = 'global') {
return sendToSAMMI('SetVariable', {
Variable: name,
Value: value,
ButtonId: buttonId,
});
},
/**
* Send a popup message to SAMMI
* @param {string} msg - message to send
*/
async popUp(msg) {
return sendToSAMMI('PopupMessage', {
Message: msg,
});
},
/**
* Send a yellow notification message to SAMMI
* @param {string} msg - message to send
*/
async alert(msg) {
return sendToSAMMI('AlertMessage', {
Message: msg,
});
},
/**
* send extension command to SAMMI
* @param {string} name - name of the extension command
* @param {string} color - box color, accepts hex/dec colors (include # for
hex), default 3355443
* @param {string} height - height of the box in pixels, 52 for regular or 80
for resizable box, default 52
* @param {Object} boxes
* - one object per box, key = boxVariable, value = array of box params
* - boxVariable = variable to save the box value under
* - boxName = name of the box shown in the user interface
* - boxType = type of the box, 0 = resizable, 2 = checkbox (true/false), 14 =
regular box, 15 = variable box, 18 = select box, see extension guide for more
* - defaultValue = default value of the variable
* - (optional) sizeModifier = horizontal box size, 1 is normal
* - (optional) [] selectOptions = array of options for the user to select
(when using Select box type)
* @param {[boxName: string, boxType: number, defaultValue: (string | number),
sizeModifier: (number|undefined), selectOptions: Array|undefined]}
boxes.boxVariable
* */
async extCommand(name, color = 3355443, height = 52, boxes) {
const ext = new SammiConstructExtCommand(name, color, height);
return sendToSAMMI('SendExtensionCommands', {
Data: [ext],
});
},
/**
* Close SAMMI Bridge connection to SAMMI Core.
*/
async close() {
return sendToSAMMI('Close');
},
/**
* Get deck and button updates
* @param {boolean} enabled - enable or disable updates
*/
async stayInformed(enabled) {
return sendToSAMMI('SetStayInformed', {
Enabled: enabled,
});
},
/**
* Request an array of all decks
* - Replies with an array ["Deck1 Name","Unique ID",crc32,"Deck2 Name","Unique
ID",crc32,...]
* - Use crc32 value to verify deck you saved localy is the same
*/
async getDeckList() {
return sendToSAMMI('GetDeckList');
},
/**
* Request a deck params
* @param {string} id - Unique deck ID retrieved from getDeckList
* - Replies with an object containing a full deck
*/
async getDeck(id) {
return sendToSAMMI('GetDeck', {
UniqueId: id,
});
},
/**
* Retrieve an image in base64
* @param {string} fileName - image file name without the path (image.png)
* - Replies with an object containing the Base64 string of the image
*/
async getImage(fileName) {
return sendToSAMMI('GetImage', {
FileName: fileName,
});
},
/**
* Retrieves CRC32 of a file
* @param {string} fileName - file name without the path (image.png)
*/
async getSum(fileName) {
return sendToSAMMI('GetSum', {
Name: fileName,
});
},
/**
* Retrieves all currently active buttons
* - Replies with an array of button param objects
*/
async getActiveButtons() {
return sendToSAMMI('GetOngoingButtons');
},
/**
* Retrieves params of all linked Twitch accounts
*/
async getTwitchList() {
return sendToSAMMI('GetTwitchList');
},
/**
* Sends a trigger
* @param {number} type - type of trigger
* - trigger types: 0 Twitch chat, 1 Twitch Sub, 2 Twitch Gift, 3 Twitch redeem
* 4 Twitch Raid, 5 Twitch Bits, 6 Twitch Follower, 7 Hotkey
* 8 Timer, 9 OBS Trigger, 10 SAMMI Bridge, 11 twitch moderation, 12 extension
trigger
* @param {object} data - whatever data is required for the trigger, see manual
*/
async trigger(type, data) {
return sendToSAMMI('SendTrigger', {
Type: type,
Data: data,
});
},
/**
* Sends a test trigger that will automatically include channel ID for
from_channel_id pull value
* @param {number} type - type of trigger
* - trigger types: 0 Twitch chat, 1 Twitch Sub, 2 Twitch Gift, 3 Twitch redeem
* 4 Twitch Raid, 5 Twitch Bits, 6 Twitch Follower, 7 Hotkey
* 8 Timer, 9 OBS Trigger, 10 SAMMI Bridge, 11 twitch moderation, 12 extension
trigger
* @param {object} data - whatever data is required for the trigger, see manual
*/
async testTrigger(type, data) {
return sendToSAMMI('SendTestTrigger', {
Type: type,
Data: data,
});
},
/**
* Triggers a button
* @param {string} id - button ID to trigger
*/
async triggerButton(id) {
return sendToSAMMI('TriggerButton', {
ButtonId: id,
});
},
/**
* Releases a button
* @param {string} id - button ID to release
*/
async releaseButton(id) {
return sendToSAMMI('ReleaseButton', {
ButtonId: id,
});
},
/**
* Modifies a button
* @param {string} id - button ID to modify
* @param {number|undefined} color - decimal button color (BGR)
* @param {string|undefined} text - button text
* @param {string|undefined} image - button image file name
* @param {number|undefined} border - border size, 0-7
* - leave parameters empty to reset button back to default values
*/
async modifyButton(id, color, text = '', image, border) {
return sendToSAMMI('ModifyButton', {
ButtonId: id,
Data: {
color: color || undefined,
text: text || undefined,
image: image || undefined,
border: border || undefined,
},
});
},
/**
* Retrieves all currently modified buttons
* - object of button objects that are currently modified
*/
async getModifiedButtons() {
return sendToSAMMI('GetModifications');
},
/**
* Sends an extension trigger
* @param {string} trigger - name of the trigger
* @param {object} data - object containing all trigger pull data
*/
async triggerExt(trigger, data = {}) {
return sendToSAMMI('ExtensionTrigger', {
Trigger: trigger,
Data: data,
});
},
/**
* Deletes a variable
* @param {string} name - name of the variable
* @param {string} buttonId - button ID for local variable, default = global
variable
*/
async deleteVariable(name, buttonId = 'global') {
return sendToSAMMI('DeleteVariable', {
Variable: name,
ButtonId: buttonId,
});
},
/**
* Inserts an array value
* @param {string} arrayName - name of the array
* @param {number} index - index to insert the new item at
* @param {string|number|object|array} value - item value
* @param {string} buttonId - button id, default is global
*/
async insertArray(arrayName, index, value, buttonId = 'global') {
return sendToSAMMI('InsertArrayValue', {
Array: arrayName,
Slot: index,
Value: value,
ButtonId: buttonId,
});
},
/**
* Deletes an array value at specified index
* @param {string} arrayName - name of the array
* @param {number} index - index of the item to delete
* @param {string} buttonId - button id, default is global
*/
async deleteArray(arrayName, slot, buttonId = 'global') {
return sendToSAMMI('DeleteArraySlot', {
Array: arrayName,
Slot: slot,
ButtonId: buttonId,
});
},
/**
* Sends a notification (tray icon bubble) message to SAMMI
* @param {string} msg - message to show
*/
async notification(msg) {
return sendToSAMMI('NotificationMessage', {
Message: msg,
});
},
generateMessage() {
const messages = [
'All that glitters is not gold. Fair is foul, and foul is fair Hover
through the fog and filthy air. These violent delights have violent ends. Hell is
empty and all the devils are here. By the pricking of my thumbs, Something wicked
this way comes. Open, locks, Whoever knocks!',
'Hello World!',
"Alright, I'll be honest with ya, Bob. My name's not Kirk. It's Skywalker.
Luke Skywalker.",
'Well, that never happened in any of the simulations.',
'You know, you blow up one sun and suddenly everyone expects you to walk on
water.',
"How's a needle in my butt gonna get water out of my ears?",
'If you immediately know the candle light is fire, then the meal was cooked
a long time ago.',
'In the middle of my backswing!?',
'If I am to remain in this body, I must shave my head.',
"I remembered something. There's a man. He is bald and wears a short sleeve
shirt. And somehow, he is important to me… I think his name is… Homer.",
'Alright, we came here in peace, we expect to go in one... piece.',
'It costs nearly a billion dollars just to turn the lights on around here',
"You wouldn't believe the things you can make from the common, simple items
lying around your planet... which reminds me, you're going to need a new
microwave.",
'Welcome, ye knights of the round table, men of honor, followers of the
path of righteousness. Only those with wealth of knowledge and truth of spirit
shall be given access to the underworld, the storehouse of riches of Ambrosius
Aurelianus. Prove ye worthy, and all shall be revealed.'
];
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
return randomMessage;
},
};
class SammiConstructExtCommand {
constructor(name, color, height) {
let p = 0;
this.name = name;
this.color = color;
this.height = height;
return SendCommand;
}
// modify UI on load
window.addEventListener('load', SAMMILoadTabsUI, false);
function SAMMILoadTabsUI() {
const tabList = {};
let tabSortList = JSON.parse(localStorage.getItem('tabsSortList')) || [];
const newtabSortList = [];
const tabsVisibility = JSON.parse(localStorage.getItem('tabsVisibility')) || [];
let lastActiveTab = localStorage.getItem('tabsActive') || 'content-basic';
lastActiveTab = (document.getElementById(lastActiveTab)) ? lastActiveTab :
'content-basic';
const installedExt = document.querySelector('#installedextensions');
const ul = document.getElementById('extensions-tab');
const parent = document.getElementById('extensions-tabContent');
const contentLi = parent.querySelectorAll('.tab-pane');
const contentAll = [].slice.call(contentLi).filter((n) =>
n.parentNode.closest('.tab-pane') === parent.closest('.tab-pane'));
const defaultContent = contentAll.filter((e) => e.dataset.type === 'default');
const addedContent = contentAll.filter((e) => e.dataset.type !== 'default');
const content = defaultContent.concat(addedContent.reverse());
const activeTab = document.getElementById(lastActiveTab);
activeTab.className = 'tab-pane active';
tabSortList = newtabSortList;
localStorage.setItem('tabsSortList', JSON.stringify(newtabSortList));
if (ev.target.checked) {
li.classList.remove('d-none');
} else {
li.classList.add('d-none');
}
SaveExttabsVisibility();
}
};
// sort tabs
function SortTabs() {
let i = 0;
do {
const childId = tabSortList[i] || Object.keys(tabList)[0];
try {
ul.appendChild(tabList[childId]);
newtabSortList.push(childId);
} catch (e) { console.log(e); }
delete tabList[childId];
i += 1;
} while (Object.keys(tabList).length > 0);
}
SAMMITestTwitchSubGift(form) {
const subForm = document.getElementById('SAMMITestTwitchSubs');
if (subForm.prime.checked) subForm.tier1.checked = true;
if (subForm.anongift.checked === false) subForm.subgift.checked = true;
const tiers = subForm.querySelectorAll('input[name="tier"]');
let selectedTier;
SAMMITestTwitchBits(form, pullData) {
const amount = parseInt(form.bitsamount.value) || 50;
const totalAmount = parseInt(form.bitstotal.value) || amount + 100;
const message = form.bitsmessage.value || SAMMI.generateMessage();
pullData.addvalues({
amount,
total_amount: totalAmount,
message,
});
sendTriggerToSAMMI(
5,
`${pullData.user_name} donated ${amount} bits (test trigger)!`,
{
amount,
},
pullData,
);
},
SAMMITestTwitchPoints(form, pullData) {
const channelID = generateName[1];
const redeemName = form.channelPointsName.value || 'Test Reward';
const userInput = form.channelPointsInput.checked;
const message = userInput
? form.channelPointsMsg.value || SAMMI.generateMessage()
: '';
const cost = parseInt(form.channelPointsCost.value) || 50;
const image = 'https://static-cdn.jtvnw.net/custom-reward-images/default-
4.png';
const rewardId = generateUUID();
const redeemId = generateUUID();
pullData.addvalues({
channel_id: channelID,
redeem_name: redeemName,
message,
cost,
image,
reward_id: rewardId,
redeem_id: redeemId,
});
sendTriggerToSAMMI(
3,
`${pullData.display_name} has redeemed ${redeemName}!`,
{
redeemname: redeemName,
message,
},
pullData,
);
},
SAMMITestTwitchRaid(form, pullData) {
const amount = parseInt(form.raidAmount.value) || 5;
pullData.addvalues({
amount,
});
sendTriggerToSAMMI(
4,
`${pullData.display_name} is raiding you with ${amount} viewers (test
trigger)!`,
{
amount,
},
pullData,
);
},
SAMMITestTwitchHost(form, pullData) {
const amount = parseInt(form.hostamount.value) || 5;
pullData.addvalues({
amount,
});
sendTriggerToSAMMI(
14,
`${pullData.display_name} is hosting you with ${amount} viewers (test
trigger)!`,
{
amount,
},
pullData,
);
},
SAMMITestTwitchPrediction(form) {
const predictSelect = form.predictType;
const amount = form.predictChoiceAmount.value || getRandomInt(2, 10);
const duration = form.predictionDuration.value || getRandomInt(60, 600);
const type = predictSelect[predictSelect.selectedIndex].text || 'Created';
const typeNum = type === 'Created' ? 0 : type === 'Voted' ? 1 : type ===
'Locked' ? 2 : 3;
const baseData = {
duration: parseInt(duration),
outcome_amount: parseInt(amount),
vote_total: type !== 'Created' ? getRandomInt(10, 300) : 0,
event: type,
prediction_id: '1621385a-1f26-4197-82fc-6352003a69db',
prediction_name: 'My Test Prediction',
winning_outcome: type === 'Resolved' ? `e960f614-d379-494a-8b45-0c7500978$
{getRandomInt(0, amount - 1)}ea` : '',
};
const pullData = populateWithOutcomeInfo(baseData, amount, type);
sendTriggerToSAMMI(
15,
`Prediction ${type} Test trigger sent!`,
{
type: typeNum,
},
pullData,
);
},
SAMMITestTwitchPoll(form) {
const pollSelect = form.pollType;
const amount = form.pollChoiceAmount.value || getRandomInt(2, 5);
const duration = form.pollDuration.value || getRandomInt(60, 600);
const type = pollSelect[pollSelect.selectedIndex].text || 'Created';
const typeNum = type === 'Created' ? 0 : type === 'Voted' ? 1 : type ===
'Ended' ? 2 : 3;
const allowBits = !!pollAllowBits.checked;
const allowPoints = !!pollAllowPoints.checked;
// total votes is 0 if the poll was just Created, else get random amount
based on choices amount
const voteTotal = (type !== 'Created') ? getRandomInt(amount, amount * 50) :
0;
const baseData = {
duration: parseInt(duration),
event: type,
poll_id: '9dd6a7a7-78f4-46ef-b674-e2864ad7fa07',
poll_name: 'My Test Poll',
choice_amount: parseInt(amount),
vote_total: voteTotal,
vote_total_base: 0,
vote_total_bits: 0,
vote_total_points: 0,
top_vote_list: Array.from(Array(amount).keys()),
};
const pullData = populateWithChoiceInfo(baseData, amount, type, allowBits,
allowPoints, voteTotal);
sendTriggerToSAMMI(
16,
`Poll ${type} Test trigger sent!`,
{
type: typeNum,
},
pullData,
);
},
async SAMMITestTwitchHypeTrain(form) {
const hypeSelect = form.hypeTrainType;
const type = hypeSelect[hypeSelect.selectedIndex].text;
const currentLevel = form.hypeTrainLevel.value || 1;
const currentGoal = getRandomInt(1000, 2000);
const goalProgres = getRandomInt(100, currentGoal - 50);
const typeNums = {
Approaching: 0, Started: 1, Updated: 2, 'Leveled Up': 3, Ended: 4,
'Cooldown Expired': 5, Progressed: 6,
};
const typeNum = typeNums[type];
let baseObj = {};
// base SAMMI trigger values are the same for the following types
if (typeNum == 1 || typeNum == 3 || typeNum == 6) {
baseObj = {
current_level: parseInt(currentLevel),
current_goal: currentGoal,
goal_progress: goalProgres,
total_progress: goalProgres,
};
}
const progress = {
remaining_seconds: typeNum !== 1 ? getRandomInt(10, 299) : 299,
total: goalProgres,
value: goalProgres,
goal: currentGoal,
level: {
impressions: getRandomInt(100, 900),
rewards: level_one_rewards,
value: parseInt(currentLevel),
goal: currentGoal,
},
};
switch (type) {
default:
data = {};
break;
case 'Approaching': {
// participants is an array of user IDs
const participants = Array(getRandomInt(2, 10)).fill(generateName()[1]);
data = {
approaching_hype_train_id: hypeTrainId,
channel_id: generateName()[1],
goal: 3,
is_boost_train: 0,
participants,
level_one_rewards,
creator_color: '639315',
// array of one single value? can it be more? probably..
events_remaining_durations: [getRandomInt(50, 300)],
};
}
break;
case 'Started': {
// started and updated at will be the same
const started = Date.now();
data = {
id: hypeTrainId,
channel_id: generateName()[1],
started_at: started,
updated_at: started,
expires_at: started + 300000,
ended_at: null,
ending_reason: null,
participations: {
// get two random participation types and generate their values
[particTypes[getRandomInt(0, 2)]]: getRandomInt(1, 1000),
[particTypes[getRandomInt(3, 8)]]: getRandomInt(1, 10),
},
conductors: {},
// this is supposed to return very long complex object, leaving blank
for tests
config: {},
progress,
is_boost_train: 0,
};
}
break;
case 'Progressed': {
const sourceSelect = form.hypeTrainSource;
const source = sourceSelect[sourceSelect.selectedIndex].text;
const [name, userID] = await getNameFromInput(form.hypeTrainName);
data = {
sequence_id: 2174,
user_profile_image_url: 'https://static-cdn.jtvnw.net/custom-reward-
images/default-4.png',
user_id: userID,
user_display_name: name,
user_login: name.toLowerCase(),
source,
progress,
is_boost_train: 0,
quantity: parseInt(form.hypeTrainAmount.value) || 10,
};
}
break;
case 'Updated': {
const [name, userID] = await getNameFromInput(form.hypeTrainName);
const sourceSelect = form.hypeTrainSource;
const source = sourceSelect[sourceSelect.selectedIndex].text;
const particType = source === 'BITS' ? particTypes[getRandomInt(0, 2)] :
particTypes[getRandomInt(3, 8)];
const particTypeValue = parseInt(form.hypeTrainAmount.value) || 10;
baseObj = {
display_name: name,
user_name: name.toLowerCase(),
user_id: userID,
};
data = {
participations: {
[particType]: particTypeValue,
},
source,
user: {
profile_image_url: 'https://static-cdn.jtvnw.net/custom-reward-
images/default-4.png',
id: userID,
display_name: name,
login: name.toLowerCase(),
},
};
}
break;
case 'Leveled Up':
data = {
progress,
time_to_expire: Date.now() + getRandomInt(10000, 300000),
is_boost_train: 0,
};
break;
case 'Ended': {
const reasonSelect = form.hypeTrainEndReason;
const reason = reasonSelect[reasonSelect.selectedIndex].text;
baseObj = {
ending_reason: reason,
};
data = {
ending_reason: reason,
ended_at: Date.now(),
is_boost_train: 0,
};
}
break;
case 'Cooldown Expired': {
// not sure what Pubsub sends for this event
data = {};
}
}
baseObj.type = typeNum;
baseObj.data = data;
sendTriggerToSAMMI(
17,
`Hype Train ${type} Test trigger sent!`,
{ type: typeNum },
baseObj,
);
},
async SAMMITestTwitchChat(form) {
const [name, userID] = await getNameFromInput(form.chatName);
const message = form.chatMsg.value || SAMMI.generateMessage();
const channel = Math.floor(Math.random() * 1000000000);
const color = '#189A8D';
const emoteList = '304822798:0-9/304682444:11-19';
const firstTime = form.chatFirstTime.checked;
const badge = [];
if (form.chatBroadcaster.checked) badge.push('broadcaster/1');
if (form.chatMod.checked) badge.push('moderator/1');
if (form.chatVip.checked) badge.push('vip/1');
if (form.chatFounder.checked) badge.push('founder/1');
if (form.chatSub.checked) {
const tier = parseInt(form.chatMsgSubTier.value);
let month = form.chatMsgSubMonth.value != 1
? parseInt(form.chatMsgSubMonth.value)
: 0;
month = month > 3 && month < 6
? (month = 3)
: month > 6 && month < 9
? (month = 6)
: month > 9 && month < 12
? (month = 9)
: month;
const subBadge = tier === 1
? `subscriber/${month}`
: tier === 2
? `subscriber/${2000 + month}`
: `subscriber/${3000 + month}`;
badge.push(subBadge);
}
const pullData = {
user_name: name.toLowerCase(),
display_name: name,
user_id: parseInt(userID),
message,
emote_list: emoteList,
badge_list: badge.join(','),
channel,
name_color: color,
first_time: firstTime,
};
SAMMI.trigger(0, {
message,
broadcaster: form.chatBroadcaster.checked,
moderator: form.chatMod.checked,
sub: form.chatSub.checked,
vip: form.chatVip.checked,
founder: form.chatFounder.checked,
trigger_data: pullData,
});
},
SAMMITestTwitchFollow(form, pullData) {
sendTriggerToSAMMI(
6,
`${pullData.display_name} followed you!`,
{},
pullData,
);
},
};
class ConstructPullData {
constructor(type) {
const name = generateName();
this.user_name = type !== 'SAMMITestTwitchHost' ? name[0].toLowerCase() :
undefined;
this.display_name = type !== 'SAMMITestTwitchBits' ? name[0] : undefined;
this.user_id = type !== 'SAMMITestTwitchHost'
? name[1]
: undefined;
function generateUUID() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (
c
^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
).toString(16));
}
function sendTriggerToSAMMI(
type,
message = 'Test trigger fired.',
data = {},
triggerData,
) {
data.trigger_data = triggerData;
SAMMI.testTrigger(type, data);
SAMMI.alert(message);
}
}
function YTLiveTestEvent(e) {
class ConstructPullData {
constructor() {
[this.display_name, this.user_id, this.picture_url] = generateName();
this.addvalues = (params) => {
Object.assign(this, params);
};
}
}
const pullData = new ConstructPullData(e.id);
switch (e.id) {
default:
break;
case 'ytLiveTestSub':
sendTriggerToSAMMI(
19,
`YouTube Subscription Test triggered by ${pullData.display_name}`,
{},
pullData,
);
break;
case 'ytLiveTestMember':
pullData.addvalues({
channel_url: `https://www.youtube.com/channel/${pullData.user_id}`,
chat_id: 'e5LT2xEURi9BQzf2rLe5eB3325081929219850',
level_name: 'Some level name',
});
sendTriggerToSAMMI(
22,
`YouTube Member Test triggered by ${pullData.display_name}`,
{},
pullData,
);
break;
case 'ytLiveTestMilestone': {
const months = document.getElementById('YTLiveMilestoneMonth').value ||
Math.ceil(Math.random() * 10);
const level_name = document.getElementById('YTLiveMilestoneLevel').value ||
'Some level name';
const message = document.getElementById('YTLiveMilestoneMsg').value ||
SAMMI.generateMessage();
pullData.addvalues({
channel_url: `https://www.youtube.com/channel/${pullData.user_id}`,
chat_id: 'e5LT2xEURi9BQzf2rLe5eB3325081929219850',
level_name,
message,
months,
});
sendTriggerToSAMMI(
22,
`YouTube Member Renewal Test triggered by ${pullData.display_name}`,
{},
pullData,
);
}
break;
case 'ytLiveTestSuperChat': {
const amount =
parseInt(document.getElementById('YTLiveSuperChatAmount').value) ||
Math.ceil(Math.random() * 1000000);
const tier = document.getElementById('YTLiveSuperChatTier').value ||
Math.ceil(Math.random() * 5);
const message = document.getElementById('YTLiveSuperChatMsg').value ||
SAMMI.generateMessage();
pullData.addvalues({
channel_url: `https://www.youtube.com/channel/${pullData.user_id}`,
chat_id: 'e5LT2xEURi9BQzf2rLe5eB3325081929219850',
amount,
amount_as_string: `${amount}`,
currency: 'USD',
message,
tier,
});
sendTriggerToSAMMI(
20,
`YouTube Super Chat Test triggered by ${pullData.display_name}`,
{ amount },
pullData,
);
}
break;
case 'ytLiveTestSuperSticker': {
const amount =
parseInt(document.getElementById('YTLiveSuperStickerAmount').value) ||
Math.ceil(Math.random() * 1000000);
pullData.addvalues({
channel_url: `https://www.youtube.com/channel/${pullData.user_id}`,
chat_id: 'e5LT2xEURi9BQzf2rLe5eB3325081929219850',
amount,
amount_as_string: `${amount}`,
currency: 'USD',
sticker_id: 'pearfect_hey_you_v2',
sticker_text: 'Pear character turning around waving his hand, saying Hey
you while lowering his glasses',
});
sendTriggerToSAMMI(
21,
`YouTube Super Sticker Test triggered by ${pullData.display_name}`,
{ amount },
pullData,
);
}
break;
case 'ytLiveTestChatMessage': {
const message = document.getElementById('YTLiveChatMessageMsg').value ||
SAMMI.generateMessage();
pullData.addvalues({
display_name: document.getElementById('YTLiveChatMessageName').value ||
pullData.display_name,
channel_url: `https://www.youtube.com/channel/${pullData.user_id}`,
chat_id: 'e5LT2xEURi9BQzf2rLe5eB3325081929219850',
message: message.replace(/"/g, "'"),
is_broadcaster: YTLiveChatMessageBroadcaster.checked,
is_moderator: YTLiveChatMessageMod.checked,
is_member: YTLiveChatMessageMember.checked,
is_verified: YTLiveChatMessageVerified.checked,
});
sendTriggerToSAMMI(
18,
`YouTube Chat Message Test triggered by ${pullData.display_name}`,
{
broadcaster: YTLiveChatMessageBroadcaster.checked,
moderator: YTLiveChatMessageMod.checked,
verified: YTLiveChatMessageVerified.checked,
sponsor: YTLiveChatMessageMember.checked,
message,
},
pullData,
);
}
break;
}
function generateName() {
const names = [
['RoadieGamer', 'UCvuULk4cLyoXHuraLDUkEpA',
'https://yt3.ggpht.com/9Mg_T4R3Po1LMKod4RcLL82x6NiZj4xFt1ztuX6hmJhvp_gAlSmExejepwLu
H2V7Wj8klJbG=s88-c-k-c0x00ffffff-no-rj'],
['SilverLink', 'UCnXFNHXAmerjr5RLvlX4ojw',
'https://yt3.ggpht.com/96qVz0pLVJHB5NVCYdjLYAWNvcEl4zXH9UukPh3F_gv2a7aTdkIg6SQ2-
L4fLGnSXhz_WN5GvA=s88-c-k-c0x00ffffff-no-rj'],
['Rams Reef', 'UCeSb7sLzpb7OVVzGe07AcHA',
'https://yt3.ggpht.com/ytc/AKedOLRpXf-yfyFyrfOJ9rdrCDDSR6PbMDgn1v0fs912=s88-c-k-
c0x00ffffff-no-rj'],
['Cyanidesugar', 'UCnifImIxwoE9BalbaWlRuVg',
'https://yt3.ggpht.com/ytc/AKedOLRqNadDTDkEa-Nlz8UCNVvxK9hykksk1XVXhjgD=s88-c-k-
c0x00ffffff-no-rj'],
['Wolbee', 'UC6e9OkB4njOHdN7jn9QYNRg', 'https://yt3.ggpht.com/_LK_RuWL5-
mx2rK8foKYfNmpFlCRjkgbDChTUoxBf8xDwh9hgDqDDT5mmKJ-risI4qjIuYTe3w=s88-c-k-
c0x00ffffff-no-rj'],
['chrizzz_1508', 'UCgs5H0txAV59us-H_xaysFw',
'https://yt3.ggpht.com/dMFvS0Z4jqWi9nuHW_Oin7xTwITC0pog7XJ9aIH7XKKkk4OjoBw_EH1J_iGy
4X67X52A58gypQ=s88-c-k-c0x00ffffff-no-rj'],
['Mr.Rubber Ducky', 'UCM-wHpDJhBXQXO4Pq4SnusA',
'https://yt3.ggpht.com/q5SJkgteZVlcNNZQLUvCb5kirAEMkZPK2ADMeEY5sc97PpGngkx-
mHiNOb9_bRyv46QKkIE_5w=s88-c-k-c0x00ffffff-no-rj'],
['Falinere', 'UCDf53fZZjoMIq-T0yOxEAIA',
'https://yt3.ggpht.com/ytc/AKedOLSC7rJLJF4kHQYguvZA37NLrVgGb_OQAJb6uCBkIQ=s88-c-k-
c0x00ffffff-no-rj'],
['Waldo & Friends', 'UCwxgRi2IFqlYVTneamjrESA',
'https://yt3.ggpht.com/ytc/AKedOLQDlThoavf6_zR7WmRUM4GB0Bg_t8QpJ2jbEi8D=s88-c-k-
c0x00ffffff-no-rj'],
['Landie', 'UC1FayVR82EazSlBS37sosrw',
'https://yt3.ggpht.com/ytc/AKedOLTgoUIiOsrTEEsGJEXgyQ1MkXmVOFrOjnUARQc_iw=s88-c-k-
c0x00ffffff-no-rj'],
['Flipstream', 'UCEKVFoETu3cCBjqV8v4Z6uQ',
'https://yt3.ggpht.com/ScwqNhrC8CSzp3J6Nr2rGgFupPa4BrN3Kuq3gUJkYtqJxTbXPYv0alhk8qla
Ia6oUOCGoDvo=s88-c-k-c0x00ffffff-no-rj'],
['griddark', 'UCn9zd0-RuMBZKE7u_myGtgA',
'https://yt3.ggpht.com/ytc/AKedOLSoo-kdU6msTEX46Wn7Q_TxGgzYgaO1hsvCzydz5Q=s88-c-k-
c0x00ffffff-no-rj'],
['JimmyPotatoTV', 'UClxlbZo0-zHZcBIMOr4M4cQ',
'https://yt3.ggpht.com/ytc/AKedOLQVW4K8CXv4e_vDORLji4_2avIgYQ9FQkRpuXcv=s88-c-k-
c0x00ffffff-no-rj'],
['Lyfesaver', 'UCqrv6kYkEfA2Rw_XGI3klWg',
'https://yt3.ggpht.com/ytc/AKedOLR5iCHlcCIkUSfcf-j4HcgV-Mh3V5rb3E7PfQae=s88-c-k-
c0x00ffffff-no-rj'],
['MisterK', 'UCQFLW-RwDB7y4KE55Kqnsyg',
'https://yt3.ggpht.com/WHMcGQDTARCDKQyfIK4-
K7Pm5xYVpp29A9D7qjIK9HQW1qDmNvHzc1Gk742FuVqCfYnbIn2Gjw=s88-c-k-c0x00ffffff-no-rj'],
['Sebas', 'UCQxIfBhgKD7YN2Gp8txd8GA',
'https://yt3.ggpht.com/ytc/AKedOLS6O3rx4NMuVngFSN_5mktw5LyT424zsS_jQIWd=s88-c-k-
c0x00ffffff-no-rj'],
];
const randomName = names[Math.floor(Math.random() * names.length)];
return randomName;
}
function sendTriggerToSAMMI(
type,
message = 'Test trigger fired.',
data = {},
triggerData,
) {
data.trigger_data = triggerData;
SAMMI.trigger(type, data);
SAMMI.alert(message);
}
}
if (
(_sammiclient = sammiclient) !== null
&& _sammiclient !== void 0
&& _sammiclient._connected
) {
p.force_close = true;
sammiclient.send('Close');
sammiclient.disconnect();
ConnectionStatus('toclient', 'disconnected', 'Connection Closed', 'red');
document.querySelector('#cnctbutton').innerText = 'Disconnecting';
} else {
console.log('SAMMI manually connected.');
try {
clearTimeout(p.waiting_to_connect);
} catch (e) {}
connecttosammi();
}
}
try {
clearTimeout(p.waiting_to_connect);
} catch (e) {}
const p = SAMMIVars;
// CONNECT TO SAMMI
sammiclient.connect({
address: `${nIPbox.value || '127.0.0.1'}:${nPortBox.value || 9425}`,
password: `${nPassBox.value || ''}`,
name: 'SAMMI Bridge',
});
// CONNECTION OPENED
sammiclient.on('ConnectionOpened', () => {
document.querySelector('#cnctbutton').innerText = 'Disconnect';
console.log('SAMMI Connection opened!');
});
sammiclient.on('error', (err) => {
sammiclient.disconnect();
});
// AUTH SUCCESSFUL
sammiclient.on('AuthenticationSuccess', async () => {
// Send all extension commands to SAMMI
sendExtensionCommands(); // Get Twitch list for extension makers
// CONNECTION CLOSED
sammiclient.on('ConnectionClosed', () => {
try {
clearTimeout(p.waiting_to_connect);
} catch (e) {}
sammiclient.removeAllListeners();
p.force_close = false;
document.querySelector('#cnctbutton').innerText = 'Connect';
});
// CONNECTION ERROR
sammiclient.on('ConnectionError', (e) => {
// Try to force close the connection
try {
sammiclient.disconnect();
} catch (e) {}
// EXECUTE COMMAND
sammiclient.on('ExecuteCommand', (json) => {
SammiExtensionReceived(json.CommandName, json.Data);
});
}
(function initDebugLogging() {
dbgBridge.checked = SAMMIVars.SAMMIdebug.core;
SAMMIDebugLog(dbgBridge);
}());
function SAMMIDebugLog(e) {
const core = document.getElementById('SAMMIcorelog');
const listening = '<samp>Listening for traffic.</samp>';
const disabled = '<samp>Logging is disabled.</samp>';
// disable or enable debug logging and display it
if (e.checked) {
if (localStorage.debug === 'sammi-websocket-js:*') {
const _debug = console.debug.bind(console);
logIt = function (...args) {
const msg = {};
_debug.apply(console, arguments);
if (!SAMMIdebugPost) return;
Object.assign(msg, args[args.length - 2]);
if (args[0].includes('Sending Message')) SAMMIdebugPost('coreSent', msg);
else if (args[0].includes('Message received')) {
SAMMIdebugPost('core', msg);
}
};
core.innerHTML = listening;
} else {
core.innerHTML = '<samp>Logging will be enabled once SAMMI Bridge is
reloaded.</samp>';
}
localStorage.debug = 'sammi-websocket-js:*';
} else {
if (localStorage.debug == 0) core.innerHTML = disabled;
else {
core.innerHTML = '<samp>Logging will be disabled once SAMMI Bridge is
reloaded.</samp>';
}
logIt = null;
localStorage.debug = 0;
}
SAMMIVars.SAMMIdebug.core = !!(e.checked);
localStorage.setItem('SAMMIdebug', JSON.stringify(SAMMIVars.SAMMIdebug));
}
function sendExtensionCommands() {
// You SAMMI Core extension commands will be inserted here
/*INSERT PART 2*/
}
</script>
<script src="https://cdn.jsdelivr.net/gh/SAMMISolutions/SAMMI-Websocket@main/dist/
sammi-websocket.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@shopify/draggable@1.0.0-beta.11/lib/
draggable.bundle.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/
tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"
integrity="sha512-
qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQ
NNQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js"
integrity="sha256-yr4fRk/GU1ehYJPAs8P4JlTgu0Hdsp4ZKrx8bDEDC3I="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/obs-websocket-js@4.0.2/dist/obs-
websocket.js"></script>
</html>