twitch-bot/src/generalConfig.vue
Knut Ahlers 4da9f592e5
[editor] Improve location of permission warning
Signed-off-by: Knut Ahlers <knut@ahlers.me>
2022-02-10 02:22:02 +01:00

629 lines
19 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_scopes[channel]"
:id="`channelPublicWarn${channel}`"
fixed-width
class="ml-1 text-warning"
:icon="['fas', 'exclamation-triangle']"
/>
<b-tooltip
:target="`channelPublicWarn${channel}`"
triggers="hover"
>
Channel cannot use features like channel-point redemptions.
See "Channel Permissions" for more info how to authorize.
</b-tooltip>
</span>
<b-button
size="sm"
variant="danger"
@click="removeChannel(channel)"
>
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'minus']"
/>
</b-button>
</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
class="mr-1"
: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
class="mr-1"
: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"
>{{ module === '*' ? 'ANY' : module }}</b-badge>
</span>
<b-button
size="sm"
variant="danger"
@click="removeAPIToken(uuid)"
>
<font-awesome-icon
fixed-width
class="mr-1"
: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. This will override the token you might have provided when starting the bot and will be automatically renewed as long as you don't change your password or revoke the apps permission on your bot account.
</p>
<ul>
<li>Copy the URL provided below</li>
<li>Open an inkognito tab or different browser you are not logged into Twitch or are logged in with your bot account</li>
<li>Open the copied URL, sign in with the bot account and accept the permissions</li>
<li>The bot will display a message containing the authorized account. If this account is wrong, just start over, the token will be overwritten.</li>
</ul>
<b-input-group>
<b-form-input
placeholder="Loading..."
readonly
:value="authURLs.update_bot_token"
@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-card
no-body
class="mb-3"
>
<b-card-header>
<font-awesome-icon
fixed-width
class="mr-1"
:icon="['fas', 'sign-in-alt']"
/>
Channel Permissions
</b-card-header>
<b-card-body>
<p>
In order to access non-public information as channel-point redemptions the bot needs additional permissions. The <strong>owner</strong> of the channel needs to grant those!
</p>
<ul>
<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-input-group>
<b-form-input
placeholder="Loading..."
readonly
:value="authURLs.update_channel_scopes"
@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-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>
</div>
</template>
<script>
import * as constants from './const.js'
import axios from 'axios'
import Vue from 'vue'
export default {
computed: {
availableModules() {
return [
{ text: 'ANY', value: '*' },
...[...this.modules || []].sort()
.filter(m => m !== 'config-editor')
.map(m => ({ text: m, value: m })),
]
},
botConnectionCardVariant() {
if (this.$parent.status.overall_status_success) {
return 'secondary'
}
return 'warning'
},
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: {},
},
modules: [],
showAPITokenEditModal: 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.authURLs.update_bot_token)
btnField = 'botConnection'
break
case 'channelPermission':
prom = navigator.clipboard.writeText(this.authURLs.update_channel_scopes)
btnField = 'channelPermission'
break
}
return prom
.then(() => {
this.copyButtonVariant[btnField] = 'success'
})
.catch(() => {
this.copyButtonVariant[btnField] = 'danger'
})
.finally(() => {
window.setTimeout(() => {
this.copyButtonVariant[btnField] = 'primary'
}, 2000)
})
},
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 => {
Vue.set(this.userProfiles, user, resp.data)
this.$bus.$emit(constants.NOTIFY_LOADING_DATA, false)
})
.catch(err => this.$bus.$emit(constants.NOTIFY_FETCH_ERROR, err))
},
newAPIToken() {
Vue.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>