2021-12-16 16:01:24 +00:00
|
|
|
import React, { useEffect, useState } from 'react';
|
2021-12-15 11:12:19 +00:00
|
|
|
import { Trans, useTranslation } from 'react-i18next';
|
2021-12-16 16:01:24 +00:00
|
|
|
import { useDispatch, useSelector } from 'react-redux';
|
2021-12-15 11:12:19 +00:00
|
|
|
import { toast } from 'react-toastify';
|
2021-12-16 16:01:24 +00:00
|
|
|
import { ExternalLinkIcon } from '@radix-ui/react-icons';
|
2021-12-15 11:12:19 +00:00
|
|
|
import { useModule, useStatus } from '../../lib/react-utils';
|
|
|
|
import Stulbe from '../../lib/stulbe-lib';
|
|
|
|
import apiReducer, { modules } from '../../store/api/reducer';
|
|
|
|
import SaveButton from '../components/utils/SaveButton';
|
|
|
|
import {
|
|
|
|
Button,
|
|
|
|
ButtonGroup,
|
|
|
|
Field,
|
|
|
|
InputBox,
|
|
|
|
Label,
|
|
|
|
PageContainer,
|
|
|
|
PageHeader,
|
|
|
|
PageTitle,
|
2021-12-16 16:01:24 +00:00
|
|
|
SectionHeader,
|
|
|
|
styled,
|
|
|
|
TabButton,
|
2021-12-18 02:25:00 +00:00
|
|
|
TabContainer,
|
2021-12-16 16:01:24 +00:00
|
|
|
TabContent,
|
|
|
|
TabList,
|
2021-12-18 02:25:00 +00:00
|
|
|
TextBlock,
|
2021-12-15 11:12:19 +00:00
|
|
|
} from '../theme';
|
2021-12-16 16:01:24 +00:00
|
|
|
import eventsubTests from '../../data/eventsub-tests';
|
|
|
|
import { RootState } from '../../store';
|
2021-12-15 11:12:19 +00:00
|
|
|
|
2021-12-16 16:01:24 +00:00
|
|
|
interface UserData {
|
|
|
|
id: string;
|
|
|
|
login: string;
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
display_name: string;
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
profile_image_url: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface SyncError {
|
|
|
|
ok: false;
|
|
|
|
error: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
const eventSubTestFn = {
|
|
|
|
'channel.update': (send) => {
|
|
|
|
send(eventsubTests['channel.update']);
|
|
|
|
},
|
|
|
|
'channel.follow': (send) => {
|
|
|
|
send(eventsubTests['channel.follow']);
|
|
|
|
},
|
|
|
|
'channel.subscribe': (send) => {
|
|
|
|
send(eventsubTests['channel.subscribe']);
|
|
|
|
},
|
|
|
|
'channel.subscription.gift': (send) => {
|
|
|
|
send(eventsubTests['channel.subscription.gift']);
|
|
|
|
setTimeout(() => {
|
|
|
|
send(eventsubTests['channel.subscribe']);
|
|
|
|
}, 2000);
|
|
|
|
},
|
|
|
|
'channel.subscription.message': (send) => {
|
|
|
|
send(eventsubTests['channel.subscribe']);
|
|
|
|
setTimeout(() => {
|
|
|
|
send(eventsubTests['channel.subscription.message']);
|
|
|
|
}, 2000);
|
|
|
|
},
|
|
|
|
'channel.cheer': (send) => {
|
|
|
|
send(eventsubTests['channel.cheer']);
|
|
|
|
},
|
|
|
|
'channel.raid': (send) => {
|
|
|
|
send(eventsubTests['channel.raid']);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const TwitchUser = styled('div', {
|
|
|
|
display: 'flex',
|
|
|
|
gap: '0.8rem',
|
|
|
|
alignItems: 'center',
|
|
|
|
fontSize: '14pt',
|
|
|
|
fontWeight: '300',
|
|
|
|
});
|
|
|
|
const TwitchPic = styled('img', {
|
|
|
|
width: '48px',
|
|
|
|
borderRadius: '50%',
|
|
|
|
});
|
|
|
|
const TwitchName = styled('p', { fontWeight: 'bold' });
|
|
|
|
|
|
|
|
function WebhookIntegration() {
|
|
|
|
const { t } = useTranslation();
|
|
|
|
const [stulbeConfig] = useModule(modules.stulbeConfig);
|
|
|
|
const kv = useSelector((state: RootState) => state.api.client);
|
|
|
|
const [userStatus, setUserStatus] = useState<UserData | SyncError>(null);
|
|
|
|
const [client, setClient] = useState<Stulbe>(null);
|
|
|
|
|
|
|
|
const getUserInfo = async () => {
|
|
|
|
try {
|
|
|
|
const res = (await client.makeRequest(
|
|
|
|
'GET',
|
|
|
|
'api/twitch/user',
|
|
|
|
)) as UserData;
|
|
|
|
setUserStatus(res);
|
|
|
|
} catch (e) {
|
|
|
|
setUserStatus({ ok: false, error: e.message });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const startAuthFlow = async () => {
|
|
|
|
const res = (await client.makeRequest('POST', 'api/twitch/authorize')) as {
|
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
auth_url: string;
|
|
|
|
};
|
|
|
|
const win = window.open(
|
|
|
|
res.auth_url,
|
|
|
|
'_blank',
|
|
|
|
'height=800,width=520,scrollbars=yes,status=yes',
|
|
|
|
);
|
|
|
|
// Hack, have to poll because no events are reliable for this
|
|
|
|
const iv = setInterval(() => {
|
|
|
|
if (win.closed) {
|
|
|
|
clearInterval(iv);
|
|
|
|
setUserStatus(null);
|
|
|
|
getUserInfo();
|
|
|
|
}
|
|
|
|
}, 1000);
|
|
|
|
};
|
|
|
|
|
|
|
|
const sendFakeEvent = async (event: keyof typeof eventSubTestFn) => {
|
|
|
|
eventSubTestFn[event]((data) => {
|
|
|
|
kv.putJSON('stulbe/ev/webhook', {
|
|
|
|
...data,
|
|
|
|
subscription: {
|
|
|
|
...data.subscription,
|
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// eslint-disable-next-line consistent-return
|
|
|
|
useEffect(() => {
|
|
|
|
if (client) {
|
|
|
|
// Get user info
|
|
|
|
getUserInfo();
|
|
|
|
} else if (
|
|
|
|
stulbeConfig &&
|
|
|
|
stulbeConfig.enabled &&
|
|
|
|
stulbeConfig.endpoint &&
|
|
|
|
stulbeConfig.auth_key &&
|
|
|
|
stulbeConfig.username
|
|
|
|
) {
|
|
|
|
const tryAuth = async () => {
|
|
|
|
// Try authenticating
|
|
|
|
const stulbeClient = new Stulbe(stulbeConfig.endpoint);
|
|
|
|
await stulbeClient.auth(stulbeConfig.username, stulbeConfig.auth_key);
|
|
|
|
setClient(stulbeClient);
|
|
|
|
};
|
|
|
|
tryAuth();
|
|
|
|
}
|
|
|
|
}, [stulbeConfig, client]);
|
|
|
|
|
|
|
|
if (!stulbeConfig || !stulbeConfig.enabled) {
|
|
|
|
return <h1>{t('pages.stulbe.err-not-enabled')}</h1>;
|
|
|
|
}
|
|
|
|
|
|
|
|
let userBlock = <i>{t('pages.stulbe.loading-data')}</i>;
|
|
|
|
if (userStatus !== null) {
|
|
|
|
if ('id' in userStatus) {
|
|
|
|
userBlock = (
|
|
|
|
<>
|
|
|
|
<TwitchUser>
|
|
|
|
<p>{t('pages.stulbe.authenticated-as')}</p>
|
|
|
|
<TwitchPic
|
|
|
|
src={userStatus.profile_image_url}
|
|
|
|
alt={t('pages.stulbe.profile-picture')}
|
|
|
|
/>
|
|
|
|
<TwitchName>{userStatus.display_name}</TwitchName>
|
|
|
|
</TwitchUser>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
userBlock = t('pages.stulbe.err-no-user');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<p>{t('pages.stulbe.auth-message')}</p>
|
2022-01-07 08:41:53 +00:00
|
|
|
<Button variation="primary" onClick={startAuthFlow} disabled={!client}>
|
2021-12-16 16:01:24 +00:00
|
|
|
<ExternalLinkIcon /> {t('pages.stulbe.auth-button')}
|
|
|
|
</Button>
|
|
|
|
<SectionHeader>{t('pages.stulbe.current-status')}</SectionHeader>
|
|
|
|
{userBlock}
|
|
|
|
<SectionHeader>{t('pages.stulbe.sim-events')}</SectionHeader>
|
|
|
|
<ButtonGroup>
|
|
|
|
{Object.keys(eventSubTestFn).map((ev: keyof typeof eventsubTests) => (
|
2022-01-07 08:41:53 +00:00
|
|
|
<Button key={ev} onClick={() => sendFakeEvent(ev)}>
|
2021-12-16 16:01:24 +00:00
|
|
|
{t(`pages.stulbe.sim.${ev}`, { defaultValue: ev })}
|
|
|
|
</Button>
|
|
|
|
))}
|
|
|
|
</ButtonGroup>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function BackendConfiguration() {
|
2021-12-15 11:12:19 +00:00
|
|
|
const [stulbeConfig, setStulbeConfig, loadStatus] = useModule(
|
|
|
|
modules.stulbeConfig,
|
|
|
|
);
|
|
|
|
const { t } = useTranslation();
|
|
|
|
const dispatch = useDispatch();
|
|
|
|
const status = useStatus(loadStatus.save);
|
|
|
|
const active = stulbeConfig?.enabled ?? false;
|
|
|
|
const busy =
|
|
|
|
loadStatus.load?.type !== 'success' || loadStatus.save?.type === 'pending';
|
|
|
|
|
|
|
|
const test = async () => {
|
|
|
|
try {
|
|
|
|
const client = new Stulbe(stulbeConfig.endpoint);
|
|
|
|
await client.auth(stulbeConfig.username, stulbeConfig.auth_key);
|
|
|
|
toast.success(t('pages.stulbe.test-success'));
|
|
|
|
} catch (e) {
|
|
|
|
toast.error(e.message);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-12-16 16:01:24 +00:00
|
|
|
return (
|
|
|
|
<form
|
|
|
|
onSubmit={(ev) => {
|
|
|
|
dispatch(setStulbeConfig(stulbeConfig));
|
|
|
|
ev.preventDefault();
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Field size="fullWidth">
|
|
|
|
<Label htmlFor="endpoint">{t('pages.stulbe.endpoint')}</Label>
|
|
|
|
<InputBox
|
|
|
|
type="text"
|
|
|
|
id="endpoint"
|
|
|
|
placeholder={t('pages.stulbe.bind-placeholder')}
|
|
|
|
value={stulbeConfig?.endpoint ?? ''}
|
|
|
|
disabled={busy}
|
|
|
|
onChange={(e) =>
|
|
|
|
dispatch(
|
|
|
|
apiReducer.actions.stulbeConfigChanged({
|
|
|
|
...stulbeConfig,
|
|
|
|
enabled: e.target.value.length > 0,
|
|
|
|
endpoint: e.target.value,
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</Field>
|
|
|
|
<Field size="fullWidth">
|
|
|
|
<Label htmlFor="username">{t('pages.stulbe.username')}</Label>
|
|
|
|
<InputBox
|
|
|
|
type="text"
|
|
|
|
id="username"
|
|
|
|
value={stulbeConfig?.username ?? ''}
|
|
|
|
required={true}
|
|
|
|
disabled={!active || busy}
|
|
|
|
onChange={(e) =>
|
|
|
|
dispatch(
|
|
|
|
apiReducer.actions.stulbeConfigChanged({
|
|
|
|
...stulbeConfig,
|
|
|
|
username: e.target.value,
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</Field>
|
|
|
|
<Field size="fullWidth">
|
|
|
|
<Label htmlFor="password">{t('pages.stulbe.auth-key')}</Label>
|
|
|
|
<InputBox
|
|
|
|
type="password"
|
|
|
|
id="password"
|
|
|
|
value={stulbeConfig?.auth_key ?? ''}
|
|
|
|
disabled={!active || busy}
|
|
|
|
required={true}
|
|
|
|
onChange={(e) =>
|
|
|
|
dispatch(
|
|
|
|
apiReducer.actions.stulbeConfigChanged({
|
|
|
|
...stulbeConfig,
|
|
|
|
auth_key: e.target.value,
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</Field>
|
|
|
|
<ButtonGroup>
|
|
|
|
<SaveButton status={status} />
|
|
|
|
<Button type="button" disabled={!active || busy} onClick={() => test()}>
|
|
|
|
{t('pages.stulbe.test-button')}
|
|
|
|
</Button>
|
|
|
|
</ButtonGroup>
|
|
|
|
</form>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default function BackendIntegrationPage(): React.ReactElement {
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
2021-12-15 11:12:19 +00:00
|
|
|
return (
|
|
|
|
<PageContainer>
|
|
|
|
<PageHeader>
|
|
|
|
<PageTitle>{t('pages.stulbe.title')}</PageTitle>
|
2021-12-18 02:25:00 +00:00
|
|
|
<TextBlock>
|
2021-12-15 11:12:19 +00:00
|
|
|
<Trans i18nKey="pages.stulbe.subtitle">
|
2021-12-16 16:01:24 +00:00
|
|
|
{' '}
|
|
|
|
<a href="https://github.com/strimertul/stulbe/">stulbe</a>
|
2021-12-15 11:12:19 +00:00
|
|
|
</Trans>
|
2021-12-18 02:25:00 +00:00
|
|
|
</TextBlock>
|
2021-12-15 11:12:19 +00:00
|
|
|
</PageHeader>
|
2021-12-16 16:01:24 +00:00
|
|
|
|
2021-12-18 02:25:00 +00:00
|
|
|
<TabContainer defaultValue="configuration">
|
2021-12-16 16:01:24 +00:00
|
|
|
<TabList>
|
|
|
|
<TabButton value="configuration">
|
|
|
|
{t('pages.stulbe.configuration')}
|
|
|
|
</TabButton>
|
|
|
|
<TabButton value="webhook">
|
|
|
|
{t('pages.stulbe.twitch-events')}
|
|
|
|
</TabButton>
|
|
|
|
</TabList>
|
|
|
|
<TabContent value="configuration">
|
|
|
|
<BackendConfiguration />
|
|
|
|
</TabContent>
|
|
|
|
<TabContent value="webhook">
|
|
|
|
<WebhookIntegration />
|
|
|
|
</TabContent>
|
2021-12-18 02:25:00 +00:00
|
|
|
</TabContainer>
|
2021-12-15 11:12:19 +00:00
|
|
|
</PageContainer>
|
|
|
|
);
|
|
|
|
}
|