Compare commits
2 commits
7725860a96
...
3671cbd7ae
Author | SHA1 | Date | |
---|---|---|---|
|
3671cbd7ae | ||
|
eea1e14906 |
26 changed files with 688 additions and 221 deletions
|
@ -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()
|
||||
],
|
||||
})
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
20
client/src/api/channel.ts
Normal 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
13
client/src/api/groups.ts
Normal 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))
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
|
||||
}
|
98
client/src/components/join/joinChannel.tsx
Normal file
98
client/src/components/join/joinChannel.tsx
Normal 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
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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`));
|
||||
|
|
|
@ -16,7 +16,8 @@ export const getBridgeHandler = async(
|
|||
select: {
|
||||
Protocol: true,
|
||||
Label: true,
|
||||
team_name: true
|
||||
team_name: true,
|
||||
id: true
|
||||
}
|
||||
})
|
||||
if (!bridge){
|
||||
|
|
|
@ -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'
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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[]
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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(){
|
||||
|
|
22
server/src/routes/channel.routes.ts
Normal file
22
server/src/routes/channel.routes.ts
Normal 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
|
|
@ -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
|
17
server/src/schemas/channel.schema.ts
Normal file
17
server/src/schemas/channel.schema.ts
Normal 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'];
|
|
@ -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!"
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue