diff --git a/packages/components/src/api/ApiConfig.ts b/packages/components/src/api/ApiConfig.ts index 7599270b08..51698d3c13 100644 --- a/packages/components/src/api/ApiConfig.ts +++ b/packages/components/src/api/ApiConfig.ts @@ -20,6 +20,7 @@ enum Domain { JungleHorse, Vocodes, FakeYou, + Storyteller, Unknown, } @@ -47,10 +48,14 @@ class ApiConfig { domain = Domain.Vocodes; } else if (document.location.host.includes("fakeyou.com")) { domain = Domain.FakeYou; + } else if (document.location.host.includes("storyteller.io")) { + domain = Domain.Storyteller; } let v2ApiHost = "api.fakeyou.com"; - if (!useSsl && (domain === Domain.Localhost || domain === Domain.JungleHorse)) { + if (domain === Domain.Storyteller) { + v2ApiHost = "api.storyteller.io"; + } else if (!useSsl && (domain === Domain.Localhost || domain === Domain.JungleHorse)) { // NB: Lack of SSL means use local development. v2ApiHost = "api.jungle.horse"; } @@ -342,6 +347,24 @@ class ApiConfig { return `${this.getScheme()}://${this.getNewApiHost()}/detect_locale`; } + // =============== Storyteller-specific =============== + + listTwitchEventRules() : string { + return `${this.getScheme()}://${this.getNewApiHost()}/twitch/event_rule/list`; + } + + createTwitchEventRule() : string { + return `${this.getScheme()}://${this.getNewApiHost()}/twitch/event_rule/create`; + } + + deleteTwitchEventRule(eventRuleToken: string) : string { + return `${this.getScheme()}://${this.getNewApiHost()}/twitch/event_rule/delete/${eventRuleToken}`; + } + + editTwitchEventRule(eventRuleToken: string) : string { + return `${this.getScheme()}://${this.getNewApiHost()}/twitch/event_rule/update/${eventRuleToken}`; + } + obsEventsWebsocket(twitchUsername: string) : string { return `ws://localhost:54321/obs/${twitchUsername}`; //return `wss://obs.storyteller.io/obs/${twitchUsername}`; diff --git a/packages/components/src/api/storyteller/twitch_event_rules/CreateTwitchEventRule.ts b/packages/components/src/api/storyteller/twitch_event_rules/CreateTwitchEventRule.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/components/src/api/storyteller/twitch_event_rules/DeleteTwitchEventRule.ts b/packages/components/src/api/storyteller/twitch_event_rules/DeleteTwitchEventRule.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/components/src/api/storyteller/twitch_event_rules/EditEventRule.ts b/packages/components/src/api/storyteller/twitch_event_rules/EditEventRule.ts new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/packages/components/src/api/storyteller/twitch_event_rules/EditEventRule.ts @@ -0,0 +1 @@ + diff --git a/packages/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules.ts b/packages/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules.ts new file mode 100644 index 0000000000..28a0f2e71d --- /dev/null +++ b/packages/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules.ts @@ -0,0 +1,62 @@ +import { ApiConfig } from "../../ApiConfig"; +import { EventMatchPredicate } from "./shared/EventMatchPredicate"; +import { EventResponse } from "./shared/EventResponse"; +import { TwitchEventCategory } from "./shared/TwitchEventCategory"; + +export interface ListTwitchEventRulesSuccessResponse { + success: boolean, + twitch_event_rules: Array +} + +export interface TwitchEventRule{ + token: string, + event_category: TwitchEventCategory, + event_match_predicate: EventMatchPredicate, + event_response: EventResponse, + user_specified_rule_order: number, + rule_is_disabled: boolean, + created_at: Date, + updated_at: Date, +} + +export interface ListTwitchEventRulesErrorResponse { + success: boolean, +} + +type ListTwitchEventRulesResponse = ListTwitchEventRulesSuccessResponse | ListTwitchEventRulesErrorResponse; + +export function ListTwitchEventRulesIsOk(response: ListTwitchEventRulesResponse): response is ListTwitchEventRulesSuccessResponse { + return response?.success === true; +} + +export function ListTwitchEventRulesIsError(response: ListTwitchEventRulesResponse): response is ListTwitchEventRulesErrorResponse { + return response?.success === false; +} + +export async function ListTwitchEventRules() : Promise +{ + const endpoint = new ApiConfig().listTwitchEventRules(); + + return await fetch(endpoint, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + credentials: 'include', + }) + .then(res => res.json()) + .then(res => { + if (!res) { + return { success : false }; // TODO: This loses error semantics and is deprecated + } + + if (res && 'success' in res) { + return res; + } else { + return { success : false }; + } + }) + .catch(e => { + return { success : false }; + }); +} diff --git a/packages/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate.ts b/packages/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate.ts new file mode 100644 index 0000000000..9046d08f25 --- /dev/null +++ b/packages/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate.ts @@ -0,0 +1,34 @@ + +export interface NotSet { +}; + +export interface BitsCheermoteNameExactMatch { + cheermote_name: string, +}; + +export interface BitsCheermotePrefixSpendThreshold { + cheermote_name_prefix: string, + minimum_bits_spent: number, +}; + +export interface BitsSpendThreshold { + minimum_bits_spent: number, +} + +export interface ChannelPointsRewardNameExactMatch { + reward_name: string, +} + +// The predicate will have one field set exclusively. +export interface EventMatchPredicate { + // Empty + not_set?: NotSet, + + // Bits + bits_cheermote_name_exact_match?: BitsCheermoteNameExactMatch, + bits_spend_threshold?: BitsSpendThreshold, + bits_cheermote_prefix_spend_threshold?: BitsCheermotePrefixSpendThreshold, + + // Channel Points + channel_points_reward_name_exact_match?: ChannelPointsRewardNameExactMatch, +} \ No newline at end of file diff --git a/packages/components/src/api/storyteller/twitch_event_rules/shared/EventResponse.ts b/packages/components/src/api/storyteller/twitch_event_rules/shared/EventResponse.ts new file mode 100644 index 0000000000..ac26158ed8 --- /dev/null +++ b/packages/components/src/api/storyteller/twitch_event_rules/shared/EventResponse.ts @@ -0,0 +1,22 @@ + +export interface NotSet { +}; + +export interface TtsSingleVoice { + tts_model_token: string, +}; + +export interface TtsRandomVoice { + tts_model_tokens: string[], +}; + + +// The response will have one field set exclusively. +export interface EventResponse { + // Empty + not_set?: NotSet, + + // Respond with TTS + tts_single_voice?: TtsSingleVoice, + tts_random_voice?: TtsRandomVoice, +} \ No newline at end of file diff --git a/packages/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory.ts b/packages/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory.ts new file mode 100644 index 0000000000..36ef9efdb4 --- /dev/null +++ b/packages/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory.ts @@ -0,0 +1,6 @@ + +export enum TwitchEventCategory { + Bits = "bits", + ChannelPoints = "channel_points", + ChatCommand = "chat_command", +}; diff --git a/packages/storyteller/src/App.tsx b/packages/storyteller/src/App.tsx index a16dd0fbe4..9f881d44c3 100644 --- a/packages/storyteller/src/App.tsx +++ b/packages/storyteller/src/App.tsx @@ -8,6 +8,7 @@ import i18n from 'i18next'; import { ComingSoonPage } from './pages/coming-soon/ComingSoonPage'; import { LoginPage } from './pages/login/LoginPage'; import { SignupPage } from './pages/signup/SignupPage'; +import { TtsConfigsIndexPage } from './pages/tts_configs/TtsConfigsIndexPage'; import { DetectLocale, DetectLocaleIsOk } from '@storyteller/components/src/api/locale/DetectLocale'; import { IndexPage } from './pages/index/IndexPage'; import { ObsLayerPage } from './pages/obs-layer/ObsLayerPage'; @@ -15,6 +16,7 @@ import { STORYTELLER_MERGED_TRANSLATIONS } from './_i18n/StorytellerTranslations import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper'; import { TopNav } from './layout/TopNav'; import { initReactI18next } from 'react-i18next'; +import { TtsConfigsCreateRulePage } from './pages/tts_configs/TtsConfigsCreateRulePage'; i18n .use(initReactI18next) // passes i18n down to react-i18next @@ -97,6 +99,16 @@ class App extends React.Component { querySessionAction={this.querySession} /> + + + + + + diff --git a/packages/storyteller/src/layout/TopNav.tsx b/packages/storyteller/src/layout/TopNav.tsx index 28b9450b33..f318b690da 100644 --- a/packages/storyteller/src/layout/TopNav.tsx +++ b/packages/storyteller/src/layout/TopNav.tsx @@ -45,7 +45,12 @@ function TopNav(props: Props) { closeHamburger()} - >Stream TTS + >Stream TTS (OLD) + + closeHamburger()} + >TTS Configs (NEW) +

TTS : Create Rule

+ + ) +} + +export { TtsConfigsCreateRulePage } \ No newline at end of file diff --git a/packages/storyteller/src/pages/tts_configs/TtsConfigsIndexPage.tsx b/packages/storyteller/src/pages/tts_configs/TtsConfigsIndexPage.tsx new file mode 100644 index 0000000000..8eb8ded230 --- /dev/null +++ b/packages/storyteller/src/pages/tts_configs/TtsConfigsIndexPage.tsx @@ -0,0 +1,37 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper'; +import { ListTwitchEventRules, ListTwitchEventRulesIsError, ListTwitchEventRulesIsOk, TwitchEventRule } from '@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules'; + +interface Props { + sessionWrapper: SessionWrapper, +} + +function TtsConfigsIndexPage(props: Props) { + const [twitchEventRules, setTwitchEventRules] = useState([]); + + const listTwitchEventRules = useCallback(async () => { + const response = await ListTwitchEventRules(); + + if (ListTwitchEventRulesIsOk(response)) { + setTwitchEventRules(response.twitch_event_rules); + } else if (ListTwitchEventRulesIsError(response)) { + // TODO + } + }, []); + + useEffect(() => { + listTwitchEventRules(); + }, [listTwitchEventRules]); + + if (!props.sessionWrapper.isLoggedIn()) { + return

Must Log In

; + } + + return ( +
+

TTS Setup

+
+ ) +} + +export { TtsConfigsIndexPage } \ No newline at end of file