discord is working
This commit is contained in:
parent
8504775ff3
commit
c138fc68d0
16 changed files with 311 additions and 49 deletions
|
@ -10,10 +10,10 @@ Very unfinished!! Mostly a programming exercise for me for now to practice fulls
|
|||
|
||||
## TODO
|
||||
|
||||
- [ ] Manage matterbridge processes
|
||||
- [x] Manage matterbridge processes
|
||||
- [ ] Sanitize user inputs
|
||||
- [ ] Check status of matterbridge processes
|
||||
- [ ] Complete slack login workflow
|
||||
- [x] Complete slack login workflow
|
||||
- [ ] Kill group processes & delete config when group is deleted
|
||||
|
||||
## Supported Clients
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>ChatBridge2</title>
|
||||
<title>ChatBridge</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
|
|
@ -5,3 +5,11 @@ export const getDiscordInstallURL = (callback: CallableFunction) => {
|
|||
callback(res.data.url)
|
||||
})
|
||||
}
|
||||
|
||||
export const getDiscordChannels = (callback: CallableFunction) => {
|
||||
fetch('api/discord/channels')
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
callback(res.data.channels)
|
||||
})
|
||||
}
|
|
@ -9,6 +9,7 @@ import {getSlackChannels, joinSlackChannel} from "../../api/slack";
|
|||
import {useEffect, useState} from "react";
|
||||
import {channelsType} from "../../types/channel";
|
||||
import {bridgeType} from "../../types/bridge";
|
||||
import {getDiscordChannels} from "../../api/discord";
|
||||
|
||||
|
||||
export interface JoinChannelProps {
|
||||
|
@ -37,6 +38,10 @@ const JoinChannel = ({
|
|||
switch(platform){
|
||||
case "Slack":
|
||||
getSlackChannels(setChannels)
|
||||
break
|
||||
case "Discord":
|
||||
getDiscordChannels(setChannels)
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [platform, bridge])
|
||||
|
@ -51,7 +56,13 @@ const JoinChannel = ({
|
|||
}
|
||||
|
||||
const onChannelChanged = (evt:any) => {
|
||||
if (platform === "Discord"){
|
||||
// Discord bots are in all channels by default - selecting one here completes the step
|
||||
setStepComplete({...stepComplete, channel:true})
|
||||
} else {
|
||||
// Otherwise, we need to do something to join the channel, so selecting means we have yet to join it.
|
||||
setStepComplete({...stepComplete, channel: false})
|
||||
}
|
||||
setSelectedChannel(evt.target.value)
|
||||
}
|
||||
|
||||
|
@ -67,7 +78,7 @@ const JoinChannel = ({
|
|||
|
||||
return (
|
||||
<div className={"list-row"}>
|
||||
<FormControl sx={{width: "50%"}}>
|
||||
<FormControl sx={{width: platform === "Slack" ? "50%" : "100%"}}>
|
||||
<InputLabel>Select Channel</InputLabel>
|
||||
<Select
|
||||
// value={selectedChannel}
|
||||
|
@ -96,6 +107,7 @@ const JoinChannel = ({
|
|||
|
||||
</Select>
|
||||
</FormControl>
|
||||
{platform === "Slack" ?
|
||||
<Button
|
||||
variant={"outlined"}
|
||||
onClick={onJoinButtonClicked}
|
||||
|
@ -104,6 +116,7 @@ const JoinChannel = ({
|
|||
>
|
||||
{stepComplete.channel ? 'Channel Joined!' : 'Join Channel'}
|
||||
</Button>
|
||||
: null }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ export const DiscordLogin = ({
|
|||
color={bridge !== undefined ? 'success': undefined}
|
||||
disabled={installLink === undefined}
|
||||
>
|
||||
{installLink === undefined ? 'Waiting for Install Link...' : 'Add to Slack'}
|
||||
{installLink === undefined ? 'Waiting for Install Link...' : 'Add to Discord'}
|
||||
</Button>
|
||||
)
|
||||
}
|
|
@ -2,6 +2,9 @@ PORT=8999
|
|||
NODE_ENV=development
|
||||
NODE_CONFIG_DIR = ./server/config
|
||||
LOG_DIR=/var/log/chatbridge
|
||||
# Full URL that chatbridge is hosted at, including any sub-paths
|
||||
# Used in the discord handler to generate an OAUTH2 callback URL
|
||||
BASE_URL=
|
||||
|
||||
POSTGRES_HOST=127.0.0.1
|
||||
POSTGRES_PORT=6500
|
||||
|
@ -33,5 +36,6 @@ SLACK_STATE_SECRET=
|
|||
# Discord ----------------
|
||||
DISCORD_TOKEN=
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
|
||||
|
||||
|
|
|
@ -15,7 +15,9 @@ export default {
|
|||
},
|
||||
discordConfig: {
|
||||
token: 'DISCORD_TOKEN',
|
||||
client_id: "DISCORD_CLIENT_ID"
|
||||
client_id: "DISCORD_CLIENT_ID",
|
||||
client_secret: 'DISCORD_CLIENT_SECRET'
|
||||
|
||||
},
|
||||
admin_token: 'ADMIN_TOKEN',
|
||||
cookies:{
|
||||
|
@ -26,5 +28,6 @@ export default {
|
|||
bin: 'MATTERBRIDGE_BINARY',
|
||||
config: 'MATTERBRIDGE_CONFIG_DIR'
|
||||
},
|
||||
logDir: 'LOG_DIR'
|
||||
logDir: 'LOG_DIR',
|
||||
baseURL: 'BASE_URL'
|
||||
}
|
||||
|
|
|
@ -5,12 +5,22 @@ import config from "config";
|
|||
import {discordConfigType} from "../types/config";
|
||||
import {DiscordBridge} from "../entities/bridge.entity";
|
||||
import {AppDataSource} from "../db/data-source";
|
||||
|
||||
import {URL} from "url";
|
||||
import {join as pathJoin} from 'path';
|
||||
import {DiscordOauthRedirectInput} from "../schemas/discord.schema";
|
||||
import logger from "../logging";
|
||||
|
||||
|
||||
const discordBridgeRepository = AppDataSource.getRepository(DiscordBridge)
|
||||
const discordConfig = config.get<discordConfigType>('discordConfig');
|
||||
|
||||
const DISCORD_AUTHORIZE_URL = 'https://discord.com/oauth2/authorize'
|
||||
const DISCORD_TOKEN_URL = 'https://discord.com/api/oauth2/token'
|
||||
|
||||
let baseURL = new URL(config.get<string>('baseURL'))
|
||||
baseURL.pathname = pathJoin(baseURL.pathname, '/api/discord/oauth_redirect')
|
||||
const REDIRECT_URL = baseURL.toString()
|
||||
|
||||
export const DiscordInstallLinkHandler = async(
|
||||
req: Request,
|
||||
res: Response
|
||||
|
@ -18,7 +28,7 @@ export const DiscordInstallLinkHandler = async(
|
|||
let state_token = randomUUID()
|
||||
req.session.state_token = state_token;
|
||||
|
||||
const url = `https://discordapp.com/oauth2/authorize?&client_id=${discordConfig.client_id}&scope=bot&permissions=536870912&state=${state_token}`
|
||||
let url = `${DISCORD_AUTHORIZE_URL}?response_type=code&client_id=${discordConfig.client_id}&scope=bot&permissions=536870912&state=${state_token}&redirect_url=${REDIRECT_URL}`
|
||||
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
|
@ -32,8 +42,118 @@ export const DiscordInstallLinkHandler = async(
|
|||
|
||||
|
||||
export const DiscordOAuthHandler = async(
|
||||
req: Request<{},{},{},DiscordOauthRedirectInput>,
|
||||
res: Response
|
||||
) => {
|
||||
if (req.session.state_token !== req.query.state){
|
||||
logger.warning('discord state token did not match on oauth redirect')
|
||||
return res.status(401).json({
|
||||
status: 'failure',
|
||||
message: 'State token does not match!'
|
||||
})
|
||||
}
|
||||
|
||||
let data = new URLSearchParams({
|
||||
client_id: discordConfig.client_id,
|
||||
client_secret: discordConfig.client_secret,
|
||||
grant_type: 'authorization_code',
|
||||
code: req.query.code,
|
||||
redirect_uri: REDIRECT_URL
|
||||
})
|
||||
|
||||
let oauth_res = await fetch(DISCORD_TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: data.toString()
|
||||
})
|
||||
// console.log(oauth_res.body)
|
||||
// console.log(await oauth_res.text())
|
||||
// console.log(oauth_res)
|
||||
// return res.send(await oauth_res.text())
|
||||
let oauth = await oauth_res.json()
|
||||
console.log(oauth)
|
||||
let bridge_data = {
|
||||
Protocol: 'discord',
|
||||
Label: oauth.guild.name,
|
||||
state_token: req.session.state_token,
|
||||
Token: discordConfig.token, // token is same for all bridges, guild ID/Server differentiates
|
||||
Server: oauth.guild.id,
|
||||
guild_id: oauth.guild.id,
|
||||
guild_name: oauth.guild.name,
|
||||
access_token: oauth.access_token,
|
||||
refresh_token: oauth.refresh_token
|
||||
}
|
||||
try {
|
||||
// check if we already have one
|
||||
let bridge = await discordBridgeRepository.findOneBy({
|
||||
Token: discordConfig.token,
|
||||
Server: oauth.guild.id
|
||||
})
|
||||
if (!bridge) {
|
||||
bridge = await discordBridgeRepository.create(bridge_data)
|
||||
await discordBridgeRepository.save(bridge);
|
||||
logger.debug(`Created new discord bridge for ${oauth.guild.name}`)
|
||||
} else {
|
||||
// update everything except user-set label
|
||||
bridge_data.Label = bridge.Label
|
||||
bridge_data = {...bridge, ...bridge_data}
|
||||
await discordBridgeRepository.save(bridge_data)
|
||||
logger.debug(`Updates discord bridge for ${oauth.guild.name}`)
|
||||
}
|
||||
return res.send('<html><body><h1>Success! Return to the chatbridge login window.</h1><h3>This tab will close in 3 seconds...</h3><script>window.setTimeout(window.close, 3000)</script></body>')
|
||||
|
||||
} catch {
|
||||
return res.status(500).json({
|
||||
status: 'failed',
|
||||
message: 'oauth succeeded, but error creating bridge in database'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const DiscordListChannelsHandler = async(
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
console.log('discord oauth', req.body, req.query)
|
||||
let bridge = await discordBridgeRepository.findOneBy({state_token: req.session.state_token})
|
||||
|
||||
try{
|
||||
fetch(`https://discord.com/api/guilds/${bridge.guild_id}/channels`,{
|
||||
headers: {
|
||||
Authorization: `Bot ${bridge.Token}`
|
||||
}
|
||||
}).then(result => result.json())
|
||||
.then(result => {
|
||||
logger.debug('got discord channels %s', result)
|
||||
let channels = result.filter(
|
||||
(res:any) => res.type === 0
|
||||
).map(
|
||||
(res:any) => {
|
||||
return {
|
||||
name: res.name,
|
||||
id: res.id
|
||||
}
|
||||
}
|
||||
)
|
||||
res.status(200).json({
|
||||
status: 'success',
|
||||
data: {
|
||||
channels
|
||||
}
|
||||
})
|
||||
})
|
||||
} catch {
|
||||
return res.status(500).json({
|
||||
status: 'failed',
|
||||
message: 'Could not list channels!'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const DiscordJoinChannelsHandler = async(
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
|
||||
}
|
|
@ -213,3 +213,5 @@ export const getBotInfo = async(
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -24,12 +24,6 @@ export class Bridge extends Model {
|
|||
@Column()
|
||||
state_token: string;
|
||||
|
||||
// Bot token for slack
|
||||
@Column({
|
||||
unique: true
|
||||
})
|
||||
Token: string;
|
||||
|
||||
@Column({
|
||||
default: true
|
||||
})
|
||||
|
@ -52,6 +46,12 @@ export class Bridge extends Model {
|
|||
|
||||
@ChildEntity()
|
||||
export class SlackBridge extends Bridge {
|
||||
// Bot token for slack
|
||||
@Column({
|
||||
unique: true
|
||||
})
|
||||
Token: string;
|
||||
|
||||
// The ID of the team
|
||||
@Column({nullable:true})
|
||||
team_id: string;
|
||||
|
@ -73,6 +73,12 @@ export class SlackBridge extends Bridge {
|
|||
|
||||
@ChildEntity()
|
||||
export class DiscordBridge extends Bridge {
|
||||
// Tokens are not unique per bridge in discord
|
||||
@Column({
|
||||
unique: false
|
||||
})
|
||||
Token: string;
|
||||
|
||||
// Server name - needed to use multiple 'servers' with a single discord app
|
||||
@Column()
|
||||
Server: string;
|
||||
|
@ -80,6 +86,25 @@ export class DiscordBridge extends Bridge {
|
|||
// Analogous to team_id in slack bridge
|
||||
@Column()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
guild_name: string;
|
||||
|
||||
@Column({
|
||||
default: true
|
||||
})
|
||||
AutoWebhooks: boolean;
|
||||
|
||||
@Column({
|
||||
default: true
|
||||
})
|
||||
PreserveThreading: boolean;
|
||||
|
||||
@Column()
|
||||
access_token: string;
|
||||
|
||||
@Column()
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
@ChildEntity()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Entity, Column, Index, ManyToOne, OneToOne } from 'typeorm';
|
||||
import Model from './model.entity';
|
||||
import { Bridge } from "./bridge.entity";
|
||||
import {Bridge, SlackBridge} from "./bridge.entity";
|
||||
import { Group } from "./group.entity";
|
||||
|
||||
@Entity('channels')
|
||||
|
@ -8,7 +8,7 @@ export class Channel extends Model {
|
|||
@Column()
|
||||
name: string;
|
||||
|
||||
@ManyToOne(() => Bridge, (bridge) => bridge.channels,
|
||||
@ManyToOne((type) => {console.log('!!!!! type', type); return Bridge}, (bridge) => bridge.channels,
|
||||
{
|
||||
eager: true
|
||||
}
|
||||
|
|
|
@ -9,10 +9,11 @@ import {Group} from "../entities/group.entity";
|
|||
const slugify = require('slugify');
|
||||
import * as TOML from '@ltd/j-toml';
|
||||
import logger from "../logging";
|
||||
import {Bridge} from "../entities/bridge.entity";
|
||||
const fs = require('fs');
|
||||
|
||||
const groupRepository = AppDataSource.getRepository(Group)
|
||||
|
||||
const bridgeRepository = AppDataSource.getRepository(Bridge)
|
||||
|
||||
|
||||
/*
|
||||
|
@ -27,7 +28,10 @@ type BridgeEntry = {
|
|||
Label: string;
|
||||
Token: string;
|
||||
PrefixMessagesWithNick: boolean;
|
||||
RemoteNickFormat: string
|
||||
RemoteNickFormat: string;
|
||||
Server?: string;
|
||||
AutoWebooks?: boolean;
|
||||
PreserveThreading?: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -53,26 +57,56 @@ type Gateway = {
|
|||
}
|
||||
|
||||
|
||||
export const getGroupConfig = async (group_name: string, group:object): Promise<Gateway> => {
|
||||
export const getGroupConfig = async (group_name: string): Promise<Gateway> => {
|
||||
let group = await groupRepository.findOne({
|
||||
where: {name: group_name},
|
||||
relations: {channels: true}
|
||||
})
|
||||
logger.debug('config group', group)
|
||||
|
||||
// Construct config in the gateway style for programmatic use
|
||||
let gateway = <Gateway>{
|
||||
name: group.name,
|
||||
enable: group.enable,
|
||||
bridges: group.channels.map((channel) => {
|
||||
bridges: await Promise.all(group.channels.map(async(channel) => {
|
||||
// Need to load raw bridge to get all fields
|
||||
let bridge = await bridgeRepository
|
||||
.createQueryBuilder('bridge')
|
||||
.where('bridge.id = :id', {id:channel.bridge.id})
|
||||
.getRawOne()
|
||||
|
||||
// remove bridge_ prefix
|
||||
Object.keys(bridge).forEach(key => {
|
||||
bridge[key.replace('bridge_', '')] = bridge[key]
|
||||
delete bridge[key]
|
||||
})
|
||||
|
||||
switch (bridge.Protocol){
|
||||
case 'slack':
|
||||
return {
|
||||
protocol: channel.bridge.Protocol,
|
||||
name: slugify(channel.bridge.Label),
|
||||
Label: channel.bridge.Label,
|
||||
Token: channel.bridge.Token,
|
||||
PrefixMessagesWithNick: channel.bridge.PrefixMessagesWithNick,
|
||||
RemoteNickFormat: channel.bridge.RemoteNickFormat
|
||||
protocol: bridge.Protocol,
|
||||
name: slugify(bridge.Label),
|
||||
Label: bridge.Label,
|
||||
Token: bridge.Token,
|
||||
PrefixMessagesWithNick: bridge.PrefixMessagesWithNick,
|
||||
RemoteNickFormat: bridge.RemoteNickFormat
|
||||
}
|
||||
}),
|
||||
case 'discord':
|
||||
return {
|
||||
protocol: bridge.Protocol,
|
||||
name: slugify(bridge.Label),
|
||||
Label: bridge.Label,
|
||||
Token: bridge.Token,
|
||||
Server: bridge.Server,
|
||||
AutoWebhooks: bridge.AutoWebhooks,
|
||||
PreserveThreading: bridge.PreserveThreading,
|
||||
PrefixMessagesWithNick: bridge.PrefixMessagesWithNick,
|
||||
RemoteNickFormat: bridge.RemoteNickFormat
|
||||
}
|
||||
default:
|
||||
logger.error(`No matching protocol format found for protocol ${channel.bridge.Protocol}`)
|
||||
}
|
||||
})),
|
||||
inOuts: group.channels.map((channel) => {
|
||||
return {
|
||||
account: `${channel.bridge.Protocol}.${slugify(channel.bridge.Label)}`,
|
||||
|
@ -140,13 +174,22 @@ export const GatewayToTOML = (gateway: Gateway) => {
|
|||
// (ie. separate the different TOML table entries rather than representing them
|
||||
// inline. See https://www.npmjs.com/package/@ltd/j-toml
|
||||
gateway.bridges.forEach((bridge) => {
|
||||
|
||||
let bridgeEntry = {
|
||||
let bridgeEntry
|
||||
if (bridge.protocol === "slack") {
|
||||
bridgeEntry = {
|
||||
Token: bridge.Token,
|
||||
PrefixMessagesWithNick: bridge.PrefixMessagesWithNick,
|
||||
RemoteNickFormat: bridge.RemoteNickFormat,
|
||||
Label: bridge.Label
|
||||
}
|
||||
} else if (bridge.protocol === "discord"){
|
||||
|
||||
const {protocol, name, ...bridgeEntryInner} = bridge;
|
||||
bridgeEntry = bridgeEntryInner
|
||||
} else {
|
||||
logger.error(`unknown protocol ${bridge.protocol} when generating toml config`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!protocols.hasOwnProperty(bridge.protocol)){
|
||||
protocols[bridge.protocol] = {};
|
||||
|
@ -155,9 +198,8 @@ export const GatewayToTOML = (gateway: Gateway) => {
|
|||
protocols[bridge.protocol][bridge.name] = TOML.Section(bridgeEntry)
|
||||
|
||||
})
|
||||
logger.debug('gateway toml protocols', protocols)
|
||||
|
||||
return {
|
||||
let gateway_toml = {
|
||||
...protocols,
|
||||
'gateway': [TOML.Section({
|
||||
name: gateway.name,
|
||||
|
@ -165,21 +207,36 @@ export const GatewayToTOML = (gateway: Gateway) => {
|
|||
inout: gateway.inOuts.map((inout) => TOML.Section(inout))
|
||||
})]
|
||||
}
|
||||
logger.debug('gateway toml', gateway_toml)
|
||||
|
||||
return gateway_toml
|
||||
}
|
||||
|
||||
export const writeTOML = (gateway_toml: object, out_file: string) => {
|
||||
let toml_string = TOML.stringify(
|
||||
let toml_string
|
||||
|
||||
try {
|
||||
toml_string = TOML.stringify(
|
||||
gateway_toml,
|
||||
{
|
||||
newline: '\n'
|
||||
}
|
||||
)
|
||||
} catch (err: any) {
|
||||
logger.error(`Error creating toml from config:
|
||||
${gateway_toml}`, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// logger.debug('toml string', toml_string)
|
||||
|
||||
try {
|
||||
fs.writeFileSync(out_file, toml_string)
|
||||
} catch (err: any){
|
||||
logger.error(`Error writing config to file ${out_file}`)
|
||||
return false
|
||||
}
|
||||
|
||||
logger.info('Wrote group config to %s', out_file)
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import {AppDataSource} from "../db/data-source";
|
|||
const pm2 = require('pm2');
|
||||
import config from "config";
|
||||
|
||||
import { writeGroupConfig } from "./config";
|
||||
import {GatewayToTOML, getGroupConfig, writeGroupConfig, writeTOML} from "./config";
|
||||
import {Group} from "../entities/group.entity";
|
||||
import slugify from "slugify";
|
||||
import logger from "../logging";
|
||||
|
@ -57,6 +57,17 @@ class MatterbridgeManager {
|
|||
async spawnProcess(group_name: string) {
|
||||
let group_name_slug = slugify(group_name)
|
||||
let group_filename = `${this.matterbridge_config_dir}/matterbridge-${group_name_slug}.toml`
|
||||
let group_config = await getGroupConfig(group_name)
|
||||
if (group_config.inOuts.length === 0){
|
||||
logger.info(`Not spawning group ${group_name} with no bridged channels`)
|
||||
return
|
||||
}
|
||||
let res = writeTOML(GatewayToTOML(group_config), group_filename)
|
||||
if (res === false){
|
||||
logger.error('Not spawning, config could not be updated')
|
||||
return
|
||||
}
|
||||
|
||||
await writeGroupConfig(group_name, group_filename);
|
||||
|
||||
pm2.connect(async(err:any) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import express from 'express';
|
||||
|
||||
import {
|
||||
DiscordInstallLinkHandler,
|
||||
DiscordInstallLinkHandler, DiscordJoinChannelsHandler, DiscordListChannelsHandler,
|
||||
DiscordOAuthHandler
|
||||
} from "../controllers/discord.controller";
|
||||
|
||||
|
@ -16,5 +16,9 @@ router.route('/install')
|
|||
router.route('/oauth_redirect')
|
||||
.get(DiscordOAuthHandler)
|
||||
|
||||
router.route('/channels')
|
||||
.get(requireStateToken, DiscordListChannelsHandler)
|
||||
.post(requireStateToken, DiscordJoinChannelsHandler)
|
||||
|
||||
|
||||
export default router
|
||||
|
|
14
server/src/schemas/discord.schema.ts
Normal file
14
server/src/schemas/discord.schema.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { object, string, TypeOf } from 'zod';
|
||||
|
||||
export const discordOauthRedirectSchema = object({
|
||||
query: object({
|
||||
code: string(),
|
||||
state: string(),
|
||||
guild_id: string(),
|
||||
permissions: string()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
||||
export type DiscordOauthRedirectInput = TypeOf<typeof discordOauthRedirectSchema>['query'];
|
|
@ -8,4 +8,5 @@ export interface slackConfigType {
|
|||
export interface discordConfigType {
|
||||
token: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue