├── .github
└── FUNDING.yml
├── .gitignore
├── BUILDING.md
├── LICENSE
├── Old Caffeinated Version Api.tar.gz
├── Old widget.castelrabs.co domain.tar.gz
├── README.md
├── app
├── createWindow.js
├── credits.html
├── css
│ ├── quill.css
│ ├── selectnsearch.css
│ └── style.css
├── index.html
├── js
│ ├── analytics.js
│ ├── caffeinated.js
│ ├── currencies.js
│ ├── fontselect.js
│ ├── forms.js
│ ├── kinoko.js
│ ├── koi.js
│ ├── lang.js
│ ├── modules.js
│ ├── navigate.js
│ ├── quillutil.js
│ ├── repomanager.js
│ ├── selectnsearch.js
│ └── util.js
├── lang
│ ├── en.js
│ ├── es.js
│ ├── fr.js
│ └── nl.js
├── main.js
├── media
│ ├── app_icon.icns
│ ├── app_icon.ico
│ ├── app_icon.png
│ ├── caffeinated.png
│ └── icon.png
├── modules
│ ├── brime
│ │ └── brime.js
│ ├── caffeine
│ │ └── caffeine.js
│ ├── ko-fi
│ │ └── kofimodule.js
│ ├── modules.json
│ └── modules
│ │ ├── bot.js
│ │ ├── chat.js
│ │ ├── chatdisplay.html
│ │ ├── chatdisplay.js
│ │ ├── chatviewers.html
│ │ ├── companion.js
│ │ ├── donation.js
│ │ ├── donationgoal.js
│ │ ├── donationticker.js
│ │ ├── followcounter.js
│ │ ├── follower.js
│ │ ├── followergoal.js
│ │ ├── nowplaying.js
│ │ ├── qr.html
│ │ ├── raidalert.js
│ │ ├── rain.js
│ │ ├── recentdonation.js
│ │ ├── recentfollow.js
│ │ ├── recentsubscription.js
│ │ ├── subscribercounter.js
│ │ ├── subscriptionalert.js
│ │ ├── subscriptiongoal.js
│ │ ├── supporters.js
│ │ ├── topdonation.js
│ │ ├── uptime.js
│ │ ├── videoshare.js
│ │ └── viewcounter.js
├── package.json
└── uri.all.js.map
├── changelog.html
├── dock
├── dock.js
├── index.html
├── spinkit.css
└── style.css
├── package.bat
├── package.sh
├── run.bat
├── run.sh
└── widgets
├── alert.html
├── chat.html
├── display.html
├── donation.html
├── emoji.html
├── goal.html
├── nowplaying.html
├── overlayutil.js
└── videoshare.html
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: Casterlabs
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *node_modules
2 | *package-lock.json
3 | *dist
4 | *yarn.lock
5 |
--------------------------------------------------------------------------------
/BUILDING.md:
--------------------------------------------------------------------------------
1 | https://www.electron.build/multi-platform-build
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Helvijs Adams
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Old Caffeinated Version Api.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thehelvijs/Caffeinated/016add60642b88667decd23411605911f865e2f8/Old Caffeinated Version Api.tar.gz
--------------------------------------------------------------------------------
/Old widget.castelrabs.co domain.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thehelvijs/Caffeinated/016add60642b88667decd23411605911f865e2f8/Old widget.castelrabs.co domain.tar.gz
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Caffeinated
2 |
3 | We've moved! Check out the new repo [here](https://github.com/Casterlabs/Casterlabs/tree/dev/caffeinated).
4 |
--------------------------------------------------------------------------------
/app/createWindow.js:
--------------------------------------------------------------------------------
1 | const { BrowserWindow } = require("electron");
2 | const windowStateKeeper = require("electron-window-state");
3 |
4 | function createWindow(baseDir) {
5 | const mainWindowState = windowStateKeeper({
6 | defaultWidth: 700,
7 | defaultHeight: 500,
8 | file: "main-window.json"
9 | });
10 |
11 | // Create the browser window.
12 | let mainWindow = new BrowserWindow({
13 | minWidth: 700,
14 | minHeight: 500,
15 | width: mainWindowState.width,
16 | height: mainWindowState.height,
17 | x: mainWindowState.x,
18 | y: mainWindowState.y,
19 | transparent: false,
20 | resizable: true,
21 | show: false,
22 | backgroundColor: "#141414",
23 | icon: baseDir + "/media/app_icon.png",
24 | frame: false,
25 | webPreferences: {
26 | nodeIntegration: true,
27 | contextIsolation: false,
28 | enableRemoteModule: true,
29 | webSecurity: false
30 | }
31 | })
32 |
33 | // and load the index.html of the app.
34 | mainWindow.loadFile(baseDir + "/index.html");
35 | mainWindowState.manage(mainWindow);
36 |
37 | // Emitted when the window is closed.
38 | mainWindow.on("closed", () => {
39 | // Dereference the window object, usually you would store windows
40 | // in an array if your app supports multi windows, this is the time
41 | // when you should delete the corresponding element.
42 | mainWindow = null;
43 | });
44 |
45 | // Emitted when the window is ready to be shown
46 | // This helps in showing the window gracefully.
47 | mainWindow.once("ready-to-show", () => {
48 | mainWindow.show();
49 | });
50 |
51 | return mainWindow;
52 | }
53 |
54 | module.exports = createWindow;
--------------------------------------------------------------------------------
/app/css/quill.css:
--------------------------------------------------------------------------------
1 | .rich-editor {
2 | height: 75px;
3 | }
4 |
5 | .ql-snow .ql-stroke {
6 | stroke: whitesmoke;
7 | }
8 |
9 | .ql-snow .ql-fill, .ql-snow .ql-stroke.ql-fill {
10 | fill: whitesmoke;
11 | }
12 |
13 | .ql-picker-label {
14 | color: whitesmoke;
15 | }
16 |
17 | .ql-picker.ql-size .ql-picker-label::before, .ql-picker.ql-size .ql-picker-item::before {
18 | content: attr(data-value) !important;
19 | }
20 |
21 | .ql-color .ql-picker-options [data-value=custom-color], .ql-background .ql-picker-options [data-value=custom-color] {
22 | background: none !important;
23 | width: 100% !important;
24 | height: 20px !important;
25 | text-align: center;
26 | }
27 |
28 | .ql-color .ql-picker-options [data-value=custom-color]:before, .ql-background .ql-picker-options [data-value=custom-color]:before {
29 | content: "Custom Color";
30 | }
31 |
32 | .ql-color .ql-picker-options [data-value=custom-color]:hover, .ql-background .ql-picker-options [data-value=custom-color]:hover {
33 | border-color: transparent !important;
34 | }
--------------------------------------------------------------------------------
/app/css/selectnsearch.css:
--------------------------------------------------------------------------------
1 |
2 | .sns-container {
3 | position: relative;
4 | }
5 |
6 | .sns-contents {
7 | width: 100%;
8 | height: 500%;
9 | overflow-y: scroll;
10 | position: absolute;
11 | background-color: white;
12 | z-index: 99999;
13 | }
14 |
15 | .sns-contents a {
16 | color: black;
17 | width: 100%;
18 | }
19 |
20 | .sns-input {
21 | width: 100%;
22 | }
23 |
--------------------------------------------------------------------------------
/app/js/analytics.js:
--------------------------------------------------------------------------------
1 | // Woopra Tracking
2 | !function () { var a, b, c, d = window, e = document, f = arguments, g = "script", h = ["config", "track", "trackForm", "trackClick", "identify", "visit", "push", "call"], i = function () { var a, b = this, c = function (a) { b[a] = function () { return b._e.push([a].concat(Array.prototype.slice.call(arguments, 0))), b } }; for (b._e = [], a = 0; a < h.length; a++)c(h[a]) }; for (d.__woo = d.__woo || {}, a = 0; a < f.length; a++)d.__woo[f[a]] = d[f[a]] = d[f[a]] || new i; b = e.createElement(g), b.async = 1, b.src = "https://static.woopra.com/js/w.js", c = e.getElementsByTagName(g)[0], c.parentNode.insertBefore(b, c) }("woopra");
3 |
4 | woopra.config({
5 | domain: "caffeinated.casterlabs.co",
6 | protocol: "https"
7 | });
8 |
9 | const ANALYTICS = (() => {
10 | let hasLoggedSignin = false;
11 | let hasTracked = false;
12 |
13 | let logSignin = false;
14 |
15 | return {
16 | async logSignin() {
17 | logSignin = true;
18 | },
19 |
20 | async logSignout() {
21 | woopra.track("signout", {});
22 |
23 | hasLoggedSignin = false;
24 | hasTracked = false;
25 |
26 | woopra.visitorData = {};
27 | },
28 |
29 | async logPuppetSignin() {
30 | woopra.track("puppet_signin", {});
31 | },
32 |
33 | async logPuppetSignout() {
34 | woopra.track("puppet_signout", {});
35 | },
36 |
37 | async logUserUpdate(userdata = CAFFEINATED.userdata) {
38 | const language = LANG.getTranslation("meta.language.name.native");
39 | const id = `${userdata.streamer.UUID};${userdata.streamer.platform}`;
40 | const platform = userdata.streamer.platform;
41 | const name = `${userdata.streamer.displayname} (${prettifyString(platform.toLowerCase())})`;
42 |
43 | woopra.identify({
44 | id: id,
45 | platform: platform,
46 | name: name,
47 | language: language
48 | });
49 |
50 | if (!hasTracked) {
51 | hasTracked = true;
52 | woopra.track();
53 | }
54 |
55 | if (logSignin) {
56 | logSignin = false;
57 |
58 | woopra.track("signin", {});
59 | }
60 |
61 | woopra.push();
62 | },
63 |
64 | async logEvent(event) {
65 | if (event && !event.isTest) {
66 | switch (event.event_type) {
67 | case "STREAM_STATUS": {
68 | if (CAFFEINATED.streamdata && (event.title != CAFFEINATED.streamdata.title)) {
69 | woopra.track("stream_title_update", {
70 | title: event.title
71 | });
72 | }
73 |
74 | if (
75 | // If there is existing stream data and it doesn't equal the previous state.
76 | (CAFFEINATED.streamdata && (event.is_live != CAFFEINATED.streamdata.is_live)) ||
77 | // OR if there isn't existing stream data and the person is live (We should log it.)
78 | (!CAFFEINATED.streamdata && event.is_live)
79 | ) {
80 | if (event.is_live) {
81 | woopra.track("stream_online", {});
82 | } else {
83 | woopra.track("stream_offline", {});
84 | }
85 | }
86 | break;
87 | }
88 | }
89 | }
90 | }
91 |
92 | };
93 | })();
94 |
--------------------------------------------------------------------------------
/app/js/fontselect.js:
--------------------------------------------------------------------------------
1 | const FONTSELECT = {
2 | version: "1.0.1",
3 | endPoint: "https://www.googleapis.com/webfonts/v1/webfonts?sort=popularity&key=AIzaSyBuFeOYplWvsOlgbPeW8OfPUejzzzTCITM", // TODO cache/proxy from Casterlabs' server
4 | fonts: [],
5 |
6 | preload() {
7 | return new Promise(async (resolve, reject) => {
8 | console.debug("Loading fonts.");
9 |
10 | fetch(this.endPoint).then((response) => response.json())
11 | .catch(reject)
12 | .then(async (fonts) => {
13 | // let toload = [];
14 |
15 | // Quickly get a list of fonts ready for the caller.
16 | fonts.items.forEach((font) => {
17 | const name = font.family;
18 |
19 | if (!this.fonts.includes(name)) {
20 | this.fonts.push(name);
21 | // toload.push(font);
22 | }
23 | });
24 |
25 | /*
26 | for (const font of toload) {
27 | const name = font.family;
28 | let url;
29 |
30 | if (font.files.hasOwnProperty("regular")) {
31 | url = font.files.regular;
32 | } else {
33 | url = Object.entries(font.files)[0][1]; // Get the entries, get the first entry, get the link.
34 | }
35 |
36 | const face = new FontFace(name, "url(" + url.replace("http:", "https:") + ")");
37 |
38 | document.fonts.add(face);
39 | }
40 | */
41 |
42 | try {
43 | const localFonts = await require("font-list").getFonts();
44 |
45 | localFonts.forEach((font) => {
46 | const name = font.replace(/\"/g, "");
47 |
48 | if (!this.fonts.includes(name)) {
49 | this.fonts.push(name);
50 |
51 | // const face = new FontFace(name, font);
52 |
53 | // document.fonts.add(face);
54 | }
55 | })
56 | } catch (e) {
57 | console.error(e);
58 | }
59 |
60 | console.debug("Finished loading fonts.");
61 | resolve();
62 | });
63 | });
64 | },
65 |
66 | apply(element, settings = { updateFont: true, selected: "Poppins" }) {
67 | return new Promise(async (resolve, reject) => {
68 | // if (element instanceof HTMLSelectElement) {
69 | if (this.fonts.length == 0) {
70 | console.debug("No fonts present, loading them now.");
71 | await this.preload();
72 | }
73 |
74 | SELECTNSEARCH.create(this.fonts, element);
75 |
76 | element.value = settings.selected;
77 | element.querySelector(".sns-input").setAttribute("value", settings.selected);
78 |
79 | resolve();
80 | //} else {
81 | // reject("Element is not a valid select element");
82 | //}
83 | });
84 | }
85 |
86 | };
87 |
--------------------------------------------------------------------------------
/app/js/forms.js:
--------------------------------------------------------------------------------
1 | // https://github.com/e3ndr/FormsJS/blob/master/forms.js - MIT
2 | const FORMSJS = {
3 | // 1.1.0 MODIFIED HEAVILY
4 |
5 | CLASS_SELECTOR: "data",
6 | NAME_PROPERTY: "name",
7 | BLANK_IS_NULL: false,
8 | PARSE_NUMBERS: false,
9 | ALLOW_FALSE: true,
10 |
11 | readForm(selector, query = document, parent = query.querySelector(selector)) {
12 | let values = {};
13 |
14 | Array.from(parent.querySelectorAll("." + this.CLASS_SELECTOR)).forEach((element) => {
15 | let name = element.getAttribute(this.NAME_PROPERTY);
16 |
17 | if (name && !values[name]) {
18 | let value = this.getElementValue(element);
19 |
20 | if (this.BLANK_IS_NULL && (value != null) && (value.length == 0)) {
21 | value = null;
22 | } else if ((value === false) && !this.ALLOW_FALSE) {
23 | return;
24 | } else if (this.PARSE_NUMBERS) {
25 | let num = parseFloat(value);
26 |
27 | if (!isNaN(num)) {
28 | value = num;
29 | }
30 | }
31 |
32 | values[name] = value;
33 | }
34 | });
35 |
36 | return values;
37 | },
38 |
39 | getElementValue(element) {
40 | let type = element.getAttribute("type");
41 |
42 | switch (type) {
43 | case "radio": {
44 | if (element.checked) {
45 | return element.value;
46 | }
47 | }
48 |
49 | case "dynamic": {
50 | let options = [];
51 |
52 | Array.from(element.querySelectorAll(".dynamic-option")).forEach((dyn) => {
53 | options.push(FORMSJS.readForm(null, element, dyn));
54 | });
55 |
56 | return options;
57 | }
58 |
59 | case "checkbox":
60 | return element.checked;
61 |
62 | case "rich":
63 | return element.lastChild.firstChild.innerHTML;
64 |
65 | case "file":
66 | return element;
67 |
68 | case "currency":
69 | return (element.getAttribute("value").toUpperCase() == "DEFAULT") ? "DEFAULT" : CURRENCY_TABLE[element.getAttribute("value")];
70 |
71 | case "number":
72 | return parseFloat(element.value);
73 |
74 | default:
75 | return (element instanceof HTMLDivElement) ? element.getAttribute("value") : element.value;
76 | }
77 | }
78 |
79 | };
80 |
--------------------------------------------------------------------------------
/app/js/kinoko.js:
--------------------------------------------------------------------------------
1 |
2 | class Kinoko {
3 |
4 | constructor(baseUri = "wss://api.casterlabs.co/v1/kinoko") {
5 | this.listeners = {};
6 | this.baseUri = baseUri;
7 | }
8 |
9 | on(type, callback) {
10 | type = type.toLowerCase();
11 |
12 | let callbacks = this.listeners[type];
13 |
14 | if (!callbacks) callbacks = [];
15 |
16 | callbacks.push(callback);
17 |
18 | this.listeners[type] = callbacks;
19 | }
20 |
21 | broadcast(type, data) {
22 | const listeners = this.listeners[type.toLowerCase()];
23 |
24 | if (listeners) {
25 | listeners.forEach((callback) => {
26 | try {
27 | callback(data);
28 | } catch (e) {
29 | console.error("An event listener produced an exception: ");
30 | console.error(e);
31 | }
32 | });
33 | }
34 | }
35 |
36 | disconnect() {
37 | if (this.ws && (this.ws.readyState == WebSocket.OPEN)) {
38 | this.ws.close();
39 | }
40 | }
41 |
42 | send(message, isJson = true) {
43 | if (this.ws && (this.ws.readyState == WebSocket.OPEN)) {
44 | if (this.proxy) {
45 | this.ws.send(message);
46 | } else {
47 | if (isJson) {
48 | this.ws.send(JSON.stringify(message));
49 | } else {
50 | this.ws.send(message);
51 | }
52 | }
53 | }
54 | }
55 |
56 | connect(channel, type = "client", proxy = false) {
57 | setTimeout(() => {
58 | const uri = this.baseUri + "?channel=" + encodeURIComponent(channel) + "&type=" + encodeURIComponent(type) + "&proxy=" + encodeURIComponent(proxy);
59 |
60 | this.disconnect();
61 |
62 | this.ws = new WebSocket(uri);
63 | this.proxy = proxy;
64 |
65 | this.ws.onerror = () => {
66 | this.connect(channel, type, proxy);
67 | }
68 |
69 | this.ws.onopen = () => {
70 | this.broadcast("open");
71 | };
72 |
73 | this.ws.onclose = () => {
74 | this.broadcast("close");
75 | };
76 |
77 | this.ws.onmessage = (message) => {
78 | const data = message.data;
79 |
80 | switch (data) {
81 | case ":ping": {
82 | if (!this.proxy) {
83 | this.ws.send(":ping");
84 | return;
85 | }
86 | }
87 |
88 | case ":orphaned": {
89 | this.broadcast("orphaned");
90 | return;
91 | }
92 |
93 | case ":adopted": {
94 | this.broadcast("adopted");
95 | return;
96 | }
97 |
98 | default: {
99 | if (this.proxy) {
100 | this.broadcast("message", data);
101 | } else {
102 | try {
103 | this.broadcast("message", JSON.parse(data));
104 | } catch (ignored) {
105 | this.broadcast("message", data);
106 | }
107 | }
108 | return
109 | }
110 | }
111 | };
112 | }, 1500);
113 | }
114 |
115 | }
116 |
117 | // Basically https://stackoverflow.com/a/8809472
118 | function generateUUID() {
119 | let micro = (performance && performance.now && (performance.now() * 1000)) || 0;
120 | let millis = new Date().getTime();
121 |
122 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
123 | let random = Math.random() * 16;
124 |
125 | if (millis > 0) {
126 | random = (millis + random) % 16 | 0;
127 | millis = Math.floor(millis / 16);
128 | } else {
129 | random = (micro + random) % 16 | 0;
130 | micro = Math.floor(micro / 16);
131 | }
132 |
133 | return ((c === "x") ? random : ((random & 0x3) | 0x8)).toString(16);
134 | });
135 | }
136 |
137 | function generateUnsafePassword(len = 32) {
138 | const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
139 |
140 | return Array(len)
141 | .fill(chars)
142 | .map((x) => {
143 | return x[Math.floor(Math.random() * x.length)]
144 | }).join("");
145 | }
146 |
147 | function generateUnsafeUniquePassword(len = 32) {
148 | return generateUUID().replace(/-/g, "") + generateUnsafePassword(len);
149 | }
150 |
151 | class AuthCallback {
152 |
153 | constructor(type = "unknown") {
154 | this.id = `auth_redirect:${generateUnsafePassword(128)}:${type}`;
155 | }
156 |
157 | disconnect() {
158 | if (this.kinoko) {
159 | this.kinoko.disconnect();
160 | }
161 |
162 | this.kinoko = new Kinoko();
163 | }
164 |
165 | awaitAuthMessage(timeout = -1) {
166 | return new Promise((resolve, reject) => {
167 | this.disconnect();
168 |
169 | let fufilled = false;
170 | const id = (timeout > 0) ? setTimeout(() => {
171 | if (!fufilled) {
172 | fufilled = true;
173 | this.disconnect();
174 | reject("TOKEN_TIMEOUT");
175 | }
176 | }, timeout) : -1;
177 |
178 | this.kinoko.connect(this.id, "parent");
179 |
180 | this.kinoko.on("close", () => {
181 | if (!fufilled) {
182 | reject("CONNECTION_CLOSED");
183 | }
184 |
185 | clearTimeout(id);
186 | });
187 |
188 | this.kinoko.on("message", (message) => {
189 | fufilled = true;
190 |
191 | this.disconnect();
192 |
193 | if (message === "NONE") {
194 | reject("NO_TOKEN_PROVIDED");
195 | } else if (message.startsWith("token:")) {
196 | const token = message.substring(6);
197 |
198 | resolve(token);
199 | } else {
200 | reject("TOKEN_MESSAGE_INVALID");
201 | }
202 | });
203 | });
204 | }
205 |
206 | getStateString() {
207 | return this.id;
208 | }
209 |
210 | }
211 |
--------------------------------------------------------------------------------
/app/js/lang.js:
--------------------------------------------------------------------------------
1 | let LANGUAGES = {};
2 |
3 | const LANG = {
4 | supportedLanguages: {},
5 |
6 | absorbLang(newLang, code) {
7 | const languageName = newLang["meta.language.name.native"];
8 |
9 | this.supportedLanguages[languageName] = code;
10 |
11 | for (const [key, value] of Object.entries(newLang)) {
12 | if (!LANGUAGES[key]) {
13 | LANGUAGES[key] = {};
14 | }
15 |
16 | LANGUAGES[key][code] = value;
17 | }
18 | },
19 |
20 | getLangKey(key, language = navigator.language) {
21 | const lang = LANGUAGES[key];
22 |
23 | if (lang) {
24 | for (const code of Object.keys(lang)) {
25 | const regex = RepoUtil.matchToRegex(code);
26 |
27 | if (language.match(regex)) {
28 | return code;
29 | }
30 | }
31 | }
32 |
33 | return null;
34 | },
35 |
36 | getSupportedLanguage(key, languages = navigator.languages) {
37 | const stored = CAFFEINATED.store.get("language");
38 |
39 | if (stored) {
40 | const code = this.getLangKey(key, stored);
41 |
42 | if (code) {
43 | return code;
44 | } else if (CAFFEINATED.store.get("experimental.no_translation_default")) {
45 | return "";
46 | }
47 | // Otherwise, figure it out based on what the OS gives us.
48 | }
49 |
50 | for (const lang of languages) {
51 | const code = this.getLangKey(key, lang);
52 |
53 | if (code) {
54 | return code;
55 | }
56 | }
57 |
58 | if (CAFFEINATED.store.get("experimental.no_translation_default")) {
59 | return "";
60 | } else {
61 | return "en-*";
62 | }
63 | },
64 |
65 | translate(parent = document, ...args) {
66 | Array.from(parent.querySelectorAll(".translatable")).forEach((element) => {
67 | const key = element.getAttribute("lang");
68 | const lang = LANGUAGES[key];
69 |
70 | let result;
71 |
72 | if (lang) {
73 | const supported = this.getSupportedLanguage(key);
74 | let translated = supported ? lang[supported] : key;
75 |
76 | if (translated === undefined) {
77 | result = key;
78 | } else {
79 | if (typeof translated === "function") {
80 | result = translated(...args);
81 | } else {
82 | result = translated;
83 | }
84 | }
85 | } else {
86 | result = key;
87 | }
88 |
89 | element.innerText = result;
90 | element.setAttribute("title", result);
91 | });
92 | },
93 |
94 | getTranslation(key, ...args) {
95 | const lang = LANGUAGES[key];
96 |
97 | if (lang) {
98 | const supported = this.getSupportedLanguage(key);
99 | let translated = supported ? lang[supported] : key;
100 |
101 | if (translated === undefined) {
102 | return key;
103 | } else {
104 | if (typeof translated === "function") {
105 | translated = translated(...args);
106 | }
107 |
108 | return translated;
109 | }
110 | } else {
111 | return key;
112 | }
113 | },
114 |
115 | formatSubscription(event) {
116 | const months = `Go here, then paste the provided url into the Webhook URL field and hit Update.
128 |
129 |
130 | You can test to see if this worked by hitting Send Test.
131 | `;
132 |
133 | this.page.firstChild.appendChild(instructions);
134 | }
135 |
136 | settingsDisplay = {
137 | enabled: "checkbox",
138 | url: {
139 | display: "Copy Webhook URL",
140 | type: "button"
141 | }
142 | };
143 |
144 | defaultSettings = {
145 | enabled: false
146 | // url: () => {}
147 | };
148 |
149 | };
150 |
--------------------------------------------------------------------------------
/app/modules/modules.json:
--------------------------------------------------------------------------------
1 | {
2 | "supported": [
3 | "1.*-*"
4 | ],
5 | "unsupported": [
6 | "0.4.*-*",
7 | "0.5.*-*"
8 | ],
9 | "version": "1.0.0",
10 | "name": "Caffeinated Default Widgets Repo",
11 | "scripts": [
12 | "modules/donation.js",
13 | "modules/follower.js",
14 | "modules/chat.js",
15 | "modules/followergoal.js",
16 | "modules/donationgoal.js",
17 | "modules/supporters.js",
18 | "modules/chatdisplay.js",
19 | "modules/companion.js",
20 | "modules/rain.js",
21 | "modules/nowplaying.js",
22 | "modules/viewcounter.js",
23 | "modules/recentfollow.js",
24 | "modules/recentdonation.js",
25 | "modules/topdonation.js",
26 | "modules/followcounter.js",
27 | "modules/donationticker.js",
28 | "modules/bot.js",
29 | "modules/raidalert.js",
30 | "modules/subscriptionalert.js",
31 | "modules/subscriptiongoal.js",
32 | "modules/recentsubscription.js",
33 | "modules/subscribercounter.js",
34 | "modules/videoshare.js",
35 | "modules/uptime.js",
36 | "caffeine/caffeine.js",
37 | "brime/brime.js",
38 | "ko-fi/kofimodule.js"
39 | ],
40 | "simple": [
41 | {
42 | "namespace": "casterlabs_chat",
43 | "id": "chat"
44 | },
45 | {
46 | "namespace": "casterlabs_donation",
47 | "id": "donation"
48 | },
49 | {
50 | "namespace": "casterlabs_donation_goal",
51 | "id": "donation_goal"
52 | },
53 | {
54 | "namespace": "casterlabs_recent_donation",
55 | "id": "recent_donation"
56 | },
57 | {
58 | "namespace": "casterlabs_top_donation",
59 | "id": "top_donation"
60 | },
61 | {
62 | "namespace": "casterlabs_donation_ticker",
63 | "id": "donation_ticker"
64 | },
65 | {
66 | "namespace": "casterlabs_rain",
67 | "id": "emoji_rain"
68 | },
69 | {
70 | "namespace": "casterlabs_follower",
71 | "id": "follower"
72 | },
73 | {
74 | "namespace": "casterlabs_follower_goal",
75 | "id": "follower_goal"
76 | },
77 | {
78 | "namespace": "casterlabs_recent_follow",
79 | "id": "recent_follow"
80 | },
81 | {
82 | "namespace": "casterlabs_follow_counter",
83 | "id": "follow_counter"
84 | },
85 | {
86 | "namespace": "casterlabs_raid",
87 | "id": "raid"
88 | },
89 | {
90 | "namespace": "casterlabs_subscription",
91 | "id": "subscription"
92 | },
93 | {
94 | "namespace": "casterlabs_subscription_goal",
95 | "id": "subscription_goal"
96 | },
97 | {
98 | "namespace": "casterlabs_recent_subscription",
99 | "id": "recent_subscription"
100 | },
101 | {
102 | "namespace": "casterlabs_subscriber_counter",
103 | "id": "subscriber_counter"
104 | },
105 | {
106 | "namespace": "casterlabs_now_playing",
107 | "id": "now_playing"
108 | },
109 | {
110 | "namespace": "casterlabs_view_counter",
111 | "id": "viewer_counter"
112 | },
113 | {
114 | "namespace": "casterlabs_uptime",
115 | "id": "uptime"
116 | }
117 | ],
118 | "required": [
119 | {
120 | "namespace": "caffeine_integration",
121 | "id": "caffeine_integration"
122 | },
123 | {
124 | "namespace": "brime_integration",
125 | "id": "brime_integration"
126 | },
127 | {
128 | "namespace": "casterlabs_bot",
129 | "id": "chat_bot"
130 | },
131 | {
132 | "namespace": "kofi_integration",
133 | "id": "ko-fi_integration"
134 | },
135 | {
136 | "namespace": "casterlabs_companion",
137 | "id": "casterlabs_companion"
138 | },
139 | {
140 | "namespace": "casterlabs_supporters",
141 | "id": "casterlabs_supporters"
142 | },
143 | {
144 | "namespace": "casterlabs_chat_display",
145 | "id": "chat_display"
146 | },
147 | {
148 | "namespace": "casterlabs_video_share",
149 | "id": "video_share"
150 | }
151 | ]
152 | }
--------------------------------------------------------------------------------
/app/modules/modules/bot.js:
--------------------------------------------------------------------------------
1 |
2 | MODULES.uniqueModuleClasses["casterlabs_bot"] = class {
3 |
4 | constructor(id) {
5 | this.namespace = "casterlabs_bot";
6 | this.displayname = "caffeinated.chatbot.title";
7 | this.type = "settings";
8 | this.id = id;
9 | this.persist = true;
10 | }
11 |
12 | getDataToStore() {
13 | return this.settings;
14 | }
15 |
16 | init() {
17 | this.limitFields();
18 |
19 | koi.addEventListener("user_update", () => {
20 | setTimeout(() => this.limitFields(), 100);
21 | });
22 |
23 | koi.addEventListener("chat", (event) => {
24 | if (this.settings.enabled) {
25 | this.processCommand(event);
26 | }
27 | });
28 |
29 | koi.addEventListener("donation", (event) => {
30 | if (this.settings.enabled) {
31 | if (this.settings.donation_callout) {
32 | koi.sendMessage(`@${event.sender.displayname} ${this.settings.donation_callout}`, event, "PUPPET");
33 | }
34 |
35 | this.processCommand(event);
36 | }
37 | });
38 |
39 | koi.addEventListener("viewer_join", (event) => {
40 | if (this.settings.enabled) {
41 | if (this.settings.welcome_callout && (event.streamer.platform === "TROVO")) {
42 | koi.sendMessage(`@${event.sender.displayname} ${this.settings.welcome_callout}`, event, "PUPPET");
43 | }
44 | }
45 | });
46 |
47 | koi.addEventListener("follow", (event) => {
48 | if (this.settings.enabled) {
49 | if (this.settings.follow_callout) {
50 | koi.sendMessage(`@${event.follower.displayname} ${this.settings.follow_callout}`, event, "PUPPET");
51 | }
52 | }
53 | });
54 | }
55 |
56 | processCommand(event) {
57 | const message = event.message.toLowerCase();
58 |
59 | if (this.settings.enable_uptime_command && message.startsWith("!uptime")) {
60 | if (CAFFEINATED.streamdata && CAFFEINATED.streamdata.is_live) {
61 | const millis = CAFFEINATED.getTimeLiveInMilliseconds();
62 | const formatted = getFriendlyTime(millis);
63 |
64 | koi.sendMessage(`@${event.sender.displayname} ${LANG.getTranslation("caffeinated.chatbot.uptime_command.format", formatted)} `, event, "PUPPET");
65 | } else {
66 | koi.sendMessage(`@${event.sender.displayname} ${LANG.getTranslation("caffeinated.chatbot.uptime_command.not_live")} `, event, "PUPPET");
67 | }
68 | return;
69 | }
70 |
71 | for (const command of this.settings.commands) {
72 | if (message.endsWith(command.reply.toLowerCase())) {
73 | return; // Loop detected.
74 | }
75 | }
76 |
77 | // Second pass.
78 | for (const command of this.settings.commands) {
79 | const trigger = command.trigger.toLowerCase();
80 |
81 | if ((command.type == "Script") && message.startsWith(trigger)) {
82 | const eventVar = "const event = arguments[0];\n";
83 | const result = looseInterpret(eventVar + command.reply, event);
84 |
85 | if (result) {
86 | if (result instanceof Promise) {
87 | result.then((message) => {
88 | if (message) {
89 | koi.sendMessage(message.toString(), event, "PUPPET");
90 | }
91 | })
92 | } else {
93 | koi.sendMessage(result.toString(), event, "PUPPET");
94 | }
95 | }
96 |
97 | return;
98 | } else if (
99 | ((command.type == "Command") && message.startsWith(trigger)) ||
100 | ((command.type == "Keyword") && message.includes(trigger))
101 | ) {
102 | koi.sendMessage(`@${event.sender.displayname} ${command.reply}`, event, "PUPPET");
103 | return;
104 | }
105 | }
106 | }
107 |
108 | limitFields() {
109 | // It's 10 less to help fit in the mention
110 | const max = koi.getMaxLength() - 10;
111 |
112 | /*
113 | Array.from(this.page.querySelectorAll("[name=reply][owner=chat_bot]")).forEach((element) => {
114 | element.setAttribute("maxlength", max);
115 | });
116 | */
117 |
118 | this.page.querySelector("[name=follow_callout][owner=chat_bot]").setAttribute("maxlength", max);
119 | this.page.querySelector("[name=donation_callout][owner=chat_bot]").setAttribute("maxlength", max);
120 | this.page.querySelector("[name=welcome_callout][owner=chat_bot]").setAttribute("maxlength", max);
121 |
122 | if (CAFFEINATED.userdata && (CAFFEINATED.userdata.streamer.platform === "TROVO")) {
123 | this.page.querySelector("[name=welcome_callout][owner=chat_bot]").parentElement.classList.remove("hide");
124 | } else {
125 | this.page.querySelector("[name=welcome_callout][owner=chat_bot]").parentElement.classList.add("hide");
126 | }
127 | }
128 |
129 | onSettingsUpdate() {
130 | this.limitFields();
131 | }
132 |
133 | settingsDisplay = {
134 | enabled: {
135 | display: "generic.enabled",
136 | type: "checkbox",
137 | isLang: true
138 | },
139 | commands: {
140 | display: "caffeinated.chatbot.commands",
141 | type: "dynamic",
142 | isLang: true
143 | },
144 | enable_uptime_command: {
145 | display: "caffeinated.chatbot.uptime_command.enable",
146 | type: "checkbox",
147 | isLang: true
148 | },
149 | follow_callout: {
150 | display: "caffeinated.chatbot.follow_callout",
151 | type: "input",
152 | isLang: true
153 | },
154 | donation_callout: {
155 | display: "caffeinated.chatbot.donation_callout",
156 | type: "input",
157 | isLang: true
158 | },
159 | welcome_callout: {
160 | display: "caffeinated.chatbot.welcome_callout",
161 | type: "input",
162 | isLang: true
163 | }
164 | };
165 |
166 | defaultSettings = {
167 | enabled: false,
168 | commands: {
169 | display: {
170 | type: {
171 | display: "caffeinated.chatbot.command_type",
172 | type: "select",
173 | isLang: true
174 | },
175 | trigger: {
176 | display: "caffeinated.chatbot.trigger",
177 | type: "input",
178 | isLang: true
179 | },
180 | reply: {
181 | display: "caffeinated.chatbot.reply",
182 | type: "textarea",
183 | isLang: true
184 | }
185 | },
186 | default: {
187 | type: ["Command", "Keyword", "Script"],
188 | trigger: "!casterlabs",
189 | mention: true,
190 | reply: LANG.getTranslation("caffeinated.chatbot.default_reply")
191 | }
192 | },
193 | enable_uptime_command: true,
194 | follow_callout: "",
195 | donation_callout: "",
196 | welcome_callout: ""
197 | };
198 |
199 | };
200 |
--------------------------------------------------------------------------------
/app/modules/modules/chat.js:
--------------------------------------------------------------------------------
1 |
2 | MODULES.moduleClasses["casterlabs_chat"] = class {
3 |
4 | constructor(id) {
5 | this.namespace = "casterlabs_chat";
6 | this.displayname = "caffeinated.chat.title";
7 | this.type = "overlay settings";
8 | this.id = id;
9 | }
10 |
11 | widgetDisplay = [
12 | {
13 | name: "Test",
14 | icon: "dice",
15 | onclick(instance) {
16 | koi.test("chat");
17 | }
18 | },
19 | {
20 | name: "Copy",
21 | icon: "copy",
22 | onclick(instance) {
23 | putInClipboard("https://caffeinated.casterlabs.co/chat.html?id=" + instance.id);
24 | }
25 | }
26 | ]
27 |
28 | getDataToStore() {
29 | return this.settings;
30 | }
31 |
32 | onConnection(socket) {
33 | MODULES.emitIO(this, "config", this.settings, socket);
34 | }
35 |
36 | init() {
37 | const instance = this;
38 |
39 | koi.addEventListener("meta", (event) => {
40 | MODULES.emitIO(instance, "event", event);
41 | });
42 |
43 | koi.addEventListener("chat", (event) => {
44 | MODULES.emitIO(instance, "event", event);
45 | });
46 |
47 | koi.addEventListener("donation", (event) => {
48 | if (instance.settings.show_donations) {
49 | MODULES.emitIO(instance, "event", event);
50 | }
51 | });
52 |
53 | koi.addEventListener("channel_points", (event) => {
54 | // this.addPointStatus(event.sender, event.reward);
55 | });
56 |
57 | koi.addEventListener("follow", (event) => {
58 | // this.addStatus(event.follower, "caffeinated.chatdisplay.follow_text");
59 | });
60 |
61 | }
62 |
63 | // TODO status messages.
64 | /*
65 | addStatus(profile, langKey) {
66 | const usernameHtml = `${escapeHtml(profile.displayname)}`;
67 | const lang = LANG.getTranslation(langKey, usernameHtml);
68 |
69 | this.addManualStatus(lang);
70 | }
71 |
72 | addPointStatus(profile, reward) {
73 | const usernameHtml = ` ${escapeHtml(profile.displayname)} `;
74 | const imageHtml = ` `;
75 |
76 | const lang = LANG.getTranslation("caffeinated.chatdisplay.reward_text", usernameHtml, reward.title, imageHtml);
77 |
78 | this.addManualStatus(lang);
79 | }
80 |
81 | addManualStatus(lang) {
82 | MODULES.emitIO(instance, "event", {
83 | type: "STATUS",
84 | lang: lang
85 | });
86 | }
87 | */
88 |
89 | onSettingsUpdate() {
90 | MODULES.emitIO(this, "config", this.settings);
91 | }
92 |
93 | settingsDisplay = {
94 | font: {
95 | display: "generic.font",
96 | type: "font",
97 | isLang: true
98 | },
99 | font_size: {
100 | display: "generic.font.size",
101 | type: "number",
102 | isLang: true
103 | },
104 | text_color: {
105 | display: "generic.text.color",
106 | type: "color",
107 | isLang: true
108 | },
109 | show_donations: {
110 | display: "caffeinated.chat.show_donations",
111 | type: "checkbox",
112 | isLang: true
113 | },
114 | chat_direction: {
115 | display: "caffeinated.chat.chat_direction",
116 | type: "select",
117 | isLang: true
118 | },
119 | chat_animation: {
120 | display: "caffeinated.chat.chat_animation",
121 | type: "select",
122 | isLang: true
123 | },
124 | text_align: {
125 | display: "caffeinated.chat.text_align",
126 | type: "select",
127 | isLang: true
128 | }
129 | };
130 |
131 | defaultSettings = {
132 | font: "Poppins",
133 | font_size: "16",
134 | show_donations: true,
135 | text_color: "#FFFFFF",
136 | chat_direction: [
137 | "Down",
138 | "Up"
139 | ],
140 | chat_animation: [
141 | "Default",
142 | "Slide",
143 | "Slide (Disappearing)",
144 | "Disappearing"
145 | ],
146 | text_align: [
147 | "Left",
148 | "Right"
149 | ]
150 | };
151 |
152 | };
153 |
--------------------------------------------------------------------------------
/app/modules/modules/companion.js:
--------------------------------------------------------------------------------
1 |
2 | MODULES.uniqueModuleClasses["casterlabs_companion"] = class {
3 |
4 | constructor(id) {
5 | this.namespace = "casterlabs_companion";
6 | this.displayname = "caffeinated.companion.title";
7 | this.type = "settings";
8 | this.id = id;
9 | this.persist = true;
10 |
11 | this.messageHistory = [];
12 | this.viewersList = [];
13 |
14 | this.kinoko = new Kinoko();
15 |
16 | this.defaultSettings.reset_link = () => {
17 | this.uuid = generateUnsafeUniquePassword(16);
18 | this.setLinkText();
19 | this.connect();
20 | MODULES.saveToStore(this);
21 | };
22 |
23 | this.defaultSettings.copy_link = () => {
24 | putInClipboard(`https://casterlabs.co/companion?key=${this.uuid}`);
25 | };
26 |
27 | this.kinoko.on("message", (data) => {
28 | switch (data.type.toLowerCase()) {
29 | case "connected": {
30 | this.sendAll();
31 | return;
32 | }
33 |
34 | }
35 | });
36 |
37 | }
38 |
39 | sendAll() {
40 | if (CAFFEINATED.userdata) {
41 | this.sendEvent("message_history", this.messageHistory, true);
42 | this.sendEvent("viewers_list", this.viewersList, true);
43 | }
44 | }
45 |
46 | sendEvent(type, event, isCatchup = false) {
47 | this.send("event", {
48 | type: type,
49 | event: event,
50 | is_catchup: isCatchup
51 | });
52 | }
53 |
54 | send(type, data) {
55 | this.kinoko.send({
56 | type: type,
57 | data: data
58 | });
59 | }
60 |
61 | getDataToStore() {
62 | return {
63 | uuid: this.uuid,
64 | enabled: this.settings.enabled
65 | };
66 | }
67 |
68 | connect() {
69 | this.kinoko.disconnect();
70 |
71 | if (this.settings.enabled) {
72 | this.kinoko.connect("companion:" + this.uuid, "parent");
73 | }
74 | }
75 |
76 | setLinkText() {
77 | this.qrWindow.setCode(`https://casterlabs.co/companion?key=${this.uuid}`);
78 | }
79 |
80 | init() {
81 | this.qrWindow = this.page.querySelector("iframe").contentWindow;
82 | this.uuid = this.settings.uuid;
83 |
84 | if (!this.uuid || this.uuid.includes("-")) {
85 | this.uuid = generateUnsafeUniquePassword(16);
86 |
87 | MODULES.saveToStore(this);
88 | }
89 |
90 | this.page.querySelector("iframe").style.marginBottom = "35px";
91 |
92 | // Give the frame time to render.
93 | setTimeout(() => {
94 | this.setLinkText();
95 | this.connect();
96 | }, 5000);
97 |
98 | koi.addEventListener("chat", (event) => {
99 | this.addMessage(event);
100 | });
101 |
102 | koi.addEventListener("donation", (event) => {
103 | this.addMessage(event);
104 | });
105 |
106 | koi.addEventListener("meta", (event) => {
107 | this.messageMeta(event);
108 | });
109 |
110 | koi.addEventListener("channel_points", (event) => {
111 | this.addPointStatus(event.sender, event.reward, "caffeinated.chatdisplay.reward_text", event.id);
112 | });
113 |
114 | koi.addEventListener("follow", (event) => {
115 | this.addStatus(event.follower, "caffeinated.chatdisplay.follow_text");
116 | });
117 |
118 | koi.addEventListener("subscription", (event) => {
119 | const profile = event.gift_recipient ?? event.subscriber;
120 |
121 | this.addManualStatus(profile, LANG.formatSubscription(event));
122 | });
123 |
124 | koi.addEventListener("viewer_join", (event) => {
125 | this.addStatus(event.viewer, "caffeinated.chatdisplay.join_text");
126 | });
127 |
128 | koi.addEventListener("viewer_leave", (event) => {
129 | this.addStatus(event.viewer, "caffeinated.chatdisplay.leave_text");
130 | });
131 |
132 | koi.addEventListener("viewer_list", (event) => {
133 | this.viewersList = event.viewers;
134 |
135 | this.sendEvent("viewers_list", this.viewersList);
136 | });
137 |
138 | }
139 |
140 | /* Handler Code */
141 | messageMeta(event) {
142 | this.messageHistory.push({
143 | type: "META",
144 | event: Object.assign({}, event)
145 | });
146 |
147 | this.sendEvent("meta", event);
148 | }
149 |
150 | addMessage(event) {
151 | this.messageHistory.push({
152 | type: "MESSAGE",
153 | event: Object.assign({}, event)
154 | });
155 |
156 | this.sendEvent("message", event);
157 | }
158 |
159 | addStatus(profile, langKey, id) {
160 | const usernameHtml = `${escapeHtml(profile.displayname)}`;
161 | const lang = LANG.getTranslation(langKey, usernameHtml);
162 |
163 | const event = {
164 | profile: profile,
165 | lang: lang,
166 | id: id
167 | };
168 |
169 | this.messageHistory.push({
170 | type: "STATUS",
171 | event: event
172 | });
173 |
174 | this.sendEvent("status", event);
175 | }
176 |
177 | addPointStatus(profile, reward, langKey) {
178 | const usernameHtml = ` ${escapeHtml(profile.displayname)} `;
179 | const imageHtml = `
`;
180 |
181 | const lang = LANG.getTranslation(langKey, usernameHtml, reward.title, imageHtml);
182 |
183 | const event = {
184 | profile: profile,
185 | lang: lang, id: id
186 | };
187 |
188 | this.messageHistory.push({
189 | type: "STATUS",
190 | event: event
191 | });
192 |
193 | this.sendEvent("status", event);
194 | }
195 |
196 | addManualStatus(profile, status) {
197 | const event = {
198 | profile: profile,
199 | lang: status
200 | };
201 |
202 | this.messageHistory.push({
203 | type: "STATUS",
204 | event: event
205 | });
206 |
207 | this.sendEvent("status", event);
208 | }
209 |
210 | onSettingsUpdate() {
211 | this.setLinkText();
212 | this.connect();
213 | }
214 |
215 | settingsDisplay = {
216 | enabled: {
217 | display: "generic.enabled",
218 | type: "checkbox",
219 | isLang: true
220 | },
221 | qr_frame: {
222 | type: "iframe-src",
223 | height: "175px",
224 | width: "175px"
225 | },
226 | copy_link: {
227 | display: "caffeinated.companion.copy",
228 | type: "button",
229 | isLang: true
230 | },
231 | reset_link: {
232 | display: "caffeinated.companion.reset",
233 | type: "button",
234 | isLang: true
235 | }
236 | };
237 |
238 | defaultSettings = {
239 | enabled: false,
240 | qr_frame: __dirname + "/modules/modules/qr.html",
241 | // copy_link: () => {},
242 | // reset_link: () => {}
243 | };
244 |
245 | };
246 |
--------------------------------------------------------------------------------
/app/modules/modules/donation.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | MODULES.moduleClasses["casterlabs_donation"] = class {
4 |
5 | constructor(id) {
6 | this.namespace = "casterlabs_donation";
7 | this.displayname = "caffeinated.donation_alert.title";
8 | this.type = "overlay settings";
9 | this.id = id;
10 | this.supportedPlatforms = ["TWITCH", "CAFFEINE", "TROVO"];
11 | }
12 |
13 | widgetDisplay = [
14 | {
15 | name: "Test",
16 | icon: "dice",
17 | onclick(instance) {
18 | koi.test("donation");
19 | }
20 | },
21 | {
22 | name: "Copy",
23 | icon: "copy",
24 | onclick(instance) {
25 | putInClipboard("https://caffeinated.casterlabs.co/donation.html?id=" + instance.id);
26 | }
27 | }
28 | ]
29 |
30 | getDataToStore() {
31 | FileStore.setFile(this, "audio_file", this.audio_file);
32 | FileStore.setFile(this, "image_file", this.image_file);
33 |
34 | return nullFields(this.settings, ["audio_file", "image_file"]);
35 | }
36 |
37 | onConnection(socket) {
38 | MODULES.emitIO(this, "config", this.settings, socket);
39 | MODULES.emitIO(this, "audio_file", this.audio_file, socket);
40 | MODULES.emitIO(this, "image_file", this.image_file, socket);
41 | }
42 |
43 | init() {
44 | koi.addEventListener("donation", (event) => {
45 | for (const donation of event.donations) {
46 | const converted = Object.assign({
47 | image: donation.image,
48 | animated_image: donation.animated_image
49 | }, event);
50 |
51 | MODULES.emitIO(this, "event", converted);
52 |
53 | // Only alert once for Caffeine props
54 | if (event.sender.platform == "CAFFEINE") {
55 | return;
56 | }
57 | }
58 | });
59 |
60 | if (this.settings.audio_file) {
61 | this.audio_file = this.settings.audio_file;
62 | delete this.settings.audio_file;
63 |
64 | MODULES.saveToStore(this);
65 | } else {
66 | this.audio_file = FileStore.getFile(this, "audio_file", this.audio_file);
67 | }
68 |
69 | if (this.settings.image_file) {
70 | this.image_file = this.settings.image_file;
71 | delete this.settings.image_file;
72 |
73 | MODULES.saveToStore(this);
74 | } else {
75 | this.image_file = FileStore.getFile(this, "image_file", this.image_file);
76 | }
77 | }
78 |
79 | async onSettingsUpdate() {
80 | MODULES.emitIO(this, "config", nullFields(this.settings, ["audio_file", "image_file"]));
81 |
82 | if (this.settings.audio_file.files.length > 0) {
83 | this.audio_file = await fileToBase64(this.settings.audio_file, "audio");
84 |
85 | MODULES.emitIO(this, "audio_file", this.audio_file);
86 | }
87 |
88 | if (this.settings.image_file.files.length > 0) {
89 | this.image_file = await fileToBase64(this.settings.image_file);
90 |
91 | MODULES.emitIO(this, "image_file", this.image_file);
92 | }
93 | }
94 |
95 | settingsDisplay = {
96 | font: {
97 | display: "generic.font",
98 | type: "font",
99 | isLang: true
100 | },
101 | font_size: {
102 | display: "generic.font.size",
103 | type: "number",
104 | isLang: true
105 | },
106 | text_color: {
107 | display: "generic.text.color",
108 | type: "color",
109 | isLang: true
110 | },
111 | volume: {
112 | display: "generic.volume",
113 | type: "range",
114 | isLang: true
115 | },
116 | text_to_speech_voice: {
117 | display: "caffeinated.donation_alert.text_to_speech_voice",
118 | type: "select",
119 | isLang: true
120 | },
121 | audio: {
122 | display: "generic.alert.audio",
123 | type: "select",
124 | isLang: true
125 | },
126 | image: {
127 | display: "generic.alert.image",
128 | type: "select",
129 | isLang: true
130 | },
131 | audio_file: {
132 | display: "generic.audio.file",
133 | type: "file",
134 | isLang: true
135 | },
136 | image_file: {
137 | display: "generic.image.file",
138 | type: "file",
139 | isLang: true
140 | }
141 | };
142 |
143 | defaultSettings = {
144 | font: "Poppins",
145 | font_size: "16",
146 | text_color: "#FFFFFF",
147 | volume: 1,
148 | text_to_speech_voice: ["Brian", "Russell", "Nicole", "Amy", "Salli", "Joanna", "Matthew", "Ivy", "Joey"],
149 | audio: ["Custom Audio", "Text To Speech", "Custom Audio & Text To Speech", "None"],
150 | image: ["Custom Image", "Animated Donation Image", "Donation Image", "None"],
151 | audio_file: "",
152 | image_file: ""
153 | };
154 |
155 | };
156 |
--------------------------------------------------------------------------------
/app/modules/modules/donationgoal.js:
--------------------------------------------------------------------------------
1 |
2 | MODULES.moduleClasses["casterlabs_donation_goal"] = class {
3 |
4 | constructor(id) {
5 | this.namespace = "casterlabs_donation_goal";
6 | this.displayname = "caffeinated.donation_goal.title";
7 | this.type = "overlay settings";
8 | this.id = id;
9 | this.supportedPlatforms = ["TWITCH", "CAFFEINE", "TROVO"];
10 | }
11 |
12 | widgetDisplay = [
13 | {
14 | name: "Reset",
15 | icon: "trash",
16 | async onclick(instance) {
17 | instance.amount = 0;
18 |
19 | instance.sendUpdates();
20 | MODULES.saveToStore(instance);
21 | }
22 | },
23 | {
24 | name: "Copy",
25 | icon: "copy",
26 | onclick(instance) {
27 | putInClipboard("https://caffeinated.casterlabs.co/goal.html?namespace=" + instance.namespace + "&id=" + instance.id);
28 | }
29 | }
30 | ]
31 |
32 | getDataToStore() {
33 | const data = Object.assign({}, this.settings);
34 |
35 | data.amount = this.amount;
36 |
37 | return data;
38 | }
39 |
40 | async onConnection(socket) {
41 | this.sendUpdates(socket);
42 | }
43 |
44 | init() {
45 | this.amount = this.settings.amount;
46 |
47 | if (!this.amount) this.amount = 0;
48 |
49 | koi.addEventListener("donation", async (event) => {
50 | if (!event.isTest) {
51 | for (const donation of event.donations) {
52 | this.amount += (await convertCurrency(donation.amount, donation.currency, "USD"));
53 | }
54 |
55 | this.sendUpdates();
56 | MODULES.saveToStore(this);
57 | }
58 | });
59 | }
60 |
61 | async onSettingsUpdate() {
62 | const current = parseFloat(this.page.querySelector("[name=current_amount]").value);
63 |
64 | if (this.oldAmount != current) {
65 | this.amount = (await convertCurrency(current, this.settings.currency, "USD"));
66 | }
67 |
68 | this.sendUpdates();
69 | }
70 |
71 | async sendUpdates(socket) {
72 | MODULES.emitIO(this, "config", this.settings, socket);
73 |
74 | this.oldAmount = this.amount;
75 |
76 | const convertedAmount = (await convertCurrency(this.amount, "USD", this.settings.currency));
77 |
78 | this.page.querySelector("[name=current_amount]").value = convertedAmount;
79 |
80 | MODULES.emitIO(this, "amount", convertedAmount, socket);
81 | MODULES.emitIO(this, "display", (await convertAndFormatCurrency(this.amount, "USD", this.settings.currency)), socket);
82 | MODULES.emitIO(this, "goaldisplay", formatCurrency(this.settings.goal_amount, this.settings.currency), socket);
83 | }
84 |
85 | settingsDisplay = {
86 | title: {
87 | display: "caffeinated.generic_goal.name",
88 | type: "input",
89 | isLang: true
90 | },
91 | currency: {
92 | display: "generic.currency",
93 | type: "currency",
94 | isLang: true
95 | },
96 | current_amount: {
97 | display: "caffeinated.donation_goal.current_amount",
98 | type: "number",
99 | isLang: true
100 | },
101 | goal_amount: {
102 | display: "caffeinated.generic_goal.goal_amount",
103 | type: "number",
104 | isLang: true
105 | },
106 | height: {
107 | display: "generic.height",
108 | type: "number",
109 | isLang: true
110 | },
111 | font: {
112 | display: "generic.font",
113 | type: "font",
114 | isLang: true
115 | },
116 | font_size: {
117 | display: "generic.font.size",
118 | type: "number",
119 | isLang: true
120 | },
121 | text_color: {
122 | display: "caffeinated.generic_goal.text_color",
123 | type: "color",
124 | isLang: true
125 | },
126 | bar_color: {
127 | display: "caffeinated.generic_goal.bar_color",
128 | type: "color",
129 | isLang: true
130 | }
131 | };
132 |
133 | defaultSettings = {
134 | title: "",
135 | currency: "USD",
136 | current_amount: 10,
137 | goal_amount: 10,
138 | height: 60,
139 | font: "Roboto",
140 | font_size: 28,
141 | text_color: "#FFFFFF",
142 | bar_color: "#31F8FF"
143 | };
144 |
145 | };
146 |
--------------------------------------------------------------------------------
/app/modules/modules/donationticker.js:
--------------------------------------------------------------------------------
1 |
2 | MODULES.moduleClasses["casterlabs_donation_ticker"] = class {
3 |
4 | constructor(id) {
5 | this.namespace = "casterlabs_donation_ticker";
6 | this.displayname = "caffeinated.donation_ticker.title";
7 | this.type = "overlay settings";
8 | this.id = id;
9 | this.supportedPlatforms = ["TWITCH", "CAFFEINE", "TROVO"];
10 | }
11 |
12 | widgetDisplay = [
13 | {
14 | name: "Copy",
15 | icon: "copy",
16 | onclick(instance) {
17 | putInClipboard("https://caffeinated.casterlabs.co/display.html?namespace=" + instance.namespace + "&id=" + instance.id);
18 | }
19 | }
20 | ]
21 |
22 | getDataToStore() {
23 | const data = Object.assign({}, this.settings);
24 |
25 | data.amount = this.amount;
26 |
27 | return data;
28 | }
29 |
30 | onConnection(socket) {
31 | this.update(socket);
32 | }
33 |
34 | init() {
35 | this.amount = this.settings.amount;
36 |
37 | if (this.amount === undefined) {
38 | this.amount = 0;
39 | }
40 |
41 | koi.addEventListener("donation", async (event) => {
42 | if (!event.isTest) {
43 | for (const donation of event.donations) {
44 | this.amount += (await convertCurrency(donation.amount, donation.currency, "USD"));
45 | }
46 |
47 | MODULES.saveToStore(this);
48 | this.update();
49 | }
50 | });
51 | }
52 |
53 | async onSettingsUpdate() {
54 | this.update();
55 | }
56 |
57 | async update(socket) {
58 | MODULES.emitIO(this, "config", this.settings, socket);
59 |
60 | const amount = await convertAndFormatCurrency(this.amount, "USD", this.settings.currency);
61 |
62 | MODULES.emitIO(this, "event", `
63 |
64 | ${amount}
65 |
66 | `, socket);
67 | }
68 |
69 | settingsDisplay = {
70 | font: {
71 | display: "generic.font",
72 | type: "font",
73 | isLang: true
74 | },
75 | font_size: {
76 | display: "generic.font.size",
77 | type: "number",
78 | isLang: true
79 | },
80 | currency: {
81 | display: "generic.currency",
82 | type: "currency",
83 | isLang: true
84 | },
85 | text_color: {
86 | display: "generic.text.color",
87 | type: "color",
88 | isLang: true
89 | }
90 | };
91 |
92 | defaultSettings = {
93 | font: "Poppins",
94 | currency: "USD",
95 | font_size: 24,
96 | text_color: "#FFFFFF"
97 | };
98 |
99 | };
100 |
--------------------------------------------------------------------------------
/app/modules/modules/followcounter.js:
--------------------------------------------------------------------------------
1 |
2 | MODULES.moduleClasses["casterlabs_follow_counter"] = class {
3 |
4 | constructor(id) {
5 | this.namespace = "casterlabs_follow_counter";
6 | this.displayname = "caffeinated.follow_counter.title";
7 | this.type = "overlay settings";
8 | this.id = id;
9 |
10 | }
11 |
12 | widgetDisplay = [
13 | {
14 | name: "Copy",
15 | icon: "copy",
16 | onclick(instance) {
17 | putInClipboard("https://caffeinated.casterlabs.co/display.html?namespace=" + instance.namespace + "&id=" + instance.id);
18 | }
19 | }
20 | ]
21 |
22 | getDataToStore() {
23 | return this.settings;
24 | }
25 |
26 | onConnection(socket) {
27 | this.update(socket);
28 | }
29 |
30 | init() {
31 | koi.addEventListener("user_data", (event) => {
32 | this.update(null, event);
33 | });
34 | }
35 |
36 | async onSettingsUpdate() {
37 | this.update();
38 | }
39 |
40 | update(socket, userdata = CAFFEINATED.userdata) {
41 | MODULES.emitIO(this, "config", this.settings, socket);
42 |
43 | if (userdata) {
44 | MODULES.emitIO(this, "event", `
45 |
46 | ${userdata.streamer.followers_count}
47 |
48 | `, socket);
49 | } else {
50 | MODULES.emitIO(this, "event", "", socket);
51 | }
52 | }
53 |
54 | settingsDisplay = {
55 | font: {
56 | display: "generic.font",
57 | type: "font",
58 | isLang: true
59 | },
60 | font_size: {
61 | display: "generic.font.size",
62 | type: "number",
63 | isLang: true
64 | },
65 | text_color: {
66 | display: "generic.text.color",
67 | type: "color",
68 | isLang: true
69 | }
70 | };
71 |
72 | defaultSettings = {
73 | font: "Poppins",
74 | font_size: 24,
75 | text_color: "#FFFFFF"
76 | };
77 |
78 | };
79 |
--------------------------------------------------------------------------------
/app/modules/modules/follower.js:
--------------------------------------------------------------------------------
1 |
2 | MODULES.moduleClasses["casterlabs_follower"] = class {
3 |
4 | constructor(id) {
5 | this.namespace = "casterlabs_follower";
6 | this.displayname = "caffeinated.follower_alert.title";
7 | this.type = "overlay settings";
8 | this.id = id;
9 | }
10 |
11 | widgetDisplay = [
12 | {
13 | name: "Test",
14 | icon: "dice",
15 | onclick(instance) {
16 | koi.test("follow");
17 | }
18 | },
19 | {
20 | name: "Copy",
21 | icon: "copy",
22 | onclick(instance) {
23 | putInClipboard("https://caffeinated.casterlabs.co/alert.html?namespace=" + instance.namespace + "&id=" + instance.id);
24 | }
25 | }
26 | ]
27 |
28 | getDataToStore() {
29 | FileStore.setFile(this, "audio_file", this.audio_file);
30 | FileStore.setFile(this, "image_file", this.image_file);
31 |
32 | return nullFields(this.settings, ["audio_file", "image_file"]);
33 | }
34 |
35 | onConnection(socket) {
36 | MODULES.emitIO(this, "config", this.settings, socket);
37 | MODULES.emitIO(this, "audio_file", this.audio_file, socket);
38 | MODULES.emitIO(this, "image_file", this.image_file, socket);
39 |
40 | }
41 |
42 | init() {
43 | koi.addEventListener("follow", (event) => {
44 | const follower = `${event.follower.displayname}`;
45 |
46 | MODULES.emitIO(this, "event", LANG.getTranslation("caffeinated.follower_alert.format.followed", follower));
47 | });
48 |
49 | if (this.settings.audio_file) {
50 | this.audio_file = this.settings.audio_file;
51 | delete this.settings.audio_file;
52 |
53 | MODULES.saveToStore(this);
54 | } else {
55 | this.audio_file = FileStore.getFile(this, "audio_file", this.audio_file);
56 | }
57 |
58 | if (this.settings.image_file) {
59 | this.image_file = this.settings.image_file;
60 | delete this.settings.image_file;
61 |
62 | MODULES.saveToStore(this);
63 | } else {
64 | this.image_file = FileStore.getFile(this, "image_file", this.image_file);
65 | }
66 | }
67 |
68 | async onSettingsUpdate() {
69 | MODULES.emitIO(this, "config", nullFields(this.settings, ["audio_file", "image_file"]));
70 |
71 | if (this.settings.audio_file.files.length > 0) {
72 | this.audio_file = await fileToBase64(this.settings.audio_file, "audio");
73 |
74 | MODULES.emitIO(this, "audio_file", this.audio_file);
75 | }
76 |
77 | if (this.settings.image_file.files.length > 0) {
78 | this.image_file = await fileToBase64(this.settings.image_file);
79 |
80 | MODULES.emitIO(this, "image_file", this.image_file);
81 | }
82 | }
83 |
84 | settingsDisplay = {
85 | font: {
86 | display: "generic.font",
87 | type: "font",
88 | isLang: true
89 | },
90 | font_size: {
91 | display: "generic.font.size",
92 | type: "number",
93 | isLang: true
94 | },
95 | text_color: {
96 | display: "generic.text.color",
97 | type: "color",
98 | isLang: true
99 | },
100 | volume: {
101 | display: "generic.volume",
102 | type: "range",
103 | isLang: true
104 | },
105 | enable_audio: {
106 | display: "generic.enable_audio",
107 | type: "checkbox",
108 | isLang: true
109 | },
110 | use_custom_image: {
111 | display: "generic.use_custom_image",
112 | type: "checkbox",
113 | isLang: true
114 | },
115 | audio_file: {
116 | display: "generic.audio.file",
117 | type: "file",
118 | isLang: true
119 | },
120 | image_file: {
121 | display: "generic.image.file",
122 | type: "file",
123 | isLang: true
124 | }
125 | };
126 |
127 | defaultSettings = {
128 | font: "Poppins",
129 | font_size: "16",
130 | text_color: "#FFFFFF",
131 | volume: 1,
132 | enable_audio: false,
133 | use_custom_image: false,
134 | audio_file: "",
135 | image_file: ""
136 | };
137 |
138 | };
139 |
--------------------------------------------------------------------------------
/app/modules/modules/followergoal.js:
--------------------------------------------------------------------------------
1 |
2 | MODULES.moduleClasses["casterlabs_follower_goal"] = class {
3 |
4 | constructor(id) {
5 | this.namespace = "casterlabs_follower_goal";
6 | this.displayname = "caffeinated.follower_goal.title";
7 | this.type = "overlay settings";
8 | this.id = id;
9 | }
10 |
11 | widgetDisplay = [
12 | {
13 | name: "Reset",
14 | icon: "trash",
15 | async onclick(instance) {
16 | instance.amount = 0;
17 |
18 | instance.sendUpdates();
19 | MODULES.saveToStore(instance);
20 | }
21 | },
22 | {
23 | name: "Copy",
24 | icon: "copy",
25 | onclick(instance) {
26 | putInClipboard("https://caffeinated.casterlabs.co/goal.html?namespace=" + instance.namespace + "&id=" + instance.id);
27 | }
28 | }
29 | ]
30 |
31 | getDataToStore() {
32 | const data = Object.assign({}, this.settings);
33 |
34 | data.amount = this.amount;
35 |
36 | return data;
37 | }
38 |
39 | async onConnection(socket) {
40 | this.sendUpdates(socket);
41 | }
42 |
43 | init() {
44 | this.amount = this.settings.amount;
45 |
46 | if (!this.amount) this.amount = 0;
47 |
48 | koi.addEventListener("user_update", (event) => {
49 | this.amount = event.streamer.followers_count;
50 |
51 | this.sendUpdates();
52 | MODULES.saveToStore(this);
53 | });
54 | }
55 |
56 | onSettingsUpdate() {
57 | this.sendUpdates();
58 | }
59 |
60 | async sendUpdates(socket) {
61 | MODULES.emitIO(this, "config", this.settings, socket);
62 |
63 | MODULES.emitIO(this, "amount", this.amount, socket);
64 | MODULES.emitIO(this, "display", this.amount, socket);
65 | MODULES.emitIO(this, "goaldisplay", this.settings.goal_amount, socket);
66 | }
67 |
68 | settingsDisplay = {
69 | title: {
70 | display: "caffeinated.generic_goal.name",
71 | type: "input",
72 | isLang: true
73 | },
74 | goal_amount: {
75 | display: "caffeinated.generic_goal.goal_amount",
76 | type: "number",
77 | isLang: true
78 | },
79 | height: {
80 | display: "generic.height",
81 | type: "number",
82 | isLang: true
83 | },
84 | font: {
85 | display: "generic.font",
86 | type: "font",
87 | isLang: true
88 | },
89 | font_size: {
90 | display: "generic.font.size",
91 | type: "number",
92 | isLang: true
93 | },
94 | text_color: {
95 | display: "caffeinated.generic_goal.text_color",
96 | type: "color",
97 | isLang: true
98 | },
99 | bar_color: {
100 | display: "caffeinated.generic_goal.bar_color",
101 | type: "color",
102 | isLang: true
103 | }
104 | };
105 |
106 | defaultSettings = {
107 | title: "",
108 | goal_amount: 10,
109 | height: 60,
110 | font: "Roboto",
111 | font_size: 28,
112 | text_color: "#FFFFFF",
113 | bar_color: "#31F8FF"
114 | };
115 |
116 | };
117 |
--------------------------------------------------------------------------------
/app/modules/modules/nowplaying.js:
--------------------------------------------------------------------------------
1 |
2 | MODULES.moduleClasses["casterlabs_now_playing"] = class {
3 |
4 | constructor(id) {
5 | this.namespace = "casterlabs_now_playing";
6 | this.displayname = "spotify.integration.title"
7 | this.type = "overlay settings";
8 | this.id = id;
9 | }
10 |
11 | widgetDisplay = [
12 | {
13 | name: "Copy",
14 | icon: "copy",
15 | onclick(instance) {
16 | putInClipboard("https://caffeinated.casterlabs.co/nowplaying.html?id=" + instance.id);
17 | }
18 | }
19 | ]
20 |
21 | async setToken(code) {
22 | const response = await fetch("https://api.casterlabs.co/v2/natsukashii/spotify?code=" + code);
23 | const authResult = await response.json();
24 |
25 | if (!authResult.error) {
26 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.logging_in");
27 |
28 | this.refreshToken = authResult.refresh_token;
29 |
30 | MODULES.saveToStore(this);
31 | } else {
32 | this.settings.token = null;
33 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.login");
34 | }
35 | }
36 |
37 | getDataToStore() {
38 | const data = Object.assign({}, this.settings);
39 |
40 | data.token = this.refreshToken;
41 |
42 | return data;
43 | }
44 |
45 | onConnection(socket) {
46 | MODULES.emitIO(this, "config", this.settings, socket);
47 |
48 | if (this.event) {
49 | MODULES.emitIO(this, "event", this.event, socket);
50 | }
51 | }
52 |
53 | init() {
54 | if (this.settings.token) {
55 | this.refreshToken = this.settings.token;
56 | this.settings.token = null;
57 | this.check();
58 | }
59 |
60 | koi.addEventListener("chat", (event) => {
61 | this.processCommand(event);
62 | });
63 |
64 | koi.addEventListener("donation", (event) => {
65 | this.processCommand(event);
66 | });
67 |
68 | setInterval(() => this.check(), 2000);
69 |
70 | const element = document.querySelector("#casterlabs_now_playing_" + this.id).querySelector("[name=login]");
71 |
72 | element.style = "overflow: hidden; background-color: rgb(30, 215, 96); margin-top: 15px;";
73 | element.innerHTML = `
74 |
75 | ${LANG.getTranslation("spotify.integration.login")}
76 | `;
77 |
78 | this.statusElement = element.querySelector("[name=text]");
79 | }
80 |
81 | processCommand(event) {
82 | const message = event.message.toLowerCase();
83 |
84 | if (message.startsWith("!song")) {
85 | if (this.settings.enable_song_command) {
86 | koi.sendMessage(`@${event.sender.displayname} ${this.event.title} - ${this.event.artist}`, event, "PUPPET");
87 | }
88 | }
89 | }
90 |
91 | async check() {
92 | if (this.refreshToken) {
93 | if (!this.accessToken) {
94 | const auth = await (await fetch("https://api.casterlabs.co/v2/natsukashii/spotify?refresh_token=" + this.refreshToken)).json();
95 |
96 | if (auth.error) {
97 | this.refreshToken = null;
98 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.login");
99 | } else {
100 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.logging_in");
101 |
102 | this.accessToken = auth.access_token;
103 | if (auth.refresh_token) {
104 | this.refreshToken = auth.refresh_token;
105 | }
106 |
107 | const profile = await (await fetch("https://api.spotify.com/v1/me", {
108 | headers: {
109 | "content-type": "application/json",
110 | authorization: "Bearer " + this.accessToken
111 | }
112 | })).json();
113 |
114 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.logged_in_as", profile.display_name);
115 | }
116 |
117 | MODULES.saveToStore(this);
118 | }
119 |
120 | const response = await fetch("https://api.spotify.com/v1/me/player", {
121 | headers: {
122 | "content-type": "application/json",
123 | authorization: "Bearer " + this.accessToken
124 | }
125 | });
126 |
127 | if ((response.status == 401) || response.error) {
128 | this.accessToken = null;
129 | this.check();
130 | } else if (response.status == 200) {
131 | const player = await response.json();
132 |
133 | if (player.item) {
134 | const image = player.item.album.images[0].url;
135 | const title = player.item.name.replace(/(\(ft.*\))|(\(feat.*\))/gi, ""); // Remove (feat. ...)
136 | let artists = [];
137 |
138 | player.item.artists.forEach((artist) => {
139 | artists.push(artist.name);
140 | });
141 |
142 | this.broadcast({
143 | title: title,
144 | artist: artists.join(", "),
145 | image: image
146 | });
147 | }
148 | }
149 | }
150 | }
151 |
152 | broadcast(event) {
153 | if (this.event) {
154 | // Don't re-notify
155 | if (this.event.title == event.title) {
156 | return;
157 | }
158 | }
159 |
160 | this.event = event;
161 |
162 | if (this.settings.announce) {
163 | koi.sendMessage(`Now playing: ${event.title} - ${event.artist}`, CAFFEINATED.userdata, "PUPPET");
164 | }
165 |
166 | MODULES.emitIO(this, "event", this.event);
167 | }
168 |
169 | onSettingsUpdate() {
170 | MODULES.emitIO(this, "config", this.settings);
171 | }
172 |
173 | settingsDisplay = {
174 | login: {
175 | display: "spotify.integration.login",
176 | type: "button",
177 | isLang: true
178 | },
179 | announce: {
180 | display: "spotify.integration.announce",
181 | type: "checkbox",
182 | isLang: true
183 | },
184 | enable_song_command: {
185 | display: "spotify.integration.enable_song_command",
186 | type: "checkbox",
187 | isLang: true
188 | },
189 | background_style: {
190 | display: "spotify.integration.background_style",
191 | type: "select",
192 | isLang: true
193 | },
194 | image_style: {
195 | display: "spotify.integration.image_style",
196 | type: "select",
197 | isLang: true
198 | }
199 | };
200 |
201 | defaultSettings = {
202 | login: () => {
203 | if (this.refreshToken) {
204 | this.refreshToken = null;
205 | this.accessToken = null;
206 | this.statusElement.innerText = LANG.getTranslation("spotify.integration.login");
207 | } else {
208 | const auth = new AuthCallback("caffeinated_spotify");
209 |
210 | // 15min timeout
211 | auth.awaitAuthMessage((15 * 1000) * 60).then((token) => {
212 | this.setToken(token);
213 | }).catch((reason) => { /* Ignored. */ });
214 |
215 | openLink("https://accounts.spotify.com/en/authorize?client_id=dff9da1136b0453983ff40e3e5e20397&response_type=code&scope=user-read-playback-state&redirect_uri=https:%2F%2Fcasterlabs.co%2Fauth%3Ftype%3Dcaffeinated_spotify&state=" + auth.getStateString());
216 | }
217 | },
218 | announce: false,
219 | enable_song_command: false,
220 | background_style: ["Blur", "Clear", "Solid"],
221 | image_style: ["Left", "Right", "None"]
222 | };
223 |
224 | };
225 |
--------------------------------------------------------------------------------
/app/modules/modules/qr.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 | Loving Caffeinated? Feel free to support the project here. 19 |
20 |
21 | Looking for a custom made overlay design?
Get your own at Reyana.org
22 |
85 | 86 |