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

View file

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

View file

@ -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>React App</title> <title>ChatBridge2</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>

View file

@ -22,8 +22,9 @@ export const setBridgeLabel = (label:string, callback: CallableFunction) => {
}) })
}).then(res => res.json()) }).then(res => res.json())
.then((res) => { .then((res) => {
if (res.status === "success") { callback(res)
callback() // 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 => { .then(res => {
console.log('Got slack channels', res); console.log('Got slack channels', res);
if (res.status === "success"){ 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 Grid from "@mui/material/Grid";
import {TextField} from "@mui/material"; 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 = ({ 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 ( return (
<Grid container spacing={2} columns={2}> <div className={"list-row"}>
<Grid item xs={1}> {/*<FormControl sx={{width: "50%"}}>*/}
{/* <InputLabel>Bridge Label</InputLabel>*/}
</Grid>
<Grid item xs={1}>
<TextField <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> </TextField>
</Grid> <Button
</Grid> 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 Button from '@mui/material/Button';
import {setBridgeLabel} from "../../api/bridge"; import {setBridgeLabel} from "../../api/bridge";
import {getSlackChannels} from "../../api/slack"; import {getSlackChannels} from "../../api/slack";
import {createChannel} from "../../api/channel";
import FormControl from "@mui/material/FormControl"; import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select" import Select from "@mui/material/Select"
import InputLabel from '@mui/material/InputLabel'; import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import {JoinBridge} from "./joinBridge";
import JoinChannel from "./joinChannel";
export interface JoinFormProps { export interface JoinFormProps {
group: Group group?: Group
invite_token: string
} }
interface stepCompleteType { export interface stepCompleteType {
login: boolean; login: boolean;
bridge: boolean; bridge: boolean;
channel: 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 [platform, setPlatform] = useState<string>();
const [channels, setChannels] = useState<string[]>(); const [channels, setChannels] = useState<Array<{
const [selectedChannel, setSelectedChannel] = useState<string>(); name: string;
id: string;
is_member: boolean;
}>
>();
const [selectedChannel, setSelectedChannel] = useState<string>('');
const [bridge, setBridge] = useState<{ const [bridge, setBridge] = useState<{
Label: string; Label: string;
Protocol: string; Protocol: string;
team_name: string; team_name: string;
id: string;
}>(); }>();
const [stepComplete, setStepComplete] = useState<stepCompleteType>({ const [stepComplete, setStepComplete] = useState<stepCompleteType>({
login: false, login: false,
@ -43,31 +55,29 @@ export const JoinForm = ({group}: JoinFormProps) => {
channel: false 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(() => { useEffect(() => {
if (bridge !== undefined){ if (bridge !== undefined){
setStepComplete({...stepComplete, login:true}) setStepComplete({...stepComplete, login:true})
} }
if (channels === undefined){ if (bridge !== undefined && channels === undefined){
getSlackChannels(setChannels) getSlackChannels(setChannels)
} }
}, [bridge]) }, [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 ( return (
<> <>
<header className={'section-header'}> <header className={'section-header'}>
@ -86,81 +96,44 @@ export const JoinForm = ({group}: JoinFormProps) => {
/> />
</JoinStep> </JoinStep>
<JoinStep <JoinStep
title={"2) Set Bridge Label"} title={"2) Configure Bridge"}
details={"A short identifier shown before your messages"} details={"Settings for all channels bridged from this platform"}
id={'bridge'} id={'bridge'}
disabled={!stepComplete.login} disabled={!stepComplete.login}
completed={stepComplete.bridge} completed={stepComplete.bridge}
> >
<Grid container spacing={2} columns={4} alignItems="center"> <JoinBridge
<Grid item xs={1}> bridge={bridge}
<Typography>Label:</Typography> setBridge={setBridge}
</Grid> setStepComplete={setStepComplete}
<Grid item xs={2}> stepComplete={stepComplete}
<TextField />
value={bridge?.Label}
onChange={onSetBridge}>
</TextField>
</Grid>
<Grid item xs={1}>
<Button
variant={"outlined"}
onClick={updateBridgeLabel}>
Update Label!
</Button>
</Grid>
</Grid>
</JoinStep> </JoinStep>
<JoinStep <JoinStep
title={"3) Select a channel!"} title={"3) Select a channel!"}
details={"The bot will join :)"} details={"The bot will join :)"}
id={'bridge'} id={'channel'}
disabled={!stepComplete.login} disabled={!stepComplete.login}
completed={stepComplete.channel} completed={stepComplete.channel}
> >
<div className={"list-row"}> <JoinChannel
<FormControl sx={{width: "50%"}}> channels={channels}
<InputLabel>Select Platform</InputLabel> selectedChannel={selectedChannel}
<Select setSelectedChannel={setSelectedChannel}
// value={platform} setStepComplete={setStepComplete}
onChange={(evt:any) => {setSelectedChannel(evt.target.value)}} stepComplete={stepComplete}
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>
</JoinStep> </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() // pingForBridge()
} }
// const pingForBridge = () =>{
// if (bridge === undefined){
// console.log('bridge is', bridge)
// getBridgeByStateToken(setBridge);
// setTimeout(pingForBridge, 1000);
// }
// }
useEffect(() => { useEffect(() => {
const pingForBridge = () => { const pingForBridge = () => {
@ -110,7 +103,7 @@ export const JoinPlatform = ({
> >
<MenuItem value={'Slack'}>Slack</MenuItem> <MenuItem value={'Slack'}>Slack</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
{ {
installLink && platform == "Slack" ? installLink && platform == "Slack" ?

View file

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

View file

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

View file

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

View file

@ -16,7 +16,8 @@ export const getBridgeHandler = async(
select: { select: {
Protocol: true, Protocol: true,
Label: true, Label: true,
team_name: true team_name: true,
id: true
} }
}) })
if (!bridge){ 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 {log} from "util";
import {Join} from "typeorm"; 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 bridgeRepository = AppDataSource.getRepository(Bridge)
const groupRepository = AppDataSource.getRepository(Group) const groupRepository = AppDataSource.getRepository(Group)
@ -22,6 +22,43 @@ const slackConfig = config.get<{
state_secret: string state_secret: string
}>('slackConfig'); }>('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({ const installer = new InstallProvider({
clientId: slackConfig.client_id, clientId: slackConfig.client_id,
clientSecret: slackConfig.client_secret, 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( export const SlackInstallLinkHandler = async(
req: Request, req: Request,
@ -83,17 +109,21 @@ export const SlackCallbackHandler = async(
) => { ) => {
// using custom success and failure handlers // using custom success and failure handlers
const callbackOptions = { 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, installOptions, req.body, req.content, req.query, req.params)
// console.log(installation.team.id, installation.team.name, installation.bot.token); // console.log(installation.team.id, installation.team.name, installation.bot.token);
console.log('istallation info', installation)
let bridge_data = { let bridge_data = {
'Protocol': 'slack', 'Protocol': 'slack',
'Label': installation.team.name, 'Label': installation.team.name,
'team_id': installation.team.id, 'team_id': installation.team.id,
'team_name': installation.team.name, 'team_name': installation.team.name,
'state_token': req.session.state_token, '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 // check if we have an entity
let bridge = await bridgeRepository.findOneBy({Token: installation.bot.token}) let bridge = await bridgeRepository.findOneBy({Token: installation.bot.token})
@ -103,16 +133,30 @@ export const SlackCallbackHandler = async(
await bridgeRepository.save(bridge); await bridgeRepository.save(bridge);
console.log('created bridge') console.log('created bridge')
} else { } else {
await bridgeRepository.update( // await bridgeRepository.update(
{Token: installation.bot.token}, // {Token: installation.bot.token},
{state_token: req.session.state_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') 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>') 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) => { failure: (error:any, installOptions:any , req:Request, res:Response) => {
console.log(error, installOptions, req.body, req.content, req.query, req.params) // console.log(error, installOptions, req.body, req.content, req.query, req.params)
res.send('failure. Something is broken about chatbridge :('); res.send('failure. Something is broken about chatbridge :(');
}, },
} }
@ -127,7 +171,7 @@ export const getChannelsHandler = async(
res: Response res: Response
) => { ) => {
let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token}) let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token})
console.log('bridge data', bridge) // console.log('bridge data', bridge)
try { try {
fetch('https://slack.com/api/conversations.list', { fetch('https://slack.com/api/conversations.list', {
@ -140,8 +184,12 @@ export const getChannelsHandler = async(
.then((result) => { .then((result) => {
console.log('channels',result) console.log('channels',result)
let channels = result.channels.map( let channels = result.channels.map(
(chan: { name: string; }) => { (chan: slackChannel) => {
return chan.name return {
'name': chan.name,
'id': chan.id,
'is_member': chan.is_member
}
} }
); );
res.status(200).json({ res.status(200).json({
@ -164,57 +212,138 @@ export const joinChannelsHandler = async(
req: Request<{}, {}, JoinSlackChannelInput>, req: Request<{}, {}, JoinSlackChannelInput>,
res: Response res: Response
) => { ) => {
let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token})
console.log('bridge data', bridge)
try { let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token})
// Get channel ID from channel name console.log('joinchannel bridge', bridge)
let channels_res = await fetch('https://slack.com/api/conversations.list', { 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", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${bridge.Token}` "Authorization": `Bearer ${bridge.user_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}`
}, },
body: JSON.stringify({ body: JSON.stringify({
channel: channel_id users: bridge.bot_id,
channel: req.body.channel_id
}) })
}).then(res => res.json()) }).then(result => result.json())
.then((res) => { .then(result => {
if (res.ok) { console.log(result);
res.status(200).json({ if (result.ok || result.error === 'already_in_channel'){
return res.status(200).json({
status: 'success', status: 'success',
data: { data: {
channels id: req.body.channel_id
} }
}) })
} else { } else {
res.status(502).json({ return res.status(502).json({
status: 'failure', status: 'failed',
message: 'Couldnt join channel' 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: 'unknown error getting auth test'
})
}
})
} catch (error) {
console.log('auth.test error', error)
return res.status(502).json({ return res.status(502).json({
status: 'failure', status:'failure',
message: "Couldn't join channel!" message:'Couldnt get bot info'
}) })
} }
} }

View file

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

View file

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

View file

@ -57,7 +57,7 @@ export const getGroupConfig = async (group_name: string, group:object): Promise<
where: {name: group_name}, where: {name: group_name},
relations: {channels: true} relations: {channels: true}
}) })
console.log('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,
@ -79,6 +79,7 @@ export const getGroupConfig = async (group_name: string, group:object): Promise<
} }
}) })
} }
console.log('config group transformed', gateway)
return gateway return gateway
} }
@ -153,6 +154,7 @@ export const GatewayToTOML = (gateway: Gateway) => {
protocols[bridge.protocol][bridge.name] = TOML.Section(bridgeEntry) protocols[bridge.protocol][bridge.name] = TOML.Section(bridgeEntry)
}) })
console.log('gateway toml protocols', protocols)
return { return {
...protocols, ...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) fs.writeFileSync(out_file, toml_string)
} }

View file

@ -11,6 +11,7 @@ import config from "config";
import { writeGroupConfig } from "./config"; import { writeGroupConfig } from "./config";
import {Group} from "../entities/group.entity"; import {Group} from "../entities/group.entity";
import slugify from "slugify";
const groupRepository = AppDataSource.getRepository(Group) const groupRepository = AppDataSource.getRepository(Group)
@ -53,21 +54,35 @@ class MatterbridgeManager {
){} ){}
async spawnProcess(group_name: string) { async spawnProcess(group_name: string) {
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_slug,
script: this.matterbridge_bin,
args: `-conf ${group_filename}`,
interpreter: 'none'
},
(err:any, apps:object) => {
console.log('error starting matterbridge process', err, apps)
}
)
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` let group_filename = `${this.matterbridge_config_dir}/matterbridge-${group_name}.toml`
await writeGroupConfig(group_name, group_filename); await writeGroupConfig(group_name, group_filename);
await pm2.start(
{
name: group_name,
script: this.matterbridge_bin,
args: `-conf ${group_filename}`,
interpreter: 'none'
},
(err:any, apps:object) => {
// TODO: Handle errors!
}
)
this.process_list.push(group_name)
} }
async spawnAll(){ 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, SlackInstallLinkHandler,
SlackCallbackHandler, SlackCallbackHandler,
getChannelsHandler, getChannelsHandler,
joinChannelsHandler joinChannelsHandler,
getBotInfo
} from '../controllers/slack.controller' } from '../controllers/slack.controller'
import { import {
@ -23,5 +24,8 @@ router.route('/channels')
.get(requireStateToken, getChannelsHandler) .get(requireStateToken, getChannelsHandler)
.post(requireStateToken, joinChannelsHandler) .post(requireStateToken, joinChannelsHandler)
router.route('/info')
.get(requireStateToken, getBotInfo)
export default router 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({ export const joinSlackChannelSchema = object({
body: object({ body: object({
channel: string({ channel_id: string({
required_error: "Channel name required!" required_error: "Channel ID required!"
}), }),
}) })
}) })

View file

@ -2127,7 +2127,7 @@
dependencies: dependencies:
debug "^4.3.1" 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" version "0.5.10"
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz#2eba163b8e7dbabb4ce3609ab5e32ab63dda3ef8" 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== 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" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== 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: react-refresh@^0.11.0:
version "0.11.0" version "0.11.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== 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: react-scripts@5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003"