mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-30 02:40:33 +00:00
Compare commits
No commits in common. "07e3a00990ccfee1854cc7a0c312088fac130794" and "bc83a743f3dc6da9e56f97bdb6adb4c739f4cb6c" have entirely different histories.
07e3a00990
...
bc83a743f3
17 changed files with 219 additions and 261 deletions
24
app.go
24
app.go
|
@ -241,26 +241,12 @@ func (a *App) GetKilovoltBind() string {
|
||||||
return a.httpServer.Config.Get().Bind
|
return a.httpServer.Config.Get().Bind
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetTwitchAuthURL(state string) string {
|
func (a *App) GetTwitchAuthURL() string {
|
||||||
return twitch.GetAuthorizationURL(a.twitchManager.Client().API, state)
|
return twitch.GetAuthorizationURL(a.twitchManager.Client().API)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetTwitchLoggedUser(key string) (helix.User, error) {
|
func (a *App) GetTwitchLoggedUser() (helix.User, error) {
|
||||||
userClient, err := twitch.GetUserClient(a.db, key, false)
|
return a.twitchManager.Client().GetLoggedUser()
|
||||||
if err != nil {
|
|
||||||
return helix.User{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
users, err := userClient.GetUsers(&helix.UsersParams{})
|
|
||||||
if err != nil {
|
|
||||||
return helix.User{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(users.Data.Users) < 1 {
|
|
||||||
return helix.User{}, errors.New("no users found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return users.Data.Users[0], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetLastLogs() []log.Entry {
|
func (a *App) GetLastLogs() []log.Entry {
|
||||||
|
@ -355,7 +341,7 @@ func (a *App) interactiveAuth(client kv.Client, message map[string]any) bool {
|
||||||
|
|
||||||
func (a *App) showFatalError(err error, text string, fields ...any) {
|
func (a *App) showFatalError(err error, text string, fields ...any) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fields = append(fields, log.ErrorSkip(err, 2), slog.String("Z", string(debug.Stack())))
|
fields = append(fields, log.Error(err), slog.String("Z", string(debug.Stack())))
|
||||||
slog.Error(text, fields...)
|
slog.Error(text, fields...)
|
||||||
runtime.EventsEmit(a.ctx, "fatalError")
|
runtime.EventsEmit(a.ctx, "fatalError")
|
||||||
a.isFatalError.Set(true)
|
a.isFatalError.Set(true)
|
||||||
|
|
|
@ -162,6 +162,7 @@ func TestLocalDBClientGetJSON(t *testing.T) {
|
||||||
A string
|
A string
|
||||||
B int
|
B int
|
||||||
}
|
}
|
||||||
|
|
||||||
testStruct := test{
|
testStruct := test{
|
||||||
A: "test",
|
A: "test",
|
||||||
B: 42,
|
B: 42,
|
||||||
|
|
|
@ -142,20 +142,18 @@ const supportedMessages: EventSubNotificationType[] = [
|
||||||
EventSubNotificationType.SubscriptionGifted,
|
EventSubNotificationType.SubscriptionGifted,
|
||||||
];
|
];
|
||||||
|
|
||||||
const eventSubKeyFunction = (ev: EventSubNotification) =>
|
|
||||||
`${ev.subscription.type}-${ev.subscription.created_at}-${JSON.stringify(
|
|
||||||
ev.event,
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
function TwitchEvent({ data }: { data: EventSubNotification }) {
|
function TwitchEvent({ data }: { data: EventSubNotification }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useAppSelector((state) => state.api.client);
|
const client = useAppSelector((state) => state.api.client);
|
||||||
|
|
||||||
const replay = () => {
|
const replay = () => {
|
||||||
void client.putJSON(
|
void client.putJSON(`twitch/ev/eventsub-event/${data.subscription.type}`, {
|
||||||
`twitch/ev/eventsub-event/${data.subscription.type}`,
|
...data,
|
||||||
data,
|
subscription: {
|
||||||
);
|
...data.subscription,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let content: JSX.Element | string;
|
let content: JSX.Element | string;
|
||||||
|
@ -392,7 +390,10 @@ function TwitchEventLog({ events }: { events: EventSubNotification[] }) {
|
||||||
Date.parse(a.subscription.created_at),
|
Date.parse(a.subscription.created_at),
|
||||||
)
|
)
|
||||||
.map((ev) => (
|
.map((ev) => (
|
||||||
<TwitchEvent key={eventSubKeyFunction(ev)} data={ev} />
|
<TwitchEvent
|
||||||
|
key={`${ev.subscription.id}-${ev.subscription.created_at}`}
|
||||||
|
data={ev}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</EventListContainer>
|
</EventListContainer>
|
||||||
</Scrollbar>
|
</Scrollbar>
|
||||||
|
@ -440,29 +441,27 @@ function TwitchSection() {
|
||||||
const kv = useAppSelector((state) => state.api.client);
|
const kv = useAppSelector((state) => state.api.client);
|
||||||
const [twitchEvents, setTwitchEvents] = useState<EventSubNotification[]>([]);
|
const [twitchEvents, setTwitchEvents] = useState<EventSubNotification[]>([]);
|
||||||
|
|
||||||
const keyfn = (ev: EventSubNotification) => JSON.stringify(ev);
|
const keyfn = (ev: EventSubNotification) =>
|
||||||
|
ev.subscription.id + ev.subscription.created_at;
|
||||||
|
|
||||||
const addTwitchEvents = (events: EventSubNotification[]) => {
|
const setCleanTwitchEvents = (events: EventSubNotification[]) => {
|
||||||
setTwitchEvents((currentEvents) => {
|
const eventKeys = events.map(keyfn);
|
||||||
const allEvents = currentEvents.concat(events);
|
|
||||||
const eventKeys = allEvents.map(keyfn);
|
|
||||||
|
|
||||||
// Clean up duplicates before setting to state
|
// Clean up duplicates before setting to state
|
||||||
const updatedEvents = allEvents.filter(
|
const uniqueEvents = events.filter(
|
||||||
(ev, pos) => eventKeys.indexOf(keyfn(ev)) === pos,
|
(ev, pos) => eventKeys.indexOf(keyfn(ev)) === pos,
|
||||||
);
|
);
|
||||||
|
setTwitchEvents(uniqueEvents);
|
||||||
return updatedEvents;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadRecentEvents = async () => {
|
const loadRecentEvents = async () => {
|
||||||
const keymap = await kv.getKeysByPrefix('twitch/eventsub-history/');
|
const keymap = await kv.getKeysByPrefix('twitch/eventsub-history/');
|
||||||
const events = Object.values(keymap)
|
const events = Object.values(keymap)
|
||||||
.map((value) => JSON.parse(value) as EventSubNotification[])
|
.map((value) => JSON.parse(value) as EventSubNotification[])
|
||||||
.flat();
|
.flat()
|
||||||
|
.sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
||||||
|
|
||||||
addTwitchEvents(events);
|
setCleanTwitchEvents(events);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -470,10 +469,7 @@ function TwitchSection() {
|
||||||
|
|
||||||
const onKeyChange = (value: string) => {
|
const onKeyChange = (value: string) => {
|
||||||
const event = JSON.parse(value) as EventSubNotification;
|
const event = JSON.parse(value) as EventSubNotification;
|
||||||
if (!supportedMessages.includes(event.subscription.type)) {
|
void setCleanTwitchEvents([event, ...twitchEvents]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
void addTwitchEvents([event]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void kv.subscribePrefix('twitch/ev/eventsub-event/', onKeyChange);
|
void kv.subscribePrefix('twitch/ev/eventsub-event/', onKeyChange);
|
||||||
|
@ -543,7 +539,7 @@ function ProblemList() {
|
||||||
};
|
};
|
||||||
void kv.subscribeKey('twitch/auth-keys', onKeyChange);
|
void kv.subscribeKey('twitch/auth-keys', onKeyChange);
|
||||||
|
|
||||||
const url = await GetTwitchAuthURL('stream');
|
const url = await GetTwitchAuthURL();
|
||||||
BrowserOpenURL(url);
|
BrowserOpenURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -450,13 +450,15 @@ function TwitchEventsStep() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [userStatus, setUserStatus] = useState<helix.User | SyncError>(null);
|
const [userStatus, setUserStatus] = useState<helix.User | SyncError>(null);
|
||||||
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||||
|
const [botConfig, setBotConfig] = useModule(modules.twitchChatConfig);
|
||||||
const [uiConfig, setUiConfig] = useModule(modules.uiConfig);
|
const [uiConfig, setUiConfig] = useModule(modules.uiConfig);
|
||||||
|
const [authKeys, setAuthKeys] = useState<TwitchCredentials>(null);
|
||||||
const kv = useSelector((state: RootState) => state.api.client);
|
const kv = useSelector((state: RootState) => state.api.client);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const getUserInfo = async () => {
|
const getUserInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await GetTwitchLoggedUser('twitch/auth-keys');
|
const res = await GetTwitchLoggedUser();
|
||||||
setUserStatus(res);
|
setUserStatus(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -465,7 +467,7 @@ function TwitchEventsStep() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const startAuthFlow = async () => {
|
const startAuthFlow = async () => {
|
||||||
const url = await GetTwitchAuthURL('stream');
|
const url = await GetTwitchAuthURL();
|
||||||
BrowserOpenURL(url);
|
BrowserOpenURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -475,6 +477,16 @@ function TwitchEventsStep() {
|
||||||
await dispatch(
|
await dispatch(
|
||||||
setTwitchConfig({
|
setTwitchConfig({
|
||||||
...twitchConfig,
|
...twitchConfig,
|
||||||
|
enable_bot: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await dispatch(
|
||||||
|
setBotConfig({
|
||||||
|
...botConfig,
|
||||||
|
username: userStatus.login,
|
||||||
|
oauth: `oauth:${authKeys.access_token}`,
|
||||||
|
channel: userStatus.login,
|
||||||
|
chat_history: 5,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -491,10 +503,17 @@ function TwitchEventsStep() {
|
||||||
// Get user info
|
// Get user info
|
||||||
void getUserInfo();
|
void getUserInfo();
|
||||||
|
|
||||||
const onKeyChange = () => {
|
const onKeyChange = (newValue: string) => {
|
||||||
|
setAuthKeys(JSON.parse(newValue) as TwitchCredentials);
|
||||||
void getUserInfo();
|
void getUserInfo();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void kv.getKey('twitch/auth-keys').then((auth) => {
|
||||||
|
if (auth) {
|
||||||
|
setAuthKeys(JSON.parse(auth) as TwitchCredentials);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
void kv.subscribeKey('twitch/auth-keys', onKeyChange);
|
void kv.subscribeKey('twitch/auth-keys', onKeyChange);
|
||||||
return () => {
|
return () => {
|
||||||
void kv.unsubscribeKey('twitch/auth-keys', onKeyChange);
|
void kv.unsubscribeKey('twitch/auth-keys', onKeyChange);
|
||||||
|
|
|
@ -53,6 +53,54 @@ const Step = styled('li', {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function TwitchChatSettings() {
|
||||||
|
const [chatConfig, setChatConfig, loadStatus] = useModule(
|
||||||
|
modules.twitchChatConfig,
|
||||||
|
);
|
||||||
|
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||||
|
const status = useStatus(loadStatus.save);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const disabled = status?.type === 'pending';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={(ev) => {
|
||||||
|
void dispatch(setTwitchConfig(twitchConfig));
|
||||||
|
void dispatch(setChatConfig(chatConfig));
|
||||||
|
ev.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SectionHeader>
|
||||||
|
{t('pages.twitch-settings.bot-chat-header')}
|
||||||
|
</SectionHeader>
|
||||||
|
<Field size="fullWidth">
|
||||||
|
<Label htmlFor="bot-chat-history">
|
||||||
|
{t('pages.twitch-settings.bot-chat-cooldown-tip')}
|
||||||
|
</Label>
|
||||||
|
<InputBox
|
||||||
|
type="number"
|
||||||
|
id="bot-chat-history"
|
||||||
|
required={true}
|
||||||
|
disabled={disabled}
|
||||||
|
defaultValue={
|
||||||
|
chatConfig ? chatConfig.command_cooldown ?? 2 : undefined
|
||||||
|
}
|
||||||
|
onChange={(ev) =>
|
||||||
|
dispatch(
|
||||||
|
apiReducer.actions.twitchChatConfigChanged({
|
||||||
|
...chatConfig,
|
||||||
|
command_cooldown: parseInt(ev.target.value, 10),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<SaveButton status={status} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type TestResult = { open: boolean; error?: Error };
|
type TestResult = { open: boolean; error?: Error };
|
||||||
|
|
||||||
function TwitchAPISettings() {
|
function TwitchAPISettings() {
|
||||||
|
@ -227,63 +275,25 @@ const TwitchPic = styled('img', {
|
||||||
});
|
});
|
||||||
const TwitchName = styled('p', { fontWeight: 'bold' });
|
const TwitchName = styled('p', { fontWeight: 'bold' });
|
||||||
|
|
||||||
async function startAuthFlow(target: string) {
|
function TwitchEventSubSettings() {
|
||||||
const url = await GetTwitchAuthURL(target);
|
|
||||||
BrowserOpenURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TwitchUserBlock({ authKey }: { authKey: string }) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [user, setUser] = useState<helix.User | SyncError>(null);
|
const [userStatus, setUserStatus] = useState<helix.User | SyncError>(null);
|
||||||
const kv = useAppSelector((state) => state.api.client);
|
const kv = useAppSelector((state) => state.api.client);
|
||||||
|
|
||||||
const getUserInfo = async () => {
|
const getUserInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await GetTwitchLoggedUser(authKey);
|
const res = await GetTwitchLoggedUser();
|
||||||
setUser(res);
|
setUserStatus(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setUser({ ok: false, error: (e as Error).message });
|
setUserStatus({ ok: false, error: (e as Error).message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const startAuthFlow = async () => {
|
||||||
// Get user info
|
const url = await GetTwitchAuthURL();
|
||||||
void getUserInfo();
|
BrowserOpenURL(url);
|
||||||
|
};
|
||||||
const onKeyChange = () => {
|
|
||||||
void getUserInfo();
|
|
||||||
};
|
|
||||||
void kv.subscribeKey(authKey, onKeyChange);
|
|
||||||
return () => {
|
|
||||||
void kv.unsubscribeKey(authKey, onKeyChange);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (user !== null) {
|
|
||||||
if ('id' in user) {
|
|
||||||
return (
|
|
||||||
<TwitchUser>
|
|
||||||
<TextBlock>
|
|
||||||
{t('pages.twitch-settings.events.authenticated-as')}
|
|
||||||
</TextBlock>
|
|
||||||
<TwitchPic
|
|
||||||
src={user.profile_image_url}
|
|
||||||
alt={t('pages.twitch-settings.events.profile-picture')}
|
|
||||||
/>
|
|
||||||
<TwitchName>{user.display_name}</TwitchName>
|
|
||||||
</TwitchUser>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <span>{t('pages.twitch-settings.events.err-no-user')}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <i>{t('pages.twitch-settings.events.loading-data')}</i>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TwitchEventSubSettings() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const kv = useAppSelector((state) => state.api.client);
|
|
||||||
|
|
||||||
const sendFakeEvent = async (event: keyof typeof eventsubTests) => {
|
const sendFakeEvent = async (event: keyof typeof eventsubTests) => {
|
||||||
const data = eventsubTests[event];
|
const data = eventsubTests[event];
|
||||||
|
@ -297,13 +307,47 @@ function TwitchEventSubSettings() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Get user info
|
||||||
|
void getUserInfo();
|
||||||
|
|
||||||
|
const onKeyChange = () => {
|
||||||
|
void getUserInfo();
|
||||||
|
};
|
||||||
|
void kv.subscribeKey('twitch/auth-keys', onKeyChange);
|
||||||
|
return () => {
|
||||||
|
void kv.unsubscribeKey('twitch/auth-keys', onKeyChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let userBlock = <i>{t('pages.twitch-settings.events.loading-data')}</i>;
|
||||||
|
if (userStatus !== null) {
|
||||||
|
if ('id' in userStatus) {
|
||||||
|
userBlock = (
|
||||||
|
<>
|
||||||
|
<TwitchUser>
|
||||||
|
<TextBlock>
|
||||||
|
{t('pages.twitch-settings.events.authenticated-as')}
|
||||||
|
</TextBlock>
|
||||||
|
<TwitchPic
|
||||||
|
src={userStatus.profile_image_url}
|
||||||
|
alt={t('pages.twitch-settings.events.profile-picture')}
|
||||||
|
/>
|
||||||
|
<TwitchName>{userStatus.display_name}</TwitchName>
|
||||||
|
</TwitchUser>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
userBlock = <span>{t('pages.twitch-settings.events.err-no-user')}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextBlock>{t('pages.twitch-settings.events.auth-message')}</TextBlock>
|
<TextBlock>{t('pages.twitch-settings.events.auth-message')}</TextBlock>
|
||||||
<Button
|
<Button
|
||||||
variation="primary"
|
variation="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void startAuthFlow('stream');
|
void startAuthFlow();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ExternalLinkIcon /> {t('pages.twitch-settings.events.auth-button')}
|
<ExternalLinkIcon /> {t('pages.twitch-settings.events.auth-button')}
|
||||||
|
@ -311,7 +355,7 @@ function TwitchEventSubSettings() {
|
||||||
<SectionHeader>
|
<SectionHeader>
|
||||||
{t('pages.twitch-settings.events.current-status')}
|
{t('pages.twitch-settings.events.current-status')}
|
||||||
</SectionHeader>
|
</SectionHeader>
|
||||||
<TwitchUserBlock authKey={'twitch/auth-keys'} />
|
{userBlock}
|
||||||
<SectionHeader>
|
<SectionHeader>
|
||||||
{t('pages.twitch-settings.events.sim-events')}
|
{t('pages.twitch-settings.events.sim-events')}
|
||||||
</SectionHeader>
|
</SectionHeader>
|
||||||
|
@ -331,54 +375,6 @@ function TwitchEventSubSettings() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TwitchChatSettings() {
|
|
||||||
const [chatConfig, setChatConfig, loadStatus] = useModule(
|
|
||||||
modules.twitchChatConfig,
|
|
||||||
);
|
|
||||||
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
|
||||||
const status = useStatus(loadStatus.save);
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const disabled = status?.type === 'pending';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={(ev) => {
|
|
||||||
void dispatch(setTwitchConfig(twitchConfig));
|
|
||||||
void dispatch(setChatConfig(chatConfig));
|
|
||||||
ev.preventDefault();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SectionHeader>
|
|
||||||
{t('pages.twitch-settings.bot-chat-header')}
|
|
||||||
</SectionHeader>
|
|
||||||
<Field size="fullWidth">
|
|
||||||
<Label htmlFor="bot-chat-history">
|
|
||||||
{t('pages.twitch-settings.bot-chat-cooldown-tip')}
|
|
||||||
</Label>
|
|
||||||
<InputBox
|
|
||||||
type="number"
|
|
||||||
id="bot-chat-history"
|
|
||||||
required={true}
|
|
||||||
disabled={disabled}
|
|
||||||
defaultValue={
|
|
||||||
chatConfig ? chatConfig.command_cooldown ?? 2 : undefined
|
|
||||||
}
|
|
||||||
onChange={(ev) =>
|
|
||||||
dispatch(
|
|
||||||
apiReducer.actions.twitchChatConfigChanged({
|
|
||||||
...chatConfig,
|
|
||||||
command_cooldown: parseInt(ev.target.value, 10),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<SaveButton status={status} />
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TwitchSettingsPage(): React.ReactElement {
|
export default function TwitchSettingsPage(): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||||
|
|
4
frontend/wailsjs/go/main/App.d.ts
vendored
4
frontend/wailsjs/go/main/App.d.ts
vendored
|
@ -19,9 +19,9 @@ export function GetLastLogs():Promise<Array<log.Entry>>;
|
||||||
|
|
||||||
export function GetProblems():Promise<Array<main.Problem>>;
|
export function GetProblems():Promise<Array<main.Problem>>;
|
||||||
|
|
||||||
export function GetTwitchAuthURL(arg1:string):Promise<string>;
|
export function GetTwitchAuthURL():Promise<string>;
|
||||||
|
|
||||||
export function GetTwitchLoggedUser(arg1:string):Promise<helix.User>;
|
export function GetTwitchLoggedUser():Promise<helix.User>;
|
||||||
|
|
||||||
export function IsFatalError():Promise<boolean>;
|
export function IsFatalError():Promise<boolean>;
|
||||||
|
|
||||||
|
|
|
@ -30,12 +30,12 @@ export function GetProblems() {
|
||||||
return window['go']['main']['App']['GetProblems']();
|
return window['go']['main']['App']['GetProblems']();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetTwitchAuthURL(arg1) {
|
export function GetTwitchAuthURL() {
|
||||||
return window['go']['main']['App']['GetTwitchAuthURL'](arg1);
|
return window['go']['main']['App']['GetTwitchAuthURL']();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetTwitchLoggedUser(arg1) {
|
export function GetTwitchLoggedUser() {
|
||||||
return window['go']['main']['App']['GetTwitchLoggedUser'](arg1);
|
return window['go']['main']['App']['GetTwitchLoggedUser']();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IsFatalError() {
|
export function IsFatalError() {
|
||||||
|
|
|
@ -7,11 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Error(err error) slog.Attr {
|
func Error(err error) slog.Attr {
|
||||||
return ErrorSkip(err, 2)
|
pc, filename, line, _ := runtime.Caller(1)
|
||||||
}
|
|
||||||
|
|
||||||
func ErrorSkip(err error, skip int) slog.Attr {
|
|
||||||
pc, filename, line, _ := runtime.Caller(skip)
|
|
||||||
|
|
||||||
return slog.Group("error",
|
return slog.Group("error",
|
||||||
slog.String("message", err.Error()),
|
slog.String("message", err.Error()),
|
||||||
|
|
|
@ -125,9 +125,8 @@ func NewManager(ctx context.Context, db database.Database, twitchManager *twitch
|
||||||
loyalty.SetBanList(config.BanList)
|
loyalty.SetBanList(config.BanList)
|
||||||
|
|
||||||
// Start twitch handler
|
// Start twitch handler
|
||||||
twitchClient := twitchManager.Client()
|
if twitchManager.Client() != nil {
|
||||||
if twitchClient != nil && twitchClient.Chat != nil {
|
loyalty.twitchIntegration = setupTwitchIntegration(loyaltyContext, loyalty, twitchManager.Client().Chat)
|
||||||
loyalty.twitchIntegration = setupTwitchIntegration(loyaltyContext, loyalty, twitchClient.Chat)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return loyalty, nil
|
return loyalty, nil
|
||||||
|
|
|
@ -25,16 +25,16 @@ type AuthResponse struct {
|
||||||
Time time.Time
|
Time time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserClient(db database.Database, keyPath string, forceRefresh bool) (*helix.Client, error) {
|
func GetUserClient(db database.Database, forceRefresh bool) (*helix.Client, error) {
|
||||||
var authResp AuthResponse
|
var authResp AuthResponse
|
||||||
if err := db.GetJSON(keyPath, &authResp); err != nil {
|
if err := db.GetJSON(AuthKey, &authResp); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle token expiration
|
// Handle token expiration
|
||||||
if forceRefresh || time.Now().After(authResp.Time.Add(time.Duration(authResp.ExpiresIn)*time.Second)) {
|
if forceRefresh || time.Now().After(authResp.Time.Add(time.Duration(authResp.ExpiresIn)*time.Second)) {
|
||||||
// Refresh tokens
|
// Refresh tokens
|
||||||
api, err := GetHelixAPI(db, "")
|
api, err := GetHelixAPI(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -49,31 +49,30 @@ func GetUserClient(db database.Database, keyPath string, forceRefresh bool) (*he
|
||||||
authResp.Time = time.Now().Add(time.Duration(refreshed.Data.ExpiresIn) * time.Second)
|
authResp.Time = time.Now().Add(time.Duration(refreshed.Data.ExpiresIn) * time.Second)
|
||||||
|
|
||||||
// Save new token pair
|
// Save new token pair
|
||||||
err = db.PutJSON(keyPath, authResp)
|
err = db.PutJSON(AuthKey, authResp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetHelixAPI(db, authResp.AccessToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetHelixAPI(db database.Database, userToken string) (*helix.Client, error) {
|
|
||||||
config, err := GetConfig(db)
|
config, err := GetConfig(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a user token is provided, create a user client
|
return helix.NewClient(&helix.Options{
|
||||||
if userToken != "" {
|
ClientID: config.APIClientID,
|
||||||
return helix.NewClient(&helix.Options{
|
ClientSecret: config.APIClientSecret,
|
||||||
ClientID: config.APIClientID,
|
UserAccessToken: authResp.AccessToken,
|
||||||
ClientSecret: config.APIClientSecret,
|
})
|
||||||
UserAccessToken: userToken,
|
}
|
||||||
})
|
|
||||||
|
func GetHelixAPI(db database.Database) (*helix.Client, error) {
|
||||||
|
config, err := GetConfig(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no user token is provided, create an app client
|
|
||||||
baseurl, err := baseURL(db)
|
baseurl, err := baseURL(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -4,7 +4,6 @@ const (
|
||||||
ActivityKey = "twitch/chat/activity"
|
ActivityKey = "twitch/chat/activity"
|
||||||
CustomCommandsKey = "twitch/chat/custom-commands"
|
CustomCommandsKey = "twitch/chat/custom-commands"
|
||||||
WriteMessageRPC = "twitch/chat/@send-message"
|
WriteMessageRPC = "twitch/chat/@send-message"
|
||||||
CustomAccountKey = "twitch/chat/chatter-account"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResponseType string
|
type ResponseType string
|
||||||
|
|
|
@ -39,20 +39,11 @@ type Module struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Setup(ctx context.Context, db database.Database, api *helix.Client, user helix.User, logger *slog.Logger, templater template.Engine) *Module {
|
func Setup(ctx context.Context, db database.Database, api *helix.Client, user helix.User, logger *slog.Logger, templater template.Engine) *Module {
|
||||||
customUserClient, customUserInfo, err := GetCustomUser(db)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, database.ErrEmptyKey) {
|
|
||||||
logger.Error("Failed to get custom user, falling back to streamer account", log.Error(err))
|
|
||||||
}
|
|
||||||
customUserClient = api
|
|
||||||
customUserInfo = user
|
|
||||||
}
|
|
||||||
|
|
||||||
mod := &Module{
|
mod := &Module{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
db: db,
|
db: db,
|
||||||
api: customUserClient,
|
api: api,
|
||||||
user: customUserInfo,
|
user: user,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
templater: templater,
|
templater: templater,
|
||||||
lastMessage: sync.NewRWSync(time.Now()),
|
lastMessage: sync.NewRWSync(time.Now()),
|
||||||
|
@ -245,7 +236,7 @@ func (mod *Module) UnregisterCommand(name string) {
|
||||||
func (mod *Module) GetChatters() (users []string) {
|
func (mod *Module) GetChatters() (users []string) {
|
||||||
cursor := ""
|
cursor := ""
|
||||||
for {
|
for {
|
||||||
userClient, err := twitch.GetUserClient(mod.db, twitch.AuthKey, false)
|
userClient, err := twitch.GetUserClient(mod.db, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Could not get user api client for list of chatters", log.Error(err))
|
slog.Error("Could not get user api client for list of chatters", log.Error(err))
|
||||||
return
|
return
|
||||||
|
@ -269,19 +260,3 @@ func (mod *Module) GetChatters() (users []string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCustomUser(db database.Database) (*helix.Client, helix.User, error) {
|
|
||||||
userClient, err := twitch.GetUserClient(db, CustomAccountKey, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, helix.User{}, err
|
|
||||||
}
|
|
||||||
users, err := userClient.GetUsers(&helix.UsersParams{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, helix.User{}, err
|
|
||||||
}
|
|
||||||
if len(users.Data.Users) < 1 {
|
|
||||||
return nil, helix.User{}, errors.New("no users found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return userClient, users.Data.Users[0], nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
|
||||||
|
|
||||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||||
|
|
||||||
"github.com/nicklaw5/helix/v2"
|
"github.com/nicklaw5/helix/v2"
|
||||||
|
@ -17,7 +15,7 @@ func (c *Client) GetLoggedUser() (helix.User, error) {
|
||||||
return c.User, nil
|
return c.User, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := twitch.GetUserClient(c.DB, twitch.AuthKey, false)
|
client, err := twitch.GetUserClient(c.DB, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return helix.User{}, fmt.Errorf("failed getting API client for user: %w", err)
|
return helix.User{}, fmt.Errorf("failed getting API client for user: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -50,15 +48,7 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check scope to see which credentials are we getting (stream/chat)
|
err = c.DB.PutJSON(twitch.AuthKey, twitch.AuthResponse{
|
||||||
state := req.URL.Query().Get("state")
|
|
||||||
key := twitch.AuthKey
|
|
||||||
switch state {
|
|
||||||
case "chat":
|
|
||||||
key = chat.CustomAccountKey
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.DB.PutJSON(key, twitch.AuthResponse{
|
|
||||||
AccessToken: userTokenResponse.Data.AccessToken,
|
AccessToken: userTokenResponse.Data.AccessToken,
|
||||||
RefreshToken: userTokenResponse.Data.RefreshToken,
|
RefreshToken: userTokenResponse.Data.RefreshToken,
|
||||||
ExpiresIn: userTokenResponse.Data.ExpiresIn,
|
ExpiresIn: userTokenResponse.Data.ExpiresIn,
|
||||||
|
@ -69,7 +59,6 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
http.Error(w, "error saving auth data for user: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "error saving auth data for user: "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/html")
|
w.Header().Add("Content-Type", "text/html")
|
||||||
_, _ = fmt.Fprintf(w, `<html><body><h2>All done, you can close me now!</h2><script>window.close();</script></body></html>`)
|
_, _ = fmt.Fprintf(w, `<html><body><h2>All done, you can close me now!</h2><script>window.close();</script></body></html>`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ func NewManager(ctx context.Context, db database.Database, server *webserver.Web
|
||||||
}
|
}
|
||||||
|
|
||||||
// New client works, replace old
|
// New client works, replace old
|
||||||
|
updatedClient.Merge(manager.client)
|
||||||
manager.client = updatedClient
|
manager.client = updatedClient
|
||||||
|
|
||||||
logger.Info("Reloaded/updated Twitch integration")
|
logger.Info("Reloaded/updated Twitch integration")
|
||||||
|
@ -97,11 +98,25 @@ type Client struct {
|
||||||
eventSub *eventsub.Client
|
eventSub *eventsub.Client
|
||||||
server *webserver.WebServer
|
server *webserver.WebServer
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
restart chan bool
|
restart chan bool
|
||||||
streamOnline *sync.RWSync[bool]
|
streamOnline *sync.RWSync[bool]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) Merge(old *Client) {
|
||||||
|
// Copy bot instance and some params
|
||||||
|
c.streamOnline.Set(old.streamOnline.Get())
|
||||||
|
c.ensureRoute()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hacky function to deal with sync issues when restarting client
|
||||||
|
func (c *Client) ensureRoute() {
|
||||||
|
if c.Config.Get().Enabled {
|
||||||
|
c.server.RegisterRoute(twitch.CallbackRoute, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newClient(ctx context.Context, config twitch.Config, db database.Database, server *webserver.WebServer) (*Client, error) {
|
func newClient(ctx context.Context, config twitch.Config, db database.Database, server *webserver.WebServer) (*Client, error) {
|
||||||
// Create Twitch client
|
// Create Twitch client
|
||||||
client := &Client{
|
client := &Client{
|
||||||
|
@ -119,14 +134,35 @@ func newClient(ctx context.Context, config twitch.Config, db database.Database,
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
client.API, err = twitch.GetHelixAPI(db, "")
|
client.API, err = twitch.GetHelixAPI(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create twitch client: %w", err)
|
return nil, fmt.Errorf("failed to create twitch client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
server.RegisterRoute(twitch.CallbackRoute, client)
|
server.RegisterRoute(twitch.CallbackRoute, client)
|
||||||
|
|
||||||
client.initializeEventSubUserFeatures()
|
if userClient, err := twitch.GetUserClient(db, true); err == nil {
|
||||||
|
users, err := userClient.GetUsers(&helix.UsersParams{})
|
||||||
|
if err != nil {
|
||||||
|
client.Logger.Error("Failed looking up user", log.Error(err))
|
||||||
|
} else if len(users.Data.Users) < 1 {
|
||||||
|
client.Logger.Error("No users found, please authenticate in Twitch configuration -> Events")
|
||||||
|
} else {
|
||||||
|
client.Logger.Info("Twitch user identified", slog.String("user", users.Data.Users[0].ID))
|
||||||
|
client.User = users.Data.Users[0]
|
||||||
|
client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, client.Logger)
|
||||||
|
if err != nil {
|
||||||
|
client.Logger.Error("Failed to setup EventSub", log.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl := client.GetTemplateEngine()
|
||||||
|
client.Chat = chat.Setup(ctx, db, userClient, client.User, client.Logger, tpl)
|
||||||
|
client.Alerts = alerts.Setup(ctx, db, client.Logger, tpl)
|
||||||
|
client.Timers = timers.Setup(ctx, db, client.Logger)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client.Logger.Warn("Twitch user not identified, this will break most features")
|
||||||
|
}
|
||||||
|
|
||||||
go client.runStatusPoll()
|
go client.runStatusPoll()
|
||||||
|
|
||||||
|
@ -138,36 +174,6 @@ func newClient(ctx context.Context, config twitch.Config, db database.Database,
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) initializeEventSubUserFeatures() {
|
|
||||||
userClient, err := twitch.GetUserClient(c.DB, twitch.AuthKey, true)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger.Warn("Twitch user not identified, this will break most features")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
users, err := userClient.GetUsers(&helix.UsersParams{})
|
|
||||||
if err != nil {
|
|
||||||
c.Logger.Error("Failed looking up user", log.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(users.Data.Users) < 1 {
|
|
||||||
c.Logger.Error("No users found, please authenticate in Twitch configuration -> Events")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Logger.Info("Twitch user identified", slog.String("user", users.Data.Users[0].ID))
|
|
||||||
c.User = users.Data.Users[0]
|
|
||||||
|
|
||||||
c.eventSub, err = eventsub.Setup(c.ctx, userClient, c.User, c.DB, c.Logger)
|
|
||||||
if err != nil {
|
|
||||||
c.Logger.Error("Failed to setup EventSub", log.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl := c.GetTemplateEngine()
|
|
||||||
c.Chat = chat.Setup(c.ctx, c.DB, userClient, c.User, c.Logger, tpl)
|
|
||||||
c.Alerts = alerts.Setup(c.ctx, c.DB, c.Logger, tpl)
|
|
||||||
c.Timers = timers.Setup(c.ctx, c.DB, c.Logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) runStatusPoll() {
|
func (c *Client) runStatusPoll() {
|
||||||
c.Logger.Info("Started polling for stream status")
|
c.Logger.Info("Started polling for stream status")
|
||||||
for {
|
for {
|
||||||
|
|
|
@ -14,7 +14,7 @@ func TestNewClient(t *testing.T) {
|
||||||
client, _ := database.CreateInMemoryLocalClient(t)
|
client, _ := database.CreateInMemoryLocalClient(t)
|
||||||
defer database.CleanupLocalClient(client)
|
defer database.CleanupLocalClient(client)
|
||||||
|
|
||||||
server, err := webserver.NewServer(context.Background(), client, webserver.DefaultServerFactory)
|
server, err := webserver.NewServer(client, webserver.DefaultServerFactory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,14 +48,12 @@ func CheckScopes(db database.Database) (bool, error) {
|
||||||
return slices.Equal(scopes, authResp.Scope), nil
|
return slices.Equal(scopes, authResp.Scope), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAuthorizationURL(api *helix.Client, state string) string {
|
func GetAuthorizationURL(api *helix.Client) string {
|
||||||
if api == nil {
|
if api == nil {
|
||||||
return "twitch-not-configured"
|
return "twitch-not-configured"
|
||||||
}
|
}
|
||||||
return api.GetAuthorizationURL(&helix.AuthorizationURLParams{
|
return api.GetAuthorizationURL(&helix.AuthorizationURLParams{
|
||||||
ResponseType: "code",
|
ResponseType: "code",
|
||||||
Scopes: scopes,
|
Scopes: scopes,
|
||||||
State: state,
|
|
||||||
ForceVerify: true,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package webserver
|
package webserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -16,7 +15,7 @@ func TestNewServer(t *testing.T) {
|
||||||
client, _ := database.CreateInMemoryLocalClient(t)
|
client, _ := database.CreateInMemoryLocalClient(t)
|
||||||
defer database.CleanupLocalClient(client)
|
defer database.CleanupLocalClient(client)
|
||||||
|
|
||||||
_, err := NewServer(context.Background(), client, DefaultServerFactory)
|
_, err := NewServer(client, DefaultServerFactory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -27,7 +26,7 @@ func TestNewServerWithTestFactory(t *testing.T) {
|
||||||
defer database.CleanupLocalClient(client)
|
defer database.CleanupLocalClient(client)
|
||||||
|
|
||||||
testServer := NewTestServer()
|
testServer := NewTestServer()
|
||||||
_, err := NewServer(context.Background(), client, testServer.Factory())
|
_, err := NewServer(client, testServer.Factory())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -39,7 +38,7 @@ func TestListen(t *testing.T) {
|
||||||
|
|
||||||
// Create a test server
|
// Create a test server
|
||||||
testServer := NewTestServer()
|
testServer := NewTestServer()
|
||||||
server, err := NewServer(context.Background(), client, testServer.Factory())
|
server, err := NewServer(client, testServer.Factory())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -96,7 +95,7 @@ func TestCustomRoute(t *testing.T) {
|
||||||
defer database.CleanupLocalClient(client)
|
defer database.CleanupLocalClient(client)
|
||||||
|
|
||||||
// Create test server
|
// Create test server
|
||||||
server, err := NewServer(context.Background(), client, nil)
|
server, err := NewServer(client, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue