From 3671cbd7aef7e25ee4ecc8f152bc195360a76a2e Mon Sep 17 00:00:00 2001 From: ansible user/allowed to read system logs Date: Mon, 31 Jul 2023 23:25:44 -0700 Subject: [PATCH] working prototype --- client/src/api/bridge.ts | 7 +- client/src/api/channel.ts | 20 ++ client/src/api/groups.ts | 13 + client/src/api/slack.ts | 24 +- client/src/components/join/joinBridge.tsx | 60 +++-- client/src/components/join/joinChannel.tsx | 98 ++++++++ client/src/components/join/joinForm.tsx | 143 +++++------ client/src/components/join/joinPlatform.tsx | 9 +- client/src/components/panels/joinPanel.tsx | 46 ++-- client/src/sass/input.scss | 10 +- server/src/app.ts | 4 +- server/src/controllers/bridge.controller.ts | 3 +- server/src/controllers/channel.controller.ts | 96 ++++++++ server/src/controllers/slack.controller.ts | 241 ++++++++++++++----- server/src/entities/bridge.entity.ts | 11 + server/src/entities/group.entity.ts | 2 +- server/src/matterbridge/config.ts | 6 +- server/src/matterbridge/process.ts | 39 ++- server/src/routes/channel.routes.ts | 22 ++ server/src/routes/slack.routes.ts | 6 +- server/src/schemas/channel.schema.ts | 17 ++ server/src/schemas/slack.schema.ts | 4 +- 22 files changed, 663 insertions(+), 218 deletions(-) create mode 100644 client/src/api/channel.ts create mode 100644 client/src/api/groups.ts create mode 100644 client/src/components/join/joinChannel.tsx create mode 100644 server/src/routes/channel.routes.ts create mode 100644 server/src/schemas/channel.schema.ts diff --git a/client/src/api/bridge.ts b/client/src/api/bridge.ts index f5818ee..6e4359d 100644 --- a/client/src/api/bridge.ts +++ b/client/src/api/bridge.ts @@ -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() + // } }) } \ No newline at end of file diff --git a/client/src/api/channel.ts b/client/src/api/channel.ts new file mode 100644 index 0000000..a8a37da --- /dev/null +++ b/client/src/api/channel.ts @@ -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) + }) +} \ No newline at end of file diff --git a/client/src/api/groups.ts b/client/src/api/groups.ts new file mode 100644 index 0000000..f971655 --- /dev/null +++ b/client/src/api/groups.ts @@ -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)) +} \ No newline at end of file diff --git a/client/src/api/slack.ts b/client/src/api/slack.ts index 641561f..4fe8f25 100644 --- a/client/src/api/slack.ts +++ b/client/src/api/slack.ts @@ -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)) } \ No newline at end of file diff --git a/client/src/components/join/joinBridge.tsx b/client/src/components/join/joinBridge.tsx index 34beeaf..1094e85 100644 --- a/client/src/components/join/joinBridge.tsx +++ b/client/src/components/join/joinBridge.tsx @@ -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 ( - - - - - +
+ {/**/} + {/* Bridge Label*/} {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"} + > - - + +
) } \ No newline at end of file diff --git a/client/src/components/join/joinChannel.tsx b/client/src/components/join/joinChannel.tsx new file mode 100644 index 0000000..e45c522 --- /dev/null +++ b/client/src/components/join/joinChannel.tsx @@ -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 ( +
+ + Select Channel + + + +
+ ) +} + +export default JoinChannel \ No newline at end of file diff --git a/client/src/components/join/joinForm.tsx b/client/src/components/join/joinForm.tsx index 8e766ba..53f4eee 100644 --- a/client/src/components/join/joinForm.tsx +++ b/client/src/components/join/joinForm.tsx @@ -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(); - const [channels, setChannels] = useState(); - const [selectedChannel, setSelectedChannel] = useState(); + const [channels, setChannels] = useState + >(); + const [selectedChannel, setSelectedChannel] = useState(''); const [bridge, setBridge] = useState<{ Label: string; Protocol: string; team_name: string; + id: string; }>(); const [stepComplete, setStepComplete] = useState({ 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 ( <>
@@ -86,81 +96,44 @@ export const JoinForm = ({group}: JoinFormProps) => { /> - - - Label: - - - - - - - - - + -
- - Select Platform - - - -
- - - Label: - - - - - - - - - +
+ ) } \ No newline at end of file diff --git a/client/src/components/join/joinPlatform.tsx b/client/src/components/join/joinPlatform.tsx index 74b01a6..742b899 100644 --- a/client/src/components/join/joinPlatform.tsx +++ b/client/src/components/join/joinPlatform.tsx @@ -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 = () => { @@ -110,7 +103,7 @@ export const JoinPlatform = ({ > Slack - + { installLink && platform == "Slack" ? diff --git a/client/src/components/panels/joinPanel.tsx b/client/src/components/panels/joinPanel.tsx index 9c6bdef..593c615 100644 --- a/client/src/components/panels/joinPanel.tsx +++ b/client/src/components/panels/joinPanel.tsx @@ -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,40 +13,26 @@ export default function JoinPanel(){ const [errorText, setErrorText] = useState(''); const [group, setGroup] = useState(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 => { - 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 onGroupLogin = (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 = () => { - getGroup() + groupInvite(text, onGroupLogin) } const textChanged = (event: React.ChangeEvent) => { setText(event.target.value) + setGroup(undefined) } return(
@@ -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} /> @@ -72,6 +55,7 @@ export default function JoinPanel(){ { group ? : undefined }
diff --git a/client/src/sass/input.scss b/client/src/sass/input.scss index 052e8fc..ff21604 100644 --- a/client/src/sass/input.scss +++ b/client/src/sass/input.scss @@ -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%; diff --git a/server/src/app.ts b/server/src/app.ts index 645a890..a20508d 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -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`)); diff --git a/server/src/controllers/bridge.controller.ts b/server/src/controllers/bridge.controller.ts index 353859e..92b8822 100644 --- a/server/src/controllers/bridge.controller.ts +++ b/server/src/controllers/bridge.controller.ts @@ -16,7 +16,8 @@ export const getBridgeHandler = async( select: { Protocol: true, Label: true, - team_name: true + team_name: true, + id: true } }) if (!bridge){ diff --git a/server/src/controllers/channel.controller.ts b/server/src/controllers/channel.controller.ts index e69de29..204541e 100644 --- a/server/src/controllers/channel.controller.ts +++ b/server/src/controllers/channel.controller.ts @@ -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' + }) + } + +} \ No newline at end of file diff --git a/server/src/controllers/slack.controller.ts b/server/src/controllers/slack.controller.ts index b1037dc..3c52992 100644 --- a/server/src/controllers/slack.controller.ts +++ b/server/src/controllers/slack.controller.ts @@ -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('

Success! Return to the chatbridge login window.

This tab will close in 3 seconds...

') }, - 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', { + let bridge = await bridgeRepository.findOneBy({state_token: req.session.state_token}) + 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[] }> 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({ - status: 'failure', - message: "Couldn't join channel!" + status:'failure', + message:'Couldnt get bot info' }) } } diff --git a/server/src/entities/bridge.entity.ts b/server/src/entities/bridge.entity.ts index e821bd2..144bc1c 100644 --- a/server/src/entities/bridge.entity.ts +++ b/server/src/entities/bridge.entity.ts @@ -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 }) diff --git a/server/src/entities/group.entity.ts b/server/src/entities/group.entity.ts index 32c4304..3d6315f 100644 --- a/server/src/entities/group.entity.ts +++ b/server/src/entities/group.entity.ts @@ -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[] } \ No newline at end of file diff --git a/server/src/matterbridge/config.ts b/server/src/matterbridge/config.ts index 33db295..9f8cd6b 100644 --- a/server/src/matterbridge/config.ts +++ b/server/src/matterbridge/config.ts @@ -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 = { 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) } diff --git a/server/src/matterbridge/process.ts b/server/src/matterbridge/process.ts index 31fe819..ce56ffa 100644 --- a/server/src/matterbridge/process.ts +++ b/server/src/matterbridge/process.ts @@ -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_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` 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(){ diff --git a/server/src/routes/channel.routes.ts b/server/src/routes/channel.routes.ts new file mode 100644 index 0000000..5e8b952 --- /dev/null +++ b/server/src/routes/channel.routes.ts @@ -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 \ No newline at end of file diff --git a/server/src/routes/slack.routes.ts b/server/src/routes/slack.routes.ts index 472af97..82f01de 100644 --- a/server/src/routes/slack.routes.ts +++ b/server/src/routes/slack.routes.ts @@ -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 \ No newline at end of file diff --git a/server/src/schemas/channel.schema.ts b/server/src/schemas/channel.schema.ts new file mode 100644 index 0000000..00c8b1b --- /dev/null +++ b/server/src/schemas/channel.schema.ts @@ -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['body']; \ No newline at end of file diff --git a/server/src/schemas/slack.schema.ts b/server/src/schemas/slack.schema.ts index f7a3724..9270807 100644 --- a/server/src/schemas/slack.schema.ts +++ b/server/src/schemas/slack.schema.ts @@ -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!" }), }) })