2 | # Foundry Stream Module #
3 | A module for integrating Twitch chat from streams into Foundry Virtual Tabletop. Foundry Stream Module sends player chats, rolls, interactions to the Twitch stream chat and allows viewers in the Twitch chat to send messages to the players as an out of character message. GM's or another configurable user role can also moderate the Twitch chat via canvas control buttons for actions such as Request Roll, Emote, Clear, Timeout, Ban, Slow and Raid. Check out our current list of streamers! [Current Streamers List](https://github.com/TabletopsAndAnvils/Foundry-Stream-Module/blob/main/streamers.md) If you would like your stream added to the list, drop at message to me over at [Discord](https://discord.gg/sR8MTsgWSc)
4 |
5 |
6 | # General #
7 | This is my first time programming anything in about 27 years, as such, there may be a few glitches here and there. Just understand that I've been a professional blacksmith and bladesmith for more than two and a half decades and programming is a lot different than swinging a hammer! I love the community around Foundry VTT and wanted to give a little back, so here it is, this is my hobby project - what I do to relax - although there's been a few tense moments just to get this first release out! If you find it useful, great! I hope that you like it! As my programming skills grow I hope this module grows with them.
17 | ## Configuration ##
18 | - `Twitch Channel` - This is your Twitch stream name.
19 | - `User Name` - For your Twitch username or registered bot name, to be used in the future for sending messages back to Twitch. Capitalization may affect echoing. If you're not receiving messages from Twitch but messages are going out, try all lowercase for the user name.
20 | - `Twitch OAuth Token` - OAuth token for the user above. Without it you will only be able to receive messages but rolls, chat etc will not be sent to Twitch. If you do not have a OAuth token for your Twitch stream, simply log in to Twitch with your browser, open a tab and go to https://twitchapps.com/tmi/ - follow the prompts and your OAuth token will be generated. This will be obfuscated on saving for added security.
21 | - `/t Twitch Chat Command` - Messages to Twitch should use /t command in chat window, messages without will be ignored and only sent to other clients in Foundry. Please note, this setting requires all connected clients to refresh on change.
22 | - `Out to Twitch` - Check to send Foundry chat messages to Twitch channel.
23 | - `Receive Twitch` - Enable to receive messages from your configured Twitch channel.
24 | - `Moderation Access Level` - Every once in a while a GM may need help with moderating a Twitch channel. Select user role to allow access to the module moderation controls.
25 | - `Chat Type` - How incoming messages from Twitch should be assigned. Depending on different modules being used, it may be desirable to change how a chat card is created. Options are OOC, IC, Emote, Roll and Other. Default setting is OOC.
26 | - `Quiet Mode` - Filters rolls such as attacks, checks and spells from being sent to Twitch, only allows OOC, IC, Emote and Other to be passed along to the stream.
27 | - `Send Connect Message` - Turn on/off announcement message when connecting to a Twitch channel chat.
28 | - `Announcements` - These are announcements to be broadcast to your channel every x seconds.
29 | - `Chat Dice` - Turn on/off the dice roller in Twitch Chat.
30 |
31 | ## Features ##
32 | - `Chat` - Two way communication between Foundry and Twitch.
33 | - `GM Moderation` - On the side controls there are buttons to Clear Twitch chat, Timeout or Ban viewers and a Raid button. Please note, in order for the Raid function to work you must be signed in through a browser as well in order to click on confirmation dialog.
34 | - `Roll Request` - GM/Moderator can request a roll from a Twitch viewer. If DSN is enabled the dice will roll within the Foundry canvas, if not the result will just be sent to the chat card.
35 |
36 | # Links #
37 | * https://www.twitch.tv/tabletopsandanvils - My Twitch.tv where we livestream.
38 | * https://www.patreon.com/tabletopsandanvils - See what else I'm doing for Foundry.
39 | * https://www.youtube.com/channel/UCx1lu5HlZtmmk4JtsU_noSw - Tabletops & Anvils on Youtube.
40 |
41 | ## Acknowledgements ##
42 | Many thanks go out to the entire FoundryVTT community especially Atropos for creating it, just about everyone over in the #module-development channel on the FVTT Discord, League of Extraordinary FoundryVTT Developers, cwendrowski for creating Tabbed Chatlog, Melbz who wrote his own Twitch-bot for Foundry (https://bitbucket.org/Melbz/foundryvtt-twitch-bot/src/master/) that pointed me in the right direction for starting this module and Pint and Pie (https://github.com/thomasmckay) who is doing things with Twitch and Foundry that are absolutely mind-numbingly insane. His play through chat module lit the spark of inspiration for Foundry Stream Module many, many months ago!
43 |
44 | ## License
45 | - This work is licensed under a [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/legalcode).
46 | - This work is licensed under the [Foundry Virtual Tabletop EULA - Limited License Agreement for Module Development](https://foundryvtt.com/article/license/).
47 |
48 | ### Bugs
49 | - View current known bugs in the [Issue Tracker Backlog](https://github.com/TabletopsAndAnvils/Foundry-Stream-Module/issues)
50 |
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | ## Release 0.2.8
2 | - Fix casting bug (streamChatType)
3 | - Add prefix in chat message from Twitch
4 | - Add settings for hiding Twitch Chat
5 | - Add settings for customizing command alias
6 | - Optimizing chat message filtering
7 | - Fix button click don't work
8 |
9 | ## Release 0.2.7
10 | - Added user requested feature for a chat command. When enabled, only messages using /t will go to Twitch and appear in the Twitch tab, all other messages will not be sent and remain in the Foundry tab. This may become the default standard going forward with FSM for 0.8.x but I plan to listen to user opinions.
11 | - Removed the IC to OOC chat configuration from the config panel.
12 | - Updated localization to reflect above changes.
13 |
14 | ## Release 0.2.6
15 | - Added user requested output to dice roll requests. Roll request now outputs the forumula + rolltotal to Twitch after a request has been rolled. Ie 'Julia rolls
16 | 1d20 = [19] Thank you for the roll!'
17 | - Updated localization file to reflect above change.
18 | - Updated default configuration for above.
19 |
20 | ## Release 0.2.5
21 | - Minor hotfix to module.json
22 |
23 | ## Release 0.2.4
24 | - Added option from directly within Request Roll to hide the roll from players. This is not a blindroll, it is a gmroll to the GM and/or their moderator(s)
25 | depending on who made the request. While this has always been possible by using the drop down selector in the chat box itself, this is here as a matter of
26 | convenience for GM's and moderators. If a GM requests a private roll it will only been seen by the GM until it is revealed, however if a moderator requests
27 | a private roll, the roll (if using DSN) and result will be seen by the moderator and the GM. As normal, a private roll can be revealed by right clicking
28 | on it.
29 | - Added Request Roll localization support from within module configuration. Users can now customize the messages sent to Twitch regarding roll requests.
30 | Two option tags are available for these strings: ${dice} returns the roll request (ie 1d20) and ${who} returns who the request is made of if required.
31 | - Updated documentation in compendium.
32 | - Cleaned up code and structure in preparation for 0.8.x migration.
33 |
34 | ## Release 0.2.3
35 | - Fixed url in modules.json
36 | - Quick fix for language modification. You can now edit the language file for custom roll messages. Be sure to still include the trigger '!gm' and the variables
37 | '${who}' for the viewer name and '${dice}' for the dice to request. This is an advance method and should only be done by people familiar and comfortable with
38 | editing .json files as any error in the formatting of this file will not allow the module to load. I highly recommend making a backup of the original first! In
39 | the future, when I have the time to re-work the configuration of the module, I will make this editable from within Foundry itself. This is just a short term
40 | fix per user requests. PS, if you're editing the language .json in a language other than English that is currently supported byt Foundry, please share it on
41 | my github or Discord server so I can consider including it in future versions!
42 |
43 | ## Release 0.2.2u
44 | - Minor update to emotes. Emotes were being sent to chat cards even when Receive from Twitch was disabled.
45 |
46 | ## Release 0.2.2r
47 | - Added confirmation dialog for Clear Twitch Chat.
48 | - Bug fix regarding turning off receiving chat messages from Twitch where even when turned off, clients were still receiving messages.
49 | - Added obfuscation of the Twitch OAuth token. On save the OAuth token will appear scrambled, it's totally fine! I just added this for streamers who may be showing
50 | backend configuration in their streams to add a little bit of protection.
51 | - Removed Sub only mode, should be temporary. Looking into whether Twitch has depreciated the tag.
52 | - Removed Hide from Stream View temporarily. It will most likely be back in the immediate future, just waiting to see what changes may be made come the release
53 | of Foundry VTT 0.8.0.
54 |
55 | ## Release 0.2.1
56 | - Twitch Emotes are now passed into Foundry from Twitch chat!
57 | - Subscriber Mode: You can now select to only allow subscriber chat messages to be passed from Twitch to Foundry. Anyone with moderator status is not affected. This
58 | also affects the Request Dice Roll system but not the Twitch only dice rolling system, that will still need to be toggled off separately. This is a great
59 | feature to add incentive to Subscribing to your channel, unfortunately Twitch does not allow Followers to be tagged at this time so a similar feature for Followers
60 | will not be in the immediate future.
61 |
62 | ## Release 0.2.0r
63 | - Added inline tabbed chatting system! FSM now comes with a tabbed chatting system built in. By default this is on, however it can be turned off by enabling Legacy Mode.
64 | The tabbed chat is simple and a convenient way to keep the Foundry chat you know but also provide a separate tab where all Twitch messages go. It has been tested with
65 | Tabbed Chatlog and doesn't appear to have any conflicts, at least on my setup, your mileage might vary depending on what modules that affect the chat system you are running.
66 | - Added /stream hiding. For those broadcasting using the built in stream resources of Foundry you may want to hide the tabbed chat from your overlay. By default this
67 | feature is not enabled.
68 | - In game documentation created as an installed Journal Entry in the Compendium.
69 | - Reorganized the code from the ground up.
70 |
71 | ## Release 0.1.7
72 | - Just some minor tweaking and cleaning up some errant code.
73 |
74 | ## Release 0.1.6r
75 | - Added GM roll requests. GM/Moderator can ask Twitch viewers for a roll (ie 1d100 or 4d6+4, etc) and the first person to respond will
76 | roll the dice in Foundry. IF GM/Moderator specifies a viewer name it will wait for that player to roll, ignoring anyone else. A great
77 | way to make your viewers more involved with your games! Also works with Dice-so-Nice!
78 | - Join notifications/messages. This may be depreciated, testing it out to see if it works. May be limited on large channels and only
79 | sends messages in batches whenever it feels like it.
80 | - Emote/ Broadcast button added to canvas layer.
81 | - Added a filter to stop excess roll results and announcements.
82 | - Added configuration option to turn off Dice Roller in Twitch chat, this does not affect GM/Mod request rolls.
83 |
84 | ## Release 0.1.5r
85 | - Added a dice roller for viewers on Twitch. Now viewers can roll along with your players in Twitch! Just type !roll 3d8+5 etc in the
86 | Twitch chat and they can roll alongside you! Added a filter to catch the roll requests so they don't clutter up the chat window. The
87 | roller can handle multiple types of dice, with a range of modifiers and mathematical functions!
88 | - Added an initial response for Subs and re-Subs. I think this works but I don't have any subs to test it out. lol
89 | - Added two customizeable timed announcements for the channel. Announcements can be set up to fire every x number of seconds, use it to
90 | let new viewers know what's going on, or even how to use that fancy new dice roller!
91 | - Created macro usuable functions, streamOut, streamIn, triggerStream and awaitStream. These will most likely be refined over time and more will be
92 | added as needed. Unfortunately the Twitch API is limited in information that it passes along so tasks such as recognizing Followers is not
93 | supported at this time. I added a file called fsmMacro.js on Github with some examples of use. Macro capability is not very robust at this
94 | time but it's definitely a start!
95 | - Reworked structure, as always.
96 |
97 | ## Release 0.1.4
98 | - Added whisper filter, this stops all private messages between players or gm/players from being relayed.
99 | - Added Quiet mode, stops rolls such as attack, checks and spells from being relayed into stream chat.
100 | - Changed the way the speaker of a message from Foundry is identified on the Twitch chat. Speaker is now highlighted as \[Speaker\]:
101 |
102 | ## Release 0.1.3
103 | - Added ability to set the chat card type. Depending on modules being used, such as Tabbed Chatlog, changing the type of message will affect
104 | where the messages from the stream appears in Foundry. Choices are OOC, IC, Emote, Roll and Other.
105 | - Added option to connect to a stream channel chat silently without announcement. While useful for testing, it may not be desireable to
106 | have it announce a connection in a situation where there might be frequent reconnects.
107 | - Changed the Moderation Role configuration to a drop down style selection instead of a string input.
108 |
109 | ## Release 0.1.2
110 | - Added filters in Foundry=>Twitch hook to remove extraneous information being sent to Twitch chat.
111 | - Created modLayers.js, moved canvas layer control functions from main module.
112 | - Updated utils.js to remove unused code.
113 |
114 | ## Initial Public Release - 21.01.19
115 | - Foundry Stream Modue is a mod for integrating Twitch chat from streams into Foundry Virtual Tabletop. Foundry Stream Module sends player chats, rolls, interactions to the Twitch stream chat and allows viewers in the Twitch chat to send messages to the players as an out of character message. GM's or another configurable user role can also moderate the Twitch chat via canvas control buttons for actions such as Clear, Timeout, Ban, Slow and Raid.
116 |
117 | ### New Features
118 | - Integrate chat between Foundry and Twitch.
119 | - Moderation of Twitch chat channels.
120 |
121 | ### Bug Fixes
122 | - Added a redundancy check to stop duplicate messages when more than one GM was on the server.
123 |
124 | ### Improvements
125 | - Improved the handling of configuration to just GM roles over the previous GM and User roles.
126 |
--------------------------------------------------------------------------------
/fsminit.js:
--------------------------------------------------------------------------------
1 | // (F O U N D R Y - S T R E A M - M O D 0 . 2 . 7)
2 |
3 | //(() => {})();
4 |
5 | import "./scripts/tmi.min.js"; // I M P O R T S
6 | import * as fsmcore from "./scripts/fsmcore.js";
7 | import { fsMod } from "./scripts/streamTwitch.js";
8 | import fsmLayer from "./scripts/modLayer.js";
9 | import * as settings from "./scripts/settings.js";
10 | import { DiceRoller } from "https://cdn.jsdelivr.net/npm/rpg-dice-roller@4.5.2/lib/esm/bundle.min.js";
11 |
12 | const roller = new DiceRoller(); // C O N S T A N T S A N D V A R I A B L E S
13 | const CHAT_MESSAGE_TYPES = {
14 | OTHER: 0,
15 | OOC: 1,
16 | IC: 2,
17 | EMOTE: 3,
18 | WHISPER: 4,
19 | ROLL: 5,
20 | };
21 |
22 | let currentTab = "foundry";
23 |
24 | Hooks.on("renderChatLog", async function (chatLog, html, user) {
25 | // S O R T T A B B E D M S G S
26 |
27 | if (fsmcore.TabbedChat()) return;
28 | if (game.settings.get("streamMod", "hideTwitchChat")) return;
29 |
30 | var toPrepend = '`;
33 | html.prepend(toPrepend);
34 |
35 | var me = this;
36 | const tabs = new TabsV2({
37 | navSelector: ".tabs",
38 | contentSelector: ".content",
39 | initial: "tab1",
40 | callback: function (event, html, tab) {
41 | currentTab = tab;
42 | let chatLog = $("#chat-log");
43 |
44 | let itemType0 = chatLog.find(".type0");
45 | let itemType1 = chatLog.find(".type1");
46 | let itemType2 = chatLog.find(".type2");
47 | let itemType3 = chatLog.find(".type3");
48 | let itemType4 = chatLog.find(".type4");
49 | let itemType5 = chatLog.find(".type5");
50 | if (tab == "foundry") {
51 | itemType0.removeClass("hardHide");
52 | itemType0.show();
53 | itemType1.hide();
54 | itemType2.removeClass("hardHide");
55 | itemType2.show();
56 | itemType3.removeClass("hardHide");
57 | itemType3.show();
58 | itemType4.removeClass("hardHide");
59 | itemType4.show();
60 | itemType5.removeClass("hardHide");
61 | itemType5.not(".gm-roll-hidden").show();
62 |
63 | $("#foundryNotification").hide();
64 | } else if (tab == "fsm") {
65 | itemType1.removeClass("hardHide");
66 | itemType1.show();
67 | itemType2.hide();
68 | itemType3.hide();
69 | itemType4.hide();
70 | itemType5.hide();
71 | itemType0.hide();
72 |
73 | $("#fsmNotification").hide();
74 | } else {
75 | console.log("Unknown tab " + tab + "!");
76 | }
77 |
78 | chatLog.scrollTop(9999999);
79 | },
80 | });
81 | tabs.bind(html[0]);
82 | });
83 |
84 | Hooks.on("renderChatMessage", (chatMessage, html, data) => {
85 | // R E N D E R T A B B E D C H A T
86 |
87 | //if (fsmcore.TabbedChat()) return;
88 |
89 | html.addClass("type" + data.message.type);
90 |
91 | var sceneMatches = true;
92 |
93 | if (
94 | data.message.type == 0 ||
95 | data.message.type == 2 ||
96 | data.message.type == 3 ||
97 | data.message.type == 4 ||
98 | data.message.type == 5
99 | ) {
100 | if (data.message.speaker.scene != undefined) {
101 | html.addClass("scenespecific");
102 | html.addClass("scene" + data.message.speaker.scene);
103 | if (data.message.speaker.scene != game.user.viewedScene) {
104 | sceneMatches = false;
105 | }
106 | }
107 | }
108 |
109 | if (currentTab == "foundry") {
110 | /*if (
111 | (fsmcore.xCmd()) &&
112 | data.message.type == 1) {
113 | data.message.type == 2}*/
114 | if (
115 | (data.message.type == 2 ||
116 | data.message.type == 0 ||
117 | data.message.type == 4 ||
118 | data.message.type == 3) &&
119 | sceneMatches
120 | ) {
121 | html.css("display", "list-item");
122 | } else if (data.message.type == 5 && sceneMatches) {
123 | if (!html.hasClass("gm-roll-hidden")) {
124 | if (
125 | game.dice3d &&
126 | game.settings.get("dice-so-nice", "settings").enabled &&
127 | game.settings.get("dice-so-nice", "enabled")
128 | ) {
129 | if (
130 | !game.settings.get("dice-so-nice", "immediatelyDisplayChatMessages")
131 | )
132 | return;
133 | }
134 | html.css("display", "list-item");
135 | }
136 | } else {
137 | html.css("display", "none");
138 | html.css("cssText", "display: none !important;");
139 | html.addClass("hardHide");
140 | }
141 | } else if (currentTab == "fsm") {
142 | if (data.message.type == 1) {
143 | html.css("display", "list-item");
144 | } else {
145 | html.css("display", "none");
146 | }
147 | }
148 | });
149 |
150 | Hooks.on("diceSoNiceRollComplete", (id) => {
151 | // P L A Y N I C E W I T H D S N
152 |
153 | if (fsmcore.TabbedChat()) return;
154 |
155 | if (currentTab != "foundry") {
156 | $("#chat-log .message[data-message-id=" + id + "]").css("display", "none");
157 | }
158 | });
159 |
160 | Hooks.on("createChatMessage", (chatMessage, content) => {
161 | // C H A T N O T I F I C A T I O N S
162 |
163 | if (fsmcore.TabbedChat()) return;
164 |
165 | var sceneMatches = true;
166 |
167 | if (chatMessage.data.speaker.scene) {
168 | if (chatMessage.data.speaker.scene != game.user.viewedScene) {
169 | sceneMatches = false;
170 | }
171 | }
172 |
173 | if (
174 | chatMessage.data.type == 0 ||
175 | chatMessage.data.type == 2 ||
176 | chatMessage.data.type == 3 ||
177 | chatMessage.data.type == 4
178 | ) {
179 | if (currentTab != "foundry" && sceneMatches) {
180 | $("#foundryNotification").show();
181 | }
182 | } else if (chatMessage.data.type == 5) {
183 | if (
184 | currentTab != "foundry" &&
185 | sceneMatches &&
186 | chatMessage.data.whisper.length == 0
187 | ) {
188 | $("#foundryNotification").show();
189 | }
190 | } else if (chatMessage.data.type == 1) {
191 | if (currentTab != "fsm" && sceneMatches) {
192 | $("#fsmNotification").show();
193 | }
194 | } else {
195 | if (currentTab != "fsm") {
196 | $("#fsmNotification").show();
197 | }
198 | }
199 | });
200 |
201 | Hooks.on("preCreateChatMessage", (chatMessage, content) => {
202 | // T U R N I C M S G S I N T O O O C I F x C m d O F F
203 | if (fsmcore.xCmd()) {
204 | if (currentTab == "foundry") {
205 | if (chatMessage.type == 2) {
206 | chatMessage.type = 2;
207 | }
208 | }
209 | }
210 | if (!fsmcore.xCmd()) {
211 | if (game.settings.get("streamMod", "icChatInOoc")) {
212 | if (currentTab == "fsm") {
213 | if (chatMessage.type == 2) {
214 | chatMessage.type = 1;
215 | delete chatMessage.speaker;
216 | console.log(chatMessage);
217 | }
218 | }
219 | }
220 | }
221 | });
222 |
223 | Hooks.on("renderSceneNavigation", (sceneNav, html, data) => {
224 | // R E N D E R N A V F O R T A B S
225 | if (fsmcore.TabbedChat()) return;
226 |
227 | var viewedScene = sceneNav.scenes.find((x) => x.isView);
228 |
229 | $(".scenespecific").hide();
230 | if (currentTab == "fsm") {
231 | $(".type1.scene" + game.user.viewedScene).removeClass("hardHide");
232 | $(".type1.scene" + viewedScene.id).show();
233 | } else if (currentTab == "foundry") {
234 | $(".type0.scene" + game.user.viewedScene).removeClass("hardHide");
235 | $(".type0.scene" + viewedScene.id).show();
236 | $(".type2.scene" + game.user.viewedScene).removeClass("hardHide");
237 | $(".type2.scene" + viewedScene.id).show();
238 | $(".type3.scene" + game.user.viewedScene).removeClass("hardHide");
239 | $(".type3.scene" + viewedScene.id).show();
240 | $(".type4.scene" + game.user.viewedScene).removeClass("hardHide");
241 | $(".type4.scene" + viewedScene.id).show();
242 | $(".type5.scene" + game.user.viewedScene).removeClass("hardHide");
243 | $(".type5.scene" + viewedScene.id)
244 | .not(".gm-roll-hidden")
245 | .show();
246 | }
247 | });
248 |
249 | Hooks.on("init", function () {
250 | // M O D - S E T T I N G S
251 | settings.registerSettings();
252 | });
253 |
254 | Hooks.once("canvasReady", () => {
255 | // C A N V A S L A Y E R
256 | // Add fsmLayer to canvas
257 | const layerct = canvas.stage.children.length;
258 | let tbLayer = new fsmLayer();
259 |
260 | tbLayer.setButtons();
261 | tbLayer.roleTest();
262 | canvas.fsMod = canvas.stage.addChildAt(tbLayer, layerct);
263 | canvas.fsMod.draw();
264 |
265 | let theLayers = Canvas.layers;
266 | theLayers.fsMod = fsmLayer;
267 |
268 | Object.defineProperty(Canvas, "layers", {
269 | get: function () {
270 | return theLayers;
271 | },
272 | });
273 | });
274 |
275 | Hooks.on("ready", function () {
276 | // O N - R E A D Y - C O N N E C T I O N S
277 |
278 | fsmcore.SetupTwitchClient();
279 | onStream();
280 | fsmcore.streamDice();
281 | fsmcore.AnnounceTime1();
282 | fsmcore.AnnounceTime2();
283 | });
284 |
285 | Hooks.on("createChatMessage", async (message) => {
286 | // F O U N D R Y => T W I T C H
287 | if (fsmcore.xCmd()) return;
288 | if (message.data.type === 4) return;
289 | let testQuiet = game.settings.get("streamMod", "streamQuiet");
290 | if (testQuiet === true) {
291 | if (message.data.type === 0) return;
292 | if (message.data.type === 5) return;
293 | if (message.export().includes("Stream Chat")) return;
294 | if (message.export().includes("#00000004.0000AC0")) return;
295 | if (game.settings.get("streamMod", "streamModEcho")) {
296 | let firstGm = game.users.find((u) => u.isGM && u.active);
297 | if (firstGm && game.user === firstGm) {
298 | let myChannel = game.settings.get("streamMod", "streamChannel");
299 | let tempAlias = message.alias;
300 | let tempM = message.export();
301 | let res = tempM.slice(23);
302 | let res1 = res.replace(/(^|\s)] \s?/g, " "); // Removes '] ' that may appear when stripping the timestamp
303 | let res2 = res1.replace(/(^|\s)Damage Apply Apply Half\s?/g, " "); // <= PF1e specific roll cleanup
304 | let res3 = res2.replace(/(^|\s)Info Attack Action\s?/g, " "); // <= PF1e specific roll cleanup
305 | let fin = res3.replace(tempAlias, "[" + tempAlias + "]: ");
306 | fsMod.client.say(myChannel, fin);
307 | }
308 | }
309 | }});
310 |
311 | Hooks.on('preCreateChatMessage', (data, opts, usr) => {
312 | if (!fsmcore.xCmd()) return
313 | if (game.user.isGM && data.type === CONST.CHAT_MESSAGE_TYPES.OOC) data.type = CONST.CHAT_MESSAGE_TYPES.IC;
314 | })
315 |
316 | Hooks.on("chatCommandsReady", function(chatCommands) {
317 | if (!fsmcore.xCmd()) {
318 | console.log("Registering Chat Commands: OFF")
319 | let ctype = game.settings.get("streamMod", "streamChatType");
320 | chatCommands.registerCommand(chatCommands.createCommandFromData({
321 | commandKey: "/t",
322 | invokeOnCommand: (chatlog, messageText, chatdata) => {
323 | ChatMessage.create({
324 | speaker: {
325 | alias: "Foundry Stream Module"
326 | },
327 | content: `Guru Meditation: Error #00000004.0000AC0 - Message not sent to Twitch because you used the /t command without having it enabled in your FSM config.`
328 | });
329 | console.log(messageText);
330 | },
331 | shouldDisplayToChat: false,
332 | createdMessageType: ctype,
333 | iconClass: "fa-sticky-note",
334 | description: "Please enable in FSM config"
335 | }));
336 | return
337 | }
338 | if (fsmcore.xCmd()) {
339 | console.log("Registering Chat Commands: ON")
340 | let ctype = game.settings.get("streamMod", "streamChatType");
341 | chatCommands.registerCommand(chatCommands.createCommandFromData({
342 | commandKey: "/t",
343 | invokeOnCommand: (chatlog, messageText, chatdata) => {
344 | let who = chatdata.speaker.alias;
345 | streamOut(messageText, who);
346 | console.log(messageText, chatdata.speaker.alias);
347 | },
348 | shouldDisplayToChat: true,
349 | createdMessageType: ctype,
350 | iconClass: "fa-sticky-note",
351 | description: "Sends message to Twitch"
352 | }));
353 | console.log("commandKey loaded")
354 | }
355 |
356 | });
--------------------------------------------------------------------------------
/img/Screen Shot 2021-01-15 at 20.26.16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/bf6a5f6d6a537dd234b20d6695288d4bb59a4333/img/Screen Shot 2021-01-15 at 20.26.16.png
--------------------------------------------------------------------------------
/img/Screen Shot 2021-01-18 at 21.26.14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/bf6a5f6d6a537dd234b20d6695288d4bb59a4333/img/Screen Shot 2021-01-18 at 21.26.14.png
--------------------------------------------------------------------------------
/img/Screen Shot 2021-01-18 at 21.27.40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/bf6a5f6d6a537dd234b20d6695288d4bb59a4333/img/Screen Shot 2021-01-18 at 21.27.40.png
--------------------------------------------------------------------------------
/img/Screen Shot 2021-01-18 at 21.31.43.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/bf6a5f6d6a537dd234b20d6695288d4bb59a4333/img/Screen Shot 2021-01-18 at 21.31.43.png
--------------------------------------------------------------------------------
/img/Screen Shot 2021-01-18 at 21.32.30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/bf6a5f6d6a537dd234b20d6695288d4bb59a4333/img/Screen Shot 2021-01-18 at 21.32.30.png
--------------------------------------------------------------------------------
/img/Screen Shot 2021-01-19 at 22.03.50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/bf6a5f6d6a537dd234b20d6695288d4bb59a4333/img/Screen Shot 2021-01-19 at 22.03.50.png
--------------------------------------------------------------------------------
/img/Screen Shot 2021-01-19 at 22.22.52.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/bf6a5f6d6a537dd234b20d6695288d4bb59a4333/img/Screen Shot 2021-01-19 at 22.22.52.png
--------------------------------------------------------------------------------
/img/Screen Shot 2021-01-25 at 17.12.24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/bf6a5f6d6a537dd234b20d6695288d4bb59a4333/img/Screen Shot 2021-01-25 at 17.12.24.png
--------------------------------------------------------------------------------
/img/Screen Shot 2021-02-01 at 02.39.54.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/bf6a5f6d6a537dd234b20d6695288d4bb59a4333/img/Screen Shot 2021-02-01 at 02.39.54.png
--------------------------------------------------------------------------------
/img/fsm-cover-mid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/bf6a5f6d6a537dd234b20d6695288d4bb59a4333/img/fsm-cover-mid.png
--------------------------------------------------------------------------------
/img/fsm-cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/bf6a5f6d6a537dd234b20d6695288d4bb59a4333/img/fsm-cover.png
--------------------------------------------------------------------------------
/lang/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "I18N.LANGUAGE": "English",
3 | "I18N.MAINTAINERS": "Mr_Underhill",
4 |
5 | ".title": "Foundry Stream Module",
6 |
7 | "fsMod.settings.twitchChannel.name": "Twitch Channel",
8 | "fsMod.settings.twitchChannel.hint": "Twitch Channel to integrate.",
9 |
10 | "fsMod.settings.twitchUN.name": "Twitch Username",
11 | "fsMod.settings.twitchUN.hint": "You or your bot's Twitch.tv username entered exactly as it appears in chat, it is case sensitive. This account should have /mod settings for your channel in order to use certain functions. Please note, that in order for a raid to work you must be logged in to this account through a browser in order to click the confirmation dialog.",
12 |
13 | "fsMod.settings.twitchAuth.name": "Twitch OAuth Token",
14 | "fsMod.settings.twitchAuth.hint": "OAuth token for the user above. Without it you will only be able to receive messages but rolls, chat etc will not be sent to Twitch. Please note, for security purposes the token will be obfuscated after saving. It is recommended that you record your token in a safe place in the event you need to change this setting in the future.",
15 |
16 | "fsMod.settings.streamRole.name": "Moderation Access Level",
17 | "fsMod.settings.streamRole.hint": "A GM may need some help with moderating a Twitch stream. Set the minimum user role level to have access to the moderation controls. Default setting is Gamemaster. This does not give the user access to the configuration of the module itself.",
18 |
19 | "fsMod.settings.subCheck.name": "Subscribers Only",
20 | "fsMod.settings.subCheck.hint": "Ignore viewers and Followers, only pass Subscriber chats to FSM. Moderators will still be able to send messages.",
21 |
22 | "fsMod.settings.streamConnect.name": "Send Connect Message",
23 | "fsMod.settings.streamConnect.hint": "By default a connection message is sent to the stream chat when a client successfully connects.",
24 |
25 | "fsMod.settings.fsbotEcho.name": "Send to Twitch",
26 | "fsMod.settings.fsbotEcho.hint": "Enable to send Foundry chats to Twitch Channel.",
27 |
28 | "fsMod.settings.fsModAllChatMessages.name": "Receive from Twitch",
29 | "fsMod.settings.fsModAllChatMessages.hint": "Enable to receive chats from Twitch Channel.",
30 |
31 | "fsMod.settings.streamQuiet.name": "Quiet Mode",
32 | "fsMod.settings.streamQuiet.hint": "Don't send rolls such as attacks, checks or spells to the stream chat.",
33 |
34 | "fsMod.settings.streamAnounce.name": "Announcement",
35 | "fsMod.settings.streamAnounce.hint": "Announcement to be posted to the stream chat.",
36 |
37 | "fsMod.settings.tabbedChat.name": "Legacy Mode",
38 | "fsMod.settings.tabbedChat.hint": "Hide the Tabbed Chat, use legacy-style Foundry chat.",
39 |
40 | "fsMod.settings.IcChatInOoc.name": "IC Chat to OOC",
41 | "fsMod.settings.IcChatInOoc.hint": "Transform In Character to Out of Character chat.",
42 |
43 | "fsMod.settings.streamDice.name": "Chat Dice",
44 | "fsMod.settings.streamDice.hint": "FSM includes a highly advanced dice roller available for viewers in chat accessible by typing !roll [formula] for your players to entertain themselves and roll along. These rolls do not appear in Foundry.",
45 |
46 | "fsMod.settings.streamRoll.name": "General Roll Request",
47 | "fsMod.settings.streamRoll.hint": "This is the message to send to Twitch when requesting a viewer to roll dice. ${dice} is for the requested dice. Example: The GM is requesting a viewer to roll! [Type !gm ${dice} to roll]",
48 |
49 | "fsMod.settings.streamRoll2.name": "Specific Roll Request",
50 | "fsMod.settings.streamRoll2.hint": "This is the message to send to Twitch when requesting a specified viewer to roll dice. ${dice} is for the requested dice, ${who} is the viewer the request is being made of. Example: The GM is requesting ${who} to roll! [Type !gm ${dice} to roll]",
51 |
52 | "fsMod.settings.streamAnounceT.name": "Interval",
53 | "fsMod.settings.streamAnounceT.hint": "How often should this be posted to the stream. Enter time in seconds, 0 to turn off. You may need to refresh after saving to make new settings take effect.",
54 |
55 | "fsMod.settings.chatType.name" : "Chat Type",
56 | "fsMod.settings.chatType.hint" : "How the incoming messages from the stream should be assigned. Depending on the modules you may be running it might be desirable to change how the chat card is assigned so it appears where you want it. Default is Out of Character which is designed to work with the inline tabbed chat feature.",
57 |
58 | "fsMod.tabs.foundry": "Foundry",
59 | "fsMod.tabs.fsm": "Twitch",
60 |
61 | "fsMod.settings.xCmd.name" : "/t Twitch Chat Command",
62 | "fsMod.settings.xCmd.hint" : "Messages to Twitch should use /t command in chat window, messages without will be ignored and only sent to other clients in Foundry. Please note, this setting requires all connected clients to refresh on change.",
63 |
64 | "fsMod.settings.thankRoll.name": "Roll Received Message",
65 | "fsMod.settings.thankRoll.hint": "How you want to thank your viewer for their roll. The output for this message is 'viewername rolls diceformula = [dicetotal] yourmessage' Example: Julia rolls 1d20 = [19] Thank you for the roll!",
66 |
67 | "fsMod.settings.hideTwitchChat.name": "Hide Twitch Chat",
68 | "fsMod.settings.hideTwitchChat.hint": "Don't show Twitch chat. (Browser refresh required)",
69 |
70 | "fsMod.settings.chatCommandAlias.name": "Chat command alias",
71 | "fsMod.settings.chatCommandAlias.hint": "Enter the word for roll command. Default if 'GM'"
72 | }
73 |
--------------------------------------------------------------------------------
/lib/bot.js:
--------------------------------------------------------------------------------
1 | /*'use strict'
2 |
3 | const tls = require('tls')
4 | const assert = require('assert')
5 | const EventEmitter = require('events').EventEmitter
6 |
7 | const parser = require('./parser')
8 | */
9 |
10 | const TwitchBot = class TwitchBot extends EventEmitter {
11 | // TODO: Make this parsing better
12 | listen() {
13 | this.irc.on('data', data => {
14 | this.checkForError(data)
15 |
16 | /* Twitch sends keep-alive PINGs, need to respond with PONGs */
17 | if(data.includes('PING :tmi.twitch.tv')) {
18 | this.irc.write('PONG :tmi.twitch.tv\r\n')
19 | }
20 |
21 | if(data.includes('PRIVMSG')) {
22 | const chatter = parser.formatPRIVMSG(data)
23 | this.emit('message', chatter)
24 | }
25 |
26 | if(data.includes('CLEARCHAT')) {
27 | const event = parser.formatCLEARCHAT(data)
28 | if(event.type === 'timeout') this.emit('timeout', event)
29 | if(event.type === 'ban') this.emit('ban', event)
30 | }
31 |
32 | if(data.includes('USERNOTICE ')) {
33 | const event = parser.formatUSERNOTICE(data)
34 | if (['sub', 'resub'].includes(event.msg_id) ){
35 | this.emit('subscription', event)
36 | }
37 | }
38 |
39 | if(data.includes(`@${this.username}.tmi.twitch.tv JOIN`)) {
40 | const channel = parser.formatJOIN(data)
41 | if(channel) {
42 | if(!this.channels.includes(channel)) {
43 | this.channels.push(channel)
44 | }
45 | this.emit('join', channel)
46 | }
47 | }
48 |
49 | if(data.includes(`@${this.username}.tmi.twitch.tv PART`)) {
50 | const channel = parser.formatPART(data)
51 | if(channel) {
52 | if(this.channels.includes(channel)) {
53 | this.channels.pop(channel)
54 | }
55 | this.emit('part', channel)
56 | }
57 | }
58 | })
59 | }
60 |
61 |
62 | writeIrcMessage(text) {
63 | this.irc.write(text + "\r\n")
64 | }
65 |
66 | join(channel) {
67 | channel = parser.formatCHANNEL(channel)
68 | this.writeIrcMessage(`JOIN ${channel}`)
69 | }
70 |
71 | part(channel) {
72 | if(!channel && this.channels.length > 0) {
73 | channel = this.channels[0]
74 | }
75 | channel = parser.formatCHANNEL(channel)
76 | this.writeIrcMessage(`PART ${channel}`)
77 | }
78 |
79 | say(message, channel, callback ) {
80 | if(!channel) {
81 | channel = this.channels[0]
82 | }
83 | if(message.length >= 500) {
84 | this.cb(callback, {
85 | sent: false,
86 | message: 'Exceeded PRIVMSG character limit (500)'
87 | })
88 | } else {
89 | this.writeIrcMessage('PRIVMSG ' + channel + ' :' + message)
90 | }
91 | }
92 |
93 | timeout(username, channel, duration=600, reason='') {
94 | if(!channel) {
95 | channel = this.channels[0]
96 | }
97 | this.say(`/timeout ${username} ${duration} ${reason}`, channel)
98 | }
99 |
100 | ban(username, channel, reason='') {
101 | if(!channel) {
102 | channel = this.channels[0]
103 | }
104 | this.say(`/ban ${username} ${reason}`, channel)
105 | }
106 |
107 | close() {
108 | this.irc.destroy()
109 | this.emit('close')
110 | }
111 |
112 | cb(callback, obj) {
113 | if(callback) {
114 | obj.ts = new Date()
115 | callback(obj)
116 | }
117 | }
118 |
119 | }
120 |
121 | module.exports = TwitchBot
--------------------------------------------------------------------------------
/lib/parser.js:
--------------------------------------------------------------------------------
1 | //const _ = require('lodash')
2 |
3 | module.exports = {
4 |
5 | formatCHANNEL(channel){
6 | channel = channel.toLowerCase()
7 | return channel.charAt(0) !== '#' ? '#' + channel : channel
8 | },
9 |
10 | formatJOIN(event){
11 | event = event.replace(/\r\n/g, '')
12 | return event.split('JOIN ')[1]
13 | },
14 |
15 | formatPART(event){
16 | event = event.replace(/\r\n/g, '')
17 | return event.split('PART ')[1]
18 | },
19 |
20 | formatPRIVMSG(event) {
21 | const parsed = {}
22 |
23 | const msg_parts = event.split('PRIVMSG ')[1]
24 | let split_msg_parts = msg_parts.split(' :')
25 | const channel = split_msg_parts[0]
26 |
27 | if(split_msg_parts.length >= 2) {
28 | split_msg_parts.shift()
29 | }
30 | const message = split_msg_parts.join(' :').replace(/\r\n/g, '')
31 |
32 | let [tags,username] = event.split('PRIVMSG')[0].split(' :')
33 | parsed.username = username.split('!')[0]
34 |
35 | Object.assign(parsed,this.formatTAGS(tags))
36 | parsed.mod = !!parsed.mod
37 | parsed.subscriber = !!parsed.subscriber
38 | parsed.turbo = !!parsed.turbo
39 |
40 | if(parsed.emote_only) parsed.emote_only = !!parsed.emote_only
41 |
42 | parsed.channel = channel
43 | parsed.message = message
44 |
45 | return parsed
46 | },
47 |
48 | formatCLEARCHAT(event) {
49 | const parsed = {}
50 |
51 | const msg_parts = event.split('CLEARCHAT ')[1]
52 | let split_msg_parts = msg_parts.split(' :')
53 |
54 | const channel = split_msg_parts[0]
55 | const target_username = split_msg_parts[1]
56 |
57 | let [tags] = event.split('CLEARCHAT')[0].split(' :')
58 | Object.assign(parsed,this.formatTAGS(tags))
59 |
60 | if(parsed.ban_reason) {
61 | parsed.ban_reason = parsed.ban_reason.replace(/\\s/g, ' ')
62 | }
63 |
64 | if(parsed.ban_duration) parsed.type = 'timeout'
65 | else parsed.type = 'ban'
66 |
67 | parsed.channel = channel
68 | if (target_username) {
69 | parsed.target_username = target_username.replace(/\r\n/g, '')
70 | }
71 |
72 | /* TODO: This needs a proper fix */
73 | parsed.tmi_sent_ts = parseInt(parsed.tmi_sent_ts)
74 |
75 | return parsed
76 | },
77 |
78 | formatTAGS(tagstring) {
79 | let tagObject = {}
80 | const tags =tagstring.replace(/\s/g,' ').split(';')
81 |
82 | tags.forEach(tag => {
83 | const split_tag = tag.split('=')
84 | const name = this.formatTagName(split_tag[0])
85 | let val = this.formatTagVal(split_tag[1])
86 | tagObject[name] = val
87 | })
88 |
89 | if (tagObject.badges){
90 | tagObject.badges = this.formatBADGES(tagObject.badges)
91 | }
92 |
93 | return tagObject
94 | },
95 |
96 | formatBADGES(badges){
97 | let badgesObj = {}
98 | if(badges) {
99 | badges = badges.split(',')
100 |
101 | badges.forEach(badge => {
102 | const split_badge = badge.split('/')
103 | badgesObj[split_badge[0]] = +split_badge[1]
104 | })
105 | }
106 | return badgesObj
107 | },
108 |
109 |
110 | formatUSERNOTICE(event){
111 | const parsed = {}
112 |
113 | const msg_parts = event.split('USERNOTICE')[1]
114 | let split_msg_parts = msg_parts.split(' :')
115 |
116 | parsed.channel = split_msg_parts[0].trim()
117 | parsed.message = split_msg_parts[1] || null
118 |
119 | let tags = event.split('USERNOTICE')[0].split(':')[0].trim()
120 |
121 | Object.assign(parsed,this.formatTAGS(tags))
122 | return parsed
123 | },
124 |
125 | formatTagName(tag) {
126 | if(tag.includes('-')) {
127 | tag = tag.replace(/-/g, '_')
128 | }
129 | if(tag.includes('@')) {
130 | tag = tag.replace('@', '')
131 | }
132 | return tag.trim()
133 | },
134 |
135 | formatTagVal(val) {
136 | if(!val) return null
137 | if(val.match(/^[0-9]+$/) !== null) {
138 | return +val
139 | }
140 | if (val.includes('\s')){
141 | val = val.replace(/\\s/g, ' ')
142 | }
143 | return val.trim()
144 | }
145 |
146 | }
--------------------------------------------------------------------------------
/module.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "streamMod",
3 | "title": "Foundry Stream Module",
4 | "description": "Implements two-way communication between Foundry VTT and Twitch, allowing for two-way chat plus adds dice rolling requests, a tabbed chat window, a separate dice engine for just for viewers, event handling and several moderation commands directly into Foundry itself, bringing your streamed game to the next level of interactivity!",
5 | "author": "MrUnderhill",
6 | "authors": [
7 | {
8 | "name": "MrUnderhill",
9 | "url": "https://www.patreon.com/tabletopsandanvils",
10 | "email": "mr_underhill@icloud.com",
11 | "discord": "MrUnderhill#4707",
12 | "patreon": "TabletopsAndAnvils"
13 | }
14 | ],
15 | "version": "0.3.0",
16 | "minimumCoreVersion": "9",
17 | "compatibleCoreVersion" : "9",
18 |
19 | "styles": [
20 | "/styles/fsmtabs.css"
21 | ],
22 |
23 | "esmodules": [
24 | "/scripts/tmi.min.js",
25 | "/scripts/fsmcore.js",
26 | "/fsminit.js"
27 | ],
28 | "languages": [
29 | {
30 | "lang": "en",
31 | "name": "English",
32 | "path": "/lang/en.json"
33 | }
34 | ],
35 | "packs":[
36 | {
37 | "name": "documentation",
38 | "label": "Foundry Stream Module Documentation 0.2.4",
39 | "path": "packs/documentation.db",
40 | "entity": "JournalEntry",
41 | "module": "streamMod"
42 | }
43 | ],
44 | "dependencies":[
45 | {
46 | "name": "_chatcommands",
47 | "type": "module",
48 | "manifest": "https://github.com/League-of-Foundry-Developers/Chat-Commands-Lib/releases/download/1.2.0/module.json"
49 | }
50 | ],
51 | "media":[
52 | {
53 | "type": "cover",
54 | "link": "https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/main/img/fsm-cover.png"
55 | }
56 | ],
57 | "bugs": "https://github.com/TabletopsAndAnvils/Foundry-Stream-Module/issues",
58 | "allowBugReporter": true,
59 | "url": "https://github.com/TabletopsAndAnvils/Foundry-Stream-Module",
60 |
61 | "manifest": "https://raw.githubusercontent.com/TabletopsAndAnvils/Foundry-Stream-Module/main/module.json",
62 | "download": "https://github.com/TabletopsAndAnvils/Foundry-Stream-Module/archive/0.3.0.zip"
63 | }
64 |
--------------------------------------------------------------------------------
/packs/documentation.db:
--------------------------------------------------------------------------------
1 | {"_id":"6LuvhGTuKyAXcwkr","name":"Foundry Stream Module Documentation","permission":{"default":0,"w1x54ohimiGGgcC6":3},"folder":"","flags":{"core":{"sourceId":"JournalEntry.mATlGTrUk9RwkBUe"}},"content":"
Thank you for installing Foundry Stream Module!
\n
\n
Implements two-way communication between Foundry VTT and Twitch, allowing for two-way chat plus adds dice rolling requests, a tabbed chat window, a separate dice engine for just for viewers, event handling and several moderation commands directly into Foundry itself, bringing your streamed game to the next level of interactivity! I hope you enjoy the module and find it useful. Be sure to check out my other projects at https://www.patreon.com/tabletopsandanvils
\n
\n
Documentation
\n
\n
Configuration:
\n
First, a little note about my setup. My Twitch channel is TabletopsAndAnvils however I created a second account for a 'bot', you can search online how to enable your email address to be used to register another account. After registering the second account I logged in with both of my accounts using separate browsers and gave the new account /mod capabilities. Without them you will not be able to use many of the functions and features of Foundry Stream Module. I'm not saying that you have to do it this way, but I found it useful. Also, our streaming name is rather long and I figured fsMod is a lot shorter for those compelled to read every character in a chat!
\n
\n
Twitch Channel - This is your Twitch stream name.
\n
\n
User Name - For your Twitch username or registered bot name, to be used in the future for sending messages back to
\n
Twitch. Capitalization may affect echoing. If you're not recieving messages from Twitch but messages are going out, try all lowercase for the user name.
\n
\n
Twitch OAuth Token - OAuth token for the user above. Without it you will only be able to recieve messages but rolls, chat etc will not be sent to Twitch. If you do not have a OAuth token for your Twitch stream, simply log in to Twitch with your browser, open a tab and go to https://twitchapps.com/tmi/ - follow the prompts and your OAuth token will be generated.
\n
\n
Moderation Level - A GM may need some help with moderating a Twitch stream. Set the minimum user roll level to have access to the moderation controls. Default setting is Gamemaster. This does not give the user access to the configuration of the module itself.
\n
\n
Chat Type - How the incoming messages from the stream should be assigned. Depending on the modules you may be running it might be desirable to change how the chat card is assigned so it appears where you want it. Default is Out of Character which is designed to work with the inline tabbed chat feature.
\n
\n
Subscribers Only - Allow only Twitch subscriber messages to be posted into Foundry. This will not affect anyone with moderation roles who is not a subscriber.
\n
\n
Send to Twitch - Check to send Foundry chat messages to Twitch channel.
\n
\n
Recieve from Twitch - Enable to recieve messages from your configured Twitch channel.
\n
Moderation Access Level` - Every once in a while a GM may need help with moderating a Twitch channel. Select user role to allow access to the module moderation controls.
\n
\n
Quiet Mode - Filters rolls such as attacks, checks and spells from being sent to Twitch, only allows OOC, IC, Emote and Other to be passed along to the stream.
\n
\n
Legacy Mode - Hides the tabbed chat, using legacy-style Foundry chat instead.
\n
\n
Hide in Stream View - Hide the tabbed chat in /stream view when using software such as OBS to create an overlay. Note, this does not effect the normal game view for you or your players.
\n
\n
IC to Twitch - Send in-character messages as Twitch messages. Automatically converts in-character messages to messages for Twitch sent from the Foundry tab. Messages from the Twitch tab all messages will be sent as the player name, not the character name.
\n
\n
Send Connect Message - Turn on/off announcement message when connecting to a Twitch channel chat.
\n
\n
Announcements - These are announcements to be broadcast to your channel every x seconds.
\n
\n
Chat Dice - Turn on/off the dice roller in Twitch Chat. FSM provides a very advance dice and mathematics engine and allows your viewers to take advantage of this and roll along with you and your players while not sending those rolls to Foundry. It works when a viewer types !roll dice+d+sides, ie !roll 1d100 or !roll 3d8+5. It can Fudge dice 'dF.1 or 4dF', roll Percentile '1d% or 4d%', Kill '3d10!k2', Min/Max modifiers '4d6max3 or 6d6min2', Explode '2d6! or 4d10!<=3', Compound '2d6!!', Penetrating '2d6!p', Reroll 'd6r, d6ro or 4d10r<=3', Keep and a whole lot more! You can read more on this incredible rolling engine at https://greenimp.github.io/rpg-dice-roller/guide/notation/modifiers.html#min-min-n
\n
\n
Join Announcement - Supposedly sends messages to people who join your channel, however it may be depreciated and therefor only here until I take it out. If it actually works, please drop me a line!
\n
\n
\n
Features:
\n
Roll Request - You can request a roll from a Twitch viewer. If the Who: field is left blank, anyone in the stream will have the chance to roll, if it's filled in, only that viewer can roll the dice. When rolled (ie !gm 1d100), the viewer will roll the dice in Foundry instead of in the Twitch chat. If you have Dice So Nice! enabled it will trigger them to roll on the screen as well. Rolls will appear in the Foundry tab.
\n
\n
Emote/Raw Message - Send a message as an emote or as raw. Raw is useful for passing commands to the Twitch IRC directly that have not been implemented in FSM at this time.
\n
\n
Clear Twitch Chat - Clears the chat history window.
\n
\n
Twitch Channel Chat Rate - Time is entered in seconds. It determines the amount of time between messages before a viewer can send aother. Default is 30 seconds.
\n
\n
Timeout - Temporarily bans a viewer from chatting. You can remove a timeout from here as well.
\n
\n
Ban - Bans a player from your channel. Unbanning is also possible.
\n
\n
Raid Channel - Raids help streamers send their viewers to another live channel at the end of their stream to introduce their audience to a new channel and have a little fun along the way. Raiding a channel at the end of your stream can be a great way to help another streamer grow his or her community. Keep in mind, you must have the browser open to Twitch to confirm the raid.
\n
\n
Release Notes:
\n
\n
## Release 0.2.1
\n
-Twitch Emotes are now passed into Foundry from Twitch chat!
\n
- Subscriber Mode: You can now select to only allow subscriber chat messages to be passed from Twitch to Foundry. Anyone with moderator status is not affected. This also affects the Request Dice Roll system but not the Twitch only dice rolling system, that will still need to be toggled off separately. This is a great feature to add incentive to Subscribing to your channel, unfortunately Twitch does not allow Followers to be tagged at this time so a similar feature for Followers will not be in the immediate future.
\n
\n
## Release 0.1.8
\n
- Added inline tabbed chatting system! FSM now comes with a tabbed chatting system built in. By default this is on, however it can be turned off by enabling Legacy Mode.
\n
The tabbed chat is simple and a convenient way to keep the Foundry chat you know but also provide a separate tab where all Twitch messages go. It has been tested with Tabbed Chatlog and doesn't appear to have any conflicts, at least on my setup, your mileage might vary depending on what modules that affect the chat system you are running.
\n
- Added /stream hiding. For those broadcasting using the built in stream resources of Foundry you may want to hide the tabbed chat from your overlay. By default this feature is not enabled.
\n
- In game documentation created as an installed Journal Entry.
\n
- Reorganized the code from the ground up.
\n \n
## Release 0.1.7
\n
- Just some minor tweaking and cleaning up some errant code.
\n \n
## Release 0.1.6r
\n
- Added GM roll requests. GM/Moderator can ask Twitch viewers for a roll (ie 1d100 or 4d6+4, etc) and the first person to respond will roll the dice in Foundry. IF GM/Moderator specifies a viewer name it will wait for that player to roll, ignoring anyone else. A great way to make your viewers more involved with your games! Also works with Dice-so-Nice!
\n
- Join notifications/messages. This may be depreciated, testing it out to see if it works. May be limited on large channels and only sends messages in batches whenever it feels like it.
\n
- Emote/ Broadcast button added to canvas layer.
\n
- Added a filter to stop excess roll results and announcements.
\n
- Added configuration option to turn off Dice Roller in Twitch chat, this does not affect GM/Mod request rolls.
\n \n
## Release 0.1.5r
\n
- Added a dice roller for viewers on Twitch. Now viewers can roll along with your players in Twitch! Just type !roll 3d8+5 etc in the Twitch chat and they can roll alongside you! Added a filter to catch the roll requests so they don't clutter up the chat window. The roller can handle multiple types of dice, with a range of modifiers and mathematical functions!
\n
- Added an initial response for Subs and re-Subs. I think this works but I don't have any subs to test it out. lol
\n
- Added two customizeable timed announcements for the channel. Announcements can be set up to fire every x number of seconds, use it to let new viewers know what's going on, or even how to use that fancy new dice roller!
\n
- Created macro usuable functions, streamOut, streamIn, triggerStream and awaitStream. These will most likely be refined over time and more will be added as needed. Unfortunately the Twitch API is limited in information that it passes along so tasks such as recognizing Followers is not supported at this time. I added a file called fsmMacro.js on Github with some examples of use. Macro capability is not very robust at this time but it's definitely a start!
\n
- Reworked structure, as always.
\n \n
## Release 0.1.4
\n
- Added whisper filter, this stops all private messages between players or gm/players from being relayed.
\n
- Added Quiet mode, stops rolls such as attack, checks and spells from being relayed into stream chat.
\n
- Changed the way the speaker of a message from Foundry is identified on the Twitch chat. Speaker is now highlighted as [Speaker]:
\n \n
## Release 0.1.3
\n
- Added ability to set the chat card type. Depending on modules being used, such as Tabbed Chatlog, changing the type of message will affect where the messages from the stream appears in Foundry. Choices are OOC, IC, Emote, Roll and Other.
\n
- Added option to connect to a stream channel chat silently without announcement. While useful for testing, it may not be desireable to have it announce a connection in a situation where there might be frequent reconnects.
\n
- Changed the Moderation Role configuration to a drop down style selection instead of a string input.
\n \n
## Release 0.1.2
\n
- Added filters in Foundry=>Twitch hook to remove extraneous information being sent to Twitch chat.
\n
- Created modLayers.js, moved canvas layer control functions from main module.
\n
- Updated utils.js to remove unused code.
\n \n
## Initial Public Release - 21.01.19
\n
- Foundry Stream Modue is a mod for integrating Twitch chat from streams into Foundry Virtual Tabletop. Foundry Stream Module sends player chats, rolls, interactions to the Twitch stream chat and allows viewers in the Twitch chat to send messages to the players as an out of character message. GM's or another configurable user role can also moderate the Twitch chat via canvas control buttons for actions such as Clear, Timeout, Ban, Slow and Raid.
\n \n
### New Features
\n
- Integrate chat between Foundry and Twitch.
\n
- Moderation of Twitch chat channels.
\n \n
### Bug Fixes
\n
- Added a redundancy check to stop duplicate messages when more than one GM was on the server.
\n \n
### Improvements
\n
- Improved the handling of configuration to just GM roles over the previous GM and User roles.
Implements two-way communication between Foundry VTT and Twitch, allowing for two-way chat plus adds dice rolling requests, a tabbed chat window, a separate dice engine for just for viewers, event handling and several moderation commands directly into Foundry itself, bringing your streamed game to the next level of interactivity! I hope you enjoy the module and find it useful. Be sure to check out my other projects at https://www.patreon.com/tabletopsandanvils
\n
\n
Documentation
\n
\n
Configuration:
\n
First, a little note about my setup. My Twitch channel is TabletopsAndAnvils however I created a second account for a 'bot', you can search online how to enable your email address to be used to register another account. After registering the second account I logged in with both of my accounts using separate browsers and gave the new account /mod capabilities. Without them you will not be able to use many of the functions and features of Foundry Stream Module. I'm not saying that you have to do it this way, but I found it useful. Also, our streaming name is rather long and I figured fsMod is a lot shorter for those compelled to read every character in a chat!
\n
\n
Twitch Channel - This is your Twitch stream name.
\n
\n
User Name - For your Twitch username or registered bot name, to be used in the future for sending messages back to
\n
Twitch. Capitalization may affect echoing. If you're not recieving messages from Twitch but messages are going out, try all lowercase for the user name.
\n
\n
Twitch OAuth Token - OAuth token for the user above. Without it you will only be able to recieve messages but rolls, chat etc will not be sent to Twitch. If you do not have a OAuth token for your Twitch stream, simply log in to Twitch with your browser, open a tab and go to https://twitchapps.com/tmi/ - follow the prompts and your OAuth token will be generated.
\n
\n
Moderation Level - A GM may need some help with moderating a Twitch stream. Set the minimum user roll level to have access to the moderation controls. Default setting is Gamemaster. This does not give the user access to the configuration of the module itself.
\n
\n
Chat Type - How the incoming messages from the stream should be assigned. Depending on the modules you may be running it might be desirable to change how the chat card is assigned so it appears where you want it. Default is Out of Character which is designed to work with the inline tabbed chat feature.
\n
\n
Subscribers Only - Allow only Twitch subscriber messages to be posted into Foundry. This will not affect anyone with moderation roles who is not a subscriber.
\n
\n
Send to Twitch - Check to send Foundry chat messages to Twitch channel.
\n
\n
Recieve from Twitch - Enable to recieve messages from your configured Twitch channel.
\n
Moderation Access Level` - Every once in a while a GM may need help with moderating a Twitch channel. Select user role to allow access to the module moderation controls.
\n
\n
Quiet Mode - Filters rolls such as attacks, checks and spells from being sent to Twitch, only allows OOC, IC, Emote and Other to be passed along to the stream.
\n
\n
Legacy Mode - Hides the tabbed chat, using legacy-style Foundry chat instead.
\n
\n
Hide in Stream View - Hide the tabbed chat in /stream view when using software such as OBS to create an overlay. Note, this does not effect the normal game view for you or your players.
\n
\n
IC to Twitch - Send in-character messages as Twitch messages. Automatically converts in-character messages to messages for Twitch sent from the Foundry tab. Messages from the Twitch tab all messages will be sent as the player name, not the character name.
\n
\n
Send Connect Message - Turn on/off announcement message when connecting to a Twitch channel chat.
\n
\n
Announcements - These are announcements to be broadcast to your channel every x seconds.
\n
\n
Chat Dice - Turn on/off the dice roller in Twitch Chat. FSM provides a very advance dice and mathematics engine and allows your viewers to take advantage of this and roll along with you and your players while not sending those rolls to Foundry. It works when a viewer types !roll dice+d+sides, ie !roll 1d100 or !roll 3d8+5. It can Fudge dice 'dF.1 or 4dF', roll Percentile '1d% or 4d%', Kill '3d10!k2', Min/Max modifiers '4d6max3 or 6d6min2', Explode '2d6! or 4d10!<=3', Compound '2d6!!', Penetrating '2d6!p', Reroll 'd6r, d6ro or 4d10r<=3', Keep and a whole lot more! You can read more on this incredible rolling engine at https://greenimp.github.io/rpg-dice-roller/guide/notation/modifiers.html#min-min-n
\n
\n
Join Announcement - Supposedly sends messages to people who join your channel, however it may be depreciated and therefor only here until I take it out. If it actually works, please drop me a line!
\n
\n
\n
Features:
\n
Roll Request - You can request a roll from a Twitch viewer. If the Who: field is left blank, anyone in the stream will have the chance to roll, if it's filled in, only that viewer can roll the dice. When rolled (ie !gm 1d100), the viewer will roll the dice in Foundry instead of in the Twitch chat. If you have Dice So Nice! enabled it will trigger them to roll on the screen as well. Rolls will appear in the Foundry tab.
\n
\n
Emote/Raw Message - Send a message as an emote or as raw. Raw is useful for passing commands to the Twitch IRC directly that have not been implemented in FSM at this time.
\n
\n
Clear Twitch Chat - Clears the chat history window.
\n
\n
Twitch Channel Chat Rate - Time is entered in seconds. It determines the amount of time between messages before a viewer can send aother. Default is 30 seconds.
\n
\n
Timeout - Temporarily bans a viewer from chatting. You can remove a timeout from here as well.
\n
\n
Ban - Bans a player from your channel. Unbanning is also possible.
\n
\n
Raid Channel - Raids help streamers send their viewers to another live channel at the end of their stream to introduce their audience to a new channel and have a little fun along the way. Raiding a channel at the end of your stream can be a great way to help another streamer grow his or her community. Keep in mind, you must have the browser open to Twitch to confirm the raid.
\n
\n
Release Notes:
\n
\n
## Release 0.2.1
\n
-Twitch Emotes are now passed into Foundry from Twitch chat!
\n
- Subscriber Mode: You can now select to only allow subscriber chat messages to be passed from Twitch to Foundry. Anyone with moderator status is not affected. This also affects the Request Dice Roll system but not the Twitch only dice rolling system, that will still need to be toggled off separately. This is a great feature to add incentive to Subscribing to your channel, unfortunately Twitch does not allow Followers to be tagged at this time so a similar feature for Followers will not be in the immediate future.
\n
\n
## Release 0.1.8
\n
- Added inline tabbed chatting system! FSM now comes with a tabbed chatting system built in. By default this is on, however it can be turned off by enabling Legacy Mode.
\n
The tabbed chat is simple and a convenient way to keep the Foundry chat you know but also provide a separate tab where all Twitch messages go. It has been tested with Tabbed Chatlog and doesn't appear to have any conflicts, at least on my setup, your mileage might vary depending on what modules that affect the chat system you are running.
\n
- Added /stream hiding. For those broadcasting using the built in stream resources of Foundry you may want to hide the tabbed chat from your overlay. By default this feature is not enabled.
\n
- In game documentation created as an installed Journal Entry.
\n
- Reorganized the code from the ground up.
\n \n
## Release 0.1.7
\n
- Just some minor tweaking and cleaning up some errant code.
\n \n
## Release 0.1.6r
\n
- Added GM roll requests. GM/Moderator can ask Twitch viewers for a roll (ie 1d100 or 4d6+4, etc) and the first person to respond will roll the dice in Foundry. IF GM/Moderator specifies a viewer name it will wait for that player to roll, ignoring anyone else. A great way to make your viewers more involved with your games! Also works with Dice-so-Nice!
\n
- Join notifications/messages. This may be depreciated, testing it out to see if it works. May be limited on large channels and only sends messages in batches whenever it feels like it.
\n
- Emote/ Broadcast button added to canvas layer.
\n
- Added a filter to stop excess roll results and announcements.
\n
- Added configuration option to turn off Dice Roller in Twitch chat, this does not affect GM/Mod request rolls.
\n \n
## Release 0.1.5r
\n
- Added a dice roller for viewers on Twitch. Now viewers can roll along with your players in Twitch! Just type !roll 3d8+5 etc in the Twitch chat and they can roll alongside you! Added a filter to catch the roll requests so they don't clutter up the chat window. The roller can handle multiple types of dice, with a range of modifiers and mathematical functions!
\n
- Added an initial response for Subs and re-Subs. I think this works but I don't have any subs to test it out. lol
\n
- Added two customizeable timed announcements for the channel. Announcements can be set up to fire every x number of seconds, use it to let new viewers know what's going on, or even how to use that fancy new dice roller!
\n
- Created macro usuable functions, streamOut, streamIn, triggerStream and awaitStream. These will most likely be refined over time and more will be added as needed. Unfortunately the Twitch API is limited in information that it passes along so tasks such as recognizing Followers is not supported at this time. I added a file called fsmMacro.js on Github with some examples of use. Macro capability is not very robust at this time but it's definitely a start!
\n
- Reworked structure, as always.
\n \n
## Release 0.1.4
\n
- Added whisper filter, this stops all private messages between players or gm/players from being relayed.
\n
- Added Quiet mode, stops rolls such as attack, checks and spells from being relayed into stream chat.
\n
- Changed the way the speaker of a message from Foundry is identified on the Twitch chat. Speaker is now highlighted as [Speaker]:
\n \n
## Release 0.1.3
\n
- Added ability to set the chat card type. Depending on modules being used, such as Tabbed Chatlog, changing the type of message will affect where the messages from the stream appears in Foundry. Choices are OOC, IC, Emote, Roll and Other.
\n
- Added option to connect to a stream channel chat silently without announcement. While useful for testing, it may not be desireable to have it announce a connection in a situation where there might be frequent reconnects.
\n
- Changed the Moderation Role configuration to a drop down style selection instead of a string input.
\n \n
## Release 0.1.2
\n
- Added filters in Foundry=>Twitch hook to remove extraneous information being sent to Twitch chat.
\n
- Created modLayers.js, moved canvas layer control functions from main module.
\n
- Updated utils.js to remove unused code.
\n \n
## Initial Public Release - 21.01.19
\n
- Foundry Stream Modue is a mod for integrating Twitch chat from streams into Foundry Virtual Tabletop. Foundry Stream Module sends player chats, rolls, interactions to the Twitch stream chat and allows viewers in the Twitch chat to send messages to the players as an out of character message. GM's or another configurable user role can also moderate the Twitch chat via canvas control buttons for actions such as Clear, Timeout, Ban, Slow and Raid.
\n \n
### New Features
\n
- Integrate chat between Foundry and Twitch.
\n
- Moderation of Twitch chat channels.
\n \n
### Bug Fixes
\n
- Added a redundancy check to stop duplicate messages when more than one GM was on the server.
\n \n
### Improvements
\n
- Improved the handling of configuration to just GM roles over the previous GM and User roles.
Implements two-way communication between Foundry VTT and Twitch, allowing for two-way chat plus adds dice rolling requests, a tabbed chat window, a separate dice engine for just for viewers, event handling and several moderation commands directly into Foundry itself, bringing your streamed game to the next level of interactivity! I hope you enjoy the module and find it useful. Be sure to check out my other projects at https://www.patreon.com/tabletopsandanvils
\n
\n
Documentation
\n
\n
Configuration:
\n
First, a little note about my setup. My Twitch channel is TabletopsAndAnvils however I created a second account for a 'bot', you can search online how to enable your email address to be used to register another account. After registering the second account I logged in with both of my accounts using separate browsers and gave the new account /mod capabilities. Without them you will not be able to use many of the functions and features of Foundry Stream Module. I'm not saying that you have to do it this way, but I found it useful. Also, our streaming name is rather long and I figured fsMod is a lot shorter for those compelled to read every character in a chat!
\n
\n
Twitch Channel - This is your Twitch stream name.
\n
\n
User Name - For your Twitch username or registered bot name, to be used in the future for sending messages back to
\n
Twitch. Capitalization may affect echoing. If you're not recieving messages from Twitch but messages are going out, try all lowercase for the user name.
\n
\n
Twitch OAuth Token - OAuth token for the user above. Without it you will only be able to recieve messages but rolls, chat etc will not be sent to Twitch. If you do not have a OAuth token for your Twitch stream, simply log in to Twitch with your browser, open a tab and go to https://twitchapps.com/tmi/ - follow the prompts and your OAuth token will be generated.
\n
\n
Moderation Level - A GM may need some help with moderating a Twitch stream. Set the minimum user roll level to have access to the moderation controls. Default setting is Gamemaster. This does not give the user access to the configuration of the module itself.
\n
\n
Chat Type - How the incoming messages from the stream should be assigned. Depending on the modules you may be running it might be desirable to change how the chat card is assigned so it appears where you want it. Default is Out of Character which is designed to work with the inline tabbed chat feature.
\n
\n
Subscribers Only - Allow only Twitch subscriber messages to be posted into Foundry. This will not affect anyone with moderation roles who is not a subscriber.
\n
\n
Send to Twitch - Check to send Foundry chat messages to Twitch channel.
\n
\n
Recieve from Twitch - Enable to recieve messages from your configured Twitch channel.
\n
Moderation Access Level` - Every once in a while a GM may need help with moderating a Twitch channel. Select user role to allow access to the module moderation controls.
\n
\n
Quiet Mode - Filters rolls such as attacks, checks and spells from being sent to Twitch, only allows OOC, IC, Emote and Other to be passed along to the stream.
\n
\n
Legacy Mode - Hides the tabbed chat, using legacy-style Foundry chat instead.
\n
\n
Hide in Stream View - Hide the tabbed chat in /stream view when using software such as OBS to create an overlay. Note, this does not effect the normal game view for you or your players.
\n
\n
IC to Twitch - Send in-character messages as Twitch messages. Automatically converts in-character messages to messages for Twitch sent from the Foundry tab. Messages from the Twitch tab all messages will be sent as the player name, not the character name.
\n
\n
Send Connect Message - Turn on/off announcement message when connecting to a Twitch channel chat.
\n
\n
Announcements - These are announcements to be broadcast to your channel every x seconds.
\n
\n
Chat Dice - Turn on/off the dice roller in Twitch Chat. FSM provides a very advance dice and mathematics engine and allows your viewers to take advantage of this and roll along with you and your players while not sending those rolls to Foundry. It works when a viewer types !roll dice+d+sides, ie !roll 1d100 or !roll 3d8+5. It can Fudge dice 'dF.1 or 4dF', roll Percentile '1d% or 4d%', Kill '3d10!k2', Min/Max modifiers '4d6max3 or 6d6min2', Explode '2d6! or 4d10!<=3', Compound '2d6!!', Penetrating '2d6!p', Reroll 'd6r, d6ro or 4d10r<=3', Keep and a whole lot more! You can read more on this incredible rolling engine at https://greenimp.github.io/rpg-dice-roller/guide/notation/modifiers.html#min-min-n
\n
\n
Join Announcement - Supposedly sends messages to people who join your channel, however it may be depreciated and therefor only here until I take it out. If it actually works, please drop me a line!
\n
\n
\n
Features:
\n
Roll Request - You can request a roll from a Twitch viewer. If the Who: field is left blank, anyone in the stream will have the chance to roll, if it's filled in, only that viewer can roll the dice. When rolled (ie !gm 1d100), the viewer will roll the dice in Foundry instead of in the Twitch chat. If you have Dice So Nice! enabled it will trigger them to roll on the screen as well. Rolls will appear in the Foundry tab. There is a checkbox available to make the roll private.
\n
\n
Emote/Raw Message - Send a message as an emote or as raw. Raw is useful for passing commands to the Twitch IRC directly that have not been implemented in FSM at this time.
\n
\n
Clear Twitch Chat - Clears the chat history window.
\n
\n
Twitch Channel Chat Rate - Time is entered in seconds. It determines the amount of time between messages before a viewer can send aother. Default is 30 seconds.
\n
\n
Timeout - Temporarily bans a viewer from chatting. You can remove a timeout from here as well.
\n
\n
Ban - Bans a player from your channel. Unbanning is also possible.
\n
\n
Raid Channel - Raids help streamers send their viewers to another live channel at the end of their stream to introduce their audience to a new channel and have a little fun along the way. Raiding a channel at the end of your stream can be a great way to help another streamer grow his or her community. Keep in mind, you must have the browser open to Twitch to confirm the raid.
\n
\n
Release Notes:
\n
\n
## Release 0.2.4
\n
- Added option from directly within Request Roll to hide the roll from players. This is not a blindroll, it is a gmroll to the GM and/or their moderator(s) depending on who made the request. While this has always been possible by using the drop down selector in the chat box itself, this is here as a matter of convenience for GM's and moderators. If a GM requests a private roll it will only been seen by the GM until it is revealed, however if a moderator requests
\n
a private roll, the roll (if using DSN) and result will be seen by the moderator and the GM. As normal, a private roll can be revealed by right clicking on it.
\n
\n
\n
- Added Request Roll localization support from within module configuration. Users can now customize the messages sent to Twitch regarding roll requests. Two option tags are available for these strings: ${dice} returns the roll request (ie 1d20) and ${who} returns who the request is made of if required.
\n
\n
\n
- Updated documentation in compendium.
\n
- Cleaned up code and structure in preparation for 0.8.x migration.
\n \n
## Release 0.2.3
\n
- Fixed url in modules.json
\n
- Quick fix for language modification. You can now edit the language file for custom roll messages. Be sure to still include the trigger '!gm' and the variables '${who}' for the viewer name and '${dice}' for the dice to request. This is an advance method and should only be done by people familiar and comfortable with editing .json files as any error in the formatting of this file will not allow the module to load. I highly recommend making a backup of the original first! In the future, when I have the time to re-work the configuration of the module, I will make this editable from within Foundry itself. This is just a short term fix per user requests. PS, if you're editing the language .json in a language other than English that is currently supported byt Foundry, please share it on my github or Discord server so I can consider including it in future versions!
\n \n
## Release 0.2.2u
\n
- Minor update to emotes. Emotes were being sent to chat cards even when Receive from Twitch was disabled.
\n \n
## Release 0.2.2r
\n
- Added confirmation dialog for Clear Twitch Chat.
\n
- Bug fix regarding turning off receiving chat messages from Twitch where even when turned off, clients were still receiving messages.
\n
- Added obfuscation of the Twitch OAuth token. On save the OAuth token will appear scrambled, it's totally fine! I just added this for streamers who may be showing backend configuration in their streams to add a little bit of protection.
\n
- Removed Sub only mode, should be temporary. Looking into whether Twitch has depreciated the tag.
\n
- Removed Hide from Stream View temporarily. It will most likely be back in the immediate future, just waiting to see what changes may be made come the release of Foundry VTT 0.8.0.
\n \n
## Release 0.2.1
\n
- Twitch Emotes are now passed into Foundry from Twitch chat!
\n
- Subscriber Mode: You can now select to only allow subscriber chat messages to be passed from Twitch to Foundry. Anyone with moderator status is not affected. This also affects the Request Dice Roll system but not the Twitch only dice rolling system, that will still need to be toggled off separately. This is a great feature to add incentive to Subscribing to your channel, unfortunately Twitch does not allow Followers to be tagged at this time so a similar feature for Followers will not be in the immediate future.
\n \n
## Release 0.2.0r
\n
- Added inline tabbed chatting system! FSM now comes with a tabbed chatting system built in. By default this is on, however it can be turned off by enabling Legacy Mode. The tabbed chat is simple and a convenient way to keep the Foundry chat you know but also provide a separate tab where all Twitch messages go. It has been tested with Tabbed Chatlog and doesn't appear to have any conflicts, at least on my setup, your mileage might vary depending on what modules that affect the chat system you are running.
\n
- Added /stream hiding. For those broadcasting using the built in stream resources of Foundry you may want to hide the tabbed chat from your overlay. By default this feature is not enabled.
\n
- In game documentation created as an installed Journal Entry in the Compendium.
\n
- Reorganized the code from the ground up.
\n \n
## Release 0.1.7
\n
- Just some minor tweaking and cleaning up some errant code.
\n \n
## Release 0.1.6r
\n
- Added GM roll requests. GM/Moderator can ask Twitch viewers for a roll (ie 1d100 or 4d6+4, etc) and the first person to respond will roll the dice in Foundry. IF GM/Moderator specifies a viewer name it will wait for that player to roll, ignoring anyone else. A great way to make your viewers more involved with your games! Also works with Dice-so-Nice!
\n
- Join notifications/messages. This may be depreciated, testing it out to see if it works. May be limited on large channels and only sends messages in batches whenever it feels like it.
\n
- Emote/ Broadcast button added to canvas layer.
\n
- Added a filter to stop excess roll results and announcements.
\n
- Added configuration option to turn off Dice Roller in Twitch chat, this does not affect GM/Mod request rolls.
\n
\n
\n
## Release 0.2.1
\n
-Twitch Emotes are now passed into Foundry from Twitch chat!
\n
- Subscriber Mode: You can now select to only allow subscriber chat messages to be passed from Twitch to Foundry. Anyone with moderator status is not affected. This also affects the Request Dice Roll system but not the Twitch only dice rolling system, that will still need to be toggled off separately. This is a great feature to add incentive to Subscribing to your channel, unfortunately Twitch does not allow Followers to be tagged at this time so a similar feature for Followers will not be in the immediate future.
\n
\n
## Release 0.1.8
\n
- Added inline tabbed chatting system! FSM now comes with a tabbed chatting system built in. By default this is on, however it can be turned off by enabling Legacy Mode.
\n
The tabbed chat is simple and a convenient way to keep the Foundry chat you know but also provide a separate tab where all Twitch messages go. It has been tested with Tabbed Chatlog and doesn't appear to have any conflicts, at least on my setup, your mileage might vary depending on what modules that affect the chat system you are running.
\n
- Added /stream hiding. For those broadcasting using the built in stream resources of Foundry you may want to hide the tabbed chat from your overlay. By default this feature is not enabled.
\n
- In game documentation created as an installed Journal Entry.
\n
- Reorganized the code from the ground up.
\n \n
## Release 0.1.7
\n
- Just some minor tweaking and cleaning up some errant code.
\n \n
## Release 0.1.6r
\n
- Added GM roll requests. GM/Moderator can ask Twitch viewers for a roll (ie 1d100 or 4d6+4, etc) and the first person to respond will roll the dice in Foundry. IF GM/Moderator specifies a viewer name it will wait for that player to roll, ignoring anyone else. A great way to make your viewers more involved with your games! Also works with Dice-so-Nice!
\n
- Join notifications/messages. This may be depreciated, testing it out to see if it works. May be limited on large channels and only sends messages in batches whenever it feels like it.
\n
- Emote/ Broadcast button added to canvas layer.
\n
- Added a filter to stop excess roll results and announcements.
\n
- Added configuration option to turn off Dice Roller in Twitch chat, this does not affect GM/Mod request rolls.
\n \n
## Release 0.1.5r
\n
- Added a dice roller for viewers on Twitch. Now viewers can roll along with your players in Twitch! Just type !roll 3d8+5 etc in the Twitch chat and they can roll alongside you! Added a filter to catch the roll requests so they don't clutter up the chat window. The roller can handle multiple types of dice, with a range of modifiers and mathematical functions!
\n
- Added an initial response for Subs and re-Subs. I think this works but I don't have any subs to test it out. lol
\n
- Added two customizeable timed announcements for the channel. Announcements can be set up to fire every x number of seconds, use it to let new viewers know what's going on, or even how to use that fancy new dice roller!
\n
- Created macro usuable functions, streamOut, streamIn, triggerStream and awaitStream. These will most likely be refined over time and more will be added as needed. Unfortunately the Twitch API is limited in information that it passes along so tasks such as recognizing Followers is not supported at this time. I added a file called fsmMacro.js on Github with some examples of use. Macro capability is not very robust at this time but it's definitely a start!
\n
- Reworked structure, as always.
\n \n
## Release 0.1.4
\n
- Added whisper filter, this stops all private messages between players or gm/players from being relayed.
\n
- Added Quiet mode, stops rolls such as attack, checks and spells from being relayed into stream chat.
\n
- Changed the way the speaker of a message from Foundry is identified on the Twitch chat. Speaker is now highlighted as [Speaker]:
\n \n
## Release 0.1.3
\n
- Added ability to set the chat card type. Depending on modules being used, such as Tabbed Chatlog, changing the type of message will affect where the messages from the stream appears in Foundry. Choices are OOC, IC, Emote, Roll and Other.
\n
- Added option to connect to a stream channel chat silently without announcement. While useful for testing, it may not be desireable to have it announce a connection in a situation where there might be frequent reconnects.
\n
- Changed the Moderation Role configuration to a drop down style selection instead of a string input.
\n \n
## Release 0.1.2
\n
- Added filters in Foundry=>Twitch hook to remove extraneous information being sent to Twitch chat.
\n
- Created modLayers.js, moved canvas layer control functions from main module.
\n
- Updated utils.js to remove unused code.
\n \n
## Initial Public Release - 21.01.19
\n
- Foundry Stream Modue is a mod for integrating Twitch chat from streams into Foundry Virtual Tabletop. Foundry Stream Module sends player chats, rolls, interactions to the Twitch stream chat and allows viewers in the Twitch chat to send messages to the players as an out of character message. GM's or another configurable user role can also moderate the Twitch chat via canvas control buttons for actions such as Clear, Timeout, Ban, Slow and Raid.
\n \n
### New Features
\n
- Integrate chat between Foundry and Twitch.
\n
- Moderation of Twitch chat channels.
\n \n
### Bug Fixes
\n
- Added a redundancy check to stop duplicate messages when more than one GM was on the server.
\n \n
### Improvements
\n
- Improved the handling of configuration to just GM roles over the previous GM and User roles.
\n
","_id":"wcrh7mmYQaB9ByGT"}
5 |
--------------------------------------------------------------------------------
/scripts/fromTwitch.js:
--------------------------------------------------------------------------------
1 | // (F O U N D R Y - S T R E A M - M O D)
2 |
3 | export const fsMod = {
4 | client: null,
5 | options: {}
6 | };
7 |
8 | window.WhisperGM = (content) => { // H A N D L E S M E S S A G E S F R O M T W I T C H
9 | ChatMessage.create({
10 | content: content,
11 | type: game.settings.get("streamMod", "streamChatType"),
12 | speaker: ChatMessage.getSpeaker({ alias: "Stream Chat" }),
13 | });
14 | };
--------------------------------------------------------------------------------
/scripts/fsmMacro.js:
--------------------------------------------------------------------------------
1 | // (F O U N D R Y - S T R E A M - M O D)
2 |
3 | /* Primary macro functions are as follows:
4 |
5 | awaitStream(trigger, response) <= sets a trigger such as !a and waits to give a response.
6 |
7 | streamOut('message') <= Sends 'message' to Twitch stream without any actor information.
8 |
9 | streamIn('message') <= Writes a message to Foundry chat as though it came from Twitch.
10 | - Probably pointless unless you want to spam your players.
11 |
12 | I'm not exactly sure that controlling the module via macro is a desired feature but I decided
13 | to do it anyway, because this community always surprises me! If you have a suggestion for other functions
14 | feel free to drop me a line. I'm not saying I'll make it happen, but there's always a possibility!
15 | */
16 |
17 | // EXAMPLE 1: This is a hook, so be careful to only run it once! If you are already running it and update, reload otherwise it
18 | // will spit out multiple messages! I know there's some sort of flag solution to keeping track how many times it's run
19 | // but I'm busy working on other things, so if you want to share, by all means!
20 |
21 | streamOut('Commands are !a, !b, !z');
22 | let message = "Bob has 23hp";
23 | awaitStream('!a', 'Is for aberation.');
24 | awaitStream('!b', message);
25 | awaitStream('!z', 'Is for zombie.');
26 |
27 | // EXAMPLE 2: Broadcasts Current Combat Round Macro
28 |
29 | let messageContent = `ROUND ${game.combat.round}`;
30 | streamOut(messageContent);
31 |
32 | // triggerStream(streamTrigger, function, args)
33 | triggerStream('!a', console.log, 'Hello World!'); // <= on '!a' in Twitch chat would run console.log('Hello World!');
--------------------------------------------------------------------------------
/scripts/fsmcore.js:
--------------------------------------------------------------------------------
1 | // (F O U N D R Y - S T R E A M - M O D)
2 |
3 | import { fsMod } from "./streamTwitch.js";
4 | import { levelCheck } from './streamTwitch.js';
5 | import { DiceRoller } from 'https://cdn.jsdelivr.net/npm/rpg-dice-roller@4.5.2/lib/esm/bundle.min.js';
6 | import { localize } from "./utils.js";
7 |
8 | const roller = new DiceRoller();
9 |
10 | // M I S C F U N C T I O N S
11 |
12 | /*
13 | export function HideForStreamView() { // H I D E S T A B B E D C H A T F R O M / S T R E A M
14 | if (game.settings.get("streamMod", "HideInStreamView")) {
15 | if (window.location.href.endsWith("/stream")) {
16 | return true;
17 | }
18 | }
19 | return false;
20 | }
21 | */
22 |
23 | export function checkAuth() {
24 | let check = game.settings.get("streamMod", "flagAuth")
25 | if (check) return
26 | else {
27 | let normal = game.settings.get("streamMod", "streamAuth")
28 | let obf = normal.obfs(13);
29 | game.settings.set("streamMod", "streamAuth", obf);
30 | game.settings.set("streamMod", "flagAuth", true);
31 | }
32 | }
33 |
34 | export function gmOnly() {
35 |
36 | if (game.settings.get("streamMod", "streamOnly")) {
37 | return true;
38 | }
39 | }
40 |
41 | export function TabbedChat() {
42 | if (game.settings.get("streamMod", "tabbedChat")) {
43 | return true;
44 | }
45 | }
46 |
47 | export function xCmd() {
48 | let a = (game.settings.get("streamMod", "streamCmd"))
49 | if (a == true) { return true } else {
50 | return false
51 | }
52 | }
53 | export function outChat() {
54 | let a = (game.settings.get("streamMod", "streamModEcho"))
55 | if (a == true) { return true } else {
56 | return false
57 | }
58 | }
59 |
60 | export function inChat() {
61 | let b = (game.settings.get("streamMod", "streamGM"))
62 | if (b == true) { return true } else {
63 | return false
64 | }
65 | }
66 |
67 | // T W I T C H S P E C I F I C F U N C T I O N S
68 |
69 | export function streamJoin() { // J O I N S S T R E A M C H E C K
70 | fsMod.client.on("join", (channel, username, self) => {
71 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
72 | let welcomeMsg = (game.settings.get("streamMod", "streamJoin"));
73 | const firstGm = game.users.find((u) => u.isGM && u.active);
74 | if (firstGm && game.user === firstGm) {
75 | fsMod.client.say(myChannel, welcomeMsg);
76 | }
77 | })
78 | }
79 |
80 | export function DisconnectTwitch() { // D I S C O N N E C T T W I T C H
81 | fsMod.client.disconnect();
82 | };
83 |
84 | export function SilentTwitchClient() { // Checking if OAuth is obfuscated
85 | console.log('OAuth token is not obfuscated!')
86 | let newvalue = game.settings.get("streamMod", "streamAuth");
87 | if (newvalue.includes("oauth:")) {
88 | game.settings.set("streamMod", "streamAuth", newvalue.obfs(13))
89 | } else return SetupTwitchClient();
90 | fsMod.client = new tmi.Client({
91 | connection: {
92 | cluster: "aws",
93 | secure: true,
94 | reconnect: true,
95 | },
96 | identity: {
97 | username: game.settings
98 | .get("streamMod", "streamUN"),
99 | password: game.settings
100 | .get("streamMod", "streamAuth")
101 | },
102 | channels: game.settings
103 | .get("streamMod", "streamChannel")
104 | .split(",")
105 | .map((c) => c.trim()),
106 | });
107 | fsMod.client.connect().catch(console.error);
108 |
109 | }
110 |
111 | export function SetupTwitchClient() { // C O N N E C T T O T W I T C H
112 | // Set up twitch chat reader
113 | let obf = game.settings.get("streamMod", "streamAuth");
114 | console.log('Checking for obfuscation..')
115 | if (obf.includes("oauth:")) return SilentTwitchClient();
116 | console.log('OAuth token is obfuscated!');
117 | let streamPW = obf.defs(13);
118 | fsMod.client = new tmi.Client({
119 | connection: {
120 | cluster: "aws",
121 | secure: true,
122 | reconnect: true,
123 | },
124 | identity: {
125 | username: game.settings
126 | .get("streamMod", "streamUN"),
127 | password: streamPW // game.settings
128 | //.get("streamMod", "streamAuth")
129 | },
130 | channels: game.settings
131 | .get("streamMod", "streamChannel")
132 | .split(",")
133 | .map((c) => c.trim()),
134 | });
135 | fsMod.client.connect().catch(console.error);
136 | fsMod.client.on('connected', (address, port) => {
137 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
138 | if (game.settings.get("streamMod", "connectMSG") === "1") {
139 | fsMod.client.say(myChannel, 'Foundry Stream Module [Connected]')
140 | }
141 | else console.log('worked');
142 | });
143 |
144 | };
145 |
146 | export function AnnounceTime1() { // A N N O U N C E M E N T S T U F F
147 | let readTime = (game.settings.get("streamMod", "streamAnnounce1T"));
148 | let T1 = (readTime * 1000);
149 | if (readTime === 0) return;
150 | setInterval(AnnounceSend1, T1)
151 | }
152 |
153 | export function AnnounceSend1() { // A N N O U N C E M E N T S T U F F
154 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
155 | let message = (game.settings.get("streamMod", "streamAnnounce1"));
156 | const firstGm = game.users.find((u) => u.isGM && u.active);
157 | if (firstGm && game.user === firstGm) {
158 | fsMod.client.say(myChannel, message);
159 | }
160 | }
161 |
162 | export function AnnounceTime2() { // A N N O U N C E M E N T S T U F F
163 | let readTime = (game.settings.get("streamMod", "streamAnnounce2T"));
164 | let T2 = (readTime * 1000);
165 | if (readTime === 0) return;
166 | setInterval(AnnounceSend2, T2)
167 | }
168 |
169 | export function AnnounceSend2() { // A N N O U N C E M E N T S T U F F
170 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
171 | let message2 = (game.settings.get("streamMod", "streamAnnounce2"));
172 | const firstGm = game.users.find((u) => u.isGM && u.active);
173 | if (firstGm && game.user === firstGm) {
174 | fsMod.client.say(myChannel, message2);
175 | }
176 | }
177 |
178 | // C A N V A S L A Y E R C O N T R O L S
179 |
180 | export function twitchKick() { // T I M E O U T V I E W E R
181 | let d = new Dialog({
182 | title: 'Viewer Timeout',
183 | content: `
184 |
192 | `,
193 | buttons: {
194 | no: {
195 | icon: '',
196 | label: 'Cancel'
197 | },
198 | yes: {
199 | icon: '',
200 | label: 'TIMEOUT',
201 | callback: (html) => {
202 | let input = html.find('[name="kickInput"]').val();
203 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
204 | fsMod.client.say(myChannel, '/timeout ' + input)
205 | },
206 | },
207 | stop: {
208 | icon: '',
209 | callback: (html) => {
210 | let input = html.find('[name="kickInput"]').val();
211 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
212 | fsMod.client.say(myChannel, '/untimeout ' + input)
213 | },
214 | },
215 | },
216 | default: 'yes',
217 | close: () => {
218 | console.log('Someones in the corner!');
219 | }
220 | }).render(true)
221 | }
222 |
223 | export function twitchBan() { // B A N V I E W E R
224 | let e = new Dialog({
225 | title: 'Ban Viewer from Channel',
226 | content: `
227 |
235 | `,
236 | buttons: {
237 | no: {
238 | icon: '',
239 | label: 'Cancel'
240 | },
241 | yes: {
242 | icon: '',
243 | label: 'BAN',
244 | callback: (html) => {
245 | let input = html.find('[name="banInput"]').val();
246 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
247 | fsMod.client.say(myChannel, '/ban ' + input)
248 | }
249 | },
250 | stop: {
251 | icon: '',
252 | callback: (html) => {
253 | let input = html.find('[name="banInput"]').val();
254 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
255 | fsMod.client.say(myChannel, '/unban ' + input)
256 | }
257 | }
258 | },
259 | default: 'no',
260 | close: () => {
261 | console.log('Another one bites the dust!');
262 | }
263 | }).render(true)
264 | }
265 |
266 | export function twitchSlow() { // S L O W C H A T R A T E
267 | let d = new Dialog({
268 | title: 'Twitch Channel Chat Rate',
269 | content: `
270 |
278 | `,
279 | buttons: {
280 | no: {
281 | icon: '',
282 | label: 'Cancel'
283 | },
284 | yes: {
285 | icon: '',
286 | label: 'SLOW',
287 | callback: (html) => {
288 | let input = html.find('[name="slowInput"]').val();
289 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
290 | fsMod.client.say(myChannel, '/slow ' + input)
291 | }
292 | },
293 | stop: {
294 | icon: '',
295 | callback: (html) => {
296 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
297 | fsMod.client.say(myChannel, '/slowoff')
298 | }
299 | },
300 | },
301 | default: 'yes',
302 | close: () => {
303 | console.log('Slowing it down a bit!');
304 | }
305 | }).render(true)
306 | }
307 |
308 | export function twitchClear() { // C L E A R T W I T C H C H A T
309 | let d = new Dialog({
310 | title: 'Clear Twitch Channel',
311 | content: `
312 |
316 | `,
317 | buttons: {
318 | no: {
319 | icon: '',
320 | label: 'Cancel'
321 | },
322 | yes: {
323 | icon: '',
324 | label: 'CLEAR',
325 | callback: (html) => {
326 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
327 | fsMod.client.say(myChannel, "/clear")
328 | }
329 | },
330 | },
331 | default: 'yes',
332 | close: () => {
333 | console.log('Nobody saw that.');
334 | }
335 | }).render(true)
336 | }
337 |
338 | export function twitchRaid() { // R A I D C H A N N E L
339 | let d = new Dialog({
340 | title: 'Raid Twitch Channel',
341 | content: `
342 |
349 | `,
350 | buttons: {
351 | no: {
352 | icon: '',
353 | label: 'Cancel'
354 | },
355 | yes: {
356 | icon: '',
357 | label: 'RAID',
358 | callback: (html) => {
359 | let input = html.find('[name="raidInput"]').val();
360 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
361 | fsMod.client.say(myChannel, '/raid ' + input)
362 | }
363 | },
364 | },
365 | default: 'yes',
366 | close: () => {
367 | console.log('Off to adventure!');
368 | }
369 | }).render(true)
370 | }
371 |
372 | export function twitchRoll() { // A S K F O R R O L L
373 | let priv = false;
374 | let d = new Dialog({
375 | title: 'Request Roll',
376 | content: `
377 |
394 | `,
395 | buttons: {
396 | no: {
397 | icon: '',
398 | label: 'Cancel'
399 | },
400 | yes: {
401 | icon: '',
402 | label: 'Request Roll',
403 | callback: (html) => {
404 | let priv = $("#rollP").is(":checked") ? "true" : "false";
405 | let myChannel = (game.settings.get("streamMod", "streamChannel"))
406 | let who = html.find('[name="whoDice"]').val();
407 | let dice = html.find('[name="rollDice"]').val();
408 | if (who != "") {
409 | let viewerRoll = game.settings.get("streamMod", "streamRollReq2"); //(localize('settings.viewerRoll.req'));
410 | let v1 = viewerRoll.replace('${who}', who);
411 | let vFin = v1.replace('${dice}', dice);
412 | fsMod.client.say(myChannel, vFin); //`The GM is requesting ${who} to roll! [Type !gm ${dice} to roll]`);
413 | diceWait(dice, who, priv);
414 | }
415 | else {
416 | let reqRoll = game.settings.get("streamMod", "streamRollReq"); //(localize('settings.reqRoll.req'));
417 | let v1 = reqRoll.replace('${who}', who);
418 | let vFin = v1.replace('${dice}', dice);
419 | fsMod.client.say(myChannel, vFin); //"The GM is requesting a viewer to roll! [Type !gm " + dice + " to roll]");
420 | diceWaitAll(dice, priv);
421 | }
422 | }
423 | },
424 | },
425 | default: 'yes',
426 | close: () => {
427 | console.log('Request roll');
428 | }
429 | }).render(true)
430 | }
431 |
432 | export function twitchEmote() { // E M O T E / B R O A D C A S T
433 | let e = new Dialog({
434 | title: 'Emote / Message Channel',
435 | content: `
436 |
444 | `,
445 | buttons: {
446 | no: {
447 | icon: '',
448 | label: 'Cancel'
449 | },
450 | yes: {
451 | icon: '',
452 | label: 'Emote',
453 | callback: (html) => {
454 | let input = html.find('[name="emoteInput"]').val();
455 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
456 | fsMod.client.say(myChannel, '/me ' + input)
457 | }
458 | },
459 | raw: {
460 | icon: '',
461 | label: 'Message',
462 | callback: (html) => {
463 | let input = html.find('[name="emoteInput"]').val();
464 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
465 | fsMod.client.say(myChannel, input)
466 | console.log(input)
467 | }
468 | }
469 | },
470 | default: 'raw',
471 | close: () => {
472 | console.log('Another one bites the dust!');
473 | }
474 | }).render(true)
475 | }
476 |
477 | // F U N S T U F F
478 |
479 | export function diceWait(dice, who, priv) { // G M R E Q U E S T R O L L
480 | fsMod.client.once("message", (channel, tags, message, self) => {
481 | let subCheck = game.settings.get("streamMod", "subCheck");
482 | let modStatus = tags["mod"];
483 | let subStatus = tags["subscriber"];
484 | if (levelCheck(subCheck, modStatus, subStatus)) return diceWait(dice, who);
485 | if (message.includes("!" + game.settings.get("streamMod", "chatCommandAlias")) && message.includes(dice).toLowerCase()) {
486 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
487 | let res = message.slice(3);
488 | let whoIs = who.toLowerCase();
489 | let idCheck = tags["display-name"].toLowerCase();
490 | let thankRoll = game.settings.get("streamMod", "streamThank"); //(localize('settings.thankRoll.req'));
491 | if (whoIs != idCheck) { return diceWait(dice, who); }
492 | if (whoIs == idCheck) {
493 | if (priv === 'true') {
494 | new Roll(res).roll().then((roll) => {
495 | let xroll = roll.toMessage({ speaker: { alias: "[Twitch] " + `${tags["display-name"]}` } }, { rollMode: "gmroll" })
496 | fsMod.client.say(myChannel, `${tags["display-name"]} rolls ` + dice + ` = [` + roll.total + `] ` + thankRoll);
497 | });
498 | } else {
499 | let roll = new Roll(res).roll().then((roll) => {
500 | let xroll = roll.toMessage({ speaker: { alias: "[Twitch] " + `${tags["display-name"]}` } });
501 | fsMod.client.say(myChannel, `${tags["display-name"]} rolls ` + dice + ` = [` + roll.total + `] ` + thankRoll);
502 | });
503 | }
504 | return;
505 | }
506 | } return diceWait(dice, who);
507 | }
508 | )
509 | }
510 |
511 | export function diceWaitAll(dice, priv) { // G M R E Q U E S T R O L L - A L L V I E W E R S
512 | fsMod.client.once("message", (channel, tags, message, self) => {
513 | let subCheck = game.settings.get("streamMod", "subCheck");
514 | let modStatus = tags["mod"];
515 | let subStatus = tags["subscriber"];
516 | if (levelCheck(subCheck, modStatus, subStatus)) return diceWaitAll(dice);
517 | if (message == "!" + game.settings.get("streamMod", "chatCommandAlias") + " " + dice) {
518 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
519 | let res = message.slice(3);
520 | let thankRoll = game.settings.get("streamMod", "streamThank"); //(localize('settings.thankRoll.req'));
521 | if (priv === 'true') {
522 | new Roll(res).roll().then((roll) => {
523 | let xroll = roll.toMessage({ speaker: { alias: "[Twitch] " + `${tags["display-name"]}` } }, { rollMode: "gmroll" })
524 | fsMod.client.say(myChannel, `${tags["display-name"]} rolls ` + dice + ` = [` + roll.total + `] ` + thankRoll);
525 | });
526 | } else {
527 | new Roll(res).roll().then((roll) => {
528 | let xroll = roll.toMessage({ speaker: { alias: "[Twitch] " + `${tags["display-name"]}` } });
529 | fsMod.client.say(myChannel, `${tags["display-name"]} rolls ` + dice + ` = [` + roll.total + `] ` + thankRoll);
530 | });
531 | }
532 | return;
533 | }
534 | else return diceWaitAll(dice);
535 | }
536 | )
537 | }
538 |
539 | export function streamDice() { // I N L I N E D I C E F O R T W I T C H
540 | fsMod.client.on("message", (channel, tags, message, self) => {
541 | if (message.includes("!roll")) {
542 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
543 | let res = message.slice(6);
544 | roller.clearLog();
545 | roller.roll(res);
546 | const firstGm = game.users.find((u) => u.isGM && u.active);
547 | if (firstGm && game.user === firstGm) {
548 | fsMod.client.say(myChannel, `${tags["display-name"]} rolls: ` + roller.output)
549 | }
550 | }
551 | })
552 | }
553 |
554 | // O B F U S C A T I O N
555 | String.prototype.obfs = function (key, n = 126) { // O B F U S C A T E S T R I N G
556 | if (!(typeof (key) === 'number' && key % 1 === 0)
557 | || !(typeof (key) === 'number' && key % 1 === 0)) {
558 | return this.toString();
559 | }
560 | var chars = this.toString().split('');
561 | for (var i = 0; i < chars.length; i++) {
562 | var c = chars[i].charCodeAt(0);
563 | if (c <= n) {
564 | chars[i] = String.fromCharCode((chars[i].charCodeAt(0) + key) % n);
565 | }
566 | }
567 | return chars.join('');
568 | };
569 |
570 | String.prototype.defs = function (key, n = 126) { // D E - O B F U S C A T E S T R I N G
571 | if (!(typeof (key) === 'number' && key % 1 === 0)
572 | || !(typeof (key) === 'number' && key % 1 === 0)) {
573 | return this.toString();
574 | }
575 | return this.toString().obfs(n - key);
576 | };
577 |
578 | /* F U T U R E D E V
579 | export function streamStart() {
580 | var rtpSendParameters = rtpSender.getParameters()
581 |
582 | try {
583 | let mediaStream = await navigator.mediaDevices.getDisplayMedia({video:true});
584 | videoElement.srcObject = mediaStream;
585 | } catch (e) {
586 | console.log('Unable to acquire screen capture: ' + e);
587 | }
588 | }*/
589 |
--------------------------------------------------------------------------------
/scripts/modLayer.js:
--------------------------------------------------------------------------------
1 | // (F O U N D R Y - S T R E A M - M O D)
2 |
3 | // ( M O D L A Y E R . J S)
4 |
5 | import { twitchBan, twitchKick, twitchSlow, twitchClear, twitchRaid, twitchRoll, twitchEmote } from "./fsmcore.js";
6 |
7 | // C A N V A S C O N T R O L B U T T O N S
8 |
9 | export default class fsmLayer extends CanvasLayer { // B U T T O N C O N F I G
10 | constructor() {
11 | super();
12 | this.layername = "fsMod";
13 | console.log("Foundry Stream Module| Drawing Layer | Loaded into Drawing Layer");
14 | }
15 |
16 | setButtons() {
17 | this.newButtons = {
18 | name: "fsMod",
19 | icon: "fab fa-twitch",
20 | layer: "fsMod",
21 | title: "FSM Controls",
22 | tools: [
23 | {
24 | icon: "fas fa-dice",
25 | name: "twitchRoll",
26 | title: "Ask for Roll",
27 | onClick: () => twitchRoll(),
28 | },
29 | {
30 | icon: "fas fa-broadcast-tower",
31 | name: "emoteChannel",
32 | title: "Emote/Raw Message",
33 | onClick: () => twitchEmote()
34 | },
35 | {
36 | icon: "fas fa-eraser",
37 | name: "ClearTwitch",
38 | title: "Clear Twitch Chat",
39 | onClick: () => twitchClear(),
40 | },
41 | {
42 | icon: "fas fa-hourglass-half",
43 | name: "SlowTwitch",
44 | title: "Slow Twitch Chat",
45 | onClick: () => twitchSlow(),
46 | },
47 | {
48 | icon: "fas fa-comment-slash",
49 | name: "KickUser",
50 | title: "Timeout Twitch User",
51 | onClick: () => twitchKick()
52 | },
53 | {
54 | icon: "fas fa-hand-middle-finger",
55 | name: "BanUser",
56 | title: "Ban Twitch User",
57 | onClick: () => twitchBan()
58 | },
59 | {
60 | icon: "fas fa-khanda",
61 | name: "RaidChannel",
62 | title: "Raid Channel",
63 | onClick: () => twitchRaid()
64 | },
65 | ],
66 | };
67 | }
68 |
69 | roleTest() {
70 | Hooks.on("getSceneControlButtons", (controls) => {
71 | console.log("Foundry Stream Module | Testing User role = " + game.user.data.role);
72 | if (game.user.data.role >= (game.settings.get("streamMod", "streamRole"))) {
73 | controls.push(this.newButtons);
74 | }
75 | });
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/scripts/settings.js:
--------------------------------------------------------------------------------
1 | // (F O U N D R Y - S T R E A M - M O D)
2 |
3 | 'use strict';
4 |
5 | import { localize } from "./utils.js";
6 |
7 | function getSetting(key) {
8 | return game.settings.get("streamMod", key);
9 | }
10 |
11 | function registerSetting(setting) {
12 | return game.settings.register("streamMod", setting.key, setting.options);
13 | }
14 |
15 | function registerSettings() { // R E G I S T E R M O D U L E S E T T I N G S
16 | const settings = [
17 | {
18 | key: "streamChannel",
19 | options: {
20 | name: localize('settings.twitchChannel.name'),
21 | hint: localize('settings.twitchChannel.hint'),
22 | scope: "world",
23 | config: true,
24 | type: String,
25 | default: "",
26 | },
27 | },
28 | {
29 | key: "streamUN",
30 | options: {
31 | name: localize('settings.twitchUN.name'),
32 | hint: localize('settings.twitchUN.hint'),
33 | scope: "world",
34 | config: true,
35 | type: String,
36 | default: "",
37 | },
38 | },
39 | {
40 | key: "streamAuth",
41 | options: {
42 | name: localize('settings.twitchAuth.name'),
43 | hint: localize('settings.twitchAuth.hint'),
44 | config: true,
45 | scope: "world",
46 | type: String,
47 | default: "",
48 | restricted: true,
49 | onChange: (value) => {
50 | let newvalue = value.toLowerCase();
51 | if (newvalue.includes("oauth:")) {
52 | game.settings.set("streamMod", "streamAuth", newvalue.obfs(13))
53 | } else return;
54 | }
55 | }
56 | },
57 | {
58 | key: "streamCmd",
59 | options: {
60 | name: localize('settings.xCmd.name'),
61 | hint: localize('settings.xCmd.hint'),
62 | config: true,
63 | scope: "world",
64 | type: Boolean,
65 | toggle: true,
66 | default: false
67 | }
68 | },
69 | {
70 | key: "streamOnly",
71 | options: {
72 | name: "GM Only Mode",
73 | hint: "Module is only available to the GM.",
74 | config: false,
75 | scope: "world",
76 | type: Boolean,
77 | toggle: true,
78 | default: false
79 | }
80 | },
81 | {
82 | key: "streamRole",
83 | options: {
84 | name: localize('settings.streamRole.name'),
85 | hint: localize('settings.streamRole.hint'),
86 | config: true,
87 | scope: "world",
88 | type: String,
89 | choices: {
90 | "1": "Player",
91 | "2": "Trusted",
92 | "3": "Assistant Gamemaster",
93 | "4": "Gamemaster"
94 | },
95 | default: "4"
96 | }
97 | },
98 | {
99 | key: "streamChatType",
100 | options: {
101 | name: localize('settings.chatType.name'),
102 | hint: localize('settings.chatType.hint'),
103 | config: true,
104 | scope: "world",
105 | type: Number,
106 | choices: {
107 | 1: "Out of Character",
108 | 2: "In Character",
109 | 3: "Emote",
110 | 5: "Roll",
111 | 0: "Other"
112 | },
113 | default: 1
114 | }
115 | },
116 | {
117 | key: "subCheck",
118 | options: {
119 | name: localize('settings.subCheck.name'),
120 | hint: localize('settings.subCheck.hint'),
121 | config: false,
122 | scope: "world",
123 | type: Boolean,
124 | toggle: true,
125 | default: false,
126 | },
127 | },
128 | {
129 | key: "streamModEcho",
130 | options: {
131 | name: localize('settings.fsbotEcho.name'),
132 | hint: localize('settings.fsbotEcho.hint'),
133 | config: true,
134 | scope: "world",
135 | type: Boolean,
136 | toggle: true,
137 | default: false,
138 | },
139 | },
140 | {
141 | key: "streamGM",
142 | options: {
143 | name: localize('settings.fsModAllChatMessages.name'),
144 | hint: localize('settings.fsModAllChatMessages.hint'),
145 | scope: "world",
146 | config: true,
147 | type: Boolean,
148 | toggle: true,
149 | default: false,
150 | //restricted: true,
151 | },
152 | },
153 | {
154 | key: "streamQuiet",
155 | options: {
156 | name: localize('settings.streamQuiet.name'),
157 | hint: localize('settings.streamQuiet.hint'),
158 | config: true,
159 | scope: "world",
160 | type: Boolean,
161 | toggle: true,
162 | default: false,
163 | },
164 | },
165 | {
166 | key: "tabbedChat",
167 | options: {
168 | name: localize("settings.tabbedChat.name"),
169 | hint: localize("settings.tabbedChat.hint"),
170 | scope: 'world',
171 | config: true,
172 | default: false,
173 | type: Boolean,
174 | },
175 | },
176 | {
177 | key: "icChatInOoc",
178 | options: {
179 | name: localize("settings.IcChatInOoc.name"),
180 | hint: localize("settings.IcChatInOoc.hint"),
181 | scope: 'world',
182 | config: false,
183 | default: true,
184 | type: Boolean,
185 | },
186 | },
187 | {
188 | key: "connectMSG",
189 | options: {
190 | name: localize('settings.streamConnect.name'),
191 | hint: localize('settings.streamConnect.hint'),
192 | config: true,
193 | scope: "world",
194 | type: String,
195 | choices: {
196 | "1": "Connect with announcement",
197 | "2": "Connect silently"
198 | },
199 | default: "1"
200 | }
201 | },
202 | {
203 | key: "streamAnnounce1",
204 | options: {
205 | name: localize('settings.streamAnounce.name'),
206 | hint: localize('settings.streamAnounce.hint'),
207 | config: true,
208 | scope: "world",
209 | type: String,
210 | default: "",
211 | restricted: true,
212 | }
213 | },
214 | {
215 | key: "streamAnnounce1T",
216 | options: {
217 | name: localize('settings.streamAnounceT.name'),
218 | hint: localize('settings.streamAnounceT.hint'),
219 | config: true,
220 | scope: "world",
221 | type: Number,
222 | default: "",
223 | restricted: true,
224 | }
225 | },
226 | {
227 | key: "streamAnnounce2",
228 | options: {
229 | name: localize('settings.streamAnounce.name'),
230 | hint: localize('settings.streamAnounce.hint'),
231 | config: true,
232 | scope: "world",
233 | type: String,
234 | default: "",
235 | restricted: true,
236 | }
237 | },
238 | {
239 | key: "streamAnnounce2T",
240 | options: {
241 | name: localize('settings.streamAnounceT.name'),
242 | hint: localize('settings.streamAnounceT.hint'),
243 | config: true,
244 | scope: "world",
245 | type: Number,
246 | default: "",
247 | restricted: true,
248 | }
249 | },
250 | {
251 | key: "streamDice",
252 | options: {
253 | name: localize('settings.streamDice.name'),
254 | hint: localize('settings.streamDice.hint'),
255 | config: true,
256 | scope: "world",
257 | type: Boolean,
258 | default: true,
259 | restricted: true,
260 | }
261 | },
262 | {
263 | key: "streamRollReq",
264 | options: {
265 | name: localize('settings.streamRoll.name'),
266 | hint: localize('settings.streamRoll.hint'),
267 | config: true,
268 | scope: "world",
269 | type: String,
270 | default: "The GM is requesting a viewer to roll! [Type !gm ${dice} to roll]",
271 | restricted: true,
272 | }
273 | },
274 | {
275 | key: "streamRollReq2",
276 | options: {
277 | name: localize('settings.streamRoll2.name'),
278 | hint: localize('settings.streamRoll2.hint'),
279 | config: true,
280 | scope: "world",
281 | type: String,
282 | default: "The GM is requesting ${who} to roll! [Type !gm ${dice} to roll]",
283 | restricted: true,
284 | }
285 | },
286 | {
287 | key: "streamThank",
288 | options: {
289 | name: localize('settings.thankRoll.name'),
290 | hint: localize('settings.thankRoll.hint'),
291 | config: true,
292 | scope: "world",
293 | type: String,
294 | default: "Thank you for the roll!",
295 | restricted: true,
296 | }
297 | },
298 | {
299 | key: "hideTwitchChat",
300 | options: {
301 | name: localize('settings.hideTwitchChat.name'),
302 | hint: localize('settings.hideTwitchChat.hint'),
303 | config: true,
304 | scope: "world",
305 | type: Boolean,
306 | default: false,
307 | restricted: true,
308 | }
309 | },
310 | {
311 | key: "chatCommandAlias",
312 | options: {
313 | name: localize('settings.chatCommandAlias.name'),
314 | hint: localize('settings.chatCommandAlias.hint'),
315 | config: true,
316 | scope: "world",
317 | type: String,
318 | default: "gm",
319 | restricted: true,
320 | }
321 | }
322 | ];
323 | settings.forEach(registerSetting);
324 | }
325 |
326 | export { getSetting, registerSettings };
327 |
328 | // O B F U S C A T I O N
329 | String.prototype.obfs = function (key, n = 126) { // O B F U S C A T E S T R I N G
330 | if (!(typeof (key) === 'number' && key % 1 === 0)
331 | || !(typeof (key) === 'number' && key % 1 === 0)) {
332 | return this.toString();
333 | }
334 | var chars = this.toString().split('');
335 | for (var i = 0; i < chars.length; i++) {
336 | var c = chars[i].charCodeAt(0);
337 | if (c <= n) {
338 | chars[i] = String.fromCharCode((chars[i].charCodeAt(0) + key) % n);
339 | }
340 | }
341 | return chars.join('');
342 | };
343 |
344 | String.prototype.defs = function (key, n = 126) { // D E - O B F U S C A T E S T R I N G
345 | if (!(typeof (key) === 'number' && key % 1 === 0)
346 | || !(typeof (key) === 'number' && key % 1 === 0)) {
347 | return this.toString();
348 | }
349 | return this.toString().obfs(n - key);
350 | };
--------------------------------------------------------------------------------
/scripts/streamTwitch.js:
--------------------------------------------------------------------------------
1 | // (F O U N D R Y - S T R E A M - M O D)
2 | import { xCmd, outChat, inChat, gmOnly } from "../scripts/fsmcore.js";
3 | export const fsMod = {
4 | client: null,
5 | options: {}
6 | };
7 |
8 |
9 | window.streamIn = (content) => { // A L I A S M E S S A G E S
10 | if (game.settings.get("streamMod", "hideTwitchChat")) return;
11 | ChatMessage.create({
12 | content: content,
13 | type: game.settings.get("streamMod", "streamChatType"),
14 | speaker: ChatMessage.getSpeaker({ alias: "Stream Chat" }),
15 | });
16 | };
17 |
18 | window.streamOut = (content, who) => { // S E N D O U T T O T W I T C H
19 | if (!outChat()) return;
20 |
21 | let fullmsg;
22 | if (who != undefined) {
23 | let tempAlias = who;
24 | let whom = ("[" + tempAlias + "]: ")
25 | fullmsg = (whom + content);
26 | } else {
27 | fullmsg = content;
28 | }
29 |
30 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
31 | //const firstGm = game.users.find((u) => u.isGM && u.active);
32 | //if (firstGm && game.user === firstGm)
33 | fsMod.client.say(myChannel, fullmsg);
34 | console.log(content);
35 | };
36 |
37 | window.onStream = () => { // H A N D L E M E S S A G E S F R O M T W I T C H + S U B S
38 | fsMod.client.on("message", (channel, tags, message, self, userstate) => {
39 | //let subCheck = game.settings.get("streamMod", "subCheck");
40 | //let modStatus = tags["mod"];
41 | //let subStatus = tags["subscriber"];
42 | //if (levelCheck(subCheck, modStatus, subStatus)) return;
43 | if (!tags["emotes"]) {
44 | let strx = game.settings.get("streamMod", "streamUN")
45 | if (self) return;
46 | if (message.includes('!r')) return;
47 | if (message.includes('!') + game.settings.get("streamMod", "chatCommandAlias")) return;
48 | if (!inChat()) return;
49 | if (tags["display-name"].includes(strx)) return
50 | const firstGm = game.users.find((u) => u.isGM && u.active);
51 | if (firstGm && game.user === firstGm) {
52 | streamIn(`${tags["display-name"]}: ${message}`);
53 | }
54 | } else {
55 | let emotes = (tags["emotes"]);
56 | return getMessageEmotes(tags, message, { emotes });
57 | }
58 | })
59 | fsMod.client.on("subscription", function (channel, username, method, message, userstate) {
60 | streamOut(username + ' just subscribed!!');
61 | });
62 | fsMod.client.on("resub", function (channel, username, months, message, userstate, methods) {
63 | streamOut(username + ' just re-subscribed for ' + months + '!!');
64 | });
65 | }
66 |
67 | window.awaitStream = (streamTrigger, content) => { // W A I T F O R I T
68 | fsMod.client.on("message", (channel, tags, message, self) => {
69 | let strx = game.settings.get("streamMod", "streamUN")
70 | if (self) return;
71 | if (!tags["display-name"] || tags["display-name"].includes(strx)) return
72 | const firstGm = game.users.find((u) => u.isGM && u.active);
73 | if (firstGm && game.user === firstGm)
74 | if (message.includes(streamTrigger)) {
75 | streamOut(content);
76 | }
77 | })
78 | }
79 |
80 | window.triggerStream = (streamTrigger, destFunc, args) => { // T R I G G E R S
81 | fsMod.client.on("message", (channel, tags, message, self) => {
82 | let strx = game.settings.get("streamMod", "streamUN")
83 | if (self) return;
84 | if (!tags["display-name"] || tags["display-name"].includes(strx)) return
85 | const firstGm = game.users.find((u) => u.isGM && u.active);
86 | if (firstGm && game.user === firstGm)
87 | if (message.includes(streamTrigger)) {
88 | destFunc(args);
89 | }
90 | })
91 | }
92 |
93 | function getMessageEmotes(tags, message, { emotes }) {
94 | if (!inChat()) return;
95 | if (!emotes) return message;
96 | let strx = game.settings.get("streamMod", "streamUN")
97 | if (!tags["display-name"] || tags["display-name"].includes(strx)) return
98 | const stringReplacements = [];
99 |
100 | Object.entries(emotes).forEach(([id, positions]) => { // iterate of emotes to access ids and positions
101 |
102 | const position = positions[0]; // use only the first position to find out the emote key word
103 | const [start, end] = position.split("-");
104 | const stringToReplace = message.substring(
105 | parseInt(start, 10),
106 | parseInt(end, 10) + 1
107 | );
108 |
109 | stringReplacements.push({
110 | stringToReplace: stringToReplace,
111 | replacement: ``,
112 | });
113 | });
114 |
115 | const messageHTML = stringReplacements.reduce( // generate HTML and replace all emote keywords with image elements
116 | (acc, { stringToReplace, replacement }) => {
117 | return acc.split(stringToReplace).join(replacement);
118 | },
119 | message
120 | );
121 |
122 | const firstGm = game.users.find((u) => u.isGM && u.active); //return messageHTML;
123 | if (firstGm && game.user === firstGm) {
124 | streamIn(`${tags["display-name"]}: ${messageHTML}`);
125 | }
126 | }
127 |
128 | export function levelCheck(subCheck, modStatus, subStatus) {
129 | if (subCheck == true) {
130 | if (modStatus == true) return false
131 | }
132 | else {
133 | if (subStatus == true) return false
134 | else return;
135 | }
136 | if (subCheck == false) return false
137 | else return true;
138 | }
--------------------------------------------------------------------------------
/scripts/temp.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | getWoundThresholdData(data = null) {
4 | data = data ?? this.data;
5 |
6 | const woundMult = this.getWoundThresholdMultiplier(data),
7 | woundLevel = getProperty(data, "data.attributes.woundThresholds.level") ?? 0,
8 | let EnduranceCheck = (canvas.tokens.controlled[0].actor.data.items.filter(f => f.type === "feat").find(f => f.name === "Endurance"))
9 | if (EnduranceCheck != "")
10 | woundPenalty = woundLevel * woundMult + (getProperty(data, "data.attributes.woundThresholds.mod") ?? 0);
11 | return {
12 | level: woundLevel,
13 | penalty: woundPenalty,
14 | multiplier: woundMult,
15 | valid: woundLevel > 0 && woundMult > 0,
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/scripts/tmi.min.js:
--------------------------------------------------------------------------------
1 | !function s(o,i,r){function a(t,e){if(!i[t]){if(!o[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(c)return c(t,!0);throw(n=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",n}n=i[t]={exports:{}},o[t][0].call(n.exports,function(e){return a(o[t][1][e]||e)},n,n.exports,s,o,i,r)}return i[t].exports}for(var c="function"==typeof require&&require,e=0;e: ").concat(m)),t.tags.hasOwnProperty("username")||(t.tags.username=M),t.tags["message-type"]="whisper";var J=H.channel(t.tags.username);this.emits(["whisper","message"],[[J,t.tags,m,!1]]);break;case"PRIVMSG":t.tags.username=t.prefix.split("!")[0],"jtv"===t.tags.username?(a=H.username(m.split(" ")[0]),c=m.includes("auto"),m.includes("hosting you for")?(J=H.extractNumber(m),this.emit("hosted",l,a,J,c)):m.includes("hosting you")&&this.emit("hosted",l,a,0,c)):(a=H.get(this.opts.options.messagesLogLevel,"info"),c=H.actionMessage(m),t.tags["message-type"]=c?"action":"chat",m=c?c[1]:m,t.tags.hasOwnProperty("bits")?this.emit("cheer",l,t.tags,m):(t.tags.hasOwnProperty("msg-id")?"highlighted-message"!==t.tags["msg-id"]&&"skip-subs-mode-message"!==t.tags["msg-id"]||(u=t.tags["msg-id"],this.emit("redeem",l,t.tags.username,u,t.tags,m)):t.tags.hasOwnProperty("custom-reward-id")&&(u=t.tags["custom-reward-id"],this.emit("redeem",l,t.tags.username,u,t.tags,m)),c?(this.log[a]("[".concat(l,"] *<").concat(t.tags.username,">: ").concat(m)),this.emits(["action","message"],[[l,t.tags,m,!1]])):(this.log[a]("[".concat(l,"] <").concat(t.tags.username,">: ").concat(m)),this.emits(["chat","message"],[[l,t.tags,m,!1]]))));break;default:this.log.warn("Could not parse message:\n".concat(JSON.stringify(t,null,4)))}}},n.prototype.connect=function(){var s=this;return new Promise(function(t,n){s.server=H.get(s.opts.connection.server,"irc-ws.chat.twitch.tv"),s.port=H.get(s.opts.connection.port,80),s.secure&&(s.port=443),443===s.port&&(s.secure=!0),s.reconnectTimer=s.reconnectTimer*s.reconnectDecay,s.reconnectTimer>=s.maxReconnectInterval&&(s.reconnectTimer=s.maxReconnectInterval),s._openConnection(),s.once("_promiseConnect",function(e){e?n(e):t([s.server,~~s.port])})})},n.prototype._openConnection=function(){this.ws=new r("".concat(this.secure?"wss":"ws","://").concat(this.server,":").concat(this.port,"/"),"irc"),this.ws.onmessage=this._onMessage.bind(this),this.ws.onerror=this._onError.bind(this),this.ws.onclose=this._onClose.bind(this),this.ws.onopen=this._onOpen.bind(this)},n.prototype._onOpen=function(){var t=this;H.isNull(this.ws)||1!==this.ws.readyState||(this.log.info("Connecting to ".concat(this.server," on port ").concat(this.port,"..")),this.emit("connecting",this.server,~~this.port),this.username=H.get(this.opts.identity.username,H.justinfan()),this._getToken().then(function(e){e=H.password(e);t.log.info("Sending authentication to server.."),t.emit("logon"),t.ws.send("CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership"),e?t.ws.send("PASS ".concat(e)):H.isJustinfan(t.username)&&t.ws.send("PASS SCHMOOPIIE"),t.ws.send("NICK ".concat(t.username))}).catch(function(e){t.emits(["_promiseConnect","disconnected"],[[e],["Could not get a token."]])}))},n.prototype._getToken=function(){var e,t=this.opts.identity.password;return"function"==typeof t?(e=t())instanceof Promise?e:Promise.resolve(e):Promise.resolve(t)},n.prototype._onMessage=function(e){var t=this;e.data.split("\r\n").forEach(function(e){H.isNull(e)||t.handleMessage(q.msg(e))})},n.prototype._onError=function(){var t=this;this.moderators={},this.userstate={},this.globaluserstate={},clearInterval(this.pingLoop),clearTimeout(this.pingTimeout),this.reason=H.isNull(this.ws)?"Connection closed.":"Unable to connect.",this.emits(["_promiseConnect","disconnected"],[[this.reason]]),this.reconnect&&this.reconnections===this.maxReconnectAttempts&&(this.emit("maxreconnect"),this.log.error("Maximum reconnection attempts reached.")),this.reconnect&&!this.reconnecting&&this.reconnections<=this.maxReconnectAttempts-1&&(this.reconnecting=!0,this.reconnections=this.reconnections+1,this.log.error("Reconnecting in ".concat(Math.round(this.reconnectTimer/1e3)," seconds..")),this.emit("reconnect"),setTimeout(function(){t.reconnecting=!1,t.connect().catch(function(e){return t.log.error(e)})},this.reconnectTimer)),this.ws=null},n.prototype._onClose=function(){var t=this;this.moderators={},this.userstate={},this.globaluserstate={},clearInterval(this.pingLoop),clearTimeout(this.pingTimeout),this.wasCloseCalled?(this.wasCloseCalled=!1,this.reason="Connection closed.",this.log.info(this.reason),this.emits(["_promiseConnect","_promiseDisconnect","disconnected"],[[this.reason],[null],[this.reason]])):(this.emits(["_promiseConnect","disconnected"],[[this.reason]]),this.reconnect&&this.reconnections===this.maxReconnectAttempts&&(this.emit("maxreconnect"),this.log.error("Maximum reconnection attempts reached.")),this.reconnect&&!this.reconnecting&&this.reconnections<=this.maxReconnectAttempts-1&&(this.reconnecting=!0,this.reconnections=this.reconnections+1,this.log.error("Could not connect to server. Reconnecting in ".concat(Math.round(this.reconnectTimer/1e3)," seconds..")),this.emit("reconnect"),setTimeout(function(){t.reconnecting=!1,t.connect().catch(function(e){return t.log.error(e)})},this.reconnectTimer))),this.ws=null},n.prototype._getPromiseDelay=function(){return this.currentLatency<=600?600:this.currentLatency+100},n.prototype._sendCommand=function(s,o,i,r){var a=this;return new Promise(function(e,t){if(H.isNull(a.ws)||1!==a.ws.readyState)return t("Not connected to server.");var n;"number"==typeof s&&H.promiseDelay(s).then(function(){return t("No response from Twitch.")}),H.isNull(o)?(a.log.info("Executing command: ".concat(i)),a.ws.send(i)):(n=H.channel(o),a.log.info("[".concat(n,"] Executing command: ").concat(i)),a.ws.send("PRIVMSG ".concat(n," :").concat(i))),"function"==typeof r?r(e,t):e()})},n.prototype._sendMessage=function(c,u,l,m){var h=this;return new Promise(function(e,t){if(H.isNull(h.ws)||1!==h.ws.readyState)return t("Not connected to server.");if(H.isJustinfan(h.getUsername()))return t("Cannot send anonymous messages.");var n,s=H.channel(u);h.userstate[s]||(h.userstate[s]={}),500<=l.length&&(n=H.splitLine(l,500),l=n[0],setTimeout(function(){h._sendMessage(c,u,n[1],function(){})},350)),h.ws.send("PRIVMSG ".concat(s," :").concat(l));var o={};Object.keys(h.emotesets).forEach(function(e){return h.emotesets[e].forEach(function(e){return(H.isRegex(e.code)?q.emoteRegex:q.emoteString)(l,e.code,e.id,o)})});var i=H.merge(h.userstate[s],q.emotes({emotes:q.transformEmotes(o)||null})),r=H.get(h.opts.options.messagesLogLevel,"info"),a=H.actionMessage(l);a?(i["message-type"]="action",h.log[r]("[".concat(s,"] *<").concat(h.getUsername(),">: ").concat(a[1])),h.emits(["action","message"],[[s,i,a[1],!0]])):(i["message-type"]="chat",h.log[r]("[".concat(s,"] <").concat(h.getUsername(),">: ").concat(l)),h.emits(["chat","message"],[[s,i,l,!0]])),"function"==typeof m?m(e,t):e()})},n.prototype._updateEmoteset=function(s){var o=this;this.emotes=s,this._getToken().then(function(e){return o.api({url:"/chat/emoticon_images?emotesets=".concat(s),headers:{Authorization:"OAuth ".concat(H.token(e)),"Client-ID":o.clientId}},function(e,t,n){return e?void setTimeout(function(){return o._updateEmoteset(s)},6e4):(o.emotesets=n.emoticon_sets||{},o.emit("emotesets",s,o.emotesets))})}).catch(function(){return setTimeout(function(){return o._updateEmoteset(s)},6e4)})},n.prototype.getUsername=function(){return this.username},n.prototype.getOptions=function(){return this.opts},n.prototype.getChannels=function(){return this.channels},n.prototype.isMod=function(e,t){e=H.channel(e);return this.moderators[e]||(this.moderators[e]=[]),this.moderators[e].includes(H.username(t))},n.prototype.readyState=function(){return H.isNull(this.ws)?"CLOSED":["CONNECTING","OPEN","CLOSING","CLOSED"][this.ws.readyState]},n.prototype._isConnected=function(){return null!==this.ws&&1===this.ws.readyState},n.prototype.disconnect=function(){var n=this;return new Promise(function(e,t){H.isNull(n.ws)||3===n.ws.readyState?(n.log.error("Cannot disconnect from server. Socket is not opened or connection is already closing."),t("Cannot disconnect from server. Socket is not opened or connection is already closing.")):(n.wasCloseCalled=!0,n.log.info("Disconnecting from server.."),n.ws.close(),n.once("_promiseDisconnect",function(){return e([n.server,~~n.port])}))})},void 0!==u&&u.exports&&(u.exports=n),"undefined"!=typeof window&&(window.tmi={},window.tmi.client=n,window.tmi.Client=n)}).call(this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./api":2,"./commands":4,"./events":5,"./logger":6,"./parser":7,"./timer":8,"./utils":9,ws:10}],4:[function(e,t,n){"use strict";var u=e("./utils");function s(s,o){var e=this;return s=u.channel(s),o=u.get(o,30),this._sendCommand(this._getPromiseDelay(),s,"/followers ".concat(o),function(t,n){e.once("_promiseFollowers",function(e){e?n(e):t([s,~~o])})})}function o(s){var e=this;return s=u.channel(s),this._sendCommand(this._getPromiseDelay(),s,"/followersoff",function(t,n){e.once("_promiseFollowersoff",function(e){e?n(e):t([s])})})}function i(s){var e=this;return s=u.channel(s),this._sendCommand(this._getPromiseDelay(),null,"PART ".concat(s),function(t,n){e.once("_promisePart",function(e){e?n(e):t([s])})})}function r(s){var e=this;return s=u.channel(s),this._sendCommand(this._getPromiseDelay(),s,"/r9kbeta",function(t,n){e.once("_promiseR9kbeta",function(e){e?n(e):t([s])})})}function a(s){var e=this;return s=u.channel(s),this._sendCommand(this._getPromiseDelay(),s,"/r9kbetaoff",function(t,n){e.once("_promiseR9kbetaoff",function(e){e?n(e):t([s])})})}function c(s,o){var e=this;return s=u.channel(s),o=u.get(o,300),this._sendCommand(this._getPromiseDelay(),s,"/slow ".concat(o),function(t,n){e.once("_promiseSlow",function(e){e?n(e):t([s,~~o])})})}function l(s){var e=this;return s=u.channel(s),this._sendCommand(this._getPromiseDelay(),s,"/slowoff",function(t,n){e.once("_promiseSlowoff",function(e){e?n(e):t([s])})})}t.exports={action:function(n,s){return n=u.channel(n),s="ACTION ".concat(s,""),this._sendMessage(this._getPromiseDelay(),n,s,function(e,t){e([n,s])})},ban:function(s,o,i){var e=this;return s=u.channel(s),o=u.username(o),i=u.get(i,""),this._sendCommand(this._getPromiseDelay(),s,"/ban ".concat(o," ").concat(i),function(t,n){e.once("_promiseBan",function(e){e?n(e):t([s,o,i])})})},clear:function(s){var e=this;return s=u.channel(s),this._sendCommand(this._getPromiseDelay(),s,"/clear",function(t,n){e.once("_promiseClear",function(e){e?n(e):t([s])})})},color:function(e,s){var o=this;return s=u.get(s,e),this._sendCommand(this._getPromiseDelay(),"#tmijs","/color ".concat(s),function(t,n){o.once("_promiseColor",function(e){e?n(e):t([s])})})},commercial:function(s,o){var e=this;return s=u.channel(s),o=u.get(o,30),this._sendCommand(this._getPromiseDelay(),s,"/commercial ".concat(o),function(t,n){e.once("_promiseCommercial",function(e){e?n(e):t([s,~~o])})})},deletemessage:function(s,e){var o=this;return s=u.channel(s),this._sendCommand(this._getPromiseDelay(),s,"/delete ".concat(e),function(t,n){o.once("_promiseDeletemessage",function(e){e?n(e):t([s])})})},emoteonly:function(s){var e=this;return s=u.channel(s),this._sendCommand(this._getPromiseDelay(),s,"/emoteonly",function(t,n){e.once("_promiseEmoteonly",function(e){e?n(e):t([s])})})},emoteonlyoff:function(s){var e=this;return s=u.channel(s),this._sendCommand(this._getPromiseDelay(),s,"/emoteonlyoff",function(t,n){e.once("_promiseEmoteonlyoff",function(e){e?n(e):t([s])})})},followersonly:s,followersmode:s,followersonlyoff:o,followersmodeoff:o,host:function(o,i){var e=this;return o=u.channel(o),i=u.username(i),this._sendCommand(2e3,o,"/host ".concat(i),function(n,s){e.once("_promiseHost",function(e,t){e?s(e):n([o,i,~~t])})})},join:function(a){var c=this;return a=u.channel(a),this._sendCommand(null,null,"JOIN ".concat(a),function(s,o){var i="_promiseJoin",r=!1,e=function e(t,n){a===u.channel(n)&&(c.removeListener(i,e),r=!0,t?o(t):s([a]))};c.on(i,e);e=c._getPromiseDelay();u.promiseDelay(e).then(function(){r||c.emit(i,"No response from Twitch.",a)})})},mod:function(s,o){var e=this;return s=u.channel(s),o=u.username(o),this._sendCommand(this._getPromiseDelay(),s,"/mod ".concat(o),function(t,n){e.once("_promiseMod",function(e){e?n(e):t([s,o])})})},mods:function(o){var i=this;return o=u.channel(o),this._sendCommand(this._getPromiseDelay(),o,"/mods",function(n,s){i.once("_promiseMods",function(e,t){e?s(e):(t.forEach(function(e){i.moderators[o]||(i.moderators[o]=[]),i.moderators[o].includes(e)||i.moderators[o].push(e)}),n(t))})})},part:i,leave:i,ping:function(){var n=this;return this._sendCommand(this._getPromiseDelay(),null,"PING",function(t,e){n.latency=new Date,n.pingTimeout=setTimeout(function(){null!==n.ws&&(n.wasCloseCalled=!1,n.log.error("Ping timeout."),n.ws.close(),clearInterval(n.pingLoop),clearTimeout(n.pingTimeout))},u.get(n.opts.connection.timeout,9999)),n.once("_promisePing",function(e){return t([parseFloat(e)])})})},r9kbeta:r,r9kmode:r,r9kbetaoff:a,r9kmodeoff:a,raw:function(n){return this._sendCommand(this._getPromiseDelay(),null,n,function(e,t){e([n])})},say:function(n,s){return n=u.channel(n),s.startsWith(".")&&!s.startsWith("..")||s.startsWith("/")||s.startsWith("\\")?"me "===s.substr(1,3)?this.action(n,s.substr(4)):this._sendCommand(this._getPromiseDelay(),n,s,function(e,t){e([n,s])}):this._sendMessage(this._getPromiseDelay(),n,s,function(e,t){e([n,s])})},slow:c,slowmode:c,slowoff:l,slowmodeoff:l,subscribers:function(s){var e=this;return s=u.channel(s),this._sendCommand(this._getPromiseDelay(),s,"/subscribers",function(t,n){e.once("_promiseSubscribers",function(e){e?n(e):t([s])})})},subscribersoff:function(s){var e=this;return s=u.channel(s),this._sendCommand(this._getPromiseDelay(),s,"/subscribersoff",function(t,n){e.once("_promiseSubscribersoff",function(e){e?n(e):t([s])})})},timeout:function(s,o,i,r){var e=this;return s=u.channel(s),o=u.username(o),u.isNull(i)||u.isInteger(i)||(r=i,i=300),i=u.get(i,300),r=u.get(r,""),this._sendCommand(this._getPromiseDelay(),s,"/timeout ".concat(o," ").concat(i," ").concat(r),function(t,n){e.once("_promiseTimeout",function(e){e?n(e):t([s,o,~~i,r])})})},unban:function(s,o){var e=this;return s=u.channel(s),o=u.username(o),this._sendCommand(this._getPromiseDelay(),s,"/unban ".concat(o),function(t,n){e.once("_promiseUnban",function(e){e?n(e):t([s,o])})})},unhost:function(s){var e=this;return s=u.channel(s),this._sendCommand(2e3,s,"/unhost",function(t,n){e.once("_promiseUnhost",function(e){e?n(e):t([s])})})},unmod:function(s,o){var e=this;return s=u.channel(s),o=u.username(o),this._sendCommand(this._getPromiseDelay(),s,"/unmod ".concat(o),function(t,n){e.once("_promiseUnmod",function(e){e?n(e):t([s,o])})})},unvip:function(s,o){var e=this;return s=u.channel(s),o=u.username(o),this._sendCommand(this._getPromiseDelay(),s,"/unvip ".concat(o),function(t,n){e.once("_promiseUnvip",function(e){e?n(e):t([s,o])})})},vip:function(s,o){var e=this;return s=u.channel(s),o=u.username(o),this._sendCommand(this._getPromiseDelay(),s,"/vip ".concat(o),function(t,n){e.once("_promiseVip",function(e){e?n(e):t([s,o])})})},vips:function(e){var t=this;return e=u.channel(e),this._sendCommand(this._getPromiseDelay(),e,"/vips",function(n,s){t.once("_promiseVips",function(e,t){e?s(e):n(t)})})},whisper:function(n,s){var o=this;return(n=u.username(n))===this.getUsername()?Promise.reject("Cannot send a whisper to the same account."):this._sendCommand(this._getPromiseDelay(),"#tmijs","/w ".concat(n," ").concat(s),function(e,t){o.once("_promiseWhisper",function(e){e&&t(e)})}).catch(function(e){if(e&&"string"==typeof e&&0!==e.indexOf("No response from Twitch."))throw e;var t=u.channel(n),e=u.merge({"message-type":"whisper","message-id":null,"thread-id":null,username:o.getUsername()},o.globaluserstate);return o.emits(["whisper","message"],[[t,e,s,!0],[t,e,s,!0]]),[n,s]})}}},{"./utils":9}],5:[function(e,t,n){"use strict";function s(e){return(s="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function o(){this._events=this._events||{},this._maxListeners=this._maxListeners||void 0}function c(e){return"function"==typeof e}function u(e){return"object"===s(e)&&null!==e}function a(e){return void 0===e}((t.exports=o).EventEmitter=o).prototype._events=void 0,o.prototype._maxListeners=void 0,o.defaultMaxListeners=10,o.prototype.setMaxListeners=function(e){if("number"!=typeof e||e<0||isNaN(e))throw TypeError("n must be a positive number");return this._maxListeners=e,this},o.prototype.emit=function(e){var t,n,s,o,i,r;if(this._events||(this._events={}),"error"===e&&(!this._events.error||u(this._events.error)&&!this._events.error.length)){if((t=arguments[1])instanceof Error)throw t;throw TypeError('Uncaught, unspecified "error" event.')}if(a(n=this._events[e]))return!1;if(c(n))switch(arguments.length){case 1:n.call(this);break;case 2:n.call(this,arguments[1]);break;case 3:n.call(this,arguments[1],arguments[2]);break;default:o=Array.prototype.slice.call(arguments,1),n.apply(this,o)}else if(u(n))for(o=Array.prototype.slice.call(arguments,1),s=(r=n.slice()).length,i=0;in&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace()),this},o.prototype.once=function(e,t){if(!c(t))throw TypeError("listener must be a function");var n=!1;if(this._events.hasOwnProperty(e)&&"_"===e.charAt(0)){var s,o=1,i=e;for(s in this._events)this._events.hasOwnProperty(s)&&s.startsWith(i)&&o++;e+=o}function r(){"_"!==e.charAt(0)||isNaN(e.substr(e.length-1))||(e=e.substring(0,e.length-1)),this.removeListener(e,r),n||(n=!0,t.apply(this,arguments))}return r.listener=t,this.on(e,r),this},o.prototype.removeListener=function(e,t){var n,s,o,i;if(!c(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(o=(n=this._events[e]).length,s=-1,n===t||c(n.listener)&&n.listener===t){if(delete this._events[e],this._events.hasOwnProperty(e+"2")&&"_"===e.charAt(0)){var r,a=e;for(r in this._events)this._events.hasOwnProperty(r)&&r.startsWith(a)&&(isNaN(parseInt(r.substr(r.length-1)))||(this._events[e+parseInt(r.substr(r.length-1)-1)]=this._events[r],delete this._events[r]));this._events[e]=this._events[e+"1"],delete this._events[e+"1"]}this._events.removeListener&&this.emit("removeListener",e,t)}else if(u(n)){for(i=o;0n?(t.command=e.slice(n),t):null;for(t.command=e.slice(n,s),n=s+1;32===e.charCodeAt(n);)n++;for(;ne.length)&&(t=e.length);for(var n=0,s=new Array(t);n").replace(/\\"\\;/g,'"').replace(/\\'\\;/g,"'")},unescapeIRC:function(e){return e&&e.includes("\\")?e.replace(a,function(e,t){return t in u?u[t]:t}):e},escapeIRC:function(e){return e&&e.replace(c,function(e,t){return t in l?"\\".concat(l[t]):t})},actionMessage:function(e){return e.match(i)},addWord:function(e,t){return e.length?e+" "+t:e+t},channel:function(e){var t=(e||"").toLowerCase();return"#"===t[0]?t:"#"+t},extractNumber:function(e){for(var t=e.split(" "),n=0;n c.trim()),
19 | });
20 | fsMod.client.connect().catch(console.error);
21 | fsMod.client.on('connected', (address, port) => {
22 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
23 | fsMod.client.say (myChannel, 'Connected.');
24 | });
25 | console.log('worked');
26 | };
27 |
28 | silentConnect();
29 |
30 | export function twitchKick() { // T I M E O U T V I E W E R
31 | let d = new Dialog({
32 | title: 'Viewer Timeout',
33 | content: `
34 |
42 | `,
43 | buttons: {
44 | no: {
45 | icon: '',
46 | label: 'Cancel'
47 | },
48 | yes: {
49 | icon: '',
50 | label: 'TIMEOUT',
51 | callback: (html) => {
52 | let input = html.find('[name="kickInput"]').val();
53 | console.log(input);
54 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
55 | fsMod.client.say(myChannel, '/timeout ' + input)
56 | },
57 | },
58 | stop: {
59 | icon: '',
60 | callback: (html) => {
61 | let input = html.find('[name="kickInput"]').val();
62 | console.log(input);
63 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
64 | fsMod.client.say(myChannel, '/untimeout ' + input)
65 | },
66 | },
67 | },
68 | default: 'yes',
69 | close: () => {
70 | console.log('Another one bites the dust!');
71 | }
72 | }).render(true)
73 | }
74 |
75 | export function twitchBan() { // B A N V I E W E R
76 | let e = new Dialog({
77 | title: 'Ban Viewer from Channel',
78 | content: `
79 |
87 | `,
88 | buttons: {
89 | no: {
90 | icon: '',
91 | label: 'Cancel'
92 | },
93 | yes: {
94 | icon: '',
95 | label: 'BAN',
96 | callback: (html) => {
97 | let input = html.find('[name="banInput"]').val();
98 | console.log(input);
99 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
100 | fsMod.client.say(myChannel, '/ban ' + input)
101 | }
102 | },
103 | stop: {
104 | icon: '',
105 | callback: (html) => {
106 | let input = html.find('[name="banInput"]').val();
107 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
108 | fsMod.client.say(myChannel, '/unban '+ input)}
109 | }
110 | },
111 | default: 'no',
112 | close: () => {
113 | console.log('Another one bites the dust!');
114 | }
115 | }).render(true)
116 | }
117 |
118 | export function twitchSlow() { // S L O W C H A T R A T E
119 | let d = new Dialog({
120 | title: 'Twitch Channel Chat Rate',
121 | content: `
122 |
130 | `,
131 | buttons: {
132 | no: {
133 | icon: '',
134 | label: 'Cancel'
135 | },
136 | yes: {
137 | icon: '',
138 | label: 'SLOW',
139 | callback: (html) => {
140 | let input = html.find('[name="slowInput"]').val();
141 | console.log(input);
142 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
143 | fsMod.client.say(myChannel, '/slow ' + input)
144 | }
145 | },
146 | stop: {
147 | icon: '',
148 | callback: (html) => {
149 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
150 | fsMod.client.say(myChannel, '/slowoff')
151 | }
152 | },
153 | },
154 | default: 'yes',
155 | close: () => {
156 | console.log('Slowing it down a bit!');
157 | }
158 | }).render(true)
159 | }
160 |
161 | export function twitchClear() { // C L E A R T W I T C H C H A T
162 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
163 | fsMod.client.say(myChannel, "/clear")
164 | }
165 |
166 | export function twitchRaid() { // R A I D C H A N N E L
167 | let d = new Dialog({
168 | title: 'Raid Twitch Channel',
169 | content: `
170 |
177 | `,
178 | buttons: {
179 | no: {
180 | icon: '',
181 | label: 'Cancel'
182 | },
183 | yes: {
184 | icon: '',
185 | label: 'RAID',
186 | callback: (html) => {
187 | let input = html.find('[name="raidInput"]').val();
188 | console.log(input);
189 | let myChannel = (game.settings.get("streamMod", "streamChannel"));
190 | fsMod.client.say(myChannel, '/raid ' + input)
191 | }
192 | },
193 | },
194 | default: 'yes',
195 | close: () => {
196 | console.log('Off to adventure!');
197 | }
198 | }).render(true)
199 | }
200 |
--------------------------------------------------------------------------------
/streamMod.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | }
6 | ],
7 | "settings": {}
8 | }
--------------------------------------------------------------------------------
/streamers.md:
--------------------------------------------------------------------------------
1 | # Streamer - Game System - URL
2 |
3 | Tabletops & Anvils : PF 1e : https://www.twitch.tv/tabletopsandanvils
4 |
5 | 2old__4this : D&D 5e : https://www.twitch.tv/2old__4this
6 |
7 | cswendrowski : 13th Age : https://www.twitch.tv/pelgranepress
8 |
9 | Chuggy305: D&D 5e : https://www.twitch.tv/chuggy305
10 |
--------------------------------------------------------------------------------
/styles/fsmtabs.css:
--------------------------------------------------------------------------------
1 | .fsmtabs.tabs {
2 | flex: 0;
3 | margin: 0 0 5px;
4 | border-bottom: 1px solid #ff6400;
5 | box-shadow: 0 0 10px red;
6 | height: 50px;
7 | }
8 |
9 | .fsmtabs .item.active {
10 | color: #FFF;
11 | border: 1px solid red;
12 | border-bottom: none;
13 | box-shadow: 0 0 6px inset #ff6400;
14 | border-radius: 5px 5px 0 0;
15 | }
16 |
17 | .fsmtabs .item {
18 | height: 18px;
19 | }
20 |
21 | .type0 {
22 | display: none;
23 | }
24 |
25 | .type1 {
26 | display: none;
27 | }
28 |
29 | .type4 {
30 | display: none;
31 | }
32 |
33 | .type5 {
34 | display: none;
35 | }
36 |
37 | .hardHide {
38 | display: none !important;
39 | }
40 |
41 | #foundryNotification {
42 | top: 35px;
43 | right: 255px;
44 | }
45 |
46 | #fsmNotification {
47 | top: 35px;
48 | right: 35px;
49 | }
50 |
--------------------------------------------------------------------------------