Compare commits

...

2 commits

Author SHA1 Message Date
ansible user/allowed to read system logs
3671cbd7ae working prototype 2023-07-31 23:25:44 -07:00
ansible user/allowed to read system logs
eea1e14906 working hot refresh 2023-07-31 15:41:22 -07:00
26 changed files with 688 additions and 221 deletions

View file

@ -2,6 +2,10 @@ const webpack = require('webpack')
const { merge } = require('webpack-merge')
const path = require( 'path' );
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const ReactRefreshTypeScript = require('react-refresh-typescript');
const common = require('./webpack.common.js')
@ -20,6 +24,10 @@ module.exports = merge(common, {
client: {
webSocketURL: 'auto://0.0.0.0:0/ws'
},
allowedHosts: [
'seed.aharoni-lab.com']
},
// Control how source maps are generated
@ -42,12 +50,13 @@ module.exports = merge(common, {
'style-loader',
'css-loader'
]
}
},
],
},
plugins: [
// Only update what has changed on hot reload
new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin()
],
})

View file

@ -32,8 +32,11 @@
]
},
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"css-loader": "^6.8.1",
"html-webpack-plugin": "^5.5.3",
"react-refresh": "^0.14.0",
"react-refresh-typescript": "^2.0.9",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
"ts-loader": "^9.4.4",

View file

@ -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>React App</title>
<title>ChatBridge2</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View file

@ -22,8 +22,9 @@ export const setBridgeLabel = (label:string, callback: CallableFunction) => {
})
}).then(res => res.json())
.then((res) => {
if (res.status === "success") {
callback()
}
callback(res)
// if (res.status === "success") {
// callback()
// }
})
}

20
client/src/api/channel.ts Normal file
View file

@ -0,0 +1,20 @@
export const createChannel = (
channel_name: string,
bridge_id: string,
invite_token: string,
callback: CallableFunction
) => {
fetch('api/channel/create',{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
channel_name, bridge_id, invite_token
})
}).then(result => result.json())
.then(result => {
console.log(result);
callback(result)
})
}

13
client/src/api/groups.ts Normal file
View file

@ -0,0 +1,13 @@
export const groupInvite = (token: string, callback: CallableFunction) => {
fetch('api/groups/invite', {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
token: token
})
})
.then(result => result.json())
.then(result => callback(result))
}

View file

@ -15,8 +15,30 @@ export const getSlackChannels = (callback: CallableFunction) => {
.then(res => {
console.log('Got slack channels', res);
if (res.status === "success"){
callback(res.data.channels.sort())
console.log('channels api client', res.data)
callback(res.data.channels)
}
})
}
export const joinSlackChannel = (channel: string, callback: CallableFunction) => {
fetch('api/slack/channels', {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
channel_id: channel
})
}).then(res => res.json())
.then(res => {
callback(res)
})
}
export const getBotInfo = (callback: CallableFunction) => {
fetch('api/slack/info')
.then(res => res.json())
.then(res => callback(res))
}

View file

@ -1,26 +1,58 @@
import {Grid} from "@mui/material";
import {TextField} from "@mui/material";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import FormControl from "@mui/material/FormControl";
import InputLabel from "@mui/material/InputLabel";
import {setBridgeLabel} from "../../api/bridge";
import {useState} from "react";
export const JoinBridge = ({
bridge, setBridge
bridge, setBridge, setStepComplete, stepComplete
}) => {
const onSetLabel = (label) => {
setBridge({...bridge, Label:label})
const [errored, setErrored] = useState(false);
const [errorMessage, setErrorMessage] = useState('')
const updateBridgeLabel = () => {
setBridgeLabel(bridge.Label, (res) => {
if (res.status === 'success') {
setErrored(false)
setStepComplete({...stepComplete, bridge:true})
} else {
setErrored(true)
setErrorMessage(res.message)
}
})
}
const onSetBridge = (evt) => {
setBridge({...bridge, Label:evt.target.value})
}
return (
<Grid container spacing={2} columns={2}>
<Grid item xs={1}>
</Grid>
<Grid item xs={1}>
<div className={"list-row"}>
{/*<FormControl sx={{width: "50%"}}>*/}
{/* <InputLabel>Bridge Label</InputLabel>*/}
<TextField
onChange={(event) => {onSetLabel(event.target.value)}}>
value={bridge ? bridge.Label : ''}
onChange={onSetBridge}
label={"Bridge Label"}
error={errored}
color={stepComplete.bridge ? 'success' : undefined}
helperText={errored ? errorMessage : "A short label shown before messages from this bridge"}
>
</TextField>
</Grid>
</Grid>
<Button
variant={"outlined"}
onClick={updateBridgeLabel}
color={errored ? 'error': stepComplete.bridge ? 'success' : undefined}
>
{stepComplete.bridge ? 'Label Updated!' : 'Update Label!'}
</Button>
</div>
)
}

View file

@ -0,0 +1,98 @@
import FormControl from "@mui/material/FormControl";
import InputLabel from "@mui/material/InputLabel";
import Select from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import Button from "@mui/material/Button";
import {stepCompleteType} from "./joinForm";
import {joinSlackChannel} from "../../api/slack";
import {useState} from "react";
export interface JoinChannelProps {
channels: Array<{
name: string;
id: string;
is_member: boolean;
}>;
selectedChannel: string;
setSelectedChannel: CallableFunction;
setStepComplete: CallableFunction;
stepComplete: stepCompleteType
}
const JoinChannel = ({
channels,
selectedChannel,
setSelectedChannel,
setStepComplete,
stepComplete
}: JoinChannelProps) => {
console.log('joinchannel channels', channels)
const [errored, setErrored] = useState(false);
const onJoinChannel = (response) => {
if (response.status === 'success'){
setErrored(false)
setStepComplete({...stepComplete, channel:true})
} else {
setErrored(true)
}
}
const onChannelChanged = (evt:any) => {
setStepComplete({...stepComplete, channel:false})
setSelectedChannel(evt.target.value)
}
const onJoinButtonClicked = () => {
let channel_id = channels.filter(chan => chan.name === selectedChannel)
.map(chan => chan.id)[0]
joinSlackChannel(channel_id, onJoinChannel)
}
return (
<div className={"list-row"}>
<FormControl sx={{width: "50%"}}>
<InputLabel>Select Channel</InputLabel>
<Select
// value={selectedChannel}
onChange={onChannelChanged}
label={"Select Channel"}
error={errored}
color={stepComplete.channel ? 'success': undefined}
>
<MenuItem value={''} key={''}>Select Channel</MenuItem>
{
channels ?
channels.map(chan => chan.name)
.sort()
.map(chan => {
return(
<MenuItem
value={chan}
key={chan}
>
{chan}
</MenuItem>)
})
: undefined
}
</Select>
</FormControl>
<Button
variant={"outlined"}
onClick={onJoinButtonClicked}
disabled={selectedChannel === ''}
color={stepComplete.channel ? 'success': undefined}
>
{stepComplete.channel ? 'Channel Joined!' : 'Join Channel'}
</Button>
</div>
)
}
export default JoinChannel

View file

@ -13,29 +13,41 @@ import TextField from "@mui/material/TextField";
import Button from '@mui/material/Button';
import {setBridgeLabel} from "../../api/bridge";
import {getSlackChannels} from "../../api/slack";
import {createChannel} from "../../api/channel";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select"
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import {JoinBridge} from "./joinBridge";
import JoinChannel from "./joinChannel";
export interface JoinFormProps {
group: Group
group?: Group
invite_token: string
}
interface stepCompleteType {
export interface stepCompleteType {
login: boolean;
bridge: boolean;
channel: boolean;
}
export const JoinForm = ({group}: JoinFormProps) => {
export const JoinForm = ({group, invite_token}: JoinFormProps) => {
const [bridgeCreated, setBridgeCreated] = useState(false);
const [bridgeErrorMessage, setBridgeErrorMessage] = useState('');
const [platform, setPlatform] = useState<string>();
const [channels, setChannels] = useState<string[]>();
const [selectedChannel, setSelectedChannel] = useState<string>();
const [channels, setChannels] = useState<Array<{
name: string;
id: string;
is_member: boolean;
}>
>();
const [selectedChannel, setSelectedChannel] = useState<string>('');
const [bridge, setBridge] = useState<{
Label: string;
Protocol: string;
team_name: string;
id: string;
}>();
const [stepComplete, setStepComplete] = useState<stepCompleteType>({
login: false,
@ -43,31 +55,29 @@ export const JoinForm = ({group}: JoinFormProps) => {
channel: false
});
const createBridgedChannel = () => {
createChannel(selectedChannel, bridge.id, invite_token, onBridgedChannelCreated)
}
const onBridgedChannelCreated = (result) => {
if (result.status === 'success'){
setBridgeCreated(true)
setBridgeErrorMessage('')
} else {
setBridgeCreated(false)
setBridgeErrorMessage(result.message)
}
}
useEffect(() => {
if (bridge !== undefined){
setStepComplete({...stepComplete, login:true})
}
if (channels === undefined){
if (bridge !== undefined && channels === undefined){
getSlackChannels(setChannels)
}
}, [bridge])
const onSetBridge = (evt) => {
setBridge({...bridge, Label:evt.target.value})
}
const updateBridgeLabel = () => {
setBridgeLabel(bridge.Label, () => {setStepComplete({...stepComplete, bridge:true})})
}
const handleSelectChannel = (evt) => {
}
const joinChannel = () => {
}
return (
<>
<header className={'section-header'}>
@ -86,81 +96,44 @@ export const JoinForm = ({group}: JoinFormProps) => {
/>
</JoinStep>
<JoinStep
title={"2) Set Bridge Label"}
details={"A short identifier shown before your messages"}
title={"2) Configure Bridge"}
details={"Settings for all channels bridged from this platform"}
id={'bridge'}
disabled={!stepComplete.login}
completed={stepComplete.bridge}
>
<Grid container spacing={2} columns={4} alignItems="center">
<Grid item xs={1}>
<Typography>Label:</Typography>
</Grid>
<Grid item xs={2}>
<TextField
value={bridge?.Label}
onChange={onSetBridge}>
</TextField>
</Grid>
<Grid item xs={1}>
<Button
variant={"outlined"}
onClick={updateBridgeLabel}>
Update Label!
</Button>
</Grid>
</Grid>
<JoinBridge
bridge={bridge}
setBridge={setBridge}
setStepComplete={setStepComplete}
stepComplete={stepComplete}
/>
</JoinStep>
<JoinStep
title={"3) Select a channel!"}
details={"The bot will join :)"}
id={'bridge'}
id={'channel'}
disabled={!stepComplete.login}
completed={stepComplete.channel}
>
<div className={"list-row"}>
<FormControl sx={{width: "50%"}}>
<InputLabel>Select Platform</InputLabel>
<Select
// value={platform}
onChange={(evt:any) => {setSelectedChannel(evt.target.value)}}
label={"Select Channel"}
>
{
channels ?
channels.map(chan => {
return(<MenuItem value={chan} key={chan}>{chan}</MenuItem>)
})
: undefined
}
</Select>
</FormControl>
<Button
variant={"outlined"}
onClick={joinChannel}>
Join Channel
</Button>
</div>
<Grid container spacing={2} columns={2} alignItems="center">
<Grid item xs={1}>
<Typography>Label:</Typography>
</Grid>
<Grid item xs={2}>
<TextField
value={bridge?.Label}
onChange={onSetBridge}>
</TextField>
</Grid>
<Grid item xs={1}>
<Button
variant={"outlined"}
onClick={updateBridgeLabel}>
Update Label!
</Button>
</Grid>
</Grid>
<JoinChannel
channels={channels}
selectedChannel={selectedChannel}
setSelectedChannel={setSelectedChannel}
setStepComplete={setStepComplete}
stepComplete={stepComplete}
/>
</JoinStep>
<Button
className={'create-button'}
sx={{marginTop: "1em"}}
variant={"outlined"}
disabled={group === undefined || !stepComplete.login || !stepComplete.bridge || !stepComplete.channel}
onClick={createBridgedChannel}
color={bridgeErrorMessage !== '' ? 'error' : bridgeCreated ? 'success' : undefined}
>
{bridgeErrorMessage !== '' ? bridgeErrorMessage : bridgeCreated ? 'Bridge Created!' : 'Create New Bridge' }
</Button>
</>
)
}

View file

@ -48,13 +48,6 @@ export const JoinPlatform = ({
// pingForBridge()
}
// const pingForBridge = () =>{
// if (bridge === undefined){
// console.log('bridge is', bridge)
// getBridgeByStateToken(setBridge);
// setTimeout(pingForBridge, 1000);
// }
// }
useEffect(() => {
const pingForBridge = () => {

View file

@ -5,6 +5,7 @@ import Button from '@mui/material/Button';
import {JoinForm} from "../join/joinForm";
import {Group} from "../../types/group";
import {groupInvite} from "../../api/groups";
export default function JoinPanel(){
const [text, setText] = useState('');
@ -12,19 +13,7 @@ export default function JoinPanel(){
const [errorText, setErrorText] = useState('');
const [group, setGroup] = useState<Group>(undefined);
const getGroup = () => {
fetch('api/groups/invite', {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
token: text
})
})
.then(result => result.json())
.then(
response => {
const onGroupLogin = (response) => {
if (response.status !== "success"){
setAuthError(true);
setErrorText(response.message);
@ -35,17 +24,15 @@ export default function JoinPanel(){
setGroup(response.data)
console.log(response)
}
}
)
}
const handleClick = () => {
getGroup()
groupInvite(text, onGroupLogin)
}
const textChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
setText(event.target.value)
setGroup(undefined)
}
return(
<div className={"JoinPanel"}>
@ -56,15 +43,11 @@ export default function JoinPanel(){
className={"Input"}
label={"Join with invite token"}
onChange={textChanged}
// disabled={loggedIn}
// color={loggedIn ? "success" : undefined}
// type={loggedIn ? "password" : undefined}
/>
<Button
variant="contained"
onClick={handleClick}
color={authError ? "error" : undefined}
// disabled={loggedIn}
>
Submit
</Button>
@ -72,6 +55,7 @@ export default function JoinPanel(){
{ group ?
<JoinForm
group = {group}
invite_token = {text}
/> : undefined
}
</div>

View file

@ -1,7 +1,7 @@
.list-row {
display: flex;
justify-content: space-around;
align-items: center;
align-items: flex-start;
gap: 2em;
&>div {
@ -14,6 +14,14 @@
}
}
.create-button {
width: 100%;
height: 3rem;
margin: {
top: 1em;
}
}
.InputSlot {
min-width: 320px;
width: 100%;

View file

@ -12,6 +12,7 @@ import groupRoutes from "./routes/group.routes";
import slackRoutes from "./routes/slack.routes";
import authRoutes from "./routes/auth.routes";
import bridgeRoutes from './routes/bridge.routes';
import channelRoutes from './routes/channel.routes';
@ -39,7 +40,8 @@ AppDataSource.initialize()
// });
app.use('/slack', slackRoutes);
app.use('/bridge', bridgeRoutes)
app.use('/bridge', bridgeRoutes);
app.use('/channel', channelRoutes);
app.all('*', (req: Request, res: Response, next: NextFunction) => {
next(new AppError(404, `Route ${req.originalUrl} not found`));

View file

@ -16,7 +16,8 @@ export const getBridgeHandler = async(
select: {
Protocol: true,
Label: true,
team_name: true
team_name: true,
id: true
}
})
if (!bridge){

View file

@ -0,0 +1,96 @@
import {NextFunction, Request, Response} from 'express';
import {CreateGroupInput, GetGroupInput, getGroupSchema} from "../schemas/group.schema";
import config from 'config';
import {Group} from "../entities/group.entity";
import {Bridge} from "../entities/bridge.entity";
import {Channel} from "../entities/channel.entity";
import AppError from "../errors/appError";
import {randomBytes} from "crypto";
import {AppDataSource} from "../db/data-source";
import {CreateChannelInput} from "../schemas/channel.schema";
import {channel} from "diagnostics_channel";
const groupRepository = AppDataSource.getRepository(Group)
const bridgeRepository = AppDataSource.getRepository(Bridge)
const channelRepository = AppDataSource.getRepository(Channel)
import MatterbridgeManager from '../matterbridge/process';
export const createChannelHandler = async(
req: Request<{}, {}, CreateChannelInput>,
res: Response
) => {
try {
// Validate that we were given the right bridge
let bridge = await bridgeRepository.findOne({
where: {state_token: req.session.state_token},
relations: {channels: true}
})
if (bridge.id !== req.body.bridge_id) {
return res.status(403).json({
status: 'failed',
message: 'Bridge id does not match session token'
})
}
let group = await groupRepository.findOne({
where: {invite_token: req.body.invite_token},
relations: {channels: true}
})
let channel = await channelRepository.findOneBy({
name: req.body.channel_name,
bridge: {id: bridge.id},
group: {id: group.id}
})
if (!channel) {
// create new
channel = new Channel()
}
// update properties
channel.name = req.body.channel_name
channel.bridge = bridge
channel.group = group
console.log('have channel', channel)
console.log('have group', group)
console.log('have bridge', bridge)
channel = await channelRepository.save(channel)
await groupRepository
.createQueryBuilder()
.relation(Group, 'channels')
.of(group)
.add(channel)
await bridgeRepository
.createQueryBuilder()
.relation(Bridge, 'channels')
.of(bridge)
.add(channel)
console.log('newchannel', channel)
console.log('saved group', group)
await MatterbridgeManager.spawnProcess(group.name)
return res.status(200).json({
status: 'success',
data: channel
})
} catch (err) {
console.log('failed to create channe', err)
return res.status(502).json({
status: 'failed',
message: 'failed to create channel'
})
}
}

View file

@ -9,7 +9,7 @@ import { randomUUID } from "crypto";
import {log} from "util";
import {Join} from "typeorm";
const scopes = ['bot', 'channels:write', 'chat:write:bot', 'chat:write:user', 'users.profile:read'];
const scopes = ['bot', 'channels:write', 'channels:write.invites', 'chat:write:bot', 'chat:write:user', 'users.profile:read'];
const bridgeRepository = AppDataSource.getRepository(Bridge)
const groupRepository = AppDataSource.getRepository(Group)
@ -22,6 +22,43 @@ const slackConfig = config.get<{
state_secret: string
}>('slackConfig');
export interface slackChannel {
id: string;
name: string;
is_channel: boolean;
is_group: boolean;
is_im: boolean;
is_mpim: boolean;
is_private: boolean;
created: number;
is_archived: boolean;
is_general: boolean;
unlinked: number;
name_normalized: string;
is_shared: boolean;
is_org_shared: boolean;
is_pending_ext_shared: boolean;
pending_shared: [];
context_team_id: string;
updated: number;
creator: string;
is_ext_shared: boolean;
shared_team_ids: string[];
is_member: boolean;
num_members: number;
}
export interface authTest {
ok: boolean;
url: string;
team: string;
user: string;
team_id: string;
user_id: string;
bot_id: string;
is_enterprise_install: boolean;
}
const installer = new InstallProvider({
clientId: slackConfig.client_id,
clientSecret: slackConfig.client_secret,
@ -35,17 +72,6 @@ const installer = new InstallProvider({
})
export const SlackInstallHandler = async(
req: Request,
res: Response
) => {
await installer.handleInstallPath(req, res, {}, {
scopes,
metadata: {'name':'my-slack-name','group':'MyGroup'}
});
}
export const SlackInstallLinkHandler = async(
req: Request,
@ -83,17 +109,21 @@ export const SlackCallbackHandler = async(
) => {
// using custom success and failure handlers
const callbackOptions = {
success: async (installation, installOptions, req, res) => {
success: async (installation:any, installOptions:any, req:Request, res:Response) => {
// console.log(installation, installOptions, req.body, req.content, req.query, req.params)
// console.log(installation.team.id, installation.team.name, installation.bot.token);
console.log('istallation info', installation)
let bridge_data = {
'Protocol': 'slack',
'Label': installation.team.name,
'team_id': installation.team.id,
'team_name': installation.team.name,
'state_token': req.session.state_token,
'Token': installation.bot.token
'Token': installation.bot.token,
'user_token': installation.user.token,
'bot_id': installation.bot.userId
}
console.log('bot token', installation.bot.token)
// check if we have an entity
let bridge = await bridgeRepository.findOneBy({Token: installation.bot.token})
@ -103,16 +133,30 @@ export const SlackCallbackHandler = async(
await bridgeRepository.save(bridge);
console.log('created bridge')
} else {
await bridgeRepository.update(
{Token: installation.bot.token},
{state_token: req.session.state_token})
// await bridgeRepository.update(
// {Token: installation.bot.token},
// {
// state_token: req.session.state_token,
// user_token: installation.access_token,
// bot_id: installation.bot.bot_user_id
//
// },
//
// )
console.log('existing bridge', bridge)
// Don't overwrite existing label
bridge_data.Label = bridge.Label
bridge_data = {...bridge, ...bridge_data}
console.log('updating bridge with', bridge_data)
let newbridge = await bridgeRepository.save(bridge_data)
console.log('updated bridge', newbridge)
console.log('updated bridge')
}
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>')
},
failure: (error, installOptions , req, res) => {
console.log(error, installOptions, req.body, req.content, req.query, req.params)
failure: (error:any, installOptions:any , req:Request, res:Response) => {
// console.log(error, installOptions, req.body, req.content, req.query, req.params)
res.send('failure. Something is broken about chatbridge :(');
},
}
@ -127,7 +171,7 @@ export const getChannelsHandler = async(
res: Response
) => {
let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token})
console.log('bridge data', bridge)
// console.log('bridge data', bridge)
try {
fetch('https://slack.com/api/conversations.list', {
@ -140,8 +184,12 @@ export const getChannelsHandler = async(
.then((result) => {
console.log('channels',result)
let channels = result.channels.map(
(chan: { name: string; }) => {
return chan.name
(chan: slackChannel) => {
return {
'name': chan.name,
'id': chan.id,
'is_member': chan.is_member
}
}
);
res.status(200).json({
@ -164,57 +212,138 @@ export const joinChannelsHandler = async(
req: Request<{}, {}, JoinSlackChannelInput>,
res: Response
) => {
let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token})
console.log('bridge data', bridge)
try {
// Get channel ID from channel name
let channels_res = await fetch('https://slack.com/api/conversations.list', {
console.log('joinchannel bridge', bridge)
console.log('joinchannel chanid', req.body.channel_id)
console.log('joinchannel userid', bridge.user_token)
console.log('joinchannel botid', bridge.bot_id)
fetch('https://slack.com/api/conversations.invite', {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${bridge.Token}`
}
})
let channels = <{ channels: { name: string, id: string }[] }>channels_res.json()
let channel_id = channels.channels.filter(
(chan) => {
return (chan.name == req.body.channel)
}
).map(chan => chan.id)[0]
console.log('channel id', channel_id)
// Join channel from ID
fetch('https://slack.com/api/conversations.join', {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${bridge.Token}`
"Authorization": `Bearer ${bridge.user_token}`
},
body: JSON.stringify({
channel: channel_id
users: bridge.bot_id,
channel: req.body.channel_id
})
}).then(res => res.json())
.then((res) => {
if (res.ok) {
res.status(200).json({
}).then(result => result.json())
.then(result => {
console.log(result);
if (result.ok || result.error === 'already_in_channel'){
return res.status(200).json({
status: 'success',
data: {
channels
id: req.body.channel_id
}
})
} else {
res.status(502).json({
status: 'failure',
message: 'Couldnt join channel'
return res.status(502).json({
status: 'failed',
message: "could not invite bot to channel!"
})
}
})
} catch {
// try and invite bot
}
// export const joinChannelsHandler = async(
// req: Request<{}, {}, JoinSlackChannelInput>,
// res: Response
// ) => {
// let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token})
// console.log('bridge data', bridge)
//
// try {
// // Get channel ID from channel name
// let channels_res = await fetch('https://slack.com/api/conversations.list', {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// "Authorization": `Bearer ${bridge.Token}`
// }
// })
// let channels = <{ channels: slackChannel[] }><unknown> await channels_res.json()
// let channel_id = channels.channels.filter(
// (chan) => {
// return (chan.name == req.body.channel)
// }
// ).map(chan => chan.id)[0]
// console.log('channel id', channel_id)
// console.log('bridge token', `Bearer ${bridge.Token}`)
// // Join channel from ID
// fetch('https://slack.com/api/conversations.join', {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// "Authorization": `Bearer ${bridge.Token}`
// },
// body: JSON.stringify({
// channel: channel_id
// })
// }).then(result => result.json())
// .then((result) => {
// console.log('result', result)
// if (res.ok) {
// res.status(200).json({
// status: 'success',
// data: {
// channels
// }
// })
// } else {
// res.status(502).json({
// status: 'failure',
// message: 'Couldnt join channel'
// })
// }
// })
//
// } catch (error) {
// console.log('channel join error', error)
// return res.status(502).json({
// status: 'failure',
// message: "Couldn't join channel!"
// })
// }
// }
export const getBotInfo = async(
req: Request,
res: Response
) => {
let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token})
// console.log('bridge data', bridge)
try {
fetch('https://slack.com/api/auth.test', {
headers: {
'Authorization': `Bearer ${bridge.Token}`
}
}).then(response => response.json())
.then((response: authTest) => {
if (response.ok){
return res.status(200).json({
status: 'success',
data: response
})
} else {
return res.status(502).json({
status: 'failure',
message: "Couldn't join channel!"
message: 'unknown error getting auth test'
})
}
})
} catch (error) {
console.log('auth.test error', error)
return res.status(502).json({
status:'failure',
message:'Couldnt get bot info'
})
}
}

View file

@ -31,11 +31,22 @@ export class Bridge extends Model {
@Column()
state_token: string;
// Bot token for slack
@Column({
unique: true
})
Token: string;
@Column({
nullable: true
})
user_token: string;
@Column({
nullable:true
})
bot_id: string;
@Column({
default: true
})

View file

@ -17,7 +17,7 @@ export class Group extends Model {
@Column()
invite_token: string;
@OneToMany(() => Channel, (channel) => channel.bridge)
@OneToMany(() => Channel, (channel) => channel.group)
channels: Channel[]
}

View file

@ -57,7 +57,7 @@ export const getGroupConfig = async (group_name: string, group:object): Promise<
where: {name: group_name},
relations: {channels: true}
})
console.log('config group', group)
// Construct config in the gateway style for programmatic use
let gateway = <Gateway>{
name: group.name,
@ -79,6 +79,7 @@ export const getGroupConfig = async (group_name: string, group:object): Promise<
}
})
}
console.log('config group transformed', gateway)
return gateway
}
@ -153,6 +154,7 @@ export const GatewayToTOML = (gateway: Gateway) => {
protocols[bridge.protocol][bridge.name] = TOML.Section(bridgeEntry)
})
console.log('gateway toml protocols', protocols)
return {
...protocols,
@ -172,6 +174,8 @@ export const writeTOML = (gateway_toml: object, out_file: string) => {
}
)
console.log('toml string', toml_string)
fs.writeFileSync(out_file, toml_string)
}

View file

@ -11,6 +11,7 @@ import config from "config";
import { writeGroupConfig } from "./config";
import {Group} from "../entities/group.entity";
import slugify from "slugify";
const groupRepository = AppDataSource.getRepository(Group)
@ -53,21 +54,35 @@ class MatterbridgeManager {
){}
async spawnProcess(group_name: string) {
let group_filename = `${this.matterbridge_config_dir}/matterbridge-${group_name}.toml`
let group_name_slug = slugify(group_name)
let group_filename = `${this.matterbridge_config_dir}/matterbridge-${group_name_slug}.toml`
await writeGroupConfig(group_name, group_filename);
console.log('matterbridge config written')
if (!this.process_list.includes(group_name_slug)){
console.log('matterbridge new process')
await pm2.start(
{
name: group_name,
name: group_name_slug,
script: this.matterbridge_bin,
args: `-conf ${group_filename}`,
interpreter: 'none'
},
(err:any, apps:object) => {
// TODO: Handle errors!
console.log('error starting matterbridge process', err, apps)
}
)
this.process_list.push(group_name)
this.process_list.push(group_name_slug)
} else {
console.log('matterbridge restarting!')
await pm2.restart(group_name_slug, (err:any, proc:any) => {
console.log('error restarting matterbridge process', err, apps)}
)
}
}
async refreshConfig(group_name: string) {
let group_filename = `${this.matterbridge_config_dir}/matterbridge-${group_name}.toml`
await writeGroupConfig(group_name, group_filename);
}
async spawnAll(){

View file

@ -0,0 +1,22 @@
import express from 'express';
import {
createChannelHandler
} from "../controllers/channel.controller";
import {
requireStateToken
} from "../middleware/cookies";
import {validate} from "../middleware/validate";
import {createChannelSchema} from "../schemas/channel.schema";
const router = express.Router();
router.route('/create')
.post(requireStateToken,
validate(createChannelSchema),
createChannelHandler)
export default router

View file

@ -4,7 +4,8 @@ import {
SlackInstallLinkHandler,
SlackCallbackHandler,
getChannelsHandler,
joinChannelsHandler
joinChannelsHandler,
getBotInfo
} from '../controllers/slack.controller'
import {
@ -23,5 +24,8 @@ router.route('/channels')
.get(requireStateToken, getChannelsHandler)
.post(requireStateToken, joinChannelsHandler)
router.route('/info')
.get(requireStateToken, getBotInfo)
export default router

View file

@ -0,0 +1,17 @@
import { object, string, TypeOf } from 'zod';
export const createChannelSchema = object({
body: object({
invite_token: string({
required_error: "Group invite token required to create channel!"
}),
bridge_id: string({
required_error: "Bridge column ID required to create channel!"
}),
channel_name: string({
required_error: "Channel name required to create channel!"
})
})
})
export type CreateChannelInput = TypeOf<typeof createChannelSchema>['body'];

View file

@ -2,8 +2,8 @@ import { object, string, TypeOf } from 'zod';
export const joinSlackChannelSchema = object({
body: object({
channel: string({
required_error: "Channel name required!"
channel_id: string({
required_error: "Channel ID required!"
}),
})
})

View file

@ -2127,7 +2127,7 @@
dependencies:
debug "^4.3.1"
"@pmmmwh/react-refresh-webpack-plugin@^0.5.3":
"@pmmmwh/react-refresh-webpack-plugin@^0.5.10", "@pmmmwh/react-refresh-webpack-plugin@^0.5.3":
version "0.5.10"
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz#2eba163b8e7dbabb4ce3609ab5e32ab63dda3ef8"
integrity sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==
@ -9667,11 +9667,21 @@ react-is@^18.0.0, react-is@^18.2.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-refresh-typescript@^2.0.9:
version "2.0.9"
resolved "https://registry.yarnpkg.com/react-refresh-typescript/-/react-refresh-typescript-2.0.9.tgz#f8a86efcb34f8d717100230564b9b57477d74b10"
integrity sha512-chAnOO4vpxm/3WkgOVmti+eN8yUtkJzeGkOigV6UA9eDFz12W34e/SsYe2H5+RwYJ3+sfSZkVbiXcG1chEBxlg==
react-refresh@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
react-refresh@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e"
integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==
react-scripts@5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003"