Remove twitter notification configuration (#2598)

This commit is contained in:
Michael David Kuckuk 2023-01-17 22:20:29 +01:00 committed by GitHub
parent 392da72c8b
commit 59e5cfefd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 0 additions and 477 deletions

View file

@ -58,28 +58,3 @@ func SetBrowserNotificationConfiguration(w http.ResponseWriter, r *http.Request)
controllers.WriteSimpleResponse(w, true, "updated browser push config with provided values") controllers.WriteSimpleResponse(w, true, "updated browser push config with provided values")
} }
// SetTwitterConfiguration will set the browser notification configuration.
func SetTwitterConfiguration(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type request struct {
Value models.TwitterConfiguration `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var config request
if err := decoder.Decode(&config); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update twitter config with provided values")
return
}
if err := data.SetTwitterConfiguration(config.Value); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update twitter config with provided values")
return
}
controllers.WriteSimpleResponse(w, true, "updated twitter config with provided values")
}

View file

@ -84,7 +84,6 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
Notifications: notificationsConfigResponse{ Notifications: notificationsConfigResponse{
Discord: data.GetDiscordConfig(), Discord: data.GetDiscordConfig(),
Browser: data.GetBrowserPushConfig(), Browser: data.GetBrowserPushConfig(),
Twitter: data.GetTwitterConfiguration(),
}, },
} }
@ -160,5 +159,4 @@ type federationConfigResponse struct {
type notificationsConfigResponse struct { type notificationsConfigResponse struct {
Browser models.BrowserNotificationConfiguration `json:"browser"` Browser models.BrowserNotificationConfiguration `json:"browser"`
Discord models.DiscordConfiguration `json:"discord"` Discord models.DiscordConfiguration `json:"discord"`
Twitter models.TwitterConfiguration `json:"twitter"`
} }

View file

@ -63,7 +63,6 @@ const (
browserPushConfigurationKey = "browser_push_configuration" browserPushConfigurationKey = "browser_push_configuration"
browserPushPublicKeyKey = "browser_push_public_key" browserPushPublicKeyKey = "browser_push_public_key"
browserPushPrivateKeyKey = "browser_push_private_key" browserPushPrivateKeyKey = "browser_push_private_key"
twitterConfigurationKey = "twitter_configuration"
hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications" hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications"
hideViewerCountKey = "hide_viewer_count" hideViewerCountKey = "hide_viewer_count"
customOfflineMessageKey = "custom_offline_message" customOfflineMessageKey = "custom_offline_message"
@ -880,27 +879,6 @@ func GetBrowserPushPrivateKey() (string, error) {
return _datastore.GetString(browserPushPrivateKeyKey) return _datastore.GetString(browserPushPrivateKeyKey)
} }
// SetTwitterConfiguration will set the Twitter configuration.
func SetTwitterConfiguration(config models.TwitterConfiguration) error {
configEntry := ConfigEntry{Key: twitterConfigurationKey, Value: config}
return _datastore.Save(configEntry)
}
// GetTwitterConfiguration will return the Twitter configuration.
func GetTwitterConfiguration() models.TwitterConfiguration {
configEntry, err := _datastore.Get(twitterConfigurationKey)
if err != nil {
return models.TwitterConfiguration{Enabled: false}
}
var config models.TwitterConfiguration
if err := configEntry.getObject(&config); err != nil {
return models.TwitterConfiguration{Enabled: false}
}
return config
}
// SetHasPerformedInitialNotificationsConfig sets when performed initial setup. // SetHasPerformedInitialNotificationsConfig sets when performed initial setup.
func SetHasPerformedInitialNotificationsConfig(hasConfigured bool) error { func SetHasPerformedInitialNotificationsConfig(hasConfigured bool) error {
return _datastore.SetBool(hasConfiguredInitialNotificationsKey, true) return _datastore.SetBool(hasConfiguredInitialNotificationsKey, true)

View file

@ -56,7 +56,6 @@ Owncast Dependencies
- HTTP routing https://github.com/gorilla/mux - HTTP routing https://github.com/gorilla/mux
- Mastodon API https://github.com/mattn/go-mastodon - Mastodon API https://github.com/mattn/go-mastodon
- Twitter API https://github.com/ChimeraCoder/anaconda
- Go ORM https://gorm.io/gorm - Go ORM https://gorm.io/gorm
- ID string generator https://github.com/teris-io/shortid - ID string generator https://github.com/teris-io/shortid
- Slug generator https://github.com/metal3d/go-slugify - Slug generator https://github.com/metal3d/go-slugify

1
go.mod
View file

@ -77,7 +77,6 @@ require github.com/SherClockHolmes/webpush-go v1.2.0
require ( require (
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/dghubble/oauth1 v0.7.2 github.com/dghubble/oauth1 v0.7.2
github.com/g8rswimmer/go-twitter/v2 v2.1.5
github.com/go-test/deep v1.0.4 // indirect github.com/go-test/deep v1.0.4 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/css v1.0.0 // indirect

2
go.sum
View file

@ -70,8 +70,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/g8rswimmer/go-twitter/v2 v2.1.5 h1:Uj9Yuof2UducrP4Xva7irnUJfB9354/VyUXKmc2D5gg=
github.com/g8rswimmer/go-twitter/v2 v2.1.5/go.mod h1:/55xWb313KQs25X7oZrNSEwLQNkYHhPsDwFstc45vhc=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=

View file

@ -14,14 +14,3 @@ type BrowserNotificationConfiguration struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
GoLiveMessage string `json:"goLiveMessage,omitempty"` GoLiveMessage string `json:"goLiveMessage,omitempty"`
} }
// TwitterConfiguration represents the configuration for Twitter access.
type TwitterConfiguration struct {
Enabled bool `json:"enabled"`
APIKey string `json:"apiKey"` // aka consumer key
APISecret string `json:"apiSecret"` // aka consumer secret
AccessToken string `json:"accessToken"`
AccessTokenSecret string `json:"accessTokenSecret"`
BearerToken string `json:"bearerToken"`
GoLiveMessage string `json:"goLiveMessage,omitempty"`
}

View file

@ -2,15 +2,12 @@ package notifications
import ( import (
"fmt" "fmt"
"strings"
"github.com/owncast/owncast/config" "github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
"github.com/owncast/owncast/notifications/browser" "github.com/owncast/owncast/notifications/browser"
"github.com/owncast/owncast/notifications/discord" "github.com/owncast/owncast/notifications/discord"
"github.com/owncast/owncast/notifications/twitter"
"github.com/owncast/owncast/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -20,7 +17,6 @@ type Notifier struct {
datastore *data.Datastore datastore *data.Datastore
browser *browser.Browser browser *browser.Browser
discord *discord.Discord discord *discord.Discord
twitter *twitter.Twitter
} }
// Setup will perform any pre-use setup for the notifier. // Setup will perform any pre-use setup for the notifier.
@ -68,9 +64,6 @@ func New(datastore *data.Datastore) (*Notifier, error) {
if err := notifier.setupDiscord(); err != nil { if err := notifier.setupDiscord(); err != nil {
log.Error(err) log.Error(err)
} }
if err := notifier.setupTwitter(); err != nil {
log.Errorln(err)
}
return &notifier, nil return &notifier, nil
} }
@ -147,36 +140,6 @@ func (n *Notifier) notifyDiscord() {
} }
} }
func (n *Notifier) setupTwitter() error {
if twitterConfig := data.GetTwitterConfiguration(); twitterConfig.Enabled {
if t, err := twitter.New(twitterConfig.APIKey, twitterConfig.APISecret, twitterConfig.AccessToken, twitterConfig.AccessTokenSecret, twitterConfig.BearerToken); err == nil {
n.twitter = t
} else if err != nil {
return errors.Wrap(err, "error creating twitter notifier")
}
}
return nil
}
func (n *Notifier) notifyTwitter() {
goLiveMessage := data.GetTwitterConfiguration().GoLiveMessage
streamTitle := data.GetStreamTitle()
if streamTitle != "" {
goLiveMessage += "\n" + streamTitle
}
tagString := ""
for _, tag := range utils.ShuffleStringSlice(data.GetServerMetadataTags()) {
tagString = fmt.Sprintf("%s #%s", tagString, tag)
}
tagString = strings.TrimSpace(tagString)
message := fmt.Sprintf("%s\n%s\n\n%s", goLiveMessage, data.GetServerURL(), tagString)
if err := n.twitter.Notify(message); err != nil {
log.Errorln("error sending twitter message", err)
}
}
// Notify will fire the different notification channels. // Notify will fire the different notification channels.
func (n *Notifier) Notify() { func (n *Notifier) Notify() {
if n.browser != nil { if n.browser != nil {
@ -186,8 +149,4 @@ func (n *Notifier) Notify() {
if n.discord != nil { if n.discord != nil {
n.notifyDiscord() n.notifyDiscord()
} }
if n.twitter != nil {
n.notifyTwitter()
}
} }

View file

@ -1,78 +0,0 @@
package twitter
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/dghubble/oauth1"
"github.com/g8rswimmer/go-twitter/v2"
)
/*
1. developer.twitter.com. Apply to be a developer if needed.
2. Projects and apps -> Your project name
3. Settings.
4. Scroll down to"User authentication settings" Edit
5. Enable OAuth 1.0a with Read/Write permissions.
6. Fill out the form with your information. Callback can be anything.
7. Go to your project "Keys and tokens"
8. Generate API key and secret.
9. Generate access token and secret. Verify it says "Read and write permissions."
10. Generate bearer token.
*/
type authorize struct {
Token string
}
func (a authorize) Add(req *http.Request) {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", a.Token))
}
// Twitter is an instance of the Twitter notifier.
type Twitter struct {
apiKey string
apiSecret string
accessToken string
accessTokenSecret string
bearerToken string
}
// New returns a new instance of the Twitter notifier.
func New(apiKey, apiSecret, accessToken, accessTokenSecret, bearerToken string) (*Twitter, error) {
if apiKey == "" || apiSecret == "" || accessToken == "" || accessTokenSecret == "" || bearerToken == "" {
return nil, errors.New("missing some or all of the required twitter configuration values")
}
return &Twitter{
apiKey: apiKey,
apiSecret: apiSecret,
accessToken: accessToken,
accessTokenSecret: accessTokenSecret,
bearerToken: bearerToken,
}, nil
}
// Notify will send a notification to Twitter with the supplied text.
func (t *Twitter) Notify(text string) error {
config := oauth1.NewConfig(t.apiKey, t.apiSecret)
token := oauth1.NewToken(t.accessToken, t.accessTokenSecret)
httpClient := config.Client(oauth1.NoContext, token)
client := &twitter.Client{
Authorizer: authorize{
Token: t.bearerToken,
},
Client: httpClient,
Host: "https://api.twitter.com",
}
req := twitter.CreateTweetRequest{
Text: text,
}
_, err := client.CreateTweet(context.Background(), req)
return err
}

View file

@ -366,7 +366,6 @@ func Start() error {
// Configure outbound notification channels. // Configure outbound notification channels.
http.HandleFunc("/api/admin/config/notifications/discord", middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)) http.HandleFunc("/api/admin/config/notifications/discord", middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration))
http.HandleFunc("/api/admin/config/notifications/browser", middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)) http.HandleFunc("/api/admin/config/notifications/browser", middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration))
http.HandleFunc("/api/admin/config/notifications/twitter", middleware.RequireAdminAuth(admin.SetTwitterConfiguration))
// Auth // Auth

View file

@ -1,220 +0,0 @@
import { Button, Typography } from 'antd';
import React, { useState, useContext, useEffect } from 'react';
import { ServerStatusContext } from '../../../utils/server-status-context';
import { TextField, TEXTFIELD_TYPE_PASSWORD } from '../TextField';
import { FormStatusIndicator } from '../FormStatusIndicator';
import {
postConfigUpdateToAPI,
RESET_TIMEOUT,
TWITTER_CONFIG_FIELDS,
} from '../../../utils/config-constants';
import { ToggleSwitch } from '../ToggleSwitch';
import {
createInputStatus,
StatusState,
STATUS_ERROR,
STATUS_SUCCESS,
} from '../../../utils/input-statuses';
import { UpdateArgs } from '../../../types/config-section';
import { TEXTFIELD_TYPE_TEXT } from '../TextFieldWithSubmit';
const { Title } = Typography;
export const ConfigNotify = () => {
const serverStatusData = useContext(ServerStatusContext);
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
const { notifications } = serverConfig || {};
const { twitter } = notifications || {};
const [formDataValues, setFormDataValues] = useState<any>({});
const [submitStatus, setSubmitStatus] = useState<StatusState>(null);
const [enableSaveButton, setEnableSaveButton] = useState<boolean>(false);
useEffect(() => {
const {
enabled,
apiKey,
apiSecret,
accessToken,
accessTokenSecret,
bearerToken,
goLiveMessage,
} = twitter || {};
setFormDataValues({
enabled,
apiKey,
apiSecret,
accessToken,
accessTokenSecret,
bearerToken,
goLiveMessage,
});
}, [twitter]);
const canSave = (): boolean => {
const { apiKey, apiSecret, accessToken, accessTokenSecret, bearerToken, goLiveMessage } =
formDataValues;
return (
!!apiKey &&
!!apiSecret &&
!!accessToken &&
!!accessTokenSecret &&
!!bearerToken &&
!!goLiveMessage
);
};
useEffect(() => {
setEnableSaveButton(canSave());
}, [formDataValues]);
// update individual values in state
const handleFieldChange = ({ fieldName, value }: UpdateArgs) => {
setFormDataValues({
...formDataValues,
[fieldName]: value,
});
};
// toggle switch.
const handleSwitchChange = (switchEnabled: boolean) => {
const previouslySaved = formDataValues.enabled;
handleFieldChange({ fieldName: 'enabled', value: switchEnabled });
return switchEnabled !== previouslySaved;
};
let resetTimer = null;
const resetStates = () => {
setSubmitStatus(null);
resetTimer = null;
clearTimeout(resetTimer);
setEnableSaveButton(false);
};
const save = async () => {
const postValue = formDataValues;
await postConfigUpdateToAPI({
apiPath: '/notifications/twitter',
data: { value: postValue },
onSuccess: () => {
setFieldInConfigState({
fieldName: 'twitter',
value: postValue,
path: 'notifications',
});
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
onError: (message: string) => {
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
},
});
};
return (
<>
<Title>Twitter</Title>
<p className="description reduced-margins">
Let your Twitter followers know each time you go live.
</p>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<p className="description reduced-margins">
<a href="https://owncast.online/docs/notifications" target="_blank" rel="noreferrer">
Read how to configure your Twitter account
</a>{' '}
to support posting from Owncast.
</p>
<p className="description reduced-margins">
<a
href="https://developer.twitter.com/en/portal/dashboard"
target="_blank"
rel="noreferrer"
>
And then get your Twitter developer credentials
</a>{' '}
to fill in below.
</p>
</div>
<ToggleSwitch
apiPath=""
fieldName="enabled"
label="Enable Twitter"
onChange={handleSwitchChange}
checked={formDataValues.enabled}
/>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.apiKey}
required
value={formDataValues.apiKey}
onChange={handleFieldChange}
/>
</div>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.apiSecret}
type={TEXTFIELD_TYPE_PASSWORD}
required
value={formDataValues.apiSecret}
onChange={handleFieldChange}
/>
</div>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.accessToken}
required
value={formDataValues.accessToken}
onChange={handleFieldChange}
/>
</div>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.accessTokenSecret}
type={TEXTFIELD_TYPE_PASSWORD}
required
value={formDataValues.accessTokenSecret}
onChange={handleFieldChange}
/>
</div>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.bearerToken}
required
value={formDataValues.bearerToken}
onChange={handleFieldChange}
/>
</div>
<div style={{ display: formDataValues.enabled ? 'block' : 'none' }}>
<TextField
{...TWITTER_CONFIG_FIELDS.goLiveMessage}
type={TEXTFIELD_TYPE_TEXT}
required
value={formDataValues.goLiveMessage}
onChange={handleFieldChange}
/>
</div>
<Button
type="primary"
onClick={save}
style={{
display: enableSaveButton ? 'inline-block' : 'none',
position: 'relative',
marginLeft: 'auto',
right: '0',
marginTop: '20px',
}}
>
Save
</Button>
<FormStatusIndicator status={submitStatus} />
</>
);
};
export default ConfigNotify;

View file

@ -4,7 +4,6 @@ import Link from 'next/link';
import Discord from '../../components/admin/notification/discord'; import Discord from '../../components/admin/notification/discord';
import Browser from '../../components/admin/notification/browser'; import Browser from '../../components/admin/notification/browser';
import Twitter from '../../components/admin/notification/twitter';
import Federation from '../../components/admin/notification/federation'; import Federation from '../../components/admin/notification/federation';
import { import {
TextFieldWithSubmit, TextFieldWithSubmit,
@ -99,13 +98,6 @@ export default function ConfigNotify() {
> >
<Browser /> <Browser />
</Col> </Col>
<Col
span={10}
className={`form-module ${enabled ? '' : 'disabled'}`}
style={{ margin: '5px', display: 'flex', flexDirection: 'column' }}
>
<Twitter />
</Col>
<Col <Col
span={10} span={10}

View file

@ -115,20 +115,9 @@ export interface DiscordNotification {
goLiveMessage: string; goLiveMessage: string;
} }
export interface TwitterNotification {
enabled: boolean;
apiKey: string;
apiSecret: string;
accessToken: string;
accessTokenSecret: string;
bearerToken: string;
goLiveMessage: string;
}
export interface NotificationsConfig { export interface NotificationsConfig {
browser: BrowserNotification; browser: BrowserNotification;
discord: DiscordNotification; discord: DiscordNotification;
twitter: TwitterNotification;
} }
export interface Health { export interface Health {

View file

@ -557,48 +557,3 @@ export const BROWSER_PUSH_CONFIG_FIELDS = {
placeholder: `I've gone live! Come watch!`, placeholder: `I've gone live! Come watch!`,
}, },
}; };
export const TWITTER_CONFIG_FIELDS = {
apiKey: {
fieldName: 'apiKey',
label: 'API Key',
maxLength: 200,
tip: '',
placeholder: `gaUQhRC2lqfrEFfElBXJgOctU`,
},
apiSecret: {
fieldName: 'apiSecret',
label: 'API Secret',
maxLength: 200,
tip: '',
placeholder: `IIz4jFZMWbUKdFOEGUprFjRwIslG56d1SPQlolJYjXwJ2y2qKS`,
},
accessToken: {
fieldName: 'accessToken',
label: 'Access Token',
maxLength: 200,
tip: '',
placeholder: `952540400-EEiwe9fkuSvWjnNC82YFa9kgpqbyAP3J7FjE2dkka`,
},
accessTokenSecret: {
fieldName: 'accessTokenSecret',
label: 'Access Token Secret',
maxLength: 200,
tip: '',
placeholder: `xO0AZWNGfZxpNsYPg3zNEKhAsPPGvNZFlzQArA2khI9Kg`,
},
bearerToken: {
fieldName: 'bearerToken',
label: 'Bearer Token',
maxLength: 200,
tip: '',
placeholder: `AAAAAAAAAAAAAAFqpXwEAAnnepHkjA8XD5ftx5jUadYIRtPtaq7AAAAwpXPpDWKDcdhiWr0tVDjsgW%2B4awGOM9VQ%3XPoMFuWcHsE42TK`,
},
goLiveMessage: {
fieldName: 'goLiveMessage',
label: 'Go Live Text',
maxLength: 200,
tip: 'The text to send when you go live.',
placeholder: `I've gone live! Come watch!`,
},
};

View file

@ -60,15 +60,6 @@ export const initialServerConfigState: ConfigDetails = {
notifications: { notifications: {
browser: { enabled: false, goLiveMessage: '' }, browser: { enabled: false, goLiveMessage: '' },
discord: { enabled: false, webhook: '', goLiveMessage: '' }, discord: { enabled: false, webhook: '', goLiveMessage: '' },
twitter: {
enabled: false,
goLiveMessage: '',
apiKey: '',
apiSecret: '',
accessToken: '',
accessTokenSecret: '',
bearerToken: '',
},
}, },
externalActions: [], externalActions: [],
supportedCodecs: [], supportedCodecs: [],