mirror of
https://git.sr.ht/~ashkeel/strimertul
synced 2024-09-30 02:40:33 +00:00
Compare commits
2 commits
0d1c60451b
...
a06b9457ea
Author | SHA1 | Date | |
---|---|---|---|
|
a06b9457ea | ||
|
31d44b950e |
30 changed files with 467 additions and 460 deletions
|
@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
- The required set of permissions has changed. Existing users must re-authenticate their users to the app connected to strimertül.
|
||||
- The `twitch/ev/eventsub-event` and `twitch/eventsub-history` keys have been replaced by a set of keys in the format `twitch/ev/eventsub-event/<event-id>` and `twitch/eventsub-history/<event-id>`. Users of the old system will have to adjust their logic. A simple trick is to change from get/subscribing from a single key to the entire prefix. The data structure is the same.
|
||||
- A lot of keys for internal use have been changed, make sure to check the new reference for fixing up any integrations you might have. A migration process will convert v3 keys to v4 keys.
|
||||
|
||||
## 3.3.1 - 2023-11-12
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ You can also build the project yourself, refer to the Building section below.
|
|||
Strimertül is a single executable app that provides the following:
|
||||
|
||||
- HTTP server for serving static assets and a websocket API
|
||||
- Twitch bot for handling chat messages and providing custom commands
|
||||
- Twitch EventSub handlers for chat functionalities such as custom commands and alerts
|
||||
- Polling-based loyalty system for rewards and community goals
|
||||
|
||||
At strimertül's core is [Kilovolt](https://git.sr.ht/~ashkeel/kilovolt), a pub/sub key-value store accessible via websocket. You can access every functionality of strimertul through the Kilovolt API. Check [this repository](https://github.com/strimertul/kilovolt-clients) for a list of officially supported kilovolt clients (or submit your own). You should be able to easily build a client yourself by just creating a websocket connection and using the [Kilovolt protocol](https://github.com/strimertul/kilovolt/blob/main/PROTOCOL.md).
|
||||
|
@ -54,6 +54,4 @@ To build a redistributable, production mode package, use `wails build`.
|
|||
|
||||
## License
|
||||
|
||||
Kilovolt's code is based on Gorilla Websocket's server example, licensed under [BSD-2-Clause](https://github.com/gorilla/websocket/blob/master/LICENSE)
|
||||
|
||||
The entire project is licensed under [AGPL-3.0-only](LICENSE) (see `LICENSE`).
|
||||
|
|
21
app.go
21
app.go
|
@ -12,15 +12,11 @@ import (
|
|||
"runtime/debug"
|
||||
"strconv"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/client"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
|
||||
kv "github.com/strimertul/kilovolt/v11"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
kv "github.com/strimertul/kilovolt/v11"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
@ -28,7 +24,9 @@ import (
|
|||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/docs"
|
||||
"git.sr.ht/~ashkeel/strimertul/loyalty"
|
||||
"git.sr.ht/~ashkeel/strimertul/migrations"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/client"
|
||||
"git.sr.ht/~ashkeel/strimertul/webserver"
|
||||
)
|
||||
|
||||
|
@ -88,6 +86,12 @@ func (a *App) startup(ctx context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Check for migrations
|
||||
if err := migrations.Run(a.driver, a.db, logger.With(zap.String("operation", "migration"))); err != nil {
|
||||
a.showFatalError(err, "Failed to migrate database to latest version")
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize components
|
||||
if err := a.initializeComponents(); err != nil {
|
||||
a.showFatalError(err, "Failed to initialize required component")
|
||||
|
@ -148,7 +152,7 @@ func (a *App) initializeComponents() error {
|
|||
}
|
||||
|
||||
// Create twitch client
|
||||
a.twitchManager, err = client.NewManager(a.db, a.httpServer, logger)
|
||||
a.twitchManager, err = client.NewManager(a.ctx, a.db, a.httpServer, logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initialize twitch client: %w", err)
|
||||
}
|
||||
|
@ -192,9 +196,6 @@ func (a *App) stop(_ context.Context) {
|
|||
if a.loyaltyManager != nil {
|
||||
warnOnError(a.loyaltyManager.Close(), "Could not cleanly close loyalty manager")
|
||||
}
|
||||
if a.twitchManager != nil {
|
||||
warnOnError(a.twitchManager.Close(), "Could not cleanly close twitch client")
|
||||
}
|
||||
if a.httpServer != nil {
|
||||
warnOnError(a.httpServer.Close(), "Could not cleanly close HTTP server")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
|
@ -24,13 +25,20 @@ var (
|
|||
type Database interface {
|
||||
GetKey(key string) (string, error)
|
||||
PutKey(key string, data string) error
|
||||
RemoveKey(key string) error
|
||||
|
||||
SubscribePrefix(fn kv.SubscriptionCallback, prefixes ...string) (cancelFn CancelFunc, err error)
|
||||
SubscribeKey(key string, fn func(string)) (cancelFn CancelFunc, err error)
|
||||
GetJSON(key string, dst any) error
|
||||
|
||||
SubscribePrefixContext(ctx context.Context, fn kv.SubscriptionCallback, prefixes ...string) error
|
||||
SubscribeKeyContext(ctx context.Context, key string, fn func(string)) error
|
||||
|
||||
GetAll(prefix string) (map[string]string, error)
|
||||
|
||||
GetJSON(key string, dst any) error
|
||||
PutJSON(key string, data any) error
|
||||
PutJSONBulk(kvs map[string]any) error
|
||||
RemoveKey(key string) error
|
||||
|
||||
Hub() *kv.Hub
|
||||
}
|
||||
|
||||
|
@ -87,6 +95,32 @@ func (mod *LocalDBClient) PutKey(key string, data string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (mod *LocalDBClient) SubscribePrefixContext(ctx context.Context, fn kv.SubscriptionCallback, prefixes ...string) error {
|
||||
cancel, err := mod.SubscribePrefix(fn, prefixes...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
cancel()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mod *LocalDBClient) SubscribeKeyContext(ctx context.Context, key string, fn func(string)) error {
|
||||
cancel, err := mod.SubscribeKey(key, fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
cancel()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mod *LocalDBClient) SubscribePrefix(fn kv.SubscriptionCallback, prefixes ...string) (cancelFn CancelFunc, err error) {
|
||||
var ids []int64
|
||||
for _, prefix := range prefixes {
|
||||
|
|
|
@ -24,9 +24,9 @@
|
|||
},
|
||||
"twitch": {
|
||||
"configuration": "Configuration",
|
||||
"bot-commands": "Chat commands",
|
||||
"bot-timers": "Chat timers",
|
||||
"bot-alerts": "Chat alerts"
|
||||
"chat-commands": "Chat commands",
|
||||
"chat-timers": "Chat timers",
|
||||
"chat-alerts": "Chat alerts"
|
||||
},
|
||||
"loyalty": {
|
||||
"configuration": "Configuration",
|
||||
|
@ -66,18 +66,16 @@
|
|||
"apiguide-4": "Once made, create a <1>New Secret</1>, then copy both fields below and save!",
|
||||
"app-client-id": "App Client ID",
|
||||
"app-client-secret": "App Client Secret",
|
||||
"subtitle": "Twitch integration with streams including chat bot and API access. If you stream on Twitch, you definitely want this on.",
|
||||
"subtitle": "Twitch integration with streams including chat interactions and API access. If you stream on Twitch, you definitely want this on.",
|
||||
"api-subheader": "Application info",
|
||||
"api-configuration": "API access",
|
||||
"eventsub": "Events",
|
||||
"bot-settings": "Bot settings",
|
||||
"enable-bot": "Enable Twitch bot",
|
||||
"chat-settings": "Chat settings",
|
||||
"enable-bot": "Enable chat features",
|
||||
"bot-channel": "Twitch channel",
|
||||
"bot-username": "Twitch account username",
|
||||
"bot-oauth": "Authorization token",
|
||||
"bot-oauth-note": "You can get this by logging in with the bot account and going here: <1>https://twitchapps.com/tmi/</1>",
|
||||
"bot-info-header": "Bot account info",
|
||||
"bot-settings-copy": "A bot can interact with chat messages and provide extra events for the platform (chat events, some notifications) but requires access to a Twitch account. You can use your own or make a new one (if enabled on your main account, you can re-use the same email for your second account!)",
|
||||
"bot-chat-header": "Chat settings",
|
||||
"bot-chat-history": "How many messages to keep in history (0 to disable)",
|
||||
"events": {
|
||||
|
@ -107,7 +105,7 @@
|
|||
"bot-chat-cooldown-tip": "Global chat cooldown for commands (in seconds)"
|
||||
},
|
||||
"botcommands": {
|
||||
"title": "Bot commands",
|
||||
"title": "Chat commands",
|
||||
"desc": "Define custom chat commands to set up autoresponders, counters, etc.",
|
||||
"add-button": "New command",
|
||||
"search-placeholder": "Search command by name",
|
||||
|
@ -135,12 +133,12 @@
|
|||
"streamer": "Streamer only"
|
||||
},
|
||||
"remove-command-title": "Remove command {{name}}?",
|
||||
"no-commands": "Chatbot has no commands configured",
|
||||
"no-commands": "There are no commands configured",
|
||||
"command-already-in-use": "Command name already in use",
|
||||
"command-invalid-format": "The response template contains errors"
|
||||
},
|
||||
"bottimers": {
|
||||
"title": "Bot timers",
|
||||
"title": "Chat timers",
|
||||
"desc": "Define reminders such as checking out your social media or ongoing events",
|
||||
"add-button": "New timer",
|
||||
"search-placeholder": "Search timer by name",
|
||||
|
@ -301,7 +299,7 @@
|
|||
"landing": "Welcome",
|
||||
"twitch-config": "Twitch integration",
|
||||
"twitch-events": "Twitch events",
|
||||
"twitch-bot": "Twitch bot",
|
||||
"twitch-bot": "Twitch chat",
|
||||
"done": "All done!"
|
||||
},
|
||||
"twitch-p1": "To set-up Twitch you will need to create an application on the Developer portal. Follow the instructions below or click the button at the bottom to skip this step.",
|
||||
|
@ -311,7 +309,7 @@
|
|||
"twitch-ev-p3": "If the above shows your account name and picture correctly, you're all set! Click the button below to complete Twitch integration.",
|
||||
"twitch-complete": "Complete Twitch integration",
|
||||
"done-header": "You're all set!",
|
||||
"done-p1": "That should be enough for now. You can always change any option later, including custom configurations not covered in this procedure (e.g. using a different Twitch account for the bot).",
|
||||
"done-p1": "That should be enough for now. You can always change any option later, including custom configurations not covered in this procedure (e.g. using a different Twitch account for the chat integrations).",
|
||||
"done-p2": "If you have questions or issues, please reach out at any of these places:",
|
||||
"done-button": "Complete onboarding",
|
||||
"done-p3": "Click the button below to finish the onboarding and go to {{APPNAME}}'s dashboard.",
|
||||
|
|
|
@ -74,9 +74,9 @@
|
|||
"extensions": "Estensioni"
|
||||
},
|
||||
"twitch": {
|
||||
"bot-alerts": "Avvisi in chat",
|
||||
"bot-commands": "Comandi bot",
|
||||
"bot-timers": "Timer bot",
|
||||
"chat-alerts": "Avvisi in chat",
|
||||
"chat-commands": "Comandi chat",
|
||||
"chat-timers": "Timer chat",
|
||||
"configuration": "Configurazione"
|
||||
}
|
||||
},
|
||||
|
@ -332,10 +332,9 @@
|
|||
"bot-channel": "Canale Twitch",
|
||||
"bot-chat-header": "Impostazioni chat",
|
||||
"bot-chat-history": "Quanti messaggi tenere nello storico (0 per disabilitare)",
|
||||
"bot-info-header": "Informazioni account del bot",
|
||||
"bot-oauth": "Token di autorizzazione",
|
||||
"bot-oauth-note": "Puoi ottenerlo accedendo con l'account del bot e andando qui: <1>https://twitchapps.com/tmi/</1>",
|
||||
"bot-settings": "Impostazioni bot",
|
||||
"chat-settings": "Impostazioni chat",
|
||||
"bot-settings-copy": "Un bot può interagire con i messaggi in chat e scriverci per avvertimenti ed altre funzionalità ma richiede l'accesso ad un account Twitch. \nPuoi usare il tuo account o crearne uno apposta (se abilitato sul tuo account principale, puoi riutilizzare la stessa email per un secondo account!)",
|
||||
"bot-username": "Nome utente dell'account Twitch",
|
||||
"enable": "Abilita integrazione Twitch",
|
||||
|
|
|
@ -20,11 +20,11 @@ import {
|
|||
LoyaltyPointsEntry,
|
||||
LoyaltyRedeem,
|
||||
LoyaltyStorage,
|
||||
TwitchBotConfig,
|
||||
TwitchChatConfig,
|
||||
TwitchConfig,
|
||||
TwitchBotCustomCommands,
|
||||
TwitchBotTimersConfig,
|
||||
TwitchBotAlertsConfig,
|
||||
TwitchChatCustomCommands,
|
||||
TwitchChatTimersConfig,
|
||||
TwitchChatAlertsConfig,
|
||||
LoyaltyConfig,
|
||||
LoyaltyReward,
|
||||
LoyaltyGoal,
|
||||
|
@ -165,32 +165,32 @@ export const modules = {
|
|||
state.moduleConfigs.twitchConfig = payload as TwitchConfig;
|
||||
},
|
||||
),
|
||||
twitchBotConfig: makeModule(
|
||||
'twitch/bot-config',
|
||||
(state) => state.moduleConfigs?.twitchBotConfig,
|
||||
twitchChatConfig: makeModule(
|
||||
'twitch/chat/config',
|
||||
(state) => state.moduleConfigs?.twitchChatConfig,
|
||||
(state, { payload }) => {
|
||||
state.moduleConfigs.twitchBotConfig = payload as TwitchBotConfig;
|
||||
state.moduleConfigs.twitchChatConfig = payload as TwitchChatConfig;
|
||||
},
|
||||
),
|
||||
twitchBotCommands: makeModule(
|
||||
'twitch/bot-custom-commands',
|
||||
(state) => state.twitchBot?.commands,
|
||||
twitchChatCommands: makeModule(
|
||||
'twitch/chat/custom-commands',
|
||||
(state) => state.twitchChat?.commands,
|
||||
(state, { payload }) => {
|
||||
state.twitchBot.commands = payload as TwitchBotCustomCommands;
|
||||
state.twitchChat.commands = payload as TwitchChatCustomCommands;
|
||||
},
|
||||
),
|
||||
twitchBotTimers: makeModule(
|
||||
'twitch/bot-modules/timers/config',
|
||||
(state) => state.twitchBot?.timers,
|
||||
twitchChatTimers: makeModule(
|
||||
'twitch/timers/config',
|
||||
(state) => state.twitchChat?.timers,
|
||||
(state, { payload }) => {
|
||||
state.twitchBot.timers = payload as TwitchBotTimersConfig;
|
||||
state.twitchChat.timers = payload as TwitchChatTimersConfig;
|
||||
},
|
||||
),
|
||||
twitchBotAlerts: makeModule(
|
||||
'twitch/bot-modules/alerts/config',
|
||||
(state) => state.twitchBot?.alerts,
|
||||
twitchChatAlerts: makeModule(
|
||||
'twitch/alerts/config',
|
||||
(state) => state.twitchChat?.alerts,
|
||||
(state, { payload }) => {
|
||||
state.twitchBot.alerts = payload as TwitchBotAlertsConfig;
|
||||
state.twitchChat.alerts = payload as TwitchChatAlertsConfig;
|
||||
},
|
||||
),
|
||||
loyaltyConfig: makeModule(
|
||||
|
@ -269,7 +269,7 @@ const initialState: APIState = {
|
|||
goals: null,
|
||||
redeemQueue: null,
|
||||
},
|
||||
twitchBot: {
|
||||
twitchChat: {
|
||||
commands: null,
|
||||
timers: null,
|
||||
alerts: null,
|
||||
|
@ -277,7 +277,7 @@ const initialState: APIState = {
|
|||
moduleConfigs: {
|
||||
httpConfig: null,
|
||||
twitchConfig: null,
|
||||
twitchBotConfig: null,
|
||||
twitchChatConfig: null,
|
||||
loyaltyConfig: null,
|
||||
},
|
||||
uiConfig: null,
|
||||
|
|
|
@ -12,16 +12,11 @@ export interface HTTPConfig {
|
|||
|
||||
export interface TwitchConfig {
|
||||
enabled: boolean;
|
||||
enable_bot: boolean;
|
||||
api_client_id: string;
|
||||
api_client_secret: string;
|
||||
}
|
||||
|
||||
export interface TwitchBotConfig {
|
||||
username: string;
|
||||
oauth: string;
|
||||
channel: string;
|
||||
chat_history: number;
|
||||
export interface TwitchChatConfig {
|
||||
command_cooldown: number;
|
||||
}
|
||||
|
||||
|
@ -36,7 +31,7 @@ export const accessLevels = [
|
|||
export type AccessLevelType = (typeof accessLevels)[number];
|
||||
|
||||
export type ReplyType = 'chat' | 'reply' | 'whisper' | 'announce';
|
||||
export interface TwitchBotCustomCommand {
|
||||
export interface TwitchChatCustomCommand {
|
||||
description: string;
|
||||
access_level: AccessLevelType;
|
||||
response: string;
|
||||
|
@ -44,7 +39,7 @@ export interface TwitchBotCustomCommand {
|
|||
enabled: boolean;
|
||||
}
|
||||
|
||||
export type TwitchBotCustomCommands = Record<string, TwitchBotCustomCommand>;
|
||||
export type TwitchChatCustomCommands = Record<string, TwitchChatCustomCommand>;
|
||||
|
||||
export interface LoyaltyConfig {
|
||||
enabled: boolean;
|
||||
|
@ -57,7 +52,7 @@ export interface LoyaltyConfig {
|
|||
banlist: string[];
|
||||
}
|
||||
|
||||
export interface TwitchBotTimer {
|
||||
export interface TwitchChatTimer {
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
minimum_chat_activity: number;
|
||||
|
@ -65,11 +60,11 @@ export interface TwitchBotTimer {
|
|||
messages: string[];
|
||||
}
|
||||
|
||||
export interface TwitchBotTimersConfig {
|
||||
timers: Record<string, TwitchBotTimer>;
|
||||
export interface TwitchChatTimersConfig {
|
||||
timers: Record<string, TwitchChatTimer>;
|
||||
}
|
||||
|
||||
export interface TwitchBotAlertsConfig {
|
||||
export interface TwitchChatAlertsConfig {
|
||||
follow: {
|
||||
enabled: boolean;
|
||||
messages: string[];
|
||||
|
@ -176,15 +171,15 @@ export interface APIState {
|
|||
goals: LoyaltyGoal[];
|
||||
redeemQueue: LoyaltyRedeem[];
|
||||
};
|
||||
twitchBot: {
|
||||
commands: TwitchBotCustomCommands;
|
||||
timers: TwitchBotTimersConfig;
|
||||
alerts: TwitchBotAlertsConfig;
|
||||
twitchChat: {
|
||||
commands: TwitchChatCustomCommands;
|
||||
timers: TwitchChatTimersConfig;
|
||||
alerts: TwitchChatAlertsConfig;
|
||||
};
|
||||
moduleConfigs: {
|
||||
httpConfig: HTTPConfig;
|
||||
twitchConfig: TwitchConfig;
|
||||
twitchBotConfig: TwitchBotConfig;
|
||||
twitchChatConfig: TwitchChatConfig;
|
||||
loyaltyConfig: LoyaltyConfig;
|
||||
};
|
||||
uiConfig: UISettings;
|
||||
|
|
|
@ -31,8 +31,8 @@ import { initializeServerInfo } from '~/store/server/reducer';
|
|||
import LogViewer from './components/LogViewer';
|
||||
import Sidebar, { RouteSection } from './components/Sidebar';
|
||||
import Scrollbar from './components/utils/Scrollbar';
|
||||
import TwitchBotCommandsPage from './pages/BotCommands';
|
||||
import TwitchBotTimersPage from './pages/BotTimers';
|
||||
import TwitchChatCommandsPage from './pages/ChatCommands';
|
||||
import TwitchChatTimersPage from './pages/ChatTimers';
|
||||
import ChatAlertsPage from './pages/ChatAlerts';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import DebugPage from './pages/Debug';
|
||||
|
@ -92,18 +92,18 @@ const sections: RouteSection[] = [
|
|||
icon: <MixerHorizontalIcon />,
|
||||
},
|
||||
{
|
||||
title: 'menu.pages.twitch.bot-commands',
|
||||
url: '/twitch/bot/commands',
|
||||
title: 'menu.pages.twitch.chat-commands',
|
||||
url: '/twitch/chat/commands',
|
||||
icon: <ChatBubbleIcon />,
|
||||
},
|
||||
{
|
||||
title: 'menu.pages.twitch.bot-timers',
|
||||
url: '/twitch/bot/timers',
|
||||
title: 'menu.pages.twitch.chat-timers',
|
||||
url: '/twitch/chat/timers',
|
||||
icon: <TimerIcon />,
|
||||
},
|
||||
{
|
||||
title: 'menu.pages.twitch.bot-alerts',
|
||||
url: '/twitch/bot/alerts',
|
||||
title: 'menu.pages.twitch.chat-alerts',
|
||||
url: '/twitch/chat/alerts',
|
||||
icon: <FrameIcon />,
|
||||
},
|
||||
],
|
||||
|
@ -277,14 +277,14 @@ export default function App(): JSX.Element {
|
|||
<Route path="/extensions" element={<ExtensionsPage />} />
|
||||
<Route path="/twitch/settings" element={<TwitchSettingsPage />} />
|
||||
<Route
|
||||
path="/twitch/bot/commands"
|
||||
element={<TwitchBotCommandsPage />}
|
||||
path="/twitch/chat/commands"
|
||||
element={<TwitchChatCommandsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/twitch/bot/timers"
|
||||
element={<TwitchBotTimersPage />}
|
||||
path="/twitch/chat/timers"
|
||||
element={<TwitchChatTimersPage />}
|
||||
/>
|
||||
<Route path="/twitch/bot/alerts" element={<ChatAlertsPage />} />
|
||||
<Route path="/twitch/chat/alerts" element={<ChatAlertsPage />} />
|
||||
<Route path="/loyalty/settings" element={<LoyaltyConfigPage />} />
|
||||
<Route path="/loyalty/users" element={<LoyaltyQueuePage />} />
|
||||
<Route path="/loyalty/rewards" element={<LoyaltyRewardsPage />} />
|
||||
|
|
|
@ -25,7 +25,7 @@ import SaveButton from '../components/forms/SaveButton';
|
|||
export default function ChatAlertsPage(): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const [alerts, setAlerts, loadStatus] = useModule(modules.twitchBotAlerts);
|
||||
const [alerts, setAlerts, loadStatus] = useModule(modules.twitchChatAlerts);
|
||||
const status = useStatus(loadStatus.save);
|
||||
|
||||
return (
|
||||
|
@ -63,7 +63,7 @@ export default function ChatAlertsPage(): React.ReactElement {
|
|||
checked={alerts?.follow?.enabled ?? false}
|
||||
onCheckedChange={(ev) => {
|
||||
void dispatch(
|
||||
apiReducer.actions.twitchBotAlertsChanged({
|
||||
apiReducer.actions.twitchChatAlertsChanged({
|
||||
...alerts,
|
||||
follow: {
|
||||
...alerts.follow,
|
||||
|
@ -93,7 +93,7 @@ export default function ChatAlertsPage(): React.ReactElement {
|
|||
required={alerts?.follow?.enabled ?? false}
|
||||
onChange={(messages) => {
|
||||
dispatch(
|
||||
apiReducer.actions.twitchBotAlertsChanged({
|
||||
apiReducer.actions.twitchChatAlertsChanged({
|
||||
...alerts,
|
||||
follow: { ...alerts.follow, messages },
|
||||
}),
|
||||
|
@ -109,7 +109,7 @@ export default function ChatAlertsPage(): React.ReactElement {
|
|||
checked={alerts?.subscription?.enabled ?? false}
|
||||
onCheckedChange={(ev) => {
|
||||
void dispatch(
|
||||
apiReducer.actions.twitchBotAlertsChanged({
|
||||
apiReducer.actions.twitchChatAlertsChanged({
|
||||
...alerts,
|
||||
subscription: {
|
||||
...alerts.subscription,
|
||||
|
@ -139,7 +139,7 @@ export default function ChatAlertsPage(): React.ReactElement {
|
|||
required={alerts?.subscription?.enabled ?? false}
|
||||
onChange={(messages) => {
|
||||
void dispatch(
|
||||
apiReducer.actions.twitchBotAlertsChanged({
|
||||
apiReducer.actions.twitchChatAlertsChanged({
|
||||
...alerts,
|
||||
subscription: { ...alerts.subscription, messages },
|
||||
}),
|
||||
|
@ -156,7 +156,7 @@ export default function ChatAlertsPage(): React.ReactElement {
|
|||
checked={alerts?.gift_sub?.enabled ?? false}
|
||||
onCheckedChange={(ev) => {
|
||||
void dispatch(
|
||||
apiReducer.actions.twitchBotAlertsChanged({
|
||||
apiReducer.actions.twitchChatAlertsChanged({
|
||||
...alerts,
|
||||
gift_sub: {
|
||||
...alerts.gift_sub,
|
||||
|
@ -186,7 +186,7 @@ export default function ChatAlertsPage(): React.ReactElement {
|
|||
required={alerts?.gift_sub?.enabled ?? false}
|
||||
onChange={(messages) => {
|
||||
void dispatch(
|
||||
apiReducer.actions.twitchBotAlertsChanged({
|
||||
apiReducer.actions.twitchChatAlertsChanged({
|
||||
...alerts,
|
||||
gift_sub: { ...alerts.gift_sub, messages },
|
||||
}),
|
||||
|
@ -203,7 +203,7 @@ export default function ChatAlertsPage(): React.ReactElement {
|
|||
checked={alerts?.raid?.enabled ?? false}
|
||||
onCheckedChange={(ev) => {
|
||||
void dispatch(
|
||||
apiReducer.actions.twitchBotAlertsChanged({
|
||||
apiReducer.actions.twitchChatAlertsChanged({
|
||||
...alerts,
|
||||
raid: {
|
||||
...alerts.raid,
|
||||
|
@ -233,7 +233,7 @@ export default function ChatAlertsPage(): React.ReactElement {
|
|||
required={alerts?.raid?.enabled ?? false}
|
||||
onChange={(messages) => {
|
||||
void dispatch(
|
||||
apiReducer.actions.twitchBotAlertsChanged({
|
||||
apiReducer.actions.twitchChatAlertsChanged({
|
||||
...alerts,
|
||||
raid: { ...alerts.raid, messages },
|
||||
}),
|
||||
|
@ -250,7 +250,7 @@ export default function ChatAlertsPage(): React.ReactElement {
|
|||
checked={alerts?.cheer?.enabled ?? false}
|
||||
onCheckedChange={(ev) => {
|
||||
void dispatch(
|
||||
apiReducer.actions.twitchBotAlertsChanged({
|
||||
apiReducer.actions.twitchChatAlertsChanged({
|
||||
...alerts,
|
||||
cheer: {
|
||||
...alerts.cheer,
|
||||
|
@ -280,7 +280,7 @@ export default function ChatAlertsPage(): React.ReactElement {
|
|||
required={alerts?.cheer?.enabled ?? false}
|
||||
onChange={(messages) => {
|
||||
void dispatch(
|
||||
apiReducer.actions.twitchBotAlertsChanged({
|
||||
apiReducer.actions.twitchChatAlertsChanged({
|
||||
...alerts,
|
||||
cheer: { ...alerts.cheer, messages },
|
||||
}),
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
accessLevels,
|
||||
AccessLevelType,
|
||||
ReplyType,
|
||||
TwitchBotCustomCommand,
|
||||
TwitchChatCustomCommand,
|
||||
} from '~/store/api/types';
|
||||
import { TestCommandTemplate } from '@wailsapp/go/main/App';
|
||||
import AlertContent from '../components/AlertContent';
|
||||
|
@ -137,7 +137,7 @@ const ACLIndicator = styled('span', {
|
|||
|
||||
interface CommandItemProps {
|
||||
name: string;
|
||||
item: TwitchBotCustomCommand;
|
||||
item: TwitchChatCustomCommand;
|
||||
onToggle?: () => void;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
|
@ -212,7 +212,7 @@ function CommandItem({
|
|||
|
||||
type DialogPrompt =
|
||||
| { kind: 'new' }
|
||||
| { kind: 'edit'; name: string; item: TwitchBotCustomCommand };
|
||||
| { kind: 'edit'; name: string; item: TwitchChatCustomCommand };
|
||||
|
||||
function CommandDialog({
|
||||
kind,
|
||||
|
@ -222,10 +222,10 @@ function CommandDialog({
|
|||
}: {
|
||||
kind: 'new' | 'edit';
|
||||
name?: string;
|
||||
item?: TwitchBotCustomCommand;
|
||||
onSubmit?: (name: string, item: TwitchBotCustomCommand) => void;
|
||||
item?: TwitchChatCustomCommand;
|
||||
onSubmit?: (name: string, item: TwitchChatCustomCommand) => void;
|
||||
}) {
|
||||
const [commands] = useModule(modules.twitchBotCommands);
|
||||
const [commands] = useModule(modules.twitchChatCommands);
|
||||
const [commandName, setCommandName] = useState(name ?? '');
|
||||
const [description, setDescription] = useState(item?.description ?? '');
|
||||
const [responseType, setResponseType] = useState(
|
||||
|
@ -376,8 +376,8 @@ function CommandDialog({
|
|||
);
|
||||
}
|
||||
|
||||
export default function TwitchBotCommandsPage(): React.ReactElement {
|
||||
const [commands, setCommands] = useModule(modules.twitchBotCommands);
|
||||
export default function TwitchChatCommandsPage(): React.ReactElement {
|
||||
const [commands, setCommands] = useModule(modules.twitchChatCommands);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null);
|
||||
const { t } = useTranslation();
|
||||
|
@ -385,7 +385,7 @@ export default function TwitchBotCommandsPage(): React.ReactElement {
|
|||
|
||||
const filterLC = filter.toLowerCase();
|
||||
|
||||
const setCommand = (newName: string, data: TwitchBotCustomCommand): void => {
|
||||
const setCommand = (newName: string, data: TwitchChatCustomCommand): void => {
|
||||
switch (activeDialog.kind) {
|
||||
case 'new':
|
||||
void dispatch(
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { useModule } from '~/lib/react';
|
||||
import { useAppDispatch } from '~/store';
|
||||
import { modules } from '~/store/api/reducer';
|
||||
import { TwitchBotTimer } from '~/store/api/types';
|
||||
import { TwitchChatTimer } from '~/store/api/types';
|
||||
import AlertContent from '../components/AlertContent';
|
||||
import DialogContent from '../components/DialogContent';
|
||||
import Interval from '../components/forms/Interval';
|
||||
|
@ -108,7 +108,7 @@ function humanTime(t: TFunction<'translation'>, secs: number): string {
|
|||
|
||||
interface TimerItemProps {
|
||||
name: string;
|
||||
item: TwitchBotTimer;
|
||||
item: TwitchChatTimer;
|
||||
onToggle?: () => void;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
|
@ -182,7 +182,7 @@ function TimerItem({
|
|||
|
||||
type DialogPrompt =
|
||||
| { kind: 'new' }
|
||||
| { kind: 'edit'; name: string; item: TwitchBotTimer };
|
||||
| { kind: 'edit'; name: string; item: TwitchChatTimer };
|
||||
|
||||
function TimerDialog({
|
||||
kind,
|
||||
|
@ -192,10 +192,10 @@ function TimerDialog({
|
|||
}: {
|
||||
kind: 'new' | 'edit';
|
||||
name?: string;
|
||||
item?: TwitchBotTimer;
|
||||
onSubmit?: (name: string, item: TwitchBotTimer) => void;
|
||||
item?: TwitchChatTimer;
|
||||
onSubmit?: (name: string, item: TwitchChatTimer) => void;
|
||||
}) {
|
||||
const [timerConfig] = useModule(modules.twitchBotTimers);
|
||||
const [timerConfig] = useModule(modules.twitchChatTimers);
|
||||
const [timerName, setName] = useState(name ?? '');
|
||||
const [messages, setMessages] = useState(item?.messages ?? ['']);
|
||||
const [minDelay, setMinDelay] = useState(item?.minimum_delay ?? 300);
|
||||
|
@ -308,8 +308,8 @@ function TimerDialog({
|
|||
);
|
||||
}
|
||||
|
||||
export default function TwitchBotTimersPage(): React.ReactElement {
|
||||
const [timerConfig, setTimerConfig] = useModule(modules.twitchBotTimers);
|
||||
export default function TwitchChatTimersPage(): React.ReactElement {
|
||||
const [timerConfig, setTimerConfig] = useModule(modules.twitchChatTimers);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [activeDialog, setActiveDialog] = useState<DialogPrompt>(null);
|
||||
const { t } = useTranslation();
|
||||
|
@ -317,7 +317,7 @@ export default function TwitchBotTimersPage(): React.ReactElement {
|
|||
|
||||
const filterLC = filter.toLowerCase();
|
||||
|
||||
const setTimer = (newName: string, data: TwitchBotTimer): void => {
|
||||
const setTimer = (newName: string, data: TwitchChatTimer): void => {
|
||||
switch (activeDialog.kind) {
|
||||
case 'new':
|
||||
void dispatch(
|
|
@ -158,7 +158,9 @@ function TwitchEvent({ data }: { data: EventSubNotification }) {
|
|||
|
||||
let content: JSX.Element | string;
|
||||
const message = unwrapEvent(data);
|
||||
let date = data.date ? new Date(data.date) : null;
|
||||
let date = data.date
|
||||
? new Date(data.date)
|
||||
: new Date(data.subscription.created_at);
|
||||
switch (message.type) {
|
||||
case EventSubNotificationType.Followed: {
|
||||
content = (
|
||||
|
@ -382,10 +384,16 @@ function TwitchEventLog({ events }: { events: EventSubNotification[] }) {
|
|||
{events
|
||||
.filter((ev) => supportedMessages.includes(ev.subscription.type))
|
||||
.sort((a, b) =>
|
||||
a.date && b.date ? Date.parse(b.date) - Date.parse(a.date) : 0,
|
||||
a.date && b.date
|
||||
? Date.parse(b.date) - Date.parse(a.date)
|
||||
: Date.parse(b.subscription.created_at) -
|
||||
Date.parse(a.subscription.created_at),
|
||||
)
|
||||
.map((ev) => (
|
||||
<TwitchEvent key={`${ev.subscription.id}-${ev.date}`} data={ev} />
|
||||
<TwitchEvent
|
||||
key={`${ev.subscription.id}-${ev.subscription.created_at}`}
|
||||
data={ev}
|
||||
/>
|
||||
))}
|
||||
</EventListContainer>
|
||||
</Scrollbar>
|
||||
|
@ -433,6 +441,19 @@ function TwitchSection() {
|
|||
const kv = useAppSelector((state) => state.api.client);
|
||||
const [twitchEvents, setTwitchEvents] = useState<EventSubNotification[]>([]);
|
||||
|
||||
const keyfn = (ev: EventSubNotification) =>
|
||||
ev.subscription.id + ev.subscription.created_at;
|
||||
|
||||
const setCleanTwitchEvents = (events: EventSubNotification[]) => {
|
||||
const eventKeys = events.map(keyfn);
|
||||
|
||||
// Clean up duplicates before setting to state
|
||||
const uniqueEvents = events.filter(
|
||||
(ev, pos) => eventKeys.indexOf(keyfn(ev)) === pos,
|
||||
);
|
||||
setTwitchEvents(uniqueEvents);
|
||||
};
|
||||
|
||||
const loadRecentEvents = async () => {
|
||||
const keymap = await kv.getKeysByPrefix('twitch/eventsub-history/');
|
||||
const events = Object.values(keymap)
|
||||
|
@ -440,7 +461,7 @@ function TwitchSection() {
|
|||
.flat()
|
||||
.sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
|
||||
|
||||
setTwitchEvents(events);
|
||||
setCleanTwitchEvents(events);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -448,7 +469,7 @@ function TwitchSection() {
|
|||
|
||||
const onKeyChange = (value: string) => {
|
||||
const event = JSON.parse(value) as EventSubNotification;
|
||||
void setTwitchEvents((prev) => [event, ...prev]);
|
||||
void setCleanTwitchEvents((prev) => [event, ...prev]);
|
||||
};
|
||||
|
||||
void kv.subscribePrefix('twitch/ev/eventsub-event/', onKeyChange);
|
||||
|
|
|
@ -450,7 +450,7 @@ function TwitchEventsStep() {
|
|||
const { t } = useTranslation();
|
||||
const [userStatus, setUserStatus] = useState<helix.User | SyncError>(null);
|
||||
const [twitchConfig, setTwitchConfig] = useModule(modules.twitchConfig);
|
||||
const [botConfig, setBotConfig] = useModule(modules.twitchBotConfig);
|
||||
const [botConfig, setBotConfig] = useModule(modules.twitchChatConfig);
|
||||
const [uiConfig, setUiConfig] = useModule(modules.uiConfig);
|
||||
const [authKeys, setAuthKeys] = useState<TwitchCredentials>(null);
|
||||
const kv = useSelector((state: RootState) => state.api.client);
|
||||
|
|
|
@ -19,7 +19,6 @@ import {
|
|||
Checkbox,
|
||||
CheckboxIndicator,
|
||||
Field,
|
||||
FieldNote,
|
||||
FlexRow,
|
||||
InputBox,
|
||||
Label,
|
||||
|
@ -54,143 +53,27 @@ const Step = styled('li', {
|
|||
},
|
||||
});
|
||||
|
||||
function TwitchBotSettings() {
|
||||
const [botConfig, setBotConfig, loadStatus] = useModule(
|
||||
modules.twitchBotConfig,
|
||||
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 [revealBotToken, setRevealBotToken] = useState(false);
|
||||
const active = twitchConfig?.enable_bot ?? false;
|
||||
const disabled = !active || status?.type === 'pending';
|
||||
const disabled = status?.type === 'pending';
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(ev) => {
|
||||
void dispatch(setTwitchConfig(twitchConfig));
|
||||
void dispatch(setBotConfig(botConfig));
|
||||
void dispatch(setChatConfig(chatConfig));
|
||||
ev.preventDefault();
|
||||
}}
|
||||
>
|
||||
<TextBlock>{t('pages.twitch-settings.bot-settings-copy')}</TextBlock>
|
||||
<Field>
|
||||
<FlexRow spacing={1}>
|
||||
<Checkbox
|
||||
checked={active}
|
||||
onCheckedChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchConfigChanged({
|
||||
...twitchConfig,
|
||||
enable_bot: !!ev,
|
||||
}),
|
||||
)
|
||||
}
|
||||
id="enable-bot"
|
||||
>
|
||||
<CheckboxIndicator>{active && <CheckIcon />}</CheckboxIndicator>
|
||||
</Checkbox>
|
||||
|
||||
<Label htmlFor="enable-bot">
|
||||
{t('pages.twitch-settings.enable-bot')}
|
||||
</Label>
|
||||
</FlexRow>
|
||||
</Field>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bot-channel">
|
||||
{t('pages.twitch-settings.bot-channel')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="bot-channel"
|
||||
required={active}
|
||||
disabled={disabled}
|
||||
value={botConfig?.channel ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchBotConfigChanged({
|
||||
...botConfig,
|
||||
channel: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<SectionHeader>
|
||||
{t('pages.twitch-settings.bot-info-header')}
|
||||
</SectionHeader>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bot-username">
|
||||
{t('pages.twitch-settings.bot-username')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="text"
|
||||
id="bot-username"
|
||||
required={active}
|
||||
disabled={disabled}
|
||||
value={botConfig?.username ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchBotConfigChanged({
|
||||
...botConfig,
|
||||
username: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bot-oauth">
|
||||
{t('pages.twitch-settings.bot-oauth')}
|
||||
<RevealLink value={revealBotToken} setter={setRevealBotToken} />
|
||||
</Label>
|
||||
<PasswordInputBox
|
||||
reveal={revealBotToken}
|
||||
id="bot-oauth"
|
||||
required={active}
|
||||
disabled={disabled}
|
||||
value={botConfig?.oauth ?? ''}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchBotConfigChanged({
|
||||
...botConfig,
|
||||
oauth: ev.target.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<FieldNote>
|
||||
<Trans i18nKey="pages.twitch-settings.bot-oauth-note">
|
||||
<BrowserLink href="https://twitchapps.com/tmi/">
|
||||
https://twitchapps.com/tmi/
|
||||
</BrowserLink>
|
||||
</Trans>
|
||||
</FieldNote>
|
||||
</Field>
|
||||
<SectionHeader>
|
||||
{t('pages.twitch-settings.bot-chat-header')}
|
||||
</SectionHeader>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bot-chat-history">
|
||||
{t('pages.twitch-settings.bot-chat-history')}
|
||||
</Label>
|
||||
<InputBox
|
||||
type="number"
|
||||
id="bot-chat-history"
|
||||
required={active}
|
||||
disabled={disabled}
|
||||
defaultValue={botConfig?.chat_history}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchBotConfigChanged({
|
||||
...botConfig,
|
||||
chat_history: parseInt(ev.target.value, 10),
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field size="fullWidth">
|
||||
<Label htmlFor="bot-chat-history">
|
||||
{t('pages.twitch-settings.bot-chat-cooldown-tip')}
|
||||
|
@ -198,13 +81,15 @@ function TwitchBotSettings() {
|
|||
<InputBox
|
||||
type="number"
|
||||
id="bot-chat-history"
|
||||
required={active}
|
||||
required={true}
|
||||
disabled={disabled}
|
||||
defaultValue={botConfig ? botConfig.command_cooldown ?? 2 : undefined}
|
||||
defaultValue={
|
||||
chatConfig ? chatConfig.command_cooldown ?? 2 : undefined
|
||||
}
|
||||
onChange={(ev) =>
|
||||
dispatch(
|
||||
apiReducer.actions.twitchBotConfigChanged({
|
||||
...botConfig,
|
||||
apiReducer.actions.twitchChatConfigChanged({
|
||||
...chatConfig,
|
||||
command_cooldown: parseInt(ev.target.value, 10),
|
||||
}),
|
||||
)
|
||||
|
@ -532,8 +417,8 @@ export default function TwitchSettingsPage(): React.ReactElement {
|
|||
<TabButton value="eventsub">
|
||||
{t('pages.twitch-settings.eventsub')}
|
||||
</TabButton>
|
||||
<TabButton value="bot-settings">
|
||||
{t('pages.twitch-settings.bot-settings')}
|
||||
<TabButton value="chat-settings">
|
||||
{t('pages.twitch-settings.chat-settings')}
|
||||
</TabButton>
|
||||
</TabList>
|
||||
<TabContent value="api-config">
|
||||
|
@ -542,8 +427,8 @@ export default function TwitchSettingsPage(): React.ReactElement {
|
|||
<TabContent value="eventsub">
|
||||
<TwitchEventSubSettings />
|
||||
</TabContent>
|
||||
<TabContent value="bot-settings">
|
||||
<TwitchBotSettings />
|
||||
<TabContent value="chat-settings">
|
||||
<TwitchChatSettings />
|
||||
</TabContent>
|
||||
</TabContainer>
|
||||
</div>
|
||||
|
|
2
go.mod
2
go.mod
|
@ -13,7 +13,7 @@ require (
|
|||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/nicklaw5/helix/v2 v2.28.0
|
||||
github.com/nicklaw5/helix/v2 v2.28.1
|
||||
github.com/strimertul/kilovolt/v11 v11.0.1
|
||||
github.com/urfave/cli/v2 v2.27.1
|
||||
github.com/wailsapp/wails/v2 v2.8.0
|
||||
|
|
4
go.sum
4
go.sum
|
@ -251,8 +251,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nicklaw5/helix/v2 v2.28.0 h1:BCpIh9gf/7dsTNyxzgY18VHpt9W6/t0zUioyuDhH6tA=
|
||||
github.com/nicklaw5/helix/v2 v2.28.0/go.mod h1:zZcKsyyBWDli34x3QleYsVMiiNGMXPAEU5NjsiZDtvY=
|
||||
github.com/nicklaw5/helix/v2 v2.28.1 h1:bLVKMrZ0MiSgCLB3nsi7+OrhognsIusqvNL4XFoRG0A=
|
||||
github.com/nicklaw5/helix/v2 v2.28.1/go.mod h1:zZcKsyyBWDli34x3QleYsVMiiNGMXPAEU5NjsiZDtvY=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||
|
|
23
migrations/common.go
Normal file
23
migrations/common.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
)
|
||||
|
||||
func renameKey(db database.Database, oldKey, newKey string, ignoreMissing bool) error {
|
||||
value, err := db.GetKey(oldKey)
|
||||
if err != nil {
|
||||
if ignoreMissing && errors.Is(err, database.ErrEmptyKey) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err = db.PutKey(newKey, value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.RemoveKey(oldKey)
|
||||
}
|
72
migrations/migration.go
Normal file
72
migrations/migration.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
)
|
||||
|
||||
const (
|
||||
SchemaKey = "strimertul/schema-version"
|
||||
SchemaVersion = 4
|
||||
)
|
||||
|
||||
// GetCurrentSchemaVersion returns the current schema version
|
||||
// from the database, or 0 if it's not set (v3.x.x or earlier)
|
||||
func GetCurrentSchemaVersion(db database.Database) (int, error) {
|
||||
versionStr, err := db.GetKey(SchemaKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, database.ErrEmptyKey) {
|
||||
return 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
if versionStr == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return strconv.Atoi(versionStr)
|
||||
}
|
||||
|
||||
func Run(driver database.Driver, db database.Database, logger *zap.Logger) (err error) {
|
||||
// Make a backup of the database
|
||||
var buffer bytes.Buffer
|
||||
if err = driver.Backup(&buffer); err != nil {
|
||||
return fmt.Errorf("failed to backup database: %s", err)
|
||||
}
|
||||
// Restore the backup if an error occurs
|
||||
defer func() {
|
||||
if err != nil {
|
||||
restoreErr := driver.Restore(bytes.NewReader(buffer.Bytes()))
|
||||
if restoreErr != nil {
|
||||
logger.Error("Failed to restore database from backup", zap.Error(restoreErr))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Get the current schema version
|
||||
var currentVersion int
|
||||
currentVersion, err = GetCurrentSchemaVersion(db)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Migrate from v3.x.x to v4.0.0
|
||||
if currentVersion < 4 {
|
||||
if err = migrateToV4(db, logger); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new schema version
|
||||
if err = db.PutKey(SchemaKey, strconv.Itoa(SchemaVersion)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
82
migrations/v3_v4.go
Normal file
82
migrations/v3_v4.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/alerts"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/timers"
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func migrateToV4(db database.Database, logger *zap.Logger) error {
|
||||
logger.Info("Migrating database from v3 to v4")
|
||||
|
||||
// Rename keys that have no schema changes
|
||||
for oldKey, newKey := range map[string]string{
|
||||
"twitch/bot-modules/timers/config": timers.ConfigKey,
|
||||
"twitch/bot-modules/alerts/config": alerts.ConfigKey,
|
||||
"twitch/bot-custom-commands": chat.CustomCommandsKey,
|
||||
} {
|
||||
if err := renameKey(db, oldKey, newKey, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Clear old event keys and IRC-related keys
|
||||
for _, key := range []string{
|
||||
"twitch/chat-activity",
|
||||
"twitch/chat-history",
|
||||
"twitch/@send-chat-message",
|
||||
"twitch/bot/@send-message",
|
||||
"twitch/ev/chat-message",
|
||||
"twitch/ev/eventsub-event",
|
||||
} {
|
||||
if err := db.RemoveKey(key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate bot config to chat config
|
||||
var botConfig struct {
|
||||
CommandCooldown int `json:"command_cooldown"`
|
||||
}
|
||||
if err := db.GetJSON("twitch/bot-config", &botConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.PutJSON(chat.ConfigKey, chat.Config{
|
||||
CommandCooldown: botConfig.CommandCooldown,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Migrate eventsub history to their new keys
|
||||
type eventSubNotification struct {
|
||||
Subscription helix.EventSubSubscription `json:"subscription"`
|
||||
Event json.RawMessage `json:"event"`
|
||||
}
|
||||
var eventsubHistory []eventSubNotification
|
||||
if err := db.GetJSON("twitch/eventsub-history", &eventsubHistory); err != nil {
|
||||
return err
|
||||
}
|
||||
eventsubHistoryMap := make(map[string][]eventSubNotification)
|
||||
for _, notification := range eventsubHistory {
|
||||
key := eventsub.HistoryKeyPrefix + notification.Subscription.Type
|
||||
eventsubHistoryMap[key] = append(eventsubHistoryMap[key], notification)
|
||||
}
|
||||
for key, notifications := range eventsubHistoryMap {
|
||||
if err := db.PutJSON(key, notifications); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Clear old eventsub history key
|
||||
if err := db.RemoveKey("twitch/eventsub-history"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,17 +1,16 @@
|
|||
package alerts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
||||
|
||||
template2 "git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
||||
template2 "git.sr.ht/~ashkeel/strimertul/twitch/template"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigFastest
|
||||
|
@ -34,20 +33,19 @@ const (
|
|||
type Module struct {
|
||||
Config Config
|
||||
|
||||
ctx context.Context
|
||||
db database.Database
|
||||
logger *zap.Logger
|
||||
templater template2.Engine
|
||||
templates templateCacheMap
|
||||
|
||||
cancelAlertSub database.CancelFunc
|
||||
cancelTwitchEventSub database.CancelFunc
|
||||
|
||||
pendingMux sync.Mutex
|
||||
pendingSubs map[string]subMixedEvent
|
||||
}
|
||||
|
||||
func Setup(db database.Database, logger *zap.Logger, templater template2.Engine) *Module {
|
||||
func Setup(ctx context.Context, db database.Database, logger *zap.Logger, templater template2.Engine) *Module {
|
||||
mod := &Module{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
logger: logger,
|
||||
templater: templater,
|
||||
|
@ -56,8 +54,7 @@ func Setup(db database.Database, logger *zap.Logger, templater template2.Engine)
|
|||
}
|
||||
|
||||
// Load config from database
|
||||
err := db.GetJSON(ConfigKey, &mod.Config)
|
||||
if err != nil {
|
||||
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
||||
logger.Debug("Config load error", zap.Error(err))
|
||||
mod.Config = Config{}
|
||||
// Save empty config
|
||||
|
@ -69,7 +66,7 @@ func Setup(db database.Database, logger *zap.Logger, templater template2.Engine)
|
|||
|
||||
mod.compileTemplates()
|
||||
|
||||
mod.cancelAlertSub, err = db.SubscribeKey(ConfigKey, func(value string) {
|
||||
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
|
||||
err := json.UnmarshalFromString(value, &mod.Config)
|
||||
if err != nil {
|
||||
logger.Warn("Error loading alert config", zap.Error(err))
|
||||
|
@ -77,13 +74,11 @@ func Setup(db database.Database, logger *zap.Logger, templater template2.Engine)
|
|||
logger.Info("Reloaded alert config")
|
||||
}
|
||||
mod.compileTemplates()
|
||||
})
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
logger.Error("Could not set-up bot alert reload subscription", zap.Error(err))
|
||||
}
|
||||
|
||||
mod.cancelTwitchEventSub, err = db.SubscribePrefix(mod.onEventSubEvent, eventsub.EventKeyPrefix)
|
||||
if err != nil {
|
||||
if err := db.SubscribePrefixContext(ctx, mod.onEventSubEvent, eventsub.EventKeyPrefix); err != nil {
|
||||
logger.Error("Could not setup twitch alert subscription", zap.Error(err))
|
||||
}
|
||||
|
||||
|
@ -91,12 +86,3 @@ func Setup(db database.Database, logger *zap.Logger, templater template2.Engine)
|
|||
|
||||
return mod
|
||||
}
|
||||
|
||||
func (m *Module) Close() {
|
||||
if m.cancelAlertSub != nil {
|
||||
m.cancelAlertSub()
|
||||
}
|
||||
if m.cancelTwitchEventSub != nil {
|
||||
m.cancelTwitchEventSub()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,6 @@ package chat
|
|||
const ConfigKey = "twitch/chat/config"
|
||||
|
||||
type Config struct {
|
||||
// How many messages to keep in twitch/chat-history
|
||||
ChatHistory int `json:"chat_history" desc:"How many messages to keep in the chat history key"`
|
||||
|
||||
// Global command cooldown in seconds
|
||||
CommandCooldown int `json:"command_cooldown" desc:"Global command cooldown in seconds"`
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package chat
|
||||
|
||||
const (
|
||||
EventKey = "twitch/chat/ev/message"
|
||||
HistoryKey = "twitch/chat/history"
|
||||
ActivityKey = "twitch/chat/activity"
|
||||
CustomCommandsKey = "twitch/chat/custom-commands"
|
||||
WriteMessageRPC = "twitch/chat/@send-message"
|
||||
|
|
|
@ -24,13 +24,12 @@ type Module struct {
|
|||
Config Config
|
||||
|
||||
ctx context.Context
|
||||
db *database.LocalDBClient
|
||||
db database.Database
|
||||
api *helix.Client
|
||||
user *helix.User
|
||||
user helix.User
|
||||
logger *zap.Logger
|
||||
templater template.Engine
|
||||
lastMessage *sync.RWSync[time.Time]
|
||||
chatHistory *sync.Slice[helix.EventSubChannelChatMessageEvent]
|
||||
|
||||
commands *sync.Map[string, Command]
|
||||
customCommands *sync.Map[string, CustomCommand]
|
||||
|
@ -38,16 +37,11 @@ type Module struct {
|
|||
customFunctions textTemplate.FuncMap
|
||||
|
||||
cancelContext context.CancelFunc
|
||||
cancelUpdateSub database.CancelFunc
|
||||
cancelWriteRPCSub database.CancelFunc
|
||||
cancelChatMessageSub database.CancelFunc
|
||||
}
|
||||
|
||||
func Setup(ctx context.Context, db *database.LocalDBClient, api *helix.Client, user *helix.User, logger *zap.Logger, templater template.Engine) *Module {
|
||||
newContext, cancel := context.WithCancel(ctx)
|
||||
|
||||
func Setup(ctx context.Context, db database.Database, api *helix.Client, user helix.User, logger *zap.Logger, templater template.Engine) *Module {
|
||||
mod := &Module{
|
||||
ctx: newContext,
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
api: api,
|
||||
user: user,
|
||||
|
@ -59,15 +53,12 @@ func Setup(ctx context.Context, db *database.LocalDBClient, api *helix.Client, u
|
|||
customCommands: sync.NewMap[string, CustomCommand](),
|
||||
customTemplates: sync.NewMap[string, *textTemplate.Template](),
|
||||
customFunctions: make(textTemplate.FuncMap),
|
||||
|
||||
cancelContext: cancel,
|
||||
}
|
||||
|
||||
// Get config
|
||||
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
||||
if errors.Is(err, database.ErrEmptyKey) {
|
||||
mod.Config = Config{
|
||||
ChatHistory: 0,
|
||||
CommandCooldown: 2,
|
||||
}
|
||||
} else {
|
||||
|
@ -75,16 +66,13 @@ func Setup(ctx context.Context, db *database.LocalDBClient, api *helix.Client, u
|
|||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
mod.cancelChatMessageSub, err = db.SubscribeKey(eventsub.EventKeyPrefix+helix.EventSubTypeChannelChatMessage, mod.onChatMessage)
|
||||
if err != nil {
|
||||
if err := db.SubscribeKeyContext(ctx, eventsub.EventKeyPrefix+helix.EventSubTypeChannelChatMessage, mod.onChatMessage); err != nil {
|
||||
logger.Error("Could not subscribe to chat messages", zap.Error(err))
|
||||
}
|
||||
|
||||
// Load custom commands
|
||||
var customCommands map[string]CustomCommand
|
||||
err = db.GetJSON(CustomCommandsKey, &customCommands)
|
||||
if err != nil {
|
||||
if err := db.GetJSON(CustomCommandsKey, &customCommands); err != nil {
|
||||
if errors.Is(err, database.ErrEmptyKey) {
|
||||
customCommands = make(map[string]CustomCommand)
|
||||
} else {
|
||||
|
@ -93,48 +81,33 @@ func Setup(ctx context.Context, db *database.LocalDBClient, api *helix.Client, u
|
|||
}
|
||||
mod.customCommands.Set(customCommands)
|
||||
|
||||
err = mod.updateTemplates()
|
||||
if err != nil {
|
||||
if err := mod.updateTemplates(); err != nil {
|
||||
logger.Error("Failed to parse custom commands", zap.Error(err))
|
||||
}
|
||||
mod.cancelUpdateSub, err = db.SubscribeKey(CustomCommandsKey, mod.updateCommands)
|
||||
if err != nil {
|
||||
|
||||
if err := db.SubscribeKeyContext(ctx, CustomCommandsKey, mod.updateCommands); err != nil {
|
||||
logger.Error("Could not set-up chat command reload subscription", zap.Error(err))
|
||||
}
|
||||
mod.cancelWriteRPCSub, err = db.SubscribeKey(WriteMessageRPC, mod.handleWriteMessageRPC)
|
||||
if err != nil {
|
||||
|
||||
if err := db.SubscribeKeyContext(ctx, WriteMessageRPC, mod.handleWriteMessageRPC); err != nil {
|
||||
logger.Error("Could not set-up chat command reload subscription", zap.Error(err))
|
||||
}
|
||||
|
||||
return mod
|
||||
}
|
||||
|
||||
func (mod *Module) Close() {
|
||||
if mod.cancelChatMessageSub != nil {
|
||||
mod.cancelChatMessageSub()
|
||||
}
|
||||
|
||||
if mod.cancelUpdateSub != nil {
|
||||
mod.cancelUpdateSub()
|
||||
}
|
||||
|
||||
if mod.cancelWriteRPCSub != nil {
|
||||
mod.cancelWriteRPCSub()
|
||||
}
|
||||
|
||||
mod.cancelContext()
|
||||
}
|
||||
|
||||
func (mod *Module) onChatMessage(newValue string) {
|
||||
var chatMessage helix.EventSubChannelChatMessageEvent
|
||||
if err := json.Unmarshal([]byte(newValue), &chatMessage); err != nil {
|
||||
var chatMessage struct {
|
||||
Event helix.EventSubChannelChatMessageEvent `json:"event"`
|
||||
}
|
||||
if err := json.UnmarshalFromString(newValue, &chatMessage); err != nil {
|
||||
mod.logger.Error("Failed to decode incoming chat message", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO Command cooldown logic here!
|
||||
|
||||
lowercaseMessage := strings.TrimSpace(strings.ToLower(chatMessage.Message.Text))
|
||||
lowercaseMessage := strings.TrimSpace(strings.ToLower(chatMessage.Event.Message.Text))
|
||||
|
||||
// Check if it's a command
|
||||
if strings.HasPrefix(lowercaseMessage, "!") {
|
||||
|
@ -150,7 +123,7 @@ func (mod *Module) onChatMessage(newValue string) {
|
|||
if parts[0] != cmd {
|
||||
continue
|
||||
}
|
||||
go data.Handler(chatMessage)
|
||||
go data.Handler(chatMessage.Event)
|
||||
mod.lastMessage.Set(time.Now())
|
||||
}
|
||||
}
|
||||
|
@ -168,25 +141,9 @@ func (mod *Module) onChatMessage(newValue string) {
|
|||
if parts[0] != lc {
|
||||
continue
|
||||
}
|
||||
go cmdCustom(mod, cmd, data, chatMessage)
|
||||
go cmdCustom(mod, cmd, data, chatMessage.Event)
|
||||
mod.lastMessage.Set(time.Now())
|
||||
}
|
||||
|
||||
err := mod.db.PutJSON(EventKey, chatMessage)
|
||||
if err != nil {
|
||||
mod.logger.Warn("Could not save chat message to key", zap.Error(err))
|
||||
}
|
||||
if mod.Config.ChatHistory > 0 {
|
||||
history := mod.chatHistory.Get()
|
||||
if len(history) >= mod.Config.ChatHistory {
|
||||
history = history[len(history)-mod.Config.ChatHistory+1:]
|
||||
}
|
||||
mod.chatHistory.Set(append(history, chatMessage))
|
||||
err = mod.db.PutJSON(HistoryKey, mod.chatHistory.Get())
|
||||
if err != nil {
|
||||
mod.logger.Warn("Could not save message to chat history", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mod *Module) handleWriteMessageRPC(value string) {
|
||||
|
|
|
@ -6,16 +6,17 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
||||
|
||||
"git.sr.ht/~ashkeel/containers/sync"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/nicklaw5/helix/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"git.sr.ht/~ashkeel/strimertul/database"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/alerts"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/chat"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/eventsub"
|
||||
"git.sr.ht/~ashkeel/strimertul/twitch/timers"
|
||||
"git.sr.ht/~ashkeel/strimertul/webserver"
|
||||
)
|
||||
|
||||
|
@ -23,10 +24,9 @@ var json = jsoniter.ConfigFastest
|
|||
|
||||
type Manager struct {
|
||||
client *Client
|
||||
cancelSubs func()
|
||||
}
|
||||
|
||||
func NewManager(db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Manager, error) {
|
||||
func NewManager(ctx context.Context, db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Manager, error) {
|
||||
// Get Twitch config
|
||||
var config twitch.Config
|
||||
if err := db.GetJSON(twitch.ConfigKey, &config); err != nil {
|
||||
|
@ -37,8 +37,10 @@ func NewManager(db database.Database, server *webserver.WebServer, logger *zap.L
|
|||
}
|
||||
|
||||
// Create new client
|
||||
client, err := newClient(config, db, server, logger)
|
||||
clientContext, cancel := context.WithCancel(ctx)
|
||||
client, err := newClient(clientContext, config, db, server, logger)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to create twitch client: %w", err)
|
||||
}
|
||||
|
||||
|
@ -47,41 +49,32 @@ func NewManager(db database.Database, server *webserver.WebServer, logger *zap.L
|
|||
}
|
||||
|
||||
// Listen for client config changes
|
||||
cancelConfigSub, err := db.SubscribeKey(twitch.ConfigKey, func(value string) {
|
||||
if err = db.SubscribeKeyContext(ctx, twitch.ConfigKey, func(value string) {
|
||||
var newConfig twitch.Config
|
||||
if err := json.UnmarshalFromString(value, &newConfig); err != nil {
|
||||
logger.Error("Failed to decode Twitch integration config", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
var updatedClient *Client
|
||||
updatedClient, err = newClient(newConfig, db, server, logger)
|
||||
clientContext, cancel = context.WithCancel(ctx)
|
||||
updatedClient, err = newClient(clientContext, newConfig, db, server, logger)
|
||||
if err != nil {
|
||||
logger.Error("Could not create twitch client with new config, keeping old", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = manager.client.Close()
|
||||
if err != nil {
|
||||
logger.Warn("Twitch client could not close cleanly", zap.Error(err))
|
||||
}
|
||||
|
||||
// New client works, replace old
|
||||
updatedClient.Merge(manager.client)
|
||||
manager.client = updatedClient
|
||||
|
||||
logger.Info("Reloaded/updated Twitch integration")
|
||||
})
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
logger.Error("Could not setup twitch config reload subscription", zap.Error(err))
|
||||
}
|
||||
|
||||
manager.cancelSubs = func() {
|
||||
if cancelConfigSub != nil {
|
||||
cancelConfigSub()
|
||||
}
|
||||
}
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
|
@ -89,16 +82,6 @@ func (m *Manager) Client() *Client {
|
|||
return m.client
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.cancelSubs()
|
||||
|
||||
if err := m.client.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Config *sync.RWSync[twitch.Config]
|
||||
DB database.Database
|
||||
|
@ -106,6 +89,10 @@ type Client struct {
|
|||
User helix.User
|
||||
Logger *zap.Logger
|
||||
|
||||
Chat *chat.Module
|
||||
Alerts *alerts.Module
|
||||
Timers *timers.Module
|
||||
|
||||
eventSub *eventsub.Client
|
||||
server *webserver.WebServer
|
||||
ctx context.Context
|
||||
|
@ -128,9 +115,8 @@ func (c *Client) ensureRoute() {
|
|||
}
|
||||
}
|
||||
|
||||
func newClient(config twitch.Config, db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Client, error) {
|
||||
func newClient(ctx context.Context, config twitch.Config, db database.Database, server *webserver.WebServer, logger *zap.Logger) (*Client, error) {
|
||||
// Create Twitch client
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
client := &Client{
|
||||
Config: sync.NewRWSync(config),
|
||||
DB: db,
|
||||
|
@ -138,11 +124,13 @@ func newClient(config twitch.Config, db database.Database, server *webserver.Web
|
|||
restart: make(chan bool, 128),
|
||||
streamOnline: sync.NewRWSync(false),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
server: server,
|
||||
}
|
||||
|
||||
if config.Enabled {
|
||||
if !config.Enabled {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
client.API, err = twitch.GetHelixAPI(db)
|
||||
if err != nil {
|
||||
|
@ -158,18 +146,28 @@ func newClient(config twitch.Config, db database.Database, server *webserver.Web
|
|||
} 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", zap.String("user", users.Data.Users[0].ID))
|
||||
client.User = users.Data.Users[0]
|
||||
client.eventSub, err = eventsub.Setup(ctx, userClient, client.User, db, logger)
|
||||
if err != nil {
|
||||
client.Logger.Error("Failed to setup EventSub", zap.Error(err))
|
||||
}
|
||||
|
||||
tpl := client.GetTemplateEngine()
|
||||
client.Chat = chat.Setup(ctx, db, userClient, client.User, logger, tpl)
|
||||
client.Alerts = alerts.Setup(ctx, db, logger, tpl)
|
||||
client.Timers = timers.Setup(ctx, db, logger)
|
||||
}
|
||||
} else {
|
||||
client.Logger.Warn("Twitch user not identified, this will break most features")
|
||||
}
|
||||
|
||||
go client.runStatusPoll()
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
server.UnregisterRoute(twitch.CallbackRoute)
|
||||
}()
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
@ -185,7 +183,7 @@ func (c *Client) runStatusPoll() {
|
|||
// Check if streamer is online, if possible
|
||||
func() {
|
||||
status, err := c.API.GetStreams(&helix.StreamsParams{
|
||||
UserLogins: []string{c.Config.Get().Channel}, // TODO Replace with something non bot dependant
|
||||
UserIDs: []string{c.User.ID},
|
||||
})
|
||||
if err != nil {
|
||||
c.Logger.Error("Error checking stream status", zap.Error(err))
|
||||
|
@ -207,18 +205,3 @@ func (c *Client) runStatusPoll() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) baseURL() (string, error) {
|
||||
var severConfig struct {
|
||||
Bind string `json:"bind"`
|
||||
}
|
||||
err := c.DB.GetJSON("http/config", &severConfig)
|
||||
return severConfig.Bind, err
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
c.server.UnregisterRoute(twitch.CallbackRoute)
|
||||
defer c.cancel()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -7,15 +7,9 @@ type Config struct {
|
|||
// Enable subsystem
|
||||
Enabled bool `json:"enabled" desc:"Enable subsystem"`
|
||||
|
||||
// Enable the chatbot
|
||||
EnableBot bool `json:"enable_bot" desc:"Enable the chatbot"`
|
||||
|
||||
// Twitch API App Client ID
|
||||
APIClientID string `json:"api_client_id" desc:"Twitch API App Client ID"`
|
||||
|
||||
// Twitch API App Client Secret
|
||||
APIClientSecret string `json:"api_client_secret" desc:"Twitch API App Client Secret"`
|
||||
|
||||
// Twitch channel to use
|
||||
Channel string `json:"channel" desc:"Twitch channel to join and use"`
|
||||
}
|
||||
|
|
|
@ -32,16 +32,6 @@ var Keys = interfaces.KeyMap{
|
|||
Description: "Configuration for chat-related features",
|
||||
Type: reflect.TypeOf(chat.Config{}),
|
||||
},
|
||||
chat.EventKey: interfaces.KeyDef{
|
||||
Description: "On chat message received",
|
||||
Type: reflect.TypeOf(helix.EventSubChannelChatMessageEvent{}),
|
||||
Tags: []interfaces.KeyTag{interfaces.TagEvent},
|
||||
},
|
||||
chat.HistoryKey: interfaces.KeyDef{
|
||||
Description: "Last chat messages received",
|
||||
Type: reflect.TypeOf([]helix.EventSubChannelChatMessageEvent{}),
|
||||
Tags: []interfaces.KeyTag{interfaces.TagHistory},
|
||||
},
|
||||
chat.ActivityKey: interfaces.KeyDef{
|
||||
Description: "Number of chat messages in the last minute",
|
||||
Type: reflect.TypeOf(0),
|
||||
|
|
|
@ -183,7 +183,6 @@ func (c *Client) processEvent(message WebsocketMessage) {
|
|||
eventKey := fmt.Sprintf("%s%s", EventKeyPrefix, notificationData.Subscription.Type)
|
||||
historyKey := fmt.Sprintf("%s%s", HistoryKeyPrefix, notificationData.Subscription.Type)
|
||||
err = c.db.PutJSON(eventKey, notificationData)
|
||||
c.logger.Info("Stored event", zap.String("key", eventKey), zap.String("notification-type", notificationData.Subscription.Type))
|
||||
if err != nil {
|
||||
c.logger.Error("Error storing event to database", zap.String("key", eventKey), zap.Error(err))
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ var scopes = []string{
|
|||
"user:bot",
|
||||
"user:manage:whispers",
|
||||
"user:read:chat",
|
||||
"user:write:chat",
|
||||
"user_read",
|
||||
"whispers:edit",
|
||||
"whispers:read",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package timers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
|
@ -24,14 +25,15 @@ type Module struct {
|
|||
|
||||
logger *zap.Logger
|
||||
db database.Database
|
||||
cancelTimerSub database.CancelFunc
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func Setup(db database.Database, logger *zap.Logger) *Module {
|
||||
func Setup(ctx context.Context, db database.Database, logger *zap.Logger) *Module {
|
||||
mod := &Module{
|
||||
lastTrigger: sync.NewMap[string, time.Time](),
|
||||
messages: sync.NewSlice[int](),
|
||||
db: db,
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
|
@ -42,8 +44,7 @@ func Setup(db database.Database, logger *zap.Logger) *Module {
|
|||
}
|
||||
|
||||
// Load config from database
|
||||
err := db.GetJSON(ConfigKey, &mod.Config)
|
||||
if err != nil {
|
||||
if err := db.GetJSON(ConfigKey, &mod.Config); err != nil {
|
||||
logger.Debug("Config load error", zap.Error(err))
|
||||
mod.Config = Config{
|
||||
Timers: make(map[string]ChatTimer),
|
||||
|
@ -55,15 +56,13 @@ func Setup(db database.Database, logger *zap.Logger) *Module {
|
|||
}
|
||||
}
|
||||
|
||||
mod.cancelTimerSub, err = db.SubscribeKey(ConfigKey, func(value string) {
|
||||
err := json.UnmarshalFromString(value, &mod.Config)
|
||||
if err != nil {
|
||||
logger.Debug("Error reloading timer config", zap.Error(err))
|
||||
} else {
|
||||
logger.Info("Reloaded timer config")
|
||||
if err := db.SubscribeKeyContext(ctx, ConfigKey, func(value string) {
|
||||
if err := json.UnmarshalFromString(value, &mod.Config); err != nil {
|
||||
logger.Warn("Error reloading timer config", zap.Error(err))
|
||||
return
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
logger.Info("Reloaded timer config")
|
||||
}); err != nil {
|
||||
logger.Error("Could not set-up timer reload subscription", zap.Error(err))
|
||||
}
|
||||
|
||||
|
@ -146,12 +145,6 @@ func (m *Module) ProcessTimer(name string, timer ChatTimer, activity int) {
|
|||
m.lastTrigger.SetKey(name, now)
|
||||
}
|
||||
|
||||
func (m *Module) Close() {
|
||||
if m.cancelTimerSub != nil {
|
||||
m.cancelTimerSub()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) currentChatActivity() int {
|
||||
total := 0
|
||||
for _, v := range m.messages.Get() {
|
||||
|
|
Loading…
Reference in a new issue