Skip to content

Commit f77e7a3

Browse files
committed
Connect to existing self-hosted bridges
1 parent aec0b82 commit f77e7a3

File tree

6 files changed

+331
-54
lines changed

6 files changed

+331
-54
lines changed

app/api/bridges/route.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {NextResponse} from 'next/server'
2+
import {gql, GraphQLClient} from 'graphql-request'
3+
4+
export async function POST(req: Request) {
5+
const {beeperToken, flyToken} = await req.json()
6+
7+
const beeper_whoami = await fetch('https://api.beeper.com/whoami', {
8+
headers: {
9+
'Authorization': `Bearer ${beeperToken}`,
10+
'Content-Type': 'application/json',
11+
},
12+
})
13+
14+
if (beeper_whoami.status != 200) {
15+
const beeper_bridge_data = await beeper_whoami.json();
16+
return NextResponse.json({ error: JSON.stringify(beeper_bridge_data) }, { status: 500 })
17+
}
18+
19+
const beeper_bridge_response = await beeper_whoami.json();
20+
const beeper_bridges = Object.keys(beeper_bridge_response.user.bridges);
21+
22+
const graphQLClient = new GraphQLClient('https://api.fly.io/graphql', {
23+
headers: {
24+
authorization: `Bearer ${flyToken}`,
25+
},
26+
})
27+
28+
const fly_bridge_query = gql`
29+
query PersonalOrganization {
30+
personalOrganization {
31+
apps {
32+
nodes {
33+
id
34+
}
35+
}
36+
}
37+
}
38+
`
39+
40+
try {
41+
const fly_bridge_response: any = await graphQLClient.request(fly_bridge_query)
42+
43+
const fly_bridges_json = fly_bridge_response.personalOrganization.apps.nodes;
44+
const fly_bridges = fly_bridges_json.map((bridge: any) => bridge.id)
45+
46+
const bridges: { id: string, onFly: boolean }[] = []
47+
beeper_bridges.forEach((bridge: any) => {
48+
if (bridge.startsWith("sh-")) {
49+
50+
const deployedOnFly = fly_bridges.includes(bridge)
51+
52+
bridges.push({
53+
id: bridge,
54+
onFly: deployedOnFly
55+
})
56+
}
57+
})
58+
59+
return NextResponse.json({"bridges": bridges})
60+
} catch (error: any) {
61+
return NextResponse.json({ error: JSON.stringify(error.response.errors[0].message) }, { status: 500 })
62+
}
63+
}

app/api/delete/route.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {NextResponse} from 'next/server'
2+
3+
export async function DELETE(req: Request) {
4+
const {beeperToken, flyToken, name, onFly} = await req.json()
5+
6+
if (onFly) {
7+
const fly_delete = await fetch(`https://api.machines.dev/v1/apps/${name}`, {
8+
method: 'DELETE',
9+
headers: {
10+
'Authorization': `Bearer ${flyToken}`,
11+
'Content-Type': 'application/json',
12+
},
13+
})
14+
15+
if (fly_delete.status != 202) {
16+
const fly_delete_data = await fly_delete.json();
17+
return NextResponse.json({ error: JSON.stringify(fly_delete_data) }, { status: 500 })
18+
}
19+
}
20+
21+
const beeper_delete = await fetch(`https://api.beeper.com/bridge/${name}`, {
22+
method: "DELETE",
23+
headers: {
24+
'Authorization': `Bearer ${beeperToken}`,
25+
'Content-Type': 'application/json',
26+
},
27+
})
28+
29+
// Wait for the app to delete before returning.
30+
let beeper_bridges: string[] = [name]
31+
while (beeper_bridges.includes(name)) {
32+
const beeper_whoami = await fetch('https://api.beeper.com/whoami', {
33+
headers: {
34+
'Authorization': `Bearer ${beeperToken}`,
35+
'Content-Type': 'application/json',
36+
},
37+
})
38+
39+
if (beeper_whoami.status != 200) {
40+
const beeper_bridge_data = await beeper_whoami.json();
41+
return NextResponse.json({ error: JSON.stringify(beeper_bridge_data) }, { status: 500 })
42+
}
43+
44+
const beeper_bridge_response = await beeper_whoami.json();
45+
beeper_bridges = Object.keys(beeper_bridge_response.user.bridges);
46+
47+
await new Promise(r => setTimeout(r, 1000));
48+
}
49+
50+
return NextResponse.json({})
51+
}

app/api/deploy/route.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {gql, GraphQLClient} from 'graphql-request'
33

44
export async function POST(req: Request) {
55

6-
const {beeperToken, flyToken, bridge} = await req.json()
6+
const {beeperToken, flyToken, bridge, region} = await req.json()
77
const app_name = `sh-${bridge}-${Date.now()}`
88

99
// Create the app
@@ -44,7 +44,7 @@ export async function POST(req: Request) {
4444
"input": {
4545
"appId": app_name,
4646
"type": "shared_v4",
47-
"region": "iad"
47+
"region": region
4848
}
4949
}
5050
const ip_request_data: any = await graphQLClient.request(ip_query, ip_variables)
@@ -100,7 +100,7 @@ export async function POST(req: Request) {
100100
'Content-Type': 'application/json',
101101
},
102102
body: JSON.stringify({
103-
"region": "iad",
103+
"region": region,
104104
"config": {
105105
"image": "ghcr.io/beeper/bridge-manager",
106106
"env": {
@@ -126,17 +126,7 @@ export async function POST(req: Request) {
126126
"protocol": "tcp",
127127
"internal_port": 8080
128128
}
129-
],
130-
// "checks": {
131-
// "httpget": {
132-
// "type": "http",
133-
// "port": 8080,
134-
// "method": "GET",
135-
// "path": "/",
136-
// "interval": "15s",
137-
// "timeout": "10s"
138-
// }
139-
// }
129+
]
140130
}
141131
})
142132
})
@@ -146,5 +136,26 @@ export async function POST(req: Request) {
146136
return NextResponse.json({ error: JSON.stringify(create_machine_data) }, { status: 500 })
147137
}
148138

139+
// Wait for the app to deploy before returning
140+
let beeper_bridges: string[] = []
141+
while (!(beeper_bridges.includes(app_name))) {
142+
const beeper_whoami = await fetch('https://api.beeper.com/whoami', {
143+
headers: {
144+
'Authorization': `Bearer ${beeperToken}`,
145+
'Content-Type': 'application/json',
146+
},
147+
})
148+
149+
if (beeper_whoami.status != 200) {
150+
const beeper_bridge_data = await beeper_whoami.json();
151+
return NextResponse.json({ error: JSON.stringify(beeper_bridge_data) }, { status: 500 })
152+
}
153+
154+
const beeper_bridge_response = await beeper_whoami.json();
155+
beeper_bridges = Object.keys(beeper_bridge_response.user.bridges);
156+
157+
await new Promise(r => setTimeout(r, 1000));
158+
}
159+
149160
return NextResponse.json({"appName": app_name})
150161
}

app/components/Bridge.tsx

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import {useState} from "react";
22

3-
export default function Bridge({name, value, beeperToken, flyToken}: any) {
3+
export default function Bridge({name, onFly, beeperToken, flyToken, onDelete}: any) {
44

5-
const [deployed, setDeployed] = useState(false)
6-
const [deployInProgress, setDeployInProgress] = useState(false)
7-
const [appId, setAppId] = useState("")
5+
const [deleteInProgress, setDeleteInProgress] = useState(false)
86
const [errorMessage, setErrorMessage] = useState("")
97

10-
async function deploy() {
11-
setDeployInProgress(true)
8+
async function deleteBridge() {
9+
setDeleteInProgress(true);
1210

13-
const res = await fetch("/api/deploy", {
14-
method: 'POST',
15-
body: JSON.stringify({beeperToken: beeperToken, flyToken: flyToken, bridge: value})
11+
const res = await fetch("/api/delete", {
12+
method: 'DELETE',
13+
body: JSON.stringify({beeperToken: beeperToken, flyToken: flyToken, name: name, onFly: onFly})
1614
})
1715

1816
if (res.status === 500) {
@@ -21,32 +19,29 @@ export default function Bridge({name, value, beeperToken, flyToken}: any) {
2119
return;
2220
}
2321

24-
const {appName} = await res.json();
25-
26-
setAppId(appName);
27-
setDeployed(true)
28-
setDeployInProgress(false)
22+
setDeleteInProgress(false);
23+
onDelete();
2924
}
3025

31-
3226
return (
3327
<tr>
3428
<td className={"border p-2"}>
3529
<p className={"p-2"}>{name}</p>
3630
</td>
37-
<td className={"border p-2"}>
38-
{!deployInProgress ? <button className={"p-2 rounded-md m-4 bg-purple-600 border-0 text-white hover:bg-purple-500"} onClick={deploy}>Deploy</button> :
39-
<button className={"p-2 rounded-md m-4 bg-purple-300 border-0 text-white"} disabled={true}>Deploying...</button>}
40-
</td>
41-
<td className={"border p-2"}>
42-
{ deployed && <a target="_blank" href={`https://fly.io/apps/${appId}/monitoring`} rel="noopener noreferrer">Machine</a> }
43-
{ errorMessage }
31+
32+
<td className={"border p-2 text-center"}>
33+
{ onFly && <button className={"p-2 rounded-md m-4 bg-gray-600 border-0 text-white hover:bg-gray-500"} onClick={() => { navigator.clipboard.writeText(`@${name}bot:beeper.local`)}}>Copy</button>}
4434
</td>
4535

46-
<td className={"border p-2"}>
47-
{ deployed && <p>{`@${appId}bot:beeper.local`}</p> }
36+
<td className={"border p-2 text-center"}>
37+
{ onFly && <a target="_blank" href={`https://fly.io/apps/${name}/monitoring`} rel="noopener noreferrer">View on Fly</a> }
4838
</td>
4939

40+
<td className={"border p-2 text-center"}>
41+
{!deleteInProgress ? <button className={"p-2 rounded-md m-4 bg-red-600 border-0 text-white hover:bg-red-500"} onClick={deleteBridge}>Delete</button> :
42+
<button className={"p-2 rounded-md m-4 bg-red-300 border-0 text-white"} disabled={true}>Deleting...</button>}
43+
{ errorMessage }
44+
</td>
5045
</tr>
5146
)
5247
}

app/components/BridgeDeploy.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {useState} from "react";
2+
3+
export default function BridgeDeploy({beeperToken, flyToken, onCreate}: any) {
4+
5+
const [deployInProgress, setDeployInProgress] = useState(false)
6+
const [errorMessage, setErrorMessage] = useState("")
7+
8+
const regions: Record<string, string> = {
9+
ams: 'Amsterdam, Netherlands',
10+
arn: 'Stockholm, Sweden',
11+
atl: 'Atlanta, Georgia (US)',
12+
bog: 'Bogotá, Colombia',
13+
bos: 'Boston, Massachusetts (US)',
14+
cdg: 'Paris, France',
15+
den: 'Denver, Colorado (US)',
16+
dfw: 'Dallas, Texas (US)',
17+
ewr: 'Secaucus, NJ (US)',
18+
eze: 'Ezeiza, Argentina',
19+
gdl: 'Guadalajara, Mexico',
20+
gig: 'Rio de Janeiro, Brazil',
21+
gru: 'Sao Paulo, Brazil',
22+
hkg: 'Hong Kong, Hong Kong',
23+
iad: 'Ashburn, Virginia (US)',
24+
jnb: 'Johannesburg, South Africa',
25+
lax: 'Los Angeles, California (US)',
26+
lhr: 'London, United Kingdom',
27+
mad: 'Madrid, Spain',
28+
mia: 'Miami, Florida (US)',
29+
nrt: 'Tokyo, Japan',
30+
ord: 'Chicago, Illinois (US)',
31+
otp: 'Bucharest, Romania',
32+
phx: 'Phoenix, Arizona (US)',
33+
qro: 'Querétaro, Mexico',
34+
scl: 'Santiago, Chile',
35+
sea: 'Seattle, Washington (US)',
36+
sin: 'Singapore, Singapore',
37+
sjc: 'San Jose, California (US)',
38+
syd: 'Sydney, Australia',
39+
waw: 'Warsaw, Poland',
40+
yul: 'Montreal, Canada',
41+
yyz: 'Toronto, Canada'
42+
};
43+
44+
const bridges: Record<string, string> = {
45+
whatsapp: "WhatsApp",
46+
gmessages: "Google Messages",
47+
instagram: "Instagram",
48+
discord: "Discord",
49+
slack: "Slack",
50+
telegram: "Telegram",
51+
twitter: "Twitter"
52+
}
53+
54+
55+
async function deploy(event: any) {
56+
event.preventDefault();
57+
58+
setDeployInProgress(true)
59+
60+
const res = await fetch("/api/deploy", {
61+
method: 'POST',
62+
body: JSON.stringify({
63+
beeperToken: beeperToken,
64+
flyToken: flyToken,
65+
bridge: event.target.bridge.value,
66+
region: event.target.region.value
67+
})
68+
})
69+
70+
if (res.status === 500) {
71+
const error_data = await res.json();
72+
setErrorMessage(error_data.error);
73+
return;
74+
}
75+
76+
setDeployInProgress(false)
77+
onCreate();
78+
}
79+
80+
return (
81+
<div className="m-20">
82+
<p className="text-center text-2xl font-bold">Deploy a new bridge</p>
83+
<form onSubmit={deploy} className="text-center">
84+
<select className="border-2 p-2 m-2" name="bridge" defaultValue="whatsapp">
85+
{Object.keys(bridges).map((bridge) => (
86+
<option key={bridge} value={bridge}>{bridges[bridge]}</option>
87+
))}
88+
</select>
89+
<select className="border-2 p-2 m-2" name="region" defaultValue="iad">
90+
{Object.keys(regions).map((region) => (
91+
<option key={region} value={region}>{regions[region]}</option>
92+
))}
93+
</select>
94+
{!deployInProgress ? <button
95+
className={"p-2 rounded-md m-4 bg-purple-600 border-0 text-white hover:bg-purple-500"}>Deploy</button> :
96+
<button className={"p-2 rounded-md m-4 bg-purple-300 border-0 text-white"}
97+
disabled={true}>Deploying...</button>}
98+
{errorMessage}
99+
</form>
100+
</div>
101+
)
102+
103+
}

0 commit comments

Comments
 (0)