1
0
Fork 0
mirror of https://github.com/Luzifer/twitch-bot.git synced 2025-03-14 02:45:02 +00:00
twitch-bot/src/generalConfig.vue
2023-12-17 13:29:53 +01:00

778 lines
23 KiB
Vue

<template>
<div>
<b-row>
<b-col>
<b-card no-body>
<b-card-header>
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'hashtag']"
/>
Channels
</b-card-header>
<b-list-group flush>
<b-list-group-item
v-for="channel in sortedChannels"
:key="channel"
class="d-flex align-items-center align-middle"
>
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'hashtag']"
/>
{{ channel }}
<span class="ml-auto mr-2">
<font-awesome-icon
v-if="!generalConfig.channel_has_token[channel]"
:id="`channelPublicWarn${channel}`"
fixed-width
class="ml-1 text-danger"
:icon="['fas', 'exclamation-triangle']"
/>
<font-awesome-icon
v-else-if="!hasAllExtendedScopes(channel)"
:id="`channelPublicWarn${channel}`"
fixed-width
class="ml-1 text-warning"
:icon="['fas', 'exclamation-triangle']"
/>
<b-tooltip
:target="`channelPublicWarn${channel}`"
triggers="hover"
>
<template v-if="!generalConfig.channel_has_token[channel]">
Bot is not authorized to access Twitch on behalf of this channels owner (tokens are missing).
Click pencil to grant permissions.
</template>
<template v-else>
Channel is missing {{ missingExtendedScopes(channel).length }} extended permissions.
Click pencil to change granted permissions.
</template>
</b-tooltip>
</span>
<b-button-group size="sm">
<b-button
variant="primary"
@click="editChannelPermissions(channel)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'pencil-alt']"
/>
</b-button>
<b-button
variant="danger"
@click="removeChannel(channel)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'minus']"
/>
</b-button>
</b-button-group>
</b-list-group-item>
<b-list-group-item>
<b-input-group>
<b-form-input
v-model="models.addChannel"
:state="!!validateUserName(models.addChannel)"
@keyup.enter="addChannel"
/>
<b-input-group-append>
<b-button
variant="success"
:disabled="!validateUserName(models.addChannel)"
@click="addChannel"
>
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'plus']"
/>
Add
</b-button>
</b-input-group-append>
</b-input-group>
</b-list-group-item>
</b-list-group>
</b-card>
</b-col>
<b-col>
<b-card
no-body
class="mb-3"
>
<b-card-header>
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'users']"
/>
Bot-Editors
</b-card-header>
<b-list-group flush>
<b-list-group-item
v-for="editor in sortedEditors"
:key="editor"
class="d-flex align-items-center align-middle"
>
<b-avatar
class="mr-3"
:src="userProfiles[editor] ? userProfiles[editor].profile_image_url : ''"
/>
<span class="mr-auto">{{ userProfiles[editor] ? userProfiles[editor].display_name : editor }}</span>
<b-button
size="sm"
variant="danger"
@click="removeEditor(editor)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'minus']"
/>
</b-button>
</b-list-group-item>
<b-list-group-item>
<b-input-group>
<b-form-input
v-model="models.addEditor"
:state="!!validateUserName(models.addEditor)"
@keyup.enter="addEditor"
/>
<b-input-group-append>
<b-button
variant="success"
:disabled="!validateUserName(models.addEditor)"
@click="addEditor"
>
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'plus']"
/>
Add
</b-button>
</b-input-group-append>
</b-input-group>
</b-list-group-item>
</b-list-group>
</b-card>
<b-card
no-body
>
<b-card-header
class="d-flex align-items-center align-middle"
>
<span class="mr-auto">
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'ticket-alt']"
/>
Auth-Tokens
</span>
<b-button-group size="sm">
<b-button
variant="success"
@click="newAPIToken"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'plus']"
/>
</b-button>
</b-button-group>
</b-card-header>
<b-list-group flush>
<b-list-group-item
v-if="createdAPIToken"
variant="success"
>
Token was created, copy it within 30s as you will not see it again:<br>
<code>{{ createdAPIToken.token }}</code>
</b-list-group-item>
<b-list-group-item
v-for="(token, uuid) in apiTokens"
:key="uuid"
class="d-flex align-items-center align-middle"
>
<span class="mr-auto">
{{ token.name }}<br>
<b-badge
v-for="module in token.modules"
:key="module"
class="mr-1"
>{{ module === '*' ? 'ANY' : module }}</b-badge>
</span>
<b-button
size="sm"
variant="danger"
@click="removeAPIToken(uuid)"
>
<font-awesome-icon
fixed-width
:icon="['fas', 'minus']"
/>
</b-button>
</b-list-group-item>
</b-list-group>
</b-card>
</b-col>
<b-col>
<b-card
no-body
class="mb-3"
:border-variant="botConnectionCardVariant"
>
<b-card-header
class="d-flex align-items-center align-middle"
:header-bg-variant="botConnectionCardVariant"
>
<span class="mr-auto">
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'sign-in-alt']"
/>
Bot Connection
</span>
<template v-if="generalConfig.bot_name">
<code
id="botUserName"
>
{{ generalConfig.bot_name }}
<b-tooltip
target="botUserName"
triggers="hover"
>
Twitch Login-Name of the bot user currently authorized
</b-tooltip>
</code>
</template>
<template v-else>
<font-awesome-icon
id="botUserNameDC"
fixed-width
class="mr-1 text-danger"
:icon="['fas', 'unlink']"
/>
<b-tooltip
target="botUserNameDC"
triggers="hover"
>
Bot is not currently authorized!
</b-tooltip>
</template>
</b-card-header>
<b-card-body>
<p>
Here you can manage your bots auth-token: it's required to communicate with Twitch Chat and APIs. The access will be valid as long as you don't change the password or revoke the apps permission in your bot account.
</p>
<ul>
<li>Copy the URL provided below</li>
<li><strong>Open an inkognito tab or different browser you are not logged into Twitch or are logged in with your bot account</strong></li>
<li>Open the copied URL, sign in with the bot account and accept the permissions</li>
<li>You will see a message containing the authorized account. If this account is wrong, just start over, the token will be overwritten.</li>
</ul>
<p
v-if="botMissingScopes > 0"
class="alert alert-warning"
>
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'exclamation-triangle']"
/>
Bot is missing {{ botMissingScopes }} of its required scopes which will cause features not to work properly. Please re-authorize the bot using the URL below.
</p>
<b-input-group>
<b-form-input
placeholder="Loading..."
readonly
:value="botAuthTokenURL"
@focus="$event.target.select()"
/>
<b-input-group-append>
<b-button
:variant="copyButtonVariant.botConnection"
@click="copyAuthURL('botConnection')"
>
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'clipboard']"
/>
Copy
</b-button>
</b-input-group-append>
</b-input-group>
</b-card-body>
</b-card>
</b-col>
</b-row>
<!-- API-Token Editor -->
<b-modal
v-if="showAPITokenEditModal"
hide-header-close
:ok-disabled="!validateAPIToken"
ok-title="Save"
size="md"
:visible="showAPITokenEditModal"
title="New API-Token"
@hidden="showAPITokenEditModal=false"
@ok="saveAPIToken"
>
<b-form-group
label="Name"
label-for="formAPITokenName"
>
<b-form-input
id="formAPITokenName"
v-model="models.apiToken.name"
:state="Boolean(models.apiToken.name)"
type="text"
/>
</b-form-group>
<b-form-group
label="Enabled for Modules"
>
<b-form-checkbox-group
v-model="models.apiToken.modules"
class="mb-3"
:options="availableModules"
text-field="text"
value-field="value"
/>
</b-form-group>
</b-modal>
<!-- Channel Permission Editor -->
<b-modal
v-if="showPermissionEditModal"
hide-footer
size="lg"
title="Edit Permissions for Channel"
:visible="showPermissionEditModal"
@hidden="showPermissionEditModal=false"
>
<b-row>
<b-col>
<p>The bot should be able to&hellip;</p>
<b-form-checkbox-group
id="channelPermissions"
v-model="models.channelPermissions"
:options="extendedPermissions"
multiple
:select-size="extendedPermissions.length"
stacked
switches
/>
<p class="mt-3">
&hellip;on this channel.
</p>
</b-col>
<b-col>
<p>
In order to access non-public information as channel-point redemptions or take actions limited to the channel owner the bot needs additional permissions. The <strong>owner</strong> of the channel needs to grant those!
</p>
<ul>
<li>Select permissions on the left side</li>
<li>Copy the URL provided below</li>
<li>Pass the URL to the channel owner and tell them to open it with their personal account logged in</li>
<li>The bot will display a message containing the updated account</li>
</ul>
</b-col>
</b-row>
<b-row>
<b-col>
<b-input-group>
<b-form-input
placeholder="Loading..."
readonly
:value="extendedPermissionsURL"
@focus="$event.target.select()"
/>
<b-input-group-append>
<b-button
:variant="copyButtonVariant.channelPermission"
@click="copyAuthURL('channelPermission')"
>
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'clipboard']"
/>
Copy
</b-button>
</b-input-group-append>
</b-input-group>
</b-col>
</b-row>
</b-modal>
</div>
</template>
<script>
import * as constants from './const.js'
import axios from 'axios'
export default {
computed: {
availableModules() {
return [
{ text: 'ANY', value: '*' },
...[...this.modules || []].sort()
.filter(m => m !== 'config-editor')
.map(m => ({ text: m, value: m })),
]
},
botAuthTokenURL() {
if (!this.authURLs || !this.authURLs.update_bot_token) {
return ''
}
let scopes = [...this.$root.vars.DefaultBotScopes]
if (this.generalConfig && this.generalConfig.channel_scopes && this.generalConfig.channel_scopes[this.generalConfig.bot_name]) {
scopes = [
...new Set([
...scopes,
...this.generalConfig.channel_scopes[this.generalConfig.bot_name],
]),
]
}
const u = new URL(this.authURLs.update_bot_token)
u.searchParams.set('scope', scopes.join(' '))
return u.toString()
},
botConnectionCardVariant() {
if (this.$parent.status.overall_status_success) {
return 'secondary'
}
return 'warning'
},
botMissingScopes() {
let missing = 0
if (!this.generalConfig || !this.generalConfig.channel_scopes || !this.generalConfig.bot_name) {
return -1
}
const grantedScopes = [...this.generalConfig.channel_scopes[this.generalConfig.bot_name] || []]
for (const scope of this.$root.vars.DefaultBotScopes) {
if (!grantedScopes.includes(scope)) {
missing++
}
}
return missing
},
extendedPermissions() {
return Object.keys(this.authURLs.available_extended_scopes || {})
.map(v => ({ text: this.authURLs.available_extended_scopes[v], value: v }))
.sort((a, b) => a.value.localeCompare(b.value))
},
extendedPermissionsURL() {
if (!this.authURLs?.update_channel_scopes) {
return null
}
const u = new URL(this.authURLs.update_channel_scopes)
u.searchParams.set('scope', this.models.channelPermissions.join(' '))
return u.toString()
},
sortedChannels() {
return [...this.generalConfig?.channels || []].sort((a, b) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()))
},
sortedEditors() {
return [...this.generalConfig?.bot_editors || []].sort((a, b) => {
const an = this.userProfiles[a]?.login || a
const bn = this.userProfiles[b]?.login || b
return an.localeCompare(bn)
})
},
validateAPIToken() {
return this.models.apiToken.modules.length > 0 && Boolean(this.models.apiToken.name)
},
},
data() {
return {
apiTokens: {},
authURLs: {},
copyButtonVariant: {
botConnection: 'primary',
channelPermission: 'primary',
},
createdAPIToken: null,
generalConfig: {},
models: {
addChannel: '',
addEditor: '',
apiToken: {},
channelPermissions: [],
},
modules: [],
showAPITokenEditModal: false,
showPermissionEditModal: false,
userProfiles: {},
}
},
methods: {
addChannel() {
if (!this.validateUserName(this.models.addChannel)) {
return
}
this.generalConfig.channels.push(this.models.addChannel.replace(/^#*/, ''))
this.models.addChannel = ''
this.updateGeneralConfig()
},
addEditor() {
if (!this.validateUserName(this.models.addEditor)) {
return
}
this.fetchProfile(this.models.addEditor)
this.generalConfig.bot_editors.push(this.models.addEditor)
this.models.addEditor = ''
this.updateGeneralConfig()
},
copyAuthURL(type) {
let prom = null
let btnField = null
switch (type) {
case 'botConnection':
prom = navigator.clipboard.writeText(this.botAuthTokenURL)
btnField = 'botConnection'
break
case 'channelPermission':
prom = navigator.clipboard.writeText(this.extendedPermissionsURL)
btnField = 'channelPermission'
break
}
return prom
.then(() => {
this.copyButtonVariant[btnField] = 'success'
})
.catch(() => {
this.copyButtonVariant[btnField] = 'danger'
})
.finally(() => {
window.setTimeout(() => {
this.copyButtonVariant[btnField] = 'primary'
}, 2000)
})
},
editChannelPermissions(channel) {
let permissionSet = [...this.generalConfig.channel_scopes[channel] || []]
if (channel === this.generalConfig.bot_name) {
permissionSet = [
...permissionSet,
...this.$root.vars.DefaultBotScopes,
]
}
this.models.channelPermissions = [...new Set(permissionSet)]
this.showPermissionEditModal = true
},
fetchAPITokens() {
this.$bus.$emit(constants.NOTIFY_LOADING_DATA, true)
return axios.get('config-editor/auth-tokens', this.$root.axiosOptions)
.then(resp => {
this.apiTokens = resp.data
})
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
fetchAuthURLs() {
this.$bus.$emit(constants.NOTIFY_LOADING_DATA, true)
return axios.get('config-editor/auth-urls', this.$root.axiosOptions)
.then(resp => {
this.authURLs = resp.data
})
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
fetchGeneralConfig() {
this.$bus.$emit(constants.NOTIFY_LOADING_DATA, true)
return axios.get('config-editor/general', this.$root.axiosOptions)
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
.then(resp => {
this.generalConfig = resp.data
const promises = []
for (const editor of this.generalConfig.bot_editors) {
promises.push(this.fetchProfile(editor))
}
return Promise.all(promises)
})
},
fetchModules() {
this.$bus.$emit(constants.NOTIFY_LOADING_DATA, true)
return axios.get('config-editor/modules')
.then(resp => {
this.modules = resp.data
})
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
fetchProfile(user) {
this.$bus.$emit(constants.NOTIFY_LOADING_DATA, true)
return axios.get(`config-editor/user?user=${user}`, this.$root.axiosOptions)
.then(resp => {
this.$set(this.userProfiles, user, resp.data)
this.$bus.$emit(constants.NOTIFY_LOADING_DATA, false)
})
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
hasAllExtendedScopes(channel) {
if (!this.generalConfig.channel_scopes[channel]) {
return false
}
for (const scope in this.authURLs.available_extended_scopes) {
if (!this.generalConfig.channel_scopes[channel].includes(scope)) {
return false
}
}
return true
},
missingExtendedScopes(channel) {
if (!this.generalConfig.channel_scopes[channel]) {
return Object.keys(this.authURLs.available_extended_scopes || {})
}
const missing = []
for (const scope in this.authURLs.available_extended_scopes) {
if (!this.generalConfig.channel_scopes[channel].includes(scope)) {
missing.push(scope)
}
}
return missing
},
newAPIToken() {
this.$set(this.models, 'apiToken', {
modules: [],
name: '',
})
this.showAPITokenEditModal = true
},
removeAPIToken(uuid) {
axios.delete(`config-editor/auth-tokens/${uuid}`, this.$root.axiosOptions)
.then(() => {
this.$bus.$emit(constants.NOTIFY_CHANGE_PENDING, true)
})
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
removeChannel(channel) {
this.generalConfig.channels = this.generalConfig.channels
.filter(ch => ch !== channel)
this.updateGeneralConfig()
},
removeEditor(editor) {
this.generalConfig.bot_editors = this.generalConfig.bot_editors
.filter(ed => ed !== editor)
this.updateGeneralConfig()
},
saveAPIToken(evt) {
if (!this.validateAPIToken) {
evt.preventDefault()
return
}
axios.post(`config-editor/auth-tokens`, this.models.apiToken, this.$root.axiosOptions)
.then(resp => {
this.createdAPIToken = resp.data
this.$bus.$emit(constants.NOTIFY_CHANGE_PENDING, true)
window.setTimeout(() => {
this.createdAPIToken = null
}, 30000)
})
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
updateGeneralConfig() {
axios.put('config-editor/general', this.generalConfig, this.$root.axiosOptions)
.then(() => {
this.$bus.$emit(constants.NOTIFY_CHANGE_PENDING, true)
})
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
validateUserName(user) {
return user.match(constants.REGEXP_USER)
},
},
mounted() {
this.$bus.$on(constants.NOTIFY_CONFIG_RELOAD, () => {
Promise.all([
this.fetchGeneralConfig(),
this.fetchAPITokens(),
this.fetchAuthURLs(),
]).then(() => {
this.$bus.$emit(constants.NOTIFY_CHANGE_PENDING, false)
this.$bus.$emit(constants.NOTIFY_LOADING_DATA, false)
})
})
Promise.all([
this.fetchGeneralConfig(),
this.fetchAPITokens(),
this.fetchAuthURLs(),
this.fetchModules(),
]).then(() => this.$bus.$emit(constants.NOTIFY_LOADING_DATA, false))
},
name: 'TwitchBotEditorAppGeneralConfig',
}
</script>