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
|
## TODO
|
||||||
|
|
||||||
- [ ] Manage matterbridge processes
|
- [x] Manage matterbridge processes
|
||||||
- [ ] Sanitize user inputs
|
- [ ] Sanitize user inputs
|
||||||
- [ ] Check status of matterbridge processes
|
- [ ] Check status of matterbridge processes
|
||||||
- [ ] Complete slack login workflow
|
- [x] Complete slack login workflow
|
||||||
- [ ] Kill group processes & delete config when group is deleted
|
- [ ] Kill group processes & delete config when group is deleted
|
||||||
|
|
||||||
## Supported Clients
|
## Supported Clients
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
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`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>ChatBridge2</title>
|
<title>ChatBridge</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|
|
@ -5,3 +5,11 @@ export const getDiscordInstallURL = (callback: CallableFunction) => {
|
||||||
callback(res.data.url)
|
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 {useEffect, useState} from "react";
|
||||||
import {channelsType} from "../../types/channel";
|
import {channelsType} from "../../types/channel";
|
||||||
import {bridgeType} from "../../types/bridge";
|
import {bridgeType} from "../../types/bridge";
|
||||||
|
import {getDiscordChannels} from "../../api/discord";
|
||||||
|
|
||||||
|
|
||||||
export interface JoinChannelProps {
|
export interface JoinChannelProps {
|
||||||
|
@ -37,6 +38,10 @@ const JoinChannel = ({
|
||||||
switch(platform){
|
switch(platform){
|
||||||
case "Slack":
|
case "Slack":
|
||||||
getSlackChannels(setChannels)
|
getSlackChannels(setChannels)
|
||||||
|
break
|
||||||
|
case "Discord":
|
||||||
|
getDiscordChannels(setChannels)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [platform, bridge])
|
}, [platform, bridge])
|
||||||
|
@ -51,7 +56,13 @@ const JoinChannel = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChannelChanged = (evt:any) => {
|
const onChannelChanged = (evt:any) => {
|
||||||
setStepComplete({...stepComplete, channel:false})
|
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)
|
setSelectedChannel(evt.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +78,7 @@ const JoinChannel = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"list-row"}>
|
<div className={"list-row"}>
|
||||||
<FormControl sx={{width: "50%"}}>
|
<FormControl sx={{width: platform === "Slack" ? "50%" : "100%"}}>
|
||||||
<InputLabel>Select Channel</InputLabel>
|
<InputLabel>Select Channel</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
// value={selectedChannel}
|
// value={selectedChannel}
|
||||||
|
@ -96,6 +107,7 @@ const JoinChannel = ({
|
||||||
|
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
{platform === "Slack" ?
|
||||||
<Button
|
<Button
|
||||||
variant={"outlined"}
|
variant={"outlined"}
|
||||||
onClick={onJoinButtonClicked}
|
onClick={onJoinButtonClicked}
|
||||||
|
@ -104,6 +116,7 @@ const JoinChannel = ({
|
||||||
>
|
>
|
||||||
{stepComplete.channel ? 'Channel Joined!' : 'Join Channel'}
|
{stepComplete.channel ? 'Channel Joined!' : 'Join Channel'}
|
||||||
</Button>
|
</Button>
|
||||||
|
: null }
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ export const DiscordLogin = ({
|
||||||
color={bridge !== undefined ? 'success': undefined}
|
color={bridge !== undefined ? 'success': undefined}
|
||||||
disabled={installLink === undefined}
|
disabled={installLink === undefined}
|
||||||
>
|
>
|
||||||
{installLink === undefined ? 'Waiting for Install Link...' : 'Add to Slack'}
|
{installLink === undefined ? 'Waiting for Install Link...' : 'Add to Discord'}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -2,6 +2,9 @@ PORT=8999
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
NODE_CONFIG_DIR = ./server/config
|
NODE_CONFIG_DIR = ./server/config
|
||||||
LOG_DIR=/var/log/chatbridge
|
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_HOST=127.0.0.1
|
||||||
POSTGRES_PORT=6500
|
POSTGRES_PORT=6500
|
||||||
|
@ -33,5 +36,6 @@ SLACK_STATE_SECRET=
|
||||||
# Discord ----------------
|
# Discord ----------------
|
||||||
DISCORD_TOKEN=
|
DISCORD_TOKEN=
|
||||||
DISCORD_CLIENT_ID=
|
DISCORD_CLIENT_ID=
|
||||||
|
DISCORD_CLIENT_SECRET=
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,9 @@ export default {
|
||||||
},
|
},
|
||||||
discordConfig: {
|
discordConfig: {
|
||||||
token: 'DISCORD_TOKEN',
|
token: 'DISCORD_TOKEN',
|
||||||
client_id: "DISCORD_CLIENT_ID"
|
client_id: "DISCORD_CLIENT_ID",
|
||||||
|
client_secret: 'DISCORD_CLIENT_SECRET'
|
||||||
|
|
||||||
},
|
},
|
||||||
admin_token: 'ADMIN_TOKEN',
|
admin_token: 'ADMIN_TOKEN',
|
||||||
cookies:{
|
cookies:{
|
||||||
|
@ -26,5 +28,6 @@ export default {
|
||||||
bin: 'MATTERBRIDGE_BINARY',
|
bin: 'MATTERBRIDGE_BINARY',
|
||||||
config: 'MATTERBRIDGE_CONFIG_DIR'
|
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 {discordConfigType} from "../types/config";
|
||||||
import {DiscordBridge} from "../entities/bridge.entity";
|
import {DiscordBridge} from "../entities/bridge.entity";
|
||||||
import {AppDataSource} from "../db/data-source";
|
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 discordBridgeRepository = AppDataSource.getRepository(DiscordBridge)
|
||||||
const discordConfig = config.get<discordConfigType>('discordConfig');
|
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(
|
export const DiscordInstallLinkHandler = async(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
|
@ -18,7 +28,7 @@ export const DiscordInstallLinkHandler = async(
|
||||||
let state_token = randomUUID()
|
let state_token = randomUUID()
|
||||||
req.session.state_token = state_token;
|
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({
|
res.status(200).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
|
@ -32,8 +42,118 @@ export const DiscordInstallLinkHandler = async(
|
||||||
|
|
||||||
|
|
||||||
export const DiscordOAuthHandler = 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,
|
req: Request,
|
||||||
res: Response
|
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()
|
@Column()
|
||||||
state_token: string;
|
state_token: string;
|
||||||
|
|
||||||
// Bot token for slack
|
|
||||||
@Column({
|
|
||||||
unique: true
|
|
||||||
})
|
|
||||||
Token: string;
|
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
default: true
|
default: true
|
||||||
})
|
})
|
||||||
|
@ -52,6 +46,12 @@ export class Bridge extends Model {
|
||||||
|
|
||||||
@ChildEntity()
|
@ChildEntity()
|
||||||
export class SlackBridge extends Bridge {
|
export class SlackBridge extends Bridge {
|
||||||
|
// Bot token for slack
|
||||||
|
@Column({
|
||||||
|
unique: true
|
||||||
|
})
|
||||||
|
Token: string;
|
||||||
|
|
||||||
// The ID of the team
|
// The ID of the team
|
||||||
@Column({nullable:true})
|
@Column({nullable:true})
|
||||||
team_id: string;
|
team_id: string;
|
||||||
|
@ -73,6 +73,12 @@ export class SlackBridge extends Bridge {
|
||||||
|
|
||||||
@ChildEntity()
|
@ChildEntity()
|
||||||
export class DiscordBridge extends Bridge {
|
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
|
// Server name - needed to use multiple 'servers' with a single discord app
|
||||||
@Column()
|
@Column()
|
||||||
Server: string;
|
Server: string;
|
||||||
|
@ -80,6 +86,25 @@ export class DiscordBridge extends Bridge {
|
||||||
// Analogous to team_id in slack bridge
|
// Analogous to team_id in slack bridge
|
||||||
@Column()
|
@Column()
|
||||||
guild_id: string;
|
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()
|
@ChildEntity()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Entity, Column, Index, ManyToOne, OneToOne } from 'typeorm';
|
import { Entity, Column, Index, ManyToOne, OneToOne } from 'typeorm';
|
||||||
import Model from './model.entity';
|
import Model from './model.entity';
|
||||||
import { Bridge } from "./bridge.entity";
|
import {Bridge, SlackBridge} from "./bridge.entity";
|
||||||
import { Group } from "./group.entity";
|
import { Group } from "./group.entity";
|
||||||
|
|
||||||
@Entity('channels')
|
@Entity('channels')
|
||||||
|
@ -8,7 +8,7 @@ export class Channel extends Model {
|
||||||
@Column()
|
@Column()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@ManyToOne(() => Bridge, (bridge) => bridge.channels,
|
@ManyToOne((type) => {console.log('!!!!! type', type); return Bridge}, (bridge) => bridge.channels,
|
||||||
{
|
{
|
||||||
eager: true
|
eager: true
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,11 @@ import {Group} from "../entities/group.entity";
|
||||||
const slugify = require('slugify');
|
const slugify = require('slugify');
|
||||||
import * as TOML from '@ltd/j-toml';
|
import * as TOML from '@ltd/j-toml';
|
||||||
import logger from "../logging";
|
import logger from "../logging";
|
||||||
|
import {Bridge} from "../entities/bridge.entity";
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const groupRepository = AppDataSource.getRepository(Group)
|
const groupRepository = AppDataSource.getRepository(Group)
|
||||||
|
const bridgeRepository = AppDataSource.getRepository(Bridge)
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -27,7 +28,10 @@ type BridgeEntry = {
|
||||||
Label: string;
|
Label: string;
|
||||||
Token: string;
|
Token: string;
|
||||||
PrefixMessagesWithNick: boolean;
|
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({
|
let group = await groupRepository.findOne({
|
||||||
where: {name: group_name},
|
where: {name: group_name},
|
||||||
relations: {channels: true}
|
relations: {channels: true}
|
||||||
})
|
})
|
||||||
logger.debug('config group', group)
|
logger.debug('config group', group)
|
||||||
|
|
||||||
// Construct config in the gateway style for programmatic use
|
// Construct config in the gateway style for programmatic use
|
||||||
let gateway = <Gateway>{
|
let gateway = <Gateway>{
|
||||||
name: group.name,
|
name: group.name,
|
||||||
enable: group.enable,
|
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 {
|
return {
|
||||||
protocol: channel.bridge.Protocol,
|
protocol: bridge.Protocol,
|
||||||
name: slugify(channel.bridge.Label),
|
name: slugify(bridge.Label),
|
||||||
Label: channel.bridge.Label,
|
Label: bridge.Label,
|
||||||
Token: channel.bridge.Token,
|
Token: bridge.Token,
|
||||||
PrefixMessagesWithNick: channel.bridge.PrefixMessagesWithNick,
|
PrefixMessagesWithNick: bridge.PrefixMessagesWithNick,
|
||||||
RemoteNickFormat: channel.bridge.RemoteNickFormat
|
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) => {
|
inOuts: group.channels.map((channel) => {
|
||||||
return {
|
return {
|
||||||
account: `${channel.bridge.Protocol}.${slugify(channel.bridge.Label)}`,
|
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
|
// (ie. separate the different TOML table entries rather than representing them
|
||||||
// inline. See https://www.npmjs.com/package/@ltd/j-toml
|
// inline. See https://www.npmjs.com/package/@ltd/j-toml
|
||||||
gateway.bridges.forEach((bridge) => {
|
gateway.bridges.forEach((bridge) => {
|
||||||
|
let bridgeEntry
|
||||||
let bridgeEntry = {
|
if (bridge.protocol === "slack") {
|
||||||
|
bridgeEntry = {
|
||||||
Token: bridge.Token,
|
Token: bridge.Token,
|
||||||
PrefixMessagesWithNick: bridge.PrefixMessagesWithNick,
|
PrefixMessagesWithNick: bridge.PrefixMessagesWithNick,
|
||||||
RemoteNickFormat: bridge.RemoteNickFormat,
|
RemoteNickFormat: bridge.RemoteNickFormat,
|
||||||
Label: bridge.Label
|
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)){
|
if (!protocols.hasOwnProperty(bridge.protocol)){
|
||||||
protocols[bridge.protocol] = {};
|
protocols[bridge.protocol] = {};
|
||||||
|
@ -155,9 +198,8 @@ export const GatewayToTOML = (gateway: Gateway) => {
|
||||||
protocols[bridge.protocol][bridge.name] = TOML.Section(bridgeEntry)
|
protocols[bridge.protocol][bridge.name] = TOML.Section(bridgeEntry)
|
||||||
|
|
||||||
})
|
})
|
||||||
logger.debug('gateway toml protocols', protocols)
|
|
||||||
|
|
||||||
return {
|
let gateway_toml = {
|
||||||
...protocols,
|
...protocols,
|
||||||
'gateway': [TOML.Section({
|
'gateway': [TOML.Section({
|
||||||
name: gateway.name,
|
name: gateway.name,
|
||||||
|
@ -165,21 +207,36 @@ export const GatewayToTOML = (gateway: Gateway) => {
|
||||||
inout: gateway.inOuts.map((inout) => TOML.Section(inout))
|
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) => {
|
export const writeTOML = (gateway_toml: object, out_file: string) => {
|
||||||
let toml_string = TOML.stringify(
|
let toml_string
|
||||||
|
|
||||||
|
try {
|
||||||
|
toml_string = TOML.stringify(
|
||||||
gateway_toml,
|
gateway_toml,
|
||||||
{
|
{
|
||||||
newline: '\n'
|
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)
|
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)
|
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');
|
const pm2 = require('pm2');
|
||||||
import config from "config";
|
import config from "config";
|
||||||
|
|
||||||
import { writeGroupConfig } from "./config";
|
import {GatewayToTOML, getGroupConfig, writeGroupConfig, writeTOML} from "./config";
|
||||||
import {Group} from "../entities/group.entity";
|
import {Group} from "../entities/group.entity";
|
||||||
import slugify from "slugify";
|
import slugify from "slugify";
|
||||||
import logger from "../logging";
|
import logger from "../logging";
|
||||||
|
@ -57,6 +57,17 @@ class MatterbridgeManager {
|
||||||
async spawnProcess(group_name: string) {
|
async spawnProcess(group_name: string) {
|
||||||
let group_name_slug = slugify(group_name)
|
let group_name_slug = slugify(group_name)
|
||||||
let group_filename = `${this.matterbridge_config_dir}/matterbridge-${group_name_slug}.toml`
|
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);
|
await writeGroupConfig(group_name, group_filename);
|
||||||
|
|
||||||
pm2.connect(async(err:any) => {
|
pm2.connect(async(err:any) => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DiscordInstallLinkHandler,
|
DiscordInstallLinkHandler, DiscordJoinChannelsHandler, DiscordListChannelsHandler,
|
||||||
DiscordOAuthHandler
|
DiscordOAuthHandler
|
||||||
} from "../controllers/discord.controller";
|
} from "../controllers/discord.controller";
|
||||||
|
|
||||||
|
@ -16,5 +16,9 @@ router.route('/install')
|
||||||
router.route('/oauth_redirect')
|
router.route('/oauth_redirect')
|
||||||
.get(DiscordOAuthHandler)
|
.get(DiscordOAuthHandler)
|
||||||
|
|
||||||
|
router.route('/channels')
|
||||||
|
.get(requireStateToken, DiscordListChannelsHandler)
|
||||||
|
.post(requireStateToken, DiscordJoinChannelsHandler)
|
||||||
|
|
||||||
|
|
||||||
export default router
|
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 {
|
export interface discordConfigType {
|
||||||
token: string;
|
token: string;
|
||||||
client_id: string;
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue