From fcfa895074d3da2b88195cc22bd4b0881c9e5b26 Mon Sep 17 00:00:00 2001 From: Eric Oestrich Date: Thu, 9 Apr 2020 23:15:08 -0400 Subject: [PATCH 1/4] Start of a media reducer - Receives Client.Media messages - Plays sound - Stops sound (based on filtering) --- .../assets/js/play/redux/mediaReducer.js | 153 ++++++++++++++++++ .../assets/js/play/redux/mediaReducer.test.js | 66 ++++++++ apps/grapevine/assets/js/play/redux/store.js | 2 + apps/grapevine/assets/package.json | 1 + apps/grapevine/assets/yarn.lock | 7 +- apps/telnet/lib/telnet/features.ex | 22 ++- 6 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 apps/grapevine/assets/js/play/redux/mediaReducer.js create mode 100644 apps/grapevine/assets/js/play/redux/mediaReducer.test.js diff --git a/apps/grapevine/assets/js/play/redux/mediaReducer.js b/apps/grapevine/assets/js/play/redux/mediaReducer.js new file mode 100644 index 00000000..1fe7ca09 --- /dev/null +++ b/apps/grapevine/assets/js/play/redux/mediaReducer.js @@ -0,0 +1,153 @@ +import {Howl, Howler} from 'howler'; +import {createReducer} from "reduxsauce"; +import _ from "underscore"; + +import {Types} from "./actions"; + +export class Player { + constructor() { + this.activeMedia = []; + } + + play(media) { + _.each(this.activeMedia, (activeMedia) => { + if (activeMedia.key == media.key) { + activeMedia.stop(); + } + }); + + this.activeMedia = _.reject(this.activeMedia, (activeMedia) => { + return activeMedia.key == media.key; + }); + + media.play(); + + this.activeMedia.push(media); + } + + stop(filter) { + _.filter(this.activeMedia, (activeMedia) => { + return activeMedia.matchFilter(filter); + }).map((media) => { + media.stop() + }); + + this.activeMedia = _.reject(this.activeMedia, (activeMedia) => { + return !activeMedia.isPlaying(); + }); + } +} + +const baseUrl = (attrs, defaults) => { + let url; + + if ("url" in attrs) { + url = attrs.url; + } else if ("url" in defaults) { + url = defaults.url; + } + + if (url && !url.endsWith("/")) { + url = url + "/"; + } + + return url; +} + +export class Media { + constructor(attrs, defaults = {}) { + this.type = attrs.type; + this.key = attrs.key; + this.priority = attrs.priority; + this.tag = attrs.tag; + this.name = attrs.name; + + this.url = baseUrl(attrs, defaults) + attrs.name; + + this.howler = new Howl({ + src: [this.url], + html5: true + }); + } + + isPlaying() { + return this.howler.playing(); + } + + play() { + console.log("playing"); + this.howler.play(); + } + + matchFilter(filter) { + let matchKeys = ["type", "name", "tag", "key"]; + + matchKeys = _.filter(matchKeys, (key) => { + return key in filter; + }); + + let matchedKeys = _.all(matchKeys, (key) => { + return filter[key] == this[key]; + }); + + return matchedKeys && this.priorityMatch(filter); + } + + priorityMatch(filter) { + if (!filter.priority) { + return true; + } + + if (!this.priority) { + return false; + } + + if (this.priority <= filter.priority) { + return true; + } + + return false; + } + + stop() { + console.log("stopping"); + this.howler.stop(); + } +} + +const INITIAL_STATE = { + defaults: {}, + player: new Player(), +} + +export const mediaReceiveGMCP = (state, action) => { + let key; + + switch (action.message) { + case "Client.Media.Default": + return {...state, defaults: action.data}; + + case "Client.Media.Play": + console.log("Want to play music..."); + console.log(action); + + let media = new Media(action.data, state.defaults); + + state.player.play(media); + + return state; + + case "Client.Media.Stop": + state.player.stop(action.data); + return state; + + default: + return state; + } +}; + +export const HANDLERS = { + [Types.SOCKET_RECEIVE_GMCP]: mediaReceiveGMCP, +} + +export const mediaReducer = createReducer(INITIAL_STATE, HANDLERS); diff --git a/apps/grapevine/assets/js/play/redux/mediaReducer.test.js b/apps/grapevine/assets/js/play/redux/mediaReducer.test.js new file mode 100644 index 00000000..b2c7545e --- /dev/null +++ b/apps/grapevine/assets/js/play/redux/mediaReducer.test.js @@ -0,0 +1,66 @@ +import {Media} from "./mediaReducer"; + +describe("media class - file url", () => { + test("uses the default url to construct the url", () => { + let media = new Media({name: "file.mp3"}, {url: "https://example.com/"}); + expect(media.url).toEqual("https://example.com/file.mp3"); + }); + + test("includes a trailing slash automatically", () => { + let media = new Media({name: "file.mp3"}, {url: "https://example.com"}); + expect(media.url).toEqual("https://example.com/file.mp3"); + }); + + test("uses the specific url if provided", () => { + let media = new Media({name: "file.mp3", url: "https://example.com"}); + expect(media.url).toEqual("https://example.com/file.mp3"); + }); +}); + +describe ("media class - match filter", () => { + test("matches based on key", () => { + let media = new Media({key: "ambient"}); + expect(media.matchFilter({key: "ambient"})).toEqual(true); + + media = new Media({key: "ambient"}); + expect(media.matchFilter({key: "other"})).toEqual(false); + }); + + test("matches based on name", () => { + let media = new Media({name: "file.mp3"}); + expect(media.matchFilter({name: "file.mp3"})).toEqual(true); + + media = new Media({name: "file.mp3"}); + expect(media.matchFilter({name: "other.mp3"})).toEqual(false); + }); + + test("matches based on type", () => { + let media = new Media({type: "music"}); + expect(media.matchFilter({type: "music"})).toEqual(true); + + media = new Media({type: "music"}); + expect(media.matchFilter({type: "sound"})).toEqual(false); + }); + + test("matches based on tag", () => { + let media = new Media({tag: "background"}); + expect(media.matchFilter({tag: "background"})).toEqual(true); + + media = new Media({tag: "background"}); + expect(media.matchFilter({tag: "foreground"})).toEqual(false); + }); + + test("matches based on priority", () => { + let media = new Media({priority: 50}); + expect(media.matchFilter({priority: 50})).toEqual(true); + + media = new Media({priority: 50}); + expect(media.matchFilter({priority: 65})).toEqual(true); + + media = new Media({priority: 50}); + expect(media.matchFilter({priority: 25})).toEqual(false); + }); + + test("combination", () => { + }); +}); diff --git a/apps/grapevine/assets/js/play/redux/store.js b/apps/grapevine/assets/js/play/redux/store.js index f492e4b7..d75f8656 100644 --- a/apps/grapevine/assets/js/play/redux/store.js +++ b/apps/grapevine/assets/js/play/redux/store.js @@ -1,5 +1,6 @@ import {combineReducers, createStore, compose} from 'redux'; +import {mediaReducer} from "./mediaReducer"; import {modalReducer} from "./modalReducer"; import {promptReducer} from "./promptReducer"; import {settingsReducer} from "./settingsReducer"; @@ -93,6 +94,7 @@ export const getVoiceVoices = (state) => { // Reducers let rootReducer = combineReducers({ + media: mediaReducer, modal: modalReducer, prompt: promptReducer, settings: settingsReducer, diff --git a/apps/grapevine/assets/package.json b/apps/grapevine/assets/package.json index ba9f79b1..c2151e48 100644 --- a/apps/grapevine/assets/package.json +++ b/apps/grapevine/assets/package.json @@ -10,6 +10,7 @@ "dependencies": { "@grapevine/ansi": "^1.0.0", "chart.js": "^2.7.3", + "howler": "^2.1.3", "jquery": "^3.3.1", "moment": "^2.24.0", "moment-timezone": "^0.5.26", diff --git a/apps/grapevine/assets/yarn.lock b/apps/grapevine/assets/yarn.lock index 3b73b561..dd5317a2 100644 --- a/apps/grapevine/assets/yarn.lock +++ b/apps/grapevine/assets/yarn.lock @@ -3209,6 +3209,11 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.4.tgz#44119abaf4bc64692a16ace34700fed9c03e2546" integrity sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ== +howler@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/howler/-/howler-2.1.3.tgz#07c88618f8767e879407a4d647fe2d6d5f15f121" + integrity sha512-PSGbOi1EYgw80C5UQbxtJM7TmzD+giJunIMBYyH3RVzHZx2fZLYBoes0SpVVHi/SFa1GoNtgXj/j6I7NOKYBxQ== + hsl-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" @@ -5192,7 +5197,7 @@ performance-now@^2.1.0: integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= "phoenix@file:../deps/phoenix": - version "1.4.6" + version "1.4.12" "phoenix_html@file:../deps/phoenix_html": version "2.13.3" diff --git a/apps/telnet/lib/telnet/features.ex b/apps/telnet/lib/telnet/features.ex index 480d88ec..988b194f 100644 --- a/apps/telnet/lib/telnet/features.ex +++ b/apps/telnet/lib/telnet/features.ex @@ -40,9 +40,17 @@ defmodule GrapevineTelnet.Features do """ def cache_message(state, message, data) + def cache_message(state, "Client.Media.Default", data) do + force_cache_message(state, "Client.Media.Default", data) + end + + def cache_message(state, "Client.Media" <> _, _data), do: state + def cache_message(state, "Client.Modals" <> _, _data), do: state - def cache_message(state, message, data) do + def cache_message(state, message, data), do: force_cache_message(state, message, data) + + defp force_cache_message(state, message, data) do cache = Map.put(state.features.message_cache, message, data) features = Map.put(state.features, :message_cache, cache) Map.put(state, :features, features) @@ -92,10 +100,18 @@ defmodule GrapevineTelnet.Features do @doc """ Base packages that the client will try to turn on """ - def base_packages(), do: ["Client.Modals 1"] + def base_packages(), do: ["Client.Modals 1", "Client.Media 1"] @doc """ Base messages that are known for the client """ - def base_messagse(), do: ["Client.Modals.Open"] + def base_messagse() do + [ + "Client.Modals.Open", + "Client.Media.Default", + "Client.Media.Load", + "Client.Media.Play", + "Client.Media.Stop" + ] + end end From 51243ebf107814a02b095be7a216330332545665 Mon Sep 17 00:00:00 2001 From: Eric Oestrich Date: Fri, 10 Apr 2020 23:35:19 -0400 Subject: [PATCH 2/4] Set up a filter class to better validate attributes --- .../assets/js/play/redux/mediaReducer.js | 110 ++++++++++---- .../assets/js/play/redux/mediaReducer.test.js | 135 +++++++++++++----- 2 files changed, 183 insertions(+), 62 deletions(-) diff --git a/apps/grapevine/assets/js/play/redux/mediaReducer.js b/apps/grapevine/assets/js/play/redux/mediaReducer.js index 1fe7ca09..28a8858d 100644 --- a/apps/grapevine/assets/js/play/redux/mediaReducer.js +++ b/apps/grapevine/assets/js/play/redux/mediaReducer.js @@ -4,6 +4,84 @@ import _ from "underscore"; import {Types} from "./actions"; +export class Filter { + constructor(attrs) { + if (this.validName(attrs.name)) { + this.name = attrs.name; + } + + if (this.validType(attrs.type)) { + this.type = attrs.type; + } + + if (this.validTag(attrs.tag)) { + this.tag = attrs.tag; + } + + if (this.validPriority(attrs.priority)) { + this.priority = attrs.priority; + } + + if (this.validKey(attrs.key)) { + this.key = attrs.key; + } + } + + matches(filter) { + let matchKeys = ["type", "name", "tag", "key"]; + + matchKeys = _.filter(matchKeys, (key) => { + return key in filter; + }); + + let matchedKeys = _.all(matchKeys, (key) => { + return filter[key] == this[key]; + }); + + return matchedKeys && this.priorityMatch(filter); + } + + /* + * Private-ish + */ + + priorityMatch(filter) { + if (!filter.priority) { + return true; + } + + if (!this.priority) { + return false; + } + + if (this.priority <= filter.priority) { + return true; + } + + return false; + } + + validKey(key) { + return typeof key == "string"; + } + + validName(name) { + return typeof name == "string" && name.endsWith(".mp3"); + } + + validPriority(priority) { + return typeof priority == "number" && priority >= 1 && priority <= 100; + } + + validTag(tag) { + return typeof tag == "string"; + } + + validType(type) { + return type == "music" || type == "sound"; + } +} + export class Player { constructor() { this.activeMedia = []; @@ -26,6 +104,8 @@ export class Player { } stop(filter) { + filter = new Filter(filter); + _.filter(this.activeMedia, (activeMedia) => { return activeMedia.matchFilter(filter); }).map((media) => { @@ -56,6 +136,8 @@ const baseUrl = (attrs, defaults) => { export class Media { constructor(attrs, defaults = {}) { + this.filter = new Filter(attrs); + this.type = attrs.type; this.key = attrs.key; this.priority = attrs.priority; @@ -80,33 +162,7 @@ export class Media { } matchFilter(filter) { - let matchKeys = ["type", "name", "tag", "key"]; - - matchKeys = _.filter(matchKeys, (key) => { - return key in filter; - }); - - let matchedKeys = _.all(matchKeys, (key) => { - return filter[key] == this[key]; - }); - - return matchedKeys && this.priorityMatch(filter); - } - - priorityMatch(filter) { - if (!filter.priority) { - return true; - } - - if (!this.priority) { - return false; - } - - if (this.priority <= filter.priority) { - return true; - } - - return false; + return this.filter.matches(filter); } stop() { diff --git a/apps/grapevine/assets/js/play/redux/mediaReducer.test.js b/apps/grapevine/assets/js/play/redux/mediaReducer.test.js index b2c7545e..77d06ad5 100644 --- a/apps/grapevine/assets/js/play/redux/mediaReducer.test.js +++ b/apps/grapevine/assets/js/play/redux/mediaReducer.test.js @@ -1,66 +1,131 @@ -import {Media} from "./mediaReducer"; +import {Filter, Media} from "./mediaReducer"; -describe("media class - file url", () => { - test("uses the default url to construct the url", () => { - let media = new Media({name: "file.mp3"}, {url: "https://example.com/"}); - expect(media.url).toEqual("https://example.com/file.mp3"); +describe("filtering", () => { + test("validates name", () => { + let filter = new Filter({name: "file.mp3"}); + expect(filter.name).toEqual("file.mp3"); + + filter = new Filter({name: "file.ogg"}); + expect(filter.name).toEqual(undefined); + + filter = new Filter({name: "anything else"}); + expect(filter.name).toEqual(undefined); }); - test("includes a trailing slash automatically", () => { - let media = new Media({name: "file.mp3"}, {url: "https://example.com"}); - expect(media.url).toEqual("https://example.com/file.mp3"); + test("validates type", () => { + let filter = new Filter({type: "music"}); + expect(filter.type).toEqual("music"); + + filter = new Filter({type: "sound"}); + expect(filter.type).toEqual("sound"); + + filter = new Filter({type: "anything else"}); + expect(filter.type).toEqual(undefined); }); - test("uses the specific url if provided", () => { - let media = new Media({name: "file.mp3", url: "https://example.com"}); - expect(media.url).toEqual("https://example.com/file.mp3"); + test("validates tag", () => { + let filter = new Filter({tag: "combat"}); + expect(filter.tag).toEqual("combat"); + + filter = new Filter({tag: 10}); + expect(filter.tag).toEqual(undefined); + }); + + test("validates priority", () => { + let filter = new Filter({priority: 50}); + expect(filter.priority).toEqual(50); + + filter = new Filter({priority: 1}); + expect(filter.priority).toEqual(1); + + filter = new Filter({priority: -1}); + expect(filter.priority).toEqual(undefined); + + filter = new Filter({priority: "anything else"}); + expect(filter.priority).toEqual(undefined); + }); + + test("validates key", () => { + let filter = new Filter({key: "a key"}); + expect(filter.key).toEqual("a key"); + + filter = new Filter({key: 1}); + expect(filter.key).toEqual(undefined); }); }); -describe ("media class - match filter", () => { +describe("filters match", () => { test("matches based on key", () => { - let media = new Media({key: "ambient"}); - expect(media.matchFilter({key: "ambient"})).toEqual(true); + let filter = new Filter({key: "ambient"}); + expect(filter.matches(new Filter({key: "ambient"}))).toEqual(true); - media = new Media({key: "ambient"}); - expect(media.matchFilter({key: "other"})).toEqual(false); + filter = new Filter({key: "ambient"}); + expect(filter.matches(new Filter({key: "other"}))).toEqual(false); }); test("matches based on name", () => { - let media = new Media({name: "file.mp3"}); - expect(media.matchFilter({name: "file.mp3"})).toEqual(true); + let filter = new Filter({name: "file.mp3"}); + expect(filter.matches({name: "file.mp3"})).toEqual(true); - media = new Media({name: "file.mp3"}); - expect(media.matchFilter({name: "other.mp3"})).toEqual(false); + filter = new Filter({name: "file.mp3"}); + expect(filter.matches({name: "other.mp3"})).toEqual(false); }); test("matches based on type", () => { - let media = new Media({type: "music"}); - expect(media.matchFilter({type: "music"})).toEqual(true); + let filter = new Filter({type: "music"}); + expect(filter.matches({type: "music"})).toEqual(true); - media = new Media({type: "music"}); - expect(media.matchFilter({type: "sound"})).toEqual(false); + filter = new Filter({type: "music"}); + expect(filter.matches({type: "sound"})).toEqual(false); }); test("matches based on tag", () => { - let media = new Media({tag: "background"}); - expect(media.matchFilter({tag: "background"})).toEqual(true); + let filter = new Filter({tag: "background"}); + expect(filter.matches({tag: "background"})).toEqual(true); - media = new Media({tag: "background"}); - expect(media.matchFilter({tag: "foreground"})).toEqual(false); + filter = new Filter({tag: "background"}); + expect(filter.matches({tag: "foreground"})).toEqual(false); }); test("matches based on priority", () => { - let media = new Media({priority: 50}); - expect(media.matchFilter({priority: 50})).toEqual(true); + let filter = new Filter({priority: 50}); + expect(filter.matches({priority: 50})).toEqual(true); - media = new Media({priority: 50}); - expect(media.matchFilter({priority: 65})).toEqual(true); + filter = new Filter({priority: 50}); + expect(filter.matches({priority: 65})).toEqual(true); - media = new Media({priority: 50}); - expect(media.matchFilter({priority: 25})).toEqual(false); + filter = new Filter({priority: 50}); + expect(filter.matches({priority: 25})).toEqual(false); }); - test("combination", () => { + test("matches based on combinations", () => { + let filter = new Filter({tag: "background", priority: 50}); + expect(filter.matches({tag: "background", priority: 65})).toEqual(true); + + filter = new Filter({type: "music", priority: 50}); + expect(filter.matches({type: "music", priority: 35})).toEqual(false); + + filter = new Filter({type: "music", priority: 50}); + expect(filter.matches({key: "background"})).toEqual(false); + + filter = new Filter({type: "music", priority: 50}); + expect(filter.matches({priority: 65})).toEqual(true); + }); +}); + +describe("media class - file url", () => { + test("uses the default url to construct the url", () => { + let media = new Media({name: "file.mp3"}, {url: "https://example.com/"}); + expect(media.url).toEqual("https://example.com/file.mp3"); + }); + + test("includes a trailing slash automatically", () => { + let media = new Media({name: "file.mp3"}, {url: "https://example.com"}); + expect(media.url).toEqual("https://example.com/file.mp3"); + }); + + test("uses the specific url if provided", () => { + let media = new Media({name: "file.mp3", url: "https://example.com"}); + expect(media.url).toEqual("https://example.com/file.mp3"); }); }); From e62710a0cf32ef3c4911c4ba4d58e4fb81f372b1 Mon Sep 17 00:00:00 2001 From: Eric Oestrich Date: Fri, 10 Apr 2020 23:49:06 -0400 Subject: [PATCH 3/4] Pull validations out of the filtering class validate Media --- .../assets/js/play/redux/mediaReducer.js | 78 +++++++++++-------- .../assets/js/play/redux/mediaReducer.test.js | 78 +++++++++---------- 2 files changed, 84 insertions(+), 72 deletions(-) diff --git a/apps/grapevine/assets/js/play/redux/mediaReducer.js b/apps/grapevine/assets/js/play/redux/mediaReducer.js index 28a8858d..27d64a2f 100644 --- a/apps/grapevine/assets/js/play/redux/mediaReducer.js +++ b/apps/grapevine/assets/js/play/redux/mediaReducer.js @@ -4,25 +4,53 @@ import _ from "underscore"; import {Types} from "./actions"; +/* + * Validations + */ + +export const validKey = (key) => { + return typeof key == "string"; +} + +export const validName = (name) => { + return typeof name == "string" && name.endsWith(".mp3"); +} + +export const validPriority = (priority) => { + return typeof priority == "number" && priority >= 1 && priority <= 100; +} + +export const validTag = (tag) => { + return typeof tag == "string"; +} + +export const validType = (type) => { + return type == "music" || type == "sound"; +} + +export const validUrl = (url) => { + return typeof url == "string" && (url.startsWith("http://") || url.startsWith("https://")); +} + export class Filter { constructor(attrs) { - if (this.validName(attrs.name)) { + if (validName(attrs.name)) { this.name = attrs.name; } - if (this.validType(attrs.type)) { + if (validType(attrs.type)) { this.type = attrs.type; } - if (this.validTag(attrs.tag)) { + if (validTag(attrs.tag)) { this.tag = attrs.tag; } - if (this.validPriority(attrs.priority)) { + if (validPriority(attrs.priority)) { this.priority = attrs.priority; } - if (this.validKey(attrs.key)) { + if (validKey(attrs.key)) { this.key = attrs.key; } } @@ -60,26 +88,6 @@ export class Filter { return false; } - - validKey(key) { - return typeof key == "string"; - } - - validName(name) { - return typeof name == "string" && name.endsWith(".mp3"); - } - - validPriority(priority) { - return typeof priority == "number" && priority >= 1 && priority <= 100; - } - - validTag(tag) { - return typeof tag == "string"; - } - - validType(type) { - return type == "music" || type == "sound"; - } } export class Player { @@ -121,9 +129,9 @@ export class Player { const baseUrl = (attrs, defaults) => { let url; - if ("url" in attrs) { + if ("url" in attrs && validUrl(attrs.url)) { url = attrs.url; - } else if ("url" in defaults) { + } else if ("url" in defaults && validUrl(defaults.url)) { url = defaults.url; } @@ -138,13 +146,17 @@ export class Media { constructor(attrs, defaults = {}) { this.filter = new Filter(attrs); - this.type = attrs.type; - this.key = attrs.key; - this.priority = attrs.priority; - this.tag = attrs.tag; - this.name = attrs.name; + this.type = this.filter.type; + this.key = this.filter.key; + this.priority = this.filter.priority; + this.tag = this.filter.tag; + this.name = this.filter.name; - this.url = baseUrl(attrs, defaults) + attrs.name; + if (baseUrl(attrs, defaults)) { + this.url = baseUrl(attrs, defaults) + this.filter.name; + } else { + throw "Invalid URL! Music cannot be played"; + } this.howler = new Howl({ src: [this.url], diff --git a/apps/grapevine/assets/js/play/redux/mediaReducer.test.js b/apps/grapevine/assets/js/play/redux/mediaReducer.test.js index 77d06ad5..910395ed 100644 --- a/apps/grapevine/assets/js/play/redux/mediaReducer.test.js +++ b/apps/grapevine/assets/js/play/redux/mediaReducer.test.js @@ -1,56 +1,50 @@ -import {Filter, Media} from "./mediaReducer"; - -describe("filtering", () => { +import { + Filter, + Media, + validKey, + validName, + validPriority, + validTag, + validType, + validUrl, +} from "./mediaReducer"; + +describe("validating", () => { test("validates name", () => { - let filter = new Filter({name: "file.mp3"}); - expect(filter.name).toEqual("file.mp3"); - - filter = new Filter({name: "file.ogg"}); - expect(filter.name).toEqual(undefined); - - filter = new Filter({name: "anything else"}); - expect(filter.name).toEqual(undefined); + expect(validName("file.mp3")).toEqual(true); + expect(validName("file.ogg")).toEqual(false); + expect(validName("anything else")).toEqual(false); }); test("validates type", () => { - let filter = new Filter({type: "music"}); - expect(filter.type).toEqual("music"); - - filter = new Filter({type: "sound"}); - expect(filter.type).toEqual("sound"); - - filter = new Filter({type: "anything else"}); - expect(filter.type).toEqual(undefined); + expect(validType("music")).toEqual(true); + expect(validType("sound")).toEqual(true); + expect(validType("anything else")).toEqual(false); }); test("validates tag", () => { - let filter = new Filter({tag: "combat"}); - expect(filter.tag).toEqual("combat"); - - filter = new Filter({tag: 10}); - expect(filter.tag).toEqual(undefined); + expect(validTag("combat")).toEqual(true); + expect(validTag(10)).toEqual(false); }); test("validates priority", () => { - let filter = new Filter({priority: 50}); - expect(filter.priority).toEqual(50); - - filter = new Filter({priority: 1}); - expect(filter.priority).toEqual(1); - - filter = new Filter({priority: -1}); - expect(filter.priority).toEqual(undefined); - - filter = new Filter({priority: "anything else"}); - expect(filter.priority).toEqual(undefined); + expect(validPriority(50)).toEqual(true); + expect(validPriority(1)).toEqual(true); + expect(validPriority(100)).toEqual(true); + expect(validPriority(-1)).toEqual(false); + expect(validPriority("anything else")).toEqual(false); }); test("validates key", () => { - let filter = new Filter({key: "a key"}); - expect(filter.key).toEqual("a key"); + expect(validKey("a key")).toEqual(true); + expect(validKey(1)).toEqual(false); + }); - filter = new Filter({key: 1}); - expect(filter.key).toEqual(undefined); + test("validates url", () => { + expect(validUrl("http://example.com/")).toEqual(true); + expect(validUrl("https://example.com/")).toEqual(true); + expect(validUrl("ftp://example.com/")).toEqual(false); + expect(validUrl("anything else")).toEqual(false); }); }); @@ -128,4 +122,10 @@ describe("media class - file url", () => { let media = new Media({name: "file.mp3", url: "https://example.com"}); expect(media.url).toEqual("https://example.com/file.mp3"); }); + + test("ignores invalid urls", () => { + expect(() => { + new Media({name: "file.mp3"}, {url: "ftp://example.com/"}); + }).toThrowError(/Music/); + }); }); From 0c04da66c03505257e17de7f8beb4ff21aa4cbf2 Mon Sep 17 00:00:00 2001 From: Eric Oestrich Date: Sat, 11 Apr 2020 00:32:48 -0400 Subject: [PATCH 4/4] Validate the default URL --- apps/grapevine/assets/js/play/redux/mediaReducer.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/grapevine/assets/js/play/redux/mediaReducer.js b/apps/grapevine/assets/js/play/redux/mediaReducer.js index 27d64a2f..350715f7 100644 --- a/apps/grapevine/assets/js/play/redux/mediaReducer.js +++ b/apps/grapevine/assets/js/play/redux/mediaReducer.js @@ -193,7 +193,13 @@ export const mediaReceiveGMCP = (state, action) => { switch (action.message) { case "Client.Media.Default": - return {...state, defaults: action.data}; + let defaults = {}; + + if (validUrl(action.data.url)) { + defaults.url = action.data.url; + } + + return {...state, defaults: defaults}; case "Client.Media.Play": console.log("Want to play music...");