Refactoring 2
This commit is contained in:
parent
dd14dfe72e
commit
e4b78ceec2
76 changed files with 870 additions and 734 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { Create } from "./_components/Create";
|
import { Create } from "bridge-ui";
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
params: { segment: string[] };
|
params: { segment: string[] };
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
import { Grid } from "@mui/material";
|
|
||||||
import { DisplayTextField } from "ui";
|
|
||||||
import { Selectable } from "kysely";
|
|
||||||
import { Database } from "@/app/_lib/database";
|
|
||||||
import { Detail as InternalDetail } from "@/app/_components/Detail";
|
|
||||||
import { generateDeleteAction } from "@/app/_lib/actions";
|
|
||||||
import { serviceConfig } from "@/app/_config/config";
|
|
||||||
|
|
||||||
type DetailProps = {
|
|
||||||
service: string;
|
|
||||||
row: Selectable<keyof Database>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Detail: FC<DetailProps> = ({ service, row }) => {
|
|
||||||
const {
|
|
||||||
[service]: { entity, table, displayName, displayFields: fields },
|
|
||||||
} = serviceConfig;
|
|
||||||
const deleteAction = generateDeleteAction({ entity, table });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InternalDetail
|
|
||||||
title={`${displayName}: ${row.name}`}
|
|
||||||
entity={entity}
|
|
||||||
id={row.id as string}
|
|
||||||
deleteAction={deleteAction}
|
|
||||||
>
|
|
||||||
<Grid container direction="row" rowSpacing={3} columnSpacing={2}>
|
|
||||||
{fields.map((field) => (
|
|
||||||
<Grid item xs={field.size ?? 6} key={field.name}>
|
|
||||||
<DisplayTextField
|
|
||||||
name={field.name}
|
|
||||||
label={field.label}
|
|
||||||
lines={field.lines ?? 1}
|
|
||||||
value={row[field.name] as string}
|
|
||||||
copyable={field.copyable ?? false}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</InternalDetail>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { db } from "@/app/_lib/database";
|
import { db } from "bridge-common";
|
||||||
import { serviceConfig } from "@/app/_config/config";
|
import { serviceConfig, Detail } from "bridge-ui";
|
||||||
import { Detail } from "./_components/Detail";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: { segment: string[] };
|
params: { segment: string[] };
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
import { useFormState } from "react-dom";
|
|
||||||
import { Grid } from "@mui/material";
|
|
||||||
import { TextField } from "ui";
|
|
||||||
import { Selectable } from "kysely";
|
|
||||||
import { Database } from "@/app/_lib/database";
|
|
||||||
import { Edit as InternalEdit } from "@/app/_components/Edit";
|
|
||||||
import { generateUpdateAction } from "@/app/_lib/actions";
|
|
||||||
import { serviceConfig } from "@/app/_config/config";
|
|
||||||
|
|
||||||
type EditProps = {
|
|
||||||
service: string;
|
|
||||||
row: Selectable<keyof Database>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Edit: FC<EditProps> = ({ service, row }) => {
|
|
||||||
const {
|
|
||||||
[service]: { entity, table, displayName, updateFields: fields },
|
|
||||||
} = serviceConfig;
|
|
||||||
const updateFieldNames = fields.map((val) => val.name);
|
|
||||||
const updateAction = generateUpdateAction({ entity, table, fields });
|
|
||||||
const updateValues = Object.fromEntries(
|
|
||||||
Object.entries(row).filter(([key]) => updateFieldNames.includes(key)),
|
|
||||||
);
|
|
||||||
updateValues.id = row.id;
|
|
||||||
const initialState = {
|
|
||||||
message: null,
|
|
||||||
errors: {},
|
|
||||||
values: updateValues,
|
|
||||||
};
|
|
||||||
const [formState, formAction] = useFormState(updateAction, initialState);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InternalEdit
|
|
||||||
title={`Edit ${displayName}: ${row.name}`}
|
|
||||||
entity={entity}
|
|
||||||
formAction={formAction}
|
|
||||||
formState={formState}
|
|
||||||
>
|
|
||||||
<Grid container direction="row" rowSpacing={3} columnSpacing={2}>
|
|
||||||
{fields.map((field) => (
|
|
||||||
<Grid key={field.name} item xs={field.size ?? 6}>
|
|
||||||
<TextField
|
|
||||||
name={field.name}
|
|
||||||
label={field.label}
|
|
||||||
lines={field.lines ?? 1}
|
|
||||||
disabled={field.disabled ?? false}
|
|
||||||
refreshable={field.refreshable ?? false}
|
|
||||||
required={field.required ?? false}
|
|
||||||
formState={formState}
|
|
||||||
helperText={field.helperText}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</InternalEdit>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { db } from "@/app/_lib/database";
|
import { db } from "bridge-common";
|
||||||
import { serviceConfig } from "@/app/_config/config";
|
import { serviceConfig, Edit } from "bridge-ui";
|
||||||
import { Edit } from "./_components/Edit";
|
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
params: { segment: string[] };
|
params: { segment: string[] };
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
import { List as InternalList } from "@/app/_components/List";
|
|
||||||
import type { Selectable } from "kysely";
|
|
||||||
import { Database } from "@/app/_lib/database";
|
|
||||||
import { serviceConfig } from "@/app/_config/config";
|
|
||||||
|
|
||||||
type ListProps = {
|
|
||||||
service: string;
|
|
||||||
rows: Selectable<keyof Database>[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const List: FC<ListProps> = ({ service, rows }) => {
|
|
||||||
const { displayName, entity, listColumns } = serviceConfig[service];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InternalList
|
|
||||||
title={`${displayName}s`}
|
|
||||||
entity={entity}
|
|
||||||
rows={rows}
|
|
||||||
columns={listColumns}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,32 +1,3 @@
|
||||||
type ServiceLayoutProps = {
|
import { ServiceLayout } from "bridge-ui";
|
||||||
children: any;
|
|
||||||
detail: any;
|
|
||||||
edit: any;
|
|
||||||
create: any;
|
|
||||||
params: {
|
|
||||||
segment: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ServiceLayout({
|
export default ServiceLayout;
|
||||||
children,
|
|
||||||
detail,
|
|
||||||
edit,
|
|
||||||
create,
|
|
||||||
params: { segment },
|
|
||||||
}: ServiceLayoutProps) {
|
|
||||||
const length = segment?.length ?? 0;
|
|
||||||
const isCreate = length === 2 && segment[1] === "create";
|
|
||||||
const isEdit = length === 3 && segment[2] === "edit";
|
|
||||||
const id = length > 1 && !isCreate ? segment[1] : null;
|
|
||||||
const isDetail = length === 2 && !!id && !isCreate && !isEdit;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
{isDetail && detail}
|
|
||||||
{isEdit && edit}
|
|
||||||
{isCreate && create}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { List } from "./_components/List";
|
import { db } from "bridge-common";
|
||||||
import { db } from "@/app/_lib/database";
|
import { serviceConfig, List } from "bridge-ui";
|
||||||
import { serviceConfig } from "@/app/_config/config";
|
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
params: {
|
params: {
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { FC, useEffect } from "react";
|
|
||||||
import { Grid } from "@mui/material";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { Button, Dialog } from "ui";
|
|
||||||
|
|
||||||
interface CreateProps {
|
|
||||||
title: string;
|
|
||||||
entity: string;
|
|
||||||
formAction: any;
|
|
||||||
formState: any;
|
|
||||||
children: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Create: FC<CreateProps> = ({
|
|
||||||
title,
|
|
||||||
entity,
|
|
||||||
formAction,
|
|
||||||
formState,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (formState.success) {
|
|
||||||
router.push(`/${entity}/${formState.values.id}`);
|
|
||||||
}
|
|
||||||
}, [formState.success, router]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open
|
|
||||||
title={title}
|
|
||||||
formAction={formAction}
|
|
||||||
onClose={() => router.push(`/${entity}`)}
|
|
||||||
buttons={
|
|
||||||
<Grid container justifyContent="space-between">
|
|
||||||
<Grid item>
|
|
||||||
<Button
|
|
||||||
text="Cancel"
|
|
||||||
kind="secondary"
|
|
||||||
onClick={() => router.push(`/${entity}`)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Button text="Save" kind="primary" type="submit" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { FC, useEffect } from "react";
|
|
||||||
import { Grid } from "@mui/material";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { Button, Dialog } from "ui";
|
|
||||||
|
|
||||||
interface EditProps {
|
|
||||||
title: string;
|
|
||||||
entity: string;
|
|
||||||
formAction: any;
|
|
||||||
formState: any;
|
|
||||||
children: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Edit: FC<EditProps> = ({
|
|
||||||
title,
|
|
||||||
entity,
|
|
||||||
formState,
|
|
||||||
formAction,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (formState.success) {
|
|
||||||
router.push(`/${entity}`);
|
|
||||||
}
|
|
||||||
}, [formState.success, router, entity]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open
|
|
||||||
title={title}
|
|
||||||
formAction={formAction}
|
|
||||||
onClose={() => router.push(`/${entity}`)}
|
|
||||||
buttons={
|
|
||||||
<Grid container justifyContent="space-between">
|
|
||||||
<Grid item>
|
|
||||||
<Button
|
|
||||||
text="Cancel"
|
|
||||||
kind="secondary"
|
|
||||||
onClick={() => router.push(`/${entity}`)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid item>
|
|
||||||
<Button text="Save" kind="primary" type="submit" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Grid } from "@mui/material";
|
||||||
import { CssBaseline } from "@mui/material";
|
import { CssBaseline } from "@mui/material";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
import { css, Global } from "@emotion/react";
|
import { css, Global } from "@emotion/react";
|
||||||
import { fonts } from "@/app/_styles/theme";
|
import { fonts } from "ui";
|
||||||
import { Sidebar } from "./Sidebar";
|
import { Sidebar } from "./Sidebar";
|
||||||
|
|
||||||
export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
|
export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ import {
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import LinkLogo from "@/app/../public/link-logo-small.png";
|
import LinkLogo from "@/app/_images/link-logo-small.png";
|
||||||
import { colors } from "@/app/_styles/theme";
|
import { colors } from "ui";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
type LoginProps = {
|
type LoginProps = {
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,8 @@ import {
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { typography, fonts } from "@/app/_styles/theme";
|
import { typography, fonts } from "ui";
|
||||||
import LinkLogo from "@/public/link-logo-small.png";
|
import LinkLogo from "@/app/_images/link-logo-small.png";
|
||||||
import { useSession, signOut } from "next-auth/react";
|
import { useSession, signOut } from "next-auth/react";
|
||||||
|
|
||||||
const openWidth = 270;
|
const openWidth = 270;
|
||||||
|
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import { selectAllAction } from "@/app/_actions/service";
|
|
||||||
import { ServiceConfig } from "@/app/_lib/service";
|
|
||||||
|
|
||||||
const tableLookup = {
|
|
||||||
whatsapp: "WhatsappBot",
|
|
||||||
facebook: "FacebookBot",
|
|
||||||
signal: "SignalBot",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const webhooksConfig: ServiceConfig = {
|
|
||||||
entity: "webhooks",
|
|
||||||
table: "Webhook",
|
|
||||||
displayName: "Webhook",
|
|
||||||
createFields: [
|
|
||||||
{
|
|
||||||
name: "name",
|
|
||||||
label: "Name",
|
|
||||||
required: true,
|
|
||||||
size: 12,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "description",
|
|
||||||
label: "Description",
|
|
||||||
size: 12,
|
|
||||||
lines: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "httpMethod",
|
|
||||||
label: "HTTP Method",
|
|
||||||
kind: "select",
|
|
||||||
getOptions: async () => [
|
|
||||||
{ value: "post", label: "POST" },
|
|
||||||
{ value: "put", label: "PUT" },
|
|
||||||
],
|
|
||||||
defaultValue: "post",
|
|
||||||
required: true,
|
|
||||||
size: 2,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "endpointUrl",
|
|
||||||
label: "Endpoint",
|
|
||||||
required: true,
|
|
||||||
size: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "backendType",
|
|
||||||
label: "Backend Type",
|
|
||||||
kind: "select",
|
|
||||||
getOptions: async (_formState: any) => [
|
|
||||||
{ value: "whatsapp", label: "WhatsApp" },
|
|
||||||
{ value: "facebook", label: "Facebook" },
|
|
||||||
{ value: "signal", label: "Signal" },
|
|
||||||
],
|
|
||||||
defaultValue: "facebook",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "backendId",
|
|
||||||
label: "Backend ID",
|
|
||||||
kind: "select",
|
|
||||||
getOptions: async (formState: any) => {
|
|
||||||
console.log({ formState });
|
|
||||||
if (!formState || !formState.values.backendType) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
const table = tableLookup[formState.values.backendType];
|
|
||||||
console.log({ table });
|
|
||||||
const result = await selectAllAction(table);
|
|
||||||
console.log({ result });
|
|
||||||
|
|
||||||
return result.map((item: any) => ({
|
|
||||||
value: item.id,
|
|
||||||
label: item.name,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "headers",
|
|
||||||
label: "HTTP Headers",
|
|
||||||
kind: "multi",
|
|
||||||
size: 12,
|
|
||||||
helperText: "Useful for including authentication headers",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
updateFields: [
|
|
||||||
{ name: "name", label: "Name", required: true, size: 12 },
|
|
||||||
{
|
|
||||||
name: "description",
|
|
||||||
label: "Description",
|
|
||||||
required: true,
|
|
||||||
size: 12,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
displayFields: [
|
|
||||||
{ name: "name", label: "Name", required: true, size: 12 },
|
|
||||||
{
|
|
||||||
name: "description",
|
|
||||||
label: "Description",
|
|
||||||
required: true,
|
|
||||||
size: 12,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
listColumns: [
|
|
||||||
{
|
|
||||||
field: "name",
|
|
||||||
headerName: "Name",
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "description",
|
|
||||||
headerName: "Description",
|
|
||||||
flex: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "updatedAt",
|
|
||||||
headerName: "Updated At",
|
|
||||||
valueGetter: (value: any) => new Date(value).toLocaleString(),
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
|
@ -1,6 +1,6 @@
|
||||||
import GoogleProvider from "next-auth/providers/google";
|
import GoogleProvider from "next-auth/providers/google";
|
||||||
import { KyselyAdapter } from "@auth/kysely-adapter";
|
import { KyselyAdapter } from "@auth/kysely-adapter";
|
||||||
import { db } from "./database";
|
import { db } from "bridge-common";
|
||||||
|
|
||||||
export const authOptions = {
|
export const authOptions = {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { makeWorkerUtils, WorkerUtils } from "graphile-worker";
|
|
||||||
import { Service } from "./service";
|
|
||||||
import { db } from "./database";
|
|
||||||
|
|
||||||
let workerUtils: WorkerUtils;
|
|
||||||
|
|
||||||
const getWorkerUtils = async () => {
|
|
||||||
if (!workerUtils) {
|
|
||||||
workerUtils = await makeWorkerUtils({
|
|
||||||
connectionString: process.env.DATABASE_URL,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return workerUtils;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendMessage = async (req: NextRequest) => {
|
|
||||||
console.log({ req });
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const receiveMessages = async (req: NextRequest) => {
|
|
||||||
console.log({ req });
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWebhook = async (req: NextRequest) => {
|
|
||||||
const { searchParams } = req.nextUrl;
|
|
||||||
const submittedToken = searchParams.get("hub.verify_token");
|
|
||||||
|
|
||||||
if (submittedToken) {
|
|
||||||
console.log({ submittedToken });
|
|
||||||
const row = await db
|
|
||||||
.selectFrom("FacebookBot")
|
|
||||||
.selectAll()
|
|
||||||
.where("verifyToken", "=", submittedToken)
|
|
||||||
.executeTakeFirst();
|
|
||||||
|
|
||||||
console.log({ row });
|
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
return NextResponse.error();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchParams.get("hub.mode") === "subscribe") {
|
|
||||||
const challenge = searchParams.get("hub.challenge");
|
|
||||||
console.log(submittedToken);
|
|
||||||
console.log(challenge);
|
|
||||||
|
|
||||||
return NextResponse.json(challenge) as any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = await req.json();
|
|
||||||
console.log({ message });
|
|
||||||
const entry = message.entry[0];
|
|
||||||
console.log({ entry });
|
|
||||||
const messaging = entry?.messaging[0];
|
|
||||||
const pageId = messaging?.recipient?.id;
|
|
||||||
console.log({ pageId });
|
|
||||||
const row = await db
|
|
||||||
.selectFrom("FacebookBot")
|
|
||||||
.selectAll()
|
|
||||||
.where("pageId", "=", pageId)
|
|
||||||
.executeTakeFirst();
|
|
||||||
|
|
||||||
console.log({ row });
|
|
||||||
|
|
||||||
const endpoint = `https://graph.facebook.com/v19.0/${pageId}/messages`;
|
|
||||||
const inMessage = messaging?.message?.text;
|
|
||||||
const outgoingMessage = {
|
|
||||||
recipient: { id: messaging?.sender?.id },
|
|
||||||
message: { text: `"${inMessage}", right back at you!` },
|
|
||||||
messaging_type: "RESPONSE",
|
|
||||||
access_token: row?.pageAccessToken,
|
|
||||||
};
|
|
||||||
console.log({ outgoingMessage });
|
|
||||||
const response = await fetch(endpoint, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(outgoingMessage),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log({ response });
|
|
||||||
|
|
||||||
console.log(message);
|
|
||||||
const wu = await getWorkerUtils();
|
|
||||||
|
|
||||||
await wu.addJob("receive_facebook_message", message);
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Facebook: Service = {
|
|
||||||
sendMessage,
|
|
||||||
receiveMessages,
|
|
||||||
handleWebhook,
|
|
||||||
};
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { getService } from "./utils";
|
|
||||||
|
|
||||||
const notFound = () => new NextResponse(null, { status: 404 });
|
|
||||||
|
|
||||||
export const getOneBot = async (req: NextRequest): Promise<NextResponse> =>
|
|
||||||
notFound();
|
|
||||||
|
|
||||||
export const sendMessage = async (req: NextRequest): Promise<NextResponse> =>
|
|
||||||
getService(req)?.sendMessage(req) ?? notFound();
|
|
||||||
|
|
||||||
export const receiveMessages = async (
|
|
||||||
req: NextRequest,
|
|
||||||
): Promise<NextResponse> => getService(req)?.receiveMessages(req) ?? notFound();
|
|
||||||
|
|
||||||
export const handleWebhook = async (req: NextRequest): Promise<NextResponse> =>
|
|
||||||
getService(req)?.handleWebhook(req) ?? notFound();
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { Service } from "./service";
|
|
||||||
|
|
||||||
const sendMessage = async (req: NextRequest) => {
|
|
||||||
console.log({ req });
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const receiveMessages = async (req: NextRequest) => {
|
|
||||||
console.log({ req });
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWebhook = async (req: NextRequest) => {
|
|
||||||
console.log({ req });
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Signal: Service = {
|
|
||||||
sendMessage,
|
|
||||||
receiveMessages,
|
|
||||||
handleWebhook,
|
|
||||||
};
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { NextRequest } from "next/server";
|
|
||||||
import { Service } from "./service";
|
|
||||||
import { Facebook } from "./facebook";
|
|
||||||
|
|
||||||
const services: Record<string, Service> = {
|
|
||||||
facebook: Facebook,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getService = (req: NextRequest): Service => {
|
|
||||||
const service = req.nextUrl.pathname.split("/")?.[2] ?? "none";
|
|
||||||
|
|
||||||
return services[service];
|
|
||||||
};
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { Service } from "./service";
|
|
||||||
|
|
||||||
const sendMessage = async (req: NextRequest) => {
|
|
||||||
console.log({ req });
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const receiveMessages = async (req: NextRequest) => {
|
|
||||||
console.log({ req });
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWebhook = async (req: NextRequest) => {
|
|
||||||
console.log({ req });
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Voice: Service = {
|
|
||||||
sendMessage,
|
|
||||||
receiveMessages,
|
|
||||||
handleWebhook,
|
|
||||||
};
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { Service } from "./service";
|
|
||||||
|
|
||||||
const sendMessage = async (req: NextRequest) => {
|
|
||||||
console.log({ req });
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const receiveMessages = async (req: NextRequest) => {
|
|
||||||
console.log({ req });
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWebhook = async (req: NextRequest) => {
|
|
||||||
console.log({ req });
|
|
||||||
|
|
||||||
return NextResponse.json({ response: "ok" });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Whatsapp: Service = {
|
|
||||||
sendMessage,
|
|
||||||
receiveMessages,
|
|
||||||
handleWebhook,
|
|
||||||
};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { receiveMessages as GET } from "@/app/_lib/routing";
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export { getOneBot as GET } from "@/app/_lib/routing";
|
export { getBot as GET } from "bridge-ui";
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export { sendMessage as POST } from "@/app/_lib/routing";
|
export { sendMessage as POST } from "bridge-ui";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import { NextRequest } from "next/server";
|
import { handleWebhook } from "bridge-ui";
|
||||||
import { handleWebhook } from "@/app/_lib/routing";
|
|
||||||
|
|
||||||
const handleRequest = async (req: NextRequest) => handleWebhook(req);
|
export { handleWebhook as GET, handleWebhook as POST };
|
||||||
|
|
||||||
export { handleRequest as GET, handleRequest as POST };
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
transpilePackages: ["ui"],
|
transpilePackages: ["ui", "bridge-common", "bridge-ui"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@
|
||||||
"react-timer-hook": "^3.0.7",
|
"react-timer-hook": "^3.0.7",
|
||||||
"tss-react": "^4.9.7",
|
"tss-react": "^4.9.7",
|
||||||
"tsx": "^4.7.3",
|
"tsx": "^4.7.3",
|
||||||
"ui": "*"
|
"ui": "*",
|
||||||
|
"bridge-common": "*",
|
||||||
|
"bridge-ui": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"kysely": "^0.27.3",
|
"kysely": "^0.27.3",
|
||||||
|
"bridge-common": "*",
|
||||||
"pg": "^8.11.5",
|
"pg": "^8.11.5",
|
||||||
"remeda": "^1.61.0",
|
"remeda": "^1.61.0",
|
||||||
"twilio": "^5.0.4"
|
"twilio": "^5.0.4"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,27 @@
|
||||||
interface ReceiveFacebookMessageTaskOptions {}
|
import { db, getWorkerUtils } from "bridge-common";
|
||||||
|
|
||||||
const receiveFacebookMessageTask = async (
|
interface ReceiveFacebookMessageTaskOptions {
|
||||||
options: ReceiveFacebookMessageTaskOptions,
|
message: any;
|
||||||
): Promise<void> => {
|
}
|
||||||
console.log(options);
|
|
||||||
// withDb(async (db: AppDatabase) => {
|
const receiveFacebookMessageTask = async ({
|
||||||
// await notifyWebhooks(db, options);
|
message,
|
||||||
// });
|
}: ReceiveFacebookMessageTaskOptions): Promise<void> => {
|
||||||
|
const worker = await getWorkerUtils();
|
||||||
|
|
||||||
|
for (const entry of message.entry) {
|
||||||
|
for (const messaging of entry.messaging) {
|
||||||
|
const pageId = messaging.recipient.id;
|
||||||
|
const row = await db
|
||||||
|
.selectFrom("FacebookBot")
|
||||||
|
.selectAll()
|
||||||
|
.where("pageId", "=", pageId)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
console.log({ row });
|
||||||
|
await worker.addJob("notify_webhooks", messaging);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default receiveFacebookMessageTask;
|
export default receiveFacebookMessageTask;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,58 @@ const sendFacebookMessageTask = async (
|
||||||
options: SendFacebookMessageTaskOptions,
|
options: SendFacebookMessageTaskOptions,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
console.log(options);
|
console.log(options);
|
||||||
|
|
||||||
|
|
||||||
|
const message = await req.json();
|
||||||
|
|
||||||
|
const message = await req.json();
|
||||||
|
for (const entry of message.entry) {
|
||||||
|
for (const messaging of entry.messaging) {
|
||||||
|
const pageId = messaging.recipient.id;
|
||||||
|
const row = await db
|
||||||
|
.selectFrom("FacebookBot")
|
||||||
|
.selectAll()
|
||||||
|
.where("pageId", "=", pageId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
console.log({ message });
|
||||||
|
const entry = message.entry[0];
|
||||||
|
console.log({ entry });
|
||||||
|
const messaging = entry?.messaging[0];
|
||||||
|
const pageId = messaging?.recipient?.id;
|
||||||
|
console.log({ pageId });
|
||||||
|
const row = await db
|
||||||
|
.selectFrom("FacebookBot")
|
||||||
|
.selectAll()
|
||||||
|
.where("pageId", "=", pageId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
console.log({ row });
|
||||||
|
|
||||||
|
const endpoint = `https://graph.facebook.com/v19.0/${pageId}/messages`;
|
||||||
|
const inMessage = messaging?.message?.text;
|
||||||
|
const outgoingMessage = {
|
||||||
|
recipient: { id: messaging?.sender?.id },
|
||||||
|
message: { text: `"${inMessage}", right back at you!` },
|
||||||
|
messaging_type: "RESPONSE",
|
||||||
|
access_token: row?.pageAccessToken,
|
||||||
|
};
|
||||||
|
console.log({ outgoingMessage });
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(outgoingMessage),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log({ response });
|
||||||
|
|
||||||
|
console.log(message);
|
||||||
|
const wu = await getWorkerUtils();
|
||||||
|
|
||||||
|
await wu.addJob("receive_facebook_message", message);
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
|
||||||
// withDb(async (db: AppDatabase) => {
|
// withDb(async (db: AppDatabase) => {
|
||||||
// await notifyWebhooks(db, options);
|
// await notifyWebhooks(db, options);
|
||||||
// });
|
// });
|
||||||
|
|
|
||||||
93
package-lock.json
generated
93
package-lock.json
generated
|
|
@ -137,6 +137,7 @@
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/wreck": "^18.1.0",
|
"@hapi/wreck": "^18.1.0",
|
||||||
|
"bridge-common": "*",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"graphile-worker": "^0.16.5",
|
"graphile-worker": "^0.16.5",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
|
|
@ -6530,6 +6531,10 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bridge-common": {
|
||||||
|
"resolved": "packages/bridge-common",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/bridge-frontend": {
|
"node_modules/bridge-frontend": {
|
||||||
"resolved": "apps/bridge-frontend",
|
"resolved": "apps/bridge-frontend",
|
||||||
"link": true
|
"link": true
|
||||||
|
|
@ -16336,6 +16341,94 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/bridge-common": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"dependencies": {
|
||||||
|
"@auth/kysely-adapter": "^1.0.0",
|
||||||
|
"kysely": "^0.26",
|
||||||
|
"pg": "^8.11.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "7.24.4",
|
||||||
|
"@babel/preset-env": "7.24.4",
|
||||||
|
"@babel/preset-typescript": "7.24.1",
|
||||||
|
"eslint": "^9.0.0",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"ts-config": "*",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/bridge-common/node_modules/@auth/kysely-adapter": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@auth/kysely-adapter/-/kysely-adapter-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-gK7Vn+CAJV2ezgtqikHp/avkiM/gJlJxdYzkKxaR8CLtstHF6ao6Xi+4pF/WaE0Mp8T25NSPeXD+HPDA+jc17w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@auth/core": "0.30.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"kysely": "^0.26.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/bridge-common/node_modules/eslint": {
|
||||||
|
"version": "9.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.1.1.tgz",
|
||||||
|
"integrity": "sha512-b4cRQ0BeZcSEzPpY2PjFY70VbO32K7BStTGtBsnIGdTSEEQzBi8hPBcGQmTG2zUvFr9uLe0TK42bw8YszuHEqg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
|
"@eslint/eslintrc": "^3.0.2",
|
||||||
|
"@eslint/js": "9.1.1",
|
||||||
|
"@humanwhocodes/config-array": "^0.13.0",
|
||||||
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
|
"@humanwhocodes/retry": "^0.2.3",
|
||||||
|
"@nodelib/fs.walk": "^1.2.8",
|
||||||
|
"ajv": "^6.12.4",
|
||||||
|
"chalk": "^4.0.0",
|
||||||
|
"cross-spawn": "^7.0.2",
|
||||||
|
"debug": "^4.3.2",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"eslint-scope": "^8.0.1",
|
||||||
|
"eslint-visitor-keys": "^4.0.0",
|
||||||
|
"espree": "^10.0.1",
|
||||||
|
"esquery": "^1.4.2",
|
||||||
|
"esutils": "^2.0.2",
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"file-entry-cache": "^8.0.0",
|
||||||
|
"find-up": "^5.0.0",
|
||||||
|
"glob-parent": "^6.0.2",
|
||||||
|
"ignore": "^5.2.0",
|
||||||
|
"imurmurhash": "^0.1.4",
|
||||||
|
"is-glob": "^4.0.0",
|
||||||
|
"is-path-inside": "^3.0.3",
|
||||||
|
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||||
|
"levn": "^0.4.1",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
|
"minimatch": "^3.1.2",
|
||||||
|
"natural-compare": "^1.4.0",
|
||||||
|
"optionator": "^0.9.3",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"text-table": "^0.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"eslint": "bin/eslint.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/bridge-common/node_modules/kysely": {
|
||||||
|
"version": "0.26.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.26.3.tgz",
|
||||||
|
"integrity": "sha512-yWSgGi9bY13b/W06DD2OCDDHQmq1kwTGYlQ4wpZkMOJqMGCstVCFIvxCCVG4KfY1/3G0MhDAcZsip/Lw8/vJWw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/bridge-ui": {
|
"packages/bridge-ui": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
|
|
||||||
2
packages/bridge-common/index.ts
Normal file
2
packages/bridge-common/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { db, type Database } from "./lib/database";
|
||||||
|
export { getWorkerUtils } from "./lib/utils";
|
||||||
13
packages/bridge-common/lib/utils.ts
Normal file
13
packages/bridge-common/lib/utils.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { makeWorkerUtils, WorkerUtils } from "graphile-worker";
|
||||||
|
|
||||||
|
let workerUtils: WorkerUtils;
|
||||||
|
|
||||||
|
export const getWorkerUtils = async () => {
|
||||||
|
if (!workerUtils) {
|
||||||
|
workerUtils = await makeWorkerUtils({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return workerUtils;
|
||||||
|
};
|
||||||
26
packages/bridge-common/package.json
Normal file
26
packages/bridge-common/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "bridge-common",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"author": "Darren Clarke <darren@redaranj.com>",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"dev": "NODE_OPTIONS=\"--loader ts-node/esm\" graphile-worker"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@auth/kysely-adapter": "^1.0.0",
|
||||||
|
"kysely": "^0.26",
|
||||||
|
"pg": "^8.11.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "7.24.4",
|
||||||
|
"@babel/preset-env": "7.24.4",
|
||||||
|
"@babel/preset-typescript": "7.24.1",
|
||||||
|
"eslint": "^9.0.0",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"ts-config": "*",
|
||||||
|
"typescript": "^5.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
packages/bridge-common/tsconfig.json
Normal file
29
packages/bridge-common/tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*", "../../node_modules/*"]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["**.d.ts", "**/*.ts", "**/*.tsx", "**/*.png, **/*.svg"],
|
||||||
|
"exclude": ["node_modules", "babel__core"]
|
||||||
|
}
|
||||||
1
packages/bridge-common/tsconfig.tsbuildinfo
Normal file
1
packages/bridge-common/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,8 +1,8 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { db, Database } from "@/app/_lib/database";
|
import { db, Database } from "bridge-common";
|
||||||
import { FieldDescription, Entity } from "@/app/_lib/service";
|
import { FieldDescription, Entity } from "../lib/service";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
const generateToken = () => {
|
const generateToken = () => {
|
||||||
|
|
@ -28,7 +28,6 @@ export const createAction = async ({
|
||||||
currentState,
|
currentState,
|
||||||
formData,
|
formData,
|
||||||
}: CreateActionArgs) => {
|
}: CreateActionArgs) => {
|
||||||
console.log(formData);
|
|
||||||
const newRecord = fields.reduce(
|
const newRecord = fields.reduce(
|
||||||
(acc: Record<string, any>, field: FieldDescription) => {
|
(acc: Record<string, any>, field: FieldDescription) => {
|
||||||
if (field.autogenerated === "token") {
|
if (field.autogenerated === "token") {
|
||||||
|
|
@ -42,14 +41,12 @@ export const createAction = async ({
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log({ newRecord });
|
|
||||||
const record = await db
|
const record = await db
|
||||||
.insertInto(table)
|
.insertInto(table)
|
||||||
.values(newRecord)
|
.values(newRecord)
|
||||||
.returning(["id"])
|
.returning(["id"])
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
console.log({ record });
|
|
||||||
revalidatePath(`/${entity}`);
|
revalidatePath(`/${entity}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { useFormState } from "react-dom";
|
import { useFormState } from "react-dom";
|
||||||
import { Grid } from "@mui/material";
|
import { Grid } from "@mui/material";
|
||||||
import { TextField, Select, MultiValueField } from "ui";
|
import { useRouter } from "next/navigation";
|
||||||
import { Create as InternalCreate } from "@/app/_components/Create";
|
import { Button, Dialog, TextField, Select, MultiValueField } from "ui";
|
||||||
import { generateCreateAction } from "@/app/_lib/actions";
|
import { generateCreateAction } from "../lib/actions";
|
||||||
import { serviceConfig } from "@/app/_config/config";
|
import { FieldDescription } from "../lib/service";
|
||||||
import { FieldDescription } from "@/app/_lib/service";
|
import { serviceConfig } from "../config/config";
|
||||||
|
|
||||||
type CreateProps = {
|
type CreateProps = {
|
||||||
service: string;
|
service: string;
|
||||||
|
|
@ -40,17 +40,45 @@ export const Create: FC<CreateProps> = ({ service }) => {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
const [formState, formAction] = useFormState(createAction, initialState);
|
const [formState, formAction] = useFormState(createAction, initialState);
|
||||||
|
const [liveFormState, setLiveFormState] = useState(formState);
|
||||||
|
const updateFormState = (field: string, value: any) => {
|
||||||
|
const newState = { ...liveFormState };
|
||||||
|
newState.values[field] = value;
|
||||||
|
setLiveFormState(newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formState.success) {
|
||||||
|
router.push(`/${entity}/${formState.values.id}`);
|
||||||
|
}
|
||||||
|
}, [formState.success, router, entity, formState.values.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InternalCreate
|
<Dialog
|
||||||
|
open
|
||||||
title={`Create ${displayName}`}
|
title={`Create ${displayName}`}
|
||||||
entity={entity}
|
|
||||||
formAction={formAction}
|
formAction={formAction}
|
||||||
formState={formState}
|
onClose={() => router.push(`/${entity}`)}
|
||||||
|
buttons={
|
||||||
|
<Grid container justifyContent="space-between">
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
text="Cancel"
|
||||||
|
kind="secondary"
|
||||||
|
onClick={() => router.push(`/${entity}`)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button text="Save" kind="primary" type="submit" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Grid container direction="row" rowSpacing={3} columnSpacing={2}>
|
<Grid container direction="row" rowSpacing={3} columnSpacing={2}>
|
||||||
{createFields.map(
|
{createFields.map(
|
||||||
(field) =>
|
(field: FieldDescription) =>
|
||||||
!field.hidden && (
|
!field.hidden && (
|
||||||
<Grid key={field.name} item xs={field.size ?? 6}>
|
<Grid key={field.name} item xs={field.size ?? 6}>
|
||||||
{field.kind === "select" && (
|
{field.kind === "select" && (
|
||||||
|
|
@ -58,8 +86,9 @@ export const Create: FC<CreateProps> = ({ service }) => {
|
||||||
name={field.name}
|
name={field.name}
|
||||||
label={field.label}
|
label={field.label}
|
||||||
required={field.required ?? false}
|
required={field.required ?? false}
|
||||||
formState={formState}
|
formState={liveFormState}
|
||||||
getOptions={field.getOptions}
|
getOptions={field.getOptions}
|
||||||
|
updateFormState={updateFormState}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{field.kind === "multi" && (
|
{field.kind === "multi" && (
|
||||||
|
|
@ -84,6 +113,6 @@ export const Create: FC<CreateProps> = ({ service }) => {
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</InternalCreate>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { Grid, Box } from "@mui/material";
|
import { Grid, Box } from "@mui/material";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { typography } from "@/app/_styles/theme";
|
import { typography } from "@/styles/theme";
|
||||||
|
|
||||||
interface DeleteDialogProps {
|
interface DeleteDialogProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -11,7 +11,11 @@ interface DeleteDialogProps {
|
||||||
children: any;
|
children: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteDialog: FC<DeleteDialogProps> = ({ title, entity, children }) => {
|
export const DeleteDialog: FC<DeleteDialogProps> = ({
|
||||||
|
title,
|
||||||
|
entity,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { h3 } = typography;
|
const { h3 } = typography;
|
||||||
|
|
@ -22,9 +26,7 @@ export const DeleteDialog: FC<DeleteDialogProps> = ({ title, entity, children })
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Box sx={h3}>{title}</Box>
|
<Box sx={h3}>{title}</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>{children}</Grid>
|
||||||
{children}
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { Box, Grid } from "@mui/material";
|
import { Grid, Box } from "@mui/material";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Dialog, Button, colors, typography } from "ui";
|
import { DisplayTextField, Button, Dialog, colors, typography } from "ui";
|
||||||
|
import { Selectable } from "kysely";
|
||||||
|
import { type Database } from "bridge-common";
|
||||||
|
import { generateDeleteAction } from "../lib/actions";
|
||||||
|
import { serviceConfig } from "../config/config";
|
||||||
|
import { FieldDescription } from "../lib/service";
|
||||||
|
|
||||||
interface DetailProps {
|
type DetailProps = {
|
||||||
title: string;
|
service: string;
|
||||||
entity: string;
|
row: Selectable<keyof Database>;
|
||||||
id: string;
|
};
|
||||||
children: any;
|
|
||||||
deleteAction?: Function;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Detail: FC<DetailProps> = ({
|
export const Detail: FC<DetailProps> = ({ service, row }) => {
|
||||||
title,
|
const {
|
||||||
entity,
|
[service]: { entity, table, displayName, displayFields: fields },
|
||||||
id,
|
} = serviceConfig;
|
||||||
children,
|
const id = row.id as string;
|
||||||
deleteAction,
|
const deleteAction = generateDeleteAction({ entity, table });
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { almostBlack } = colors;
|
const { almostBlack } = colors;
|
||||||
const { bodyLarge } = typography;
|
const { bodyLarge } = typography;
|
||||||
|
|
@ -35,20 +36,18 @@ export const Detail: FC<DetailProps> = ({
|
||||||
<>
|
<>
|
||||||
<Dialog
|
<Dialog
|
||||||
open
|
open
|
||||||
title={title}
|
title={`${displayName} Detail`}
|
||||||
onClose={() => router.push(`/${entity}`)}
|
onClose={() => router.push(`/${entity}`)}
|
||||||
buttons={
|
buttons={
|
||||||
<Grid container justifyContent="space-between">
|
<Grid container justifyContent="space-between">
|
||||||
<Grid item container xs="auto" spacing={2}>
|
<Grid item container xs="auto" spacing={2}>
|
||||||
{deleteAction && (
|
<Grid item>
|
||||||
<Grid item>
|
<Button
|
||||||
<Button
|
text="Delete"
|
||||||
text="Delete"
|
kind="destructive"
|
||||||
kind="destructive"
|
onClick={() => setShowDeleteConfirmation(true)}
|
||||||
onClick={() => setShowDeleteConfirmation(true)}
|
/>
|
||||||
/>
|
</Grid>
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Button
|
<Button
|
||||||
text="Edit"
|
text="Edit"
|
||||||
|
|
@ -63,7 +62,19 @@ export const Detail: FC<DetailProps> = ({
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children}
|
<Grid container direction="row" rowSpacing={3} columnSpacing={2}>
|
||||||
|
{fields.map((field: FieldDescription) => (
|
||||||
|
<Grid item xs={field.size ?? 6} key={field.name}>
|
||||||
|
<DisplayTextField
|
||||||
|
name={field.name}
|
||||||
|
label={field.label}
|
||||||
|
lines={field.lines ?? 1}
|
||||||
|
value={row[field.name] as string}
|
||||||
|
copyable={field.copyable ?? false}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<Dialog
|
<Dialog
|
||||||
open={showDeleteConfirmation}
|
open={showDeleteConfirmation}
|
||||||
115
packages/bridge-ui/components/Edit.tsx
Normal file
115
packages/bridge-ui/components/Edit.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC, useEffect, useState } from "react";
|
||||||
|
import { useFormState } from "react-dom";
|
||||||
|
import { Grid } from "@mui/material";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { TextField, Dialog, Button, Select, MultiValueField } from "ui";
|
||||||
|
import { Selectable } from "kysely";
|
||||||
|
import { type Database } from "bridge-common";
|
||||||
|
import { generateUpdateAction } from "../lib/actions";
|
||||||
|
import { serviceConfig } from "../config/config";
|
||||||
|
import { FieldDescription } from "../lib/service";
|
||||||
|
|
||||||
|
type EditProps = {
|
||||||
|
service: string;
|
||||||
|
row: Selectable<keyof Database>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Edit: FC<EditProps> = ({ service, row }) => {
|
||||||
|
const {
|
||||||
|
[service]: { entity, table, displayName, updateFields },
|
||||||
|
} = serviceConfig;
|
||||||
|
const fields = updateFields.map((field: any) => {
|
||||||
|
const copy = { ...field };
|
||||||
|
Object.keys(copy).forEach((key: any) => {
|
||||||
|
if (typeof copy[key] === "function") {
|
||||||
|
delete copy[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
const updateFieldNames = fields.map((val: FieldDescription) => val.name);
|
||||||
|
const updateAction = generateUpdateAction({ entity, table, fields });
|
||||||
|
const updateValues = Object.fromEntries(
|
||||||
|
Object.entries(row).filter(([key]) => updateFieldNames.includes(key)),
|
||||||
|
);
|
||||||
|
updateValues.id = row.id;
|
||||||
|
const initialState = {
|
||||||
|
message: null,
|
||||||
|
errors: {},
|
||||||
|
values: updateValues,
|
||||||
|
};
|
||||||
|
const [formState, formAction] = useFormState(updateAction, initialState);
|
||||||
|
const router = useRouter();
|
||||||
|
const [liveFormState, setLiveFormState] = useState(formState);
|
||||||
|
const updateFormState = (field: string, value: any) => {
|
||||||
|
const newState = { ...liveFormState };
|
||||||
|
newState.values[field] = value;
|
||||||
|
setLiveFormState(newState);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formState.success) {
|
||||||
|
router.push(`/${entity}`);
|
||||||
|
}
|
||||||
|
}, [formState.success, router, entity]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open
|
||||||
|
title={`Edit ${displayName}`}
|
||||||
|
formAction={formAction}
|
||||||
|
onClose={() => router.push(`/${entity}`)}
|
||||||
|
buttons={
|
||||||
|
<Grid container justifyContent="space-between">
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
text="Cancel"
|
||||||
|
kind="secondary"
|
||||||
|
onClick={() => router.push(`/${entity}`)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button text="Save" kind="primary" type="submit" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Grid container direction="row" rowSpacing={3} columnSpacing={2}>
|
||||||
|
{updateFields.map((field: FieldDescription) => (
|
||||||
|
<Grid key={field.name} item xs={field.size ?? 6}>
|
||||||
|
{field.kind === "select" && (
|
||||||
|
<Select
|
||||||
|
name={field.name}
|
||||||
|
label={field.label}
|
||||||
|
required={field.required ?? false}
|
||||||
|
formState={liveFormState}
|
||||||
|
getOptions={field.getOptions}
|
||||||
|
updateFormState={updateFormState}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{field.kind === "multi" && (
|
||||||
|
<MultiValueField
|
||||||
|
name={field.name}
|
||||||
|
label={field.label}
|
||||||
|
formState={formState}
|
||||||
|
helperText={field.helperText}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(!field.kind || field.kind === "text") && (
|
||||||
|
<TextField
|
||||||
|
name={field.name}
|
||||||
|
label={field.label}
|
||||||
|
lines={field.lines ?? 1}
|
||||||
|
required={field.required ?? false}
|
||||||
|
formState={formState}
|
||||||
|
helperText={field.helperText}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { GridColDef } from "@mui/x-data-grid-pro";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { List as InternalList, Button } from "ui";
|
import { List as InternalList, Button } from "ui";
|
||||||
import type { Selectable } from "kysely";
|
import { type Selectable } from "kysely";
|
||||||
import { Database } from "@/app/_lib/database";
|
import { type Database } from "bridge-common";
|
||||||
|
import { serviceConfig } from "../config/config";
|
||||||
|
|
||||||
interface ListProps {
|
type ListProps = {
|
||||||
title: string;
|
service: string;
|
||||||
entity: string;
|
|
||||||
rows: Selectable<keyof Database>[];
|
rows: Selectable<keyof Database>[];
|
||||||
columns: GridColDef<any>[];
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const List: FC<ListProps> = ({ title, entity, rows, columns }) => {
|
export const List: FC<ListProps> = ({ service, rows }) => {
|
||||||
|
const { displayName, entity, listColumns } = serviceConfig[service];
|
||||||
|
const title = `${displayName}s`;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const onRowClick = (id: string) => {
|
const onRowClick = (id: string) => {
|
||||||
|
|
@ -25,7 +25,7 @@ export const List: FC<ListProps> = ({ title, entity, rows, columns }) => {
|
||||||
<InternalList
|
<InternalList
|
||||||
title={title}
|
title={title}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={listColumns}
|
||||||
onRowClick={onRowClick}
|
onRowClick={onRowClick}
|
||||||
buttons={
|
buttons={
|
||||||
<Button text="Create" kind="primary" href={`/${entity}/create`} />
|
<Button text="Create" kind="primary" href={`/${entity}/create`} />
|
||||||
|
|
@ -16,10 +16,10 @@ export const ServiceLayout = ({
|
||||||
params: { segment },
|
params: { segment },
|
||||||
}: ServiceLayoutProps) => {
|
}: ServiceLayoutProps) => {
|
||||||
const length = segment?.length ?? 0;
|
const length = segment?.length ?? 0;
|
||||||
const isCreate = length === 1 && segment[0] === "create";
|
const isCreate = length === 2 && segment[1] === "create";
|
||||||
const isEdit = length === 2 && segment[1] === "edit";
|
const isEdit = length === 3 && segment[2] === "edit";
|
||||||
const id = length > 0 && !isCreate ? segment[0] : null;
|
const id = length > 0 && !isCreate ? segment[1] : null;
|
||||||
const isDetail = length === 1 && !!id && !isCreate && !isEdit;
|
const isDetail = length === 2 && !!id && !isCreate && !isEdit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { ServiceConfig } from "@/app/_lib/service";
|
import { type Database } from "bridge-common";
|
||||||
|
import type { ServiceConfig } from "../lib/service";
|
||||||
import { facebookConfig as facebook } from "./facebook";
|
import { facebookConfig as facebook } from "./facebook";
|
||||||
import { signalConfig as signal } from "./signal";
|
import { signalConfig as signal } from "./signal";
|
||||||
import { whatsappConfig as whatsapp } from "./whatsapp";
|
import { whatsappConfig as whatsapp } from "./whatsapp";
|
||||||
|
|
@ -14,3 +15,19 @@ export const serviceConfig: Record<string, ServiceConfig> = {
|
||||||
webhooks,
|
webhooks,
|
||||||
users,
|
users,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getServiceTable = (service: string): keyof Database => {
|
||||||
|
const tableLookup: Record<string, keyof Database> = {
|
||||||
|
facebook: "FacebookBot",
|
||||||
|
signal: "SignalBot",
|
||||||
|
whatsapp: "WhatsappBot",
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = tableLookup[service];
|
||||||
|
|
||||||
|
if (!table) {
|
||||||
|
throw new Error("Table not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Service, ServiceConfig } from "@/app/_lib/service";
|
import { ServiceConfig } from "../lib/service";
|
||||||
|
|
||||||
export const facebookConfig: ServiceConfig = {
|
export const facebookConfig: ServiceConfig = {
|
||||||
entity: "facebook",
|
entity: "facebook",
|
||||||
|
|
@ -94,7 +94,6 @@ export const facebookConfig: ServiceConfig = {
|
||||||
name: "pageId",
|
name: "pageId",
|
||||||
label: "Page ID",
|
label: "Page ID",
|
||||||
required: true,
|
required: true,
|
||||||
copyable: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "pageAccessToken",
|
name: "pageAccessToken",
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ServiceConfig } from "@/app/_lib/service";
|
import { ServiceConfig } from "../lib/service";
|
||||||
|
|
||||||
export const signalConfig: ServiceConfig = {
|
export const signalConfig: ServiceConfig = {
|
||||||
entity: "signal",
|
entity: "signal",
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ServiceConfig } from "@/app/_lib/service";
|
import { ServiceConfig } from "../lib/service";
|
||||||
|
|
||||||
export const usersConfig: ServiceConfig = {
|
export const usersConfig: ServiceConfig = {
|
||||||
entity: "users",
|
entity: "users",
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ServiceConfig } from "@/app/_lib/service";
|
import { ServiceConfig } from "../lib/service";
|
||||||
|
|
||||||
export const voiceConfig: ServiceConfig = {
|
export const voiceConfig: ServiceConfig = {
|
||||||
entity: "voice",
|
entity: "voice",
|
||||||
163
packages/bridge-ui/config/webhooks.ts
Normal file
163
packages/bridge-ui/config/webhooks.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
import { selectAllAction } from "../actions/service";
|
||||||
|
import { ServiceConfig } from "../lib/service";
|
||||||
|
import { getServiceTable } from "../config/config";
|
||||||
|
|
||||||
|
const getHTTPMethodOptions = async () => [
|
||||||
|
{ value: "post", label: "POST" },
|
||||||
|
{ value: "put", label: "PUT" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getBackendTypeOptions = async (_formState: any) => [
|
||||||
|
{ value: "whatsapp", label: "WhatsApp" },
|
||||||
|
{ value: "facebook", label: "Facebook" },
|
||||||
|
{ value: "signal", label: "Signal" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getBackendIDOptions = async (formState: any) => {
|
||||||
|
if (!formState || !formState.values.backendType) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = getServiceTable(formState.values.backendType);
|
||||||
|
const result = await selectAllAction(table);
|
||||||
|
|
||||||
|
return result.map((item: any) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: item.name,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const webhooksConfig: ServiceConfig = {
|
||||||
|
entity: "webhooks",
|
||||||
|
table: "Webhook",
|
||||||
|
displayName: "Webhook",
|
||||||
|
createFields: [
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
label: "Name",
|
||||||
|
required: true,
|
||||||
|
size: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
label: "Description",
|
||||||
|
size: 12,
|
||||||
|
lines: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "httpMethod",
|
||||||
|
label: "HTTP Method",
|
||||||
|
kind: "select",
|
||||||
|
getOptions: getHTTPMethodOptions,
|
||||||
|
defaultValue: "post",
|
||||||
|
required: true,
|
||||||
|
size: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "endpointUrl",
|
||||||
|
label: "Endpoint",
|
||||||
|
required: true,
|
||||||
|
size: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "backendType",
|
||||||
|
label: "Backend Type",
|
||||||
|
kind: "select",
|
||||||
|
getOptions: getBackendTypeOptions,
|
||||||
|
defaultValue: "facebook",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "backendId",
|
||||||
|
label: "Backend ID",
|
||||||
|
kind: "select",
|
||||||
|
getOptions: getBackendIDOptions,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "headers",
|
||||||
|
label: "HTTP Headers",
|
||||||
|
kind: "multi",
|
||||||
|
size: 12,
|
||||||
|
helperText: "Useful for authentication, etc.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
updateFields: [
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
label: "Name",
|
||||||
|
required: true,
|
||||||
|
size: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
label: "Description",
|
||||||
|
size: 12,
|
||||||
|
lines: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "httpMethod",
|
||||||
|
label: "HTTP Method",
|
||||||
|
kind: "select",
|
||||||
|
getOptions: getHTTPMethodOptions,
|
||||||
|
defaultValue: "post",
|
||||||
|
required: true,
|
||||||
|
size: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "endpointUrl",
|
||||||
|
label: "Endpoint",
|
||||||
|
required: true,
|
||||||
|
size: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "backendType",
|
||||||
|
label: "Backend Type",
|
||||||
|
kind: "select",
|
||||||
|
getOptions: getBackendTypeOptions,
|
||||||
|
defaultValue: "facebook",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "backendId",
|
||||||
|
label: "Backend ID",
|
||||||
|
kind: "select",
|
||||||
|
getOptions: getBackendIDOptions,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "headers",
|
||||||
|
label: "HTTP Headers",
|
||||||
|
kind: "multi",
|
||||||
|
size: 12,
|
||||||
|
helperText: "Useful for authentication, etc.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayFields: [
|
||||||
|
{ name: "name", label: "Name", required: true, size: 12 },
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
label: "Description",
|
||||||
|
required: true,
|
||||||
|
size: 12,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
listColumns: [
|
||||||
|
{
|
||||||
|
field: "name",
|
||||||
|
headerName: "Name",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "description",
|
||||||
|
headerName: "Description",
|
||||||
|
flex: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "updatedAt",
|
||||||
|
headerName: "Updated At",
|
||||||
|
valueGetter: (value: any) => new Date(value).toLocaleString(),
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ServiceConfig } from "@/app/_lib/service";
|
import { ServiceConfig } from "../lib/service";
|
||||||
|
|
||||||
export const whatsappConfig: ServiceConfig = {
|
export const whatsappConfig: ServiceConfig = {
|
||||||
entity: "whatsapp",
|
entity: "whatsapp",
|
||||||
16
packages/bridge-ui/images.d.ts
vendored
Normal file
16
packages/bridge-ui/images.d.ts
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
declare module "*.jpg" {
|
||||||
|
const value: any;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
declare module "*.jpeg" {
|
||||||
|
const value: any;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
declare module "*.svg" {
|
||||||
|
const value: any;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
declare module "*.png" {
|
||||||
|
const value: any;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
7
packages/bridge-ui/index.ts
Normal file
7
packages/bridge-ui/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export { List } from "./components/List";
|
||||||
|
export { Create } from "./components/Create";
|
||||||
|
export { Edit } from "./components/Edit";
|
||||||
|
export { Detail } from "./components/Detail";
|
||||||
|
export { ServiceLayout } from "./components/ServiceLayout";
|
||||||
|
export { serviceConfig, getServiceTable } from "./config/config";
|
||||||
|
export { getBot, sendMessage, handleWebhook } from "./lib/routing";
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Database } from "./database";
|
import { Database } from "bridge-common";
|
||||||
import { FieldDescription, Entity } from "./service";
|
|
||||||
import {
|
import {
|
||||||
createAction,
|
createAction,
|
||||||
updateAction,
|
updateAction,
|
||||||
deleteAction,
|
deleteAction,
|
||||||
selectAllAction,
|
selectAllAction,
|
||||||
} from "@/app/_actions/service";
|
} from "../actions/service";
|
||||||
|
import { FieldDescription, Entity } from "./service";
|
||||||
|
|
||||||
type GenerateCreateActionArgs = {
|
type GenerateCreateActionArgs = {
|
||||||
entity: Entity;
|
entity: Entity;
|
||||||
|
|
@ -19,8 +19,6 @@ export function generateCreateAction({
|
||||||
fields,
|
fields,
|
||||||
}: GenerateCreateActionArgs) {
|
}: GenerateCreateActionArgs) {
|
||||||
return async (currentState: any, formData: FormData) => {
|
return async (currentState: any, formData: FormData) => {
|
||||||
console.log({ entity, table, fields });
|
|
||||||
console.log({ currentState, formData });
|
|
||||||
return createAction({
|
return createAction({
|
||||||
entity,
|
entity,
|
||||||
table,
|
table,
|
||||||
32
packages/bridge-ui/lib/facebook.ts
Normal file
32
packages/bridge-ui/lib/facebook.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { Service } from "./service";
|
||||||
|
import { db, getWorkerUtils } from "bridge-common";
|
||||||
|
|
||||||
|
export class Facebook extends Service {
|
||||||
|
async handleWebhook(req: NextRequest) {
|
||||||
|
const { searchParams } = req.nextUrl;
|
||||||
|
const submittedToken = searchParams.get("hub.verify_token");
|
||||||
|
|
||||||
|
if (submittedToken) {
|
||||||
|
await db
|
||||||
|
.selectFrom("FacebookBot")
|
||||||
|
.selectAll()
|
||||||
|
.where("verifyToken", "=", submittedToken)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
|
if (searchParams.get("hub.mode") === "subscribe") {
|
||||||
|
const challenge = searchParams.get("hub.challenge");
|
||||||
|
|
||||||
|
return NextResponse.json(challenge) as any;
|
||||||
|
} else {
|
||||||
|
return NextResponse.error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await req.json();
|
||||||
|
const worker = await getWorkerUtils();
|
||||||
|
await worker.addJob("receive_facebook_message", message);
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/bridge-ui/lib/routing.ts
Normal file
18
packages/bridge-ui/lib/routing.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { ServiceParams } from "./service";
|
||||||
|
import { getService } from "./utils";
|
||||||
|
|
||||||
|
export const getBot = async (
|
||||||
|
_req: NextRequest,
|
||||||
|
params: ServiceParams,
|
||||||
|
): Promise<NextResponse> => getService(params)?.getBot(params);
|
||||||
|
|
||||||
|
export const sendMessage = async (
|
||||||
|
req: NextRequest,
|
||||||
|
params: ServiceParams,
|
||||||
|
): Promise<NextResponse> => getService(params)?.sendMessage(req, params);
|
||||||
|
|
||||||
|
export const handleWebhook = async (
|
||||||
|
req: NextRequest,
|
||||||
|
params: ServiceParams,
|
||||||
|
): Promise<NextResponse> => getService(params)?.handleWebhook(req);
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { GridColDef } from "@mui/x-data-grid-pro";
|
import { GridColDef } from "@mui/x-data-grid-pro";
|
||||||
import { Database } from "./database";
|
import { Database, db, getWorkerUtils } from "bridge-common";
|
||||||
|
import { getServiceTable } from "../config/config";
|
||||||
|
|
||||||
const entities = [
|
const entities = [
|
||||||
"facebook",
|
"facebook",
|
||||||
|
|
@ -46,18 +47,35 @@ export type ServiceConfig = {
|
||||||
listColumns: GridColDef[];
|
listColumns: GridColDef[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ServiceParams = {
|
||||||
|
service: string;
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class Service {
|
export class Service {
|
||||||
sendMessage: (req: NextRequest) => Promise<NextResponse> = async (req) => {
|
async getBot({ service, token }: ServiceParams): Promise<NextResponse> {
|
||||||
return NextResponse.json({ ok: "nice" });
|
const table = getServiceTable(service);
|
||||||
};
|
const row = await db
|
||||||
|
.selectFrom(table)
|
||||||
|
.selectAll()
|
||||||
|
.where("token", "=", token ?? "NEVER_MATCH")
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
receiveMessages: (req: NextRequest) => Promise<NextResponse> = async (
|
return NextResponse.json(row);
|
||||||
req,
|
}
|
||||||
) => {
|
|
||||||
return NextResponse.json({ ok: "nice" });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleWebhook: (req: NextRequest) => Promise<NextResponse> = async (req) => {
|
async sendMessage(
|
||||||
return NextResponse.json({ ok: "nice" });
|
req: NextRequest,
|
||||||
};
|
{ service, token }: ServiceParams,
|
||||||
|
): Promise<NextResponse> {
|
||||||
|
const message = await req.json();
|
||||||
|
const worker = await getWorkerUtils();
|
||||||
|
await worker.addJob(`send_${service}_message`, { token, message });
|
||||||
|
|
||||||
|
return NextResponse.json({ response: "ok" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleWebhook(_req: NextRequest): Promise<NextResponse> {
|
||||||
|
return NextResponse.error() as any;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
3
packages/bridge-ui/lib/signal.ts
Normal file
3
packages/bridge-ui/lib/signal.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { Service } from "./service";
|
||||||
|
|
||||||
|
export class Signal extends Service {}
|
||||||
16
packages/bridge-ui/lib/utils.ts
Normal file
16
packages/bridge-ui/lib/utils.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Service, ServiceParams } from "./service";
|
||||||
|
import { Facebook } from "./facebook";
|
||||||
|
import { Signal } from "./signal";
|
||||||
|
import { Whatsapp } from "./whatsapp";
|
||||||
|
|
||||||
|
export const getService = ({ service }: ServiceParams): Service => {
|
||||||
|
if (service === "facebook") {
|
||||||
|
return new Facebook();
|
||||||
|
} else if (service === "signal") {
|
||||||
|
return new Signal();
|
||||||
|
} else if (service === "whatsapp") {
|
||||||
|
return new Whatsapp();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Service not found");
|
||||||
|
};
|
||||||
3
packages/bridge-ui/lib/voice.ts
Normal file
3
packages/bridge-ui/lib/voice.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { Service } from "./service";
|
||||||
|
|
||||||
|
export class Voice extends Service {}
|
||||||
3
packages/bridge-ui/lib/whatsapp.ts
Normal file
3
packages/bridge-ui/lib/whatsapp.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { Service } from "./service";
|
||||||
|
|
||||||
|
export class Whatsapp extends Service {}
|
||||||
|
|
@ -1,11 +1,46 @@
|
||||||
{
|
{
|
||||||
"name": "bridge-ui",
|
"name": "bridge-ui",
|
||||||
"version": "1.0.0",
|
"version": "0.2.0",
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"build": "tsc -p tsconfig.json"
|
||||||
},
|
},
|
||||||
"author": "",
|
"dependencies": {
|
||||||
"license": "ISC"
|
"@emotion/cache": "^11.11.0",
|
||||||
|
"@emotion/react": "^11.11.4",
|
||||||
|
"@emotion/server": "^11.11.0",
|
||||||
|
"@emotion/styled": "^11.11.5",
|
||||||
|
"@mui/icons-material": "^5",
|
||||||
|
"@mui/lab": "^5.0.0-alpha.170",
|
||||||
|
"@mui/material": "^5",
|
||||||
|
"@mui/x-data-grid-pro": "^7.3.1",
|
||||||
|
"@mui/x-date-pickers-pro": "^7.3.1",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
|
"material-ui-popup-state": "^5.1.0",
|
||||||
|
"next": "14.2.3",
|
||||||
|
"react": "18.3.0",
|
||||||
|
"react-cookie": "^7.1.4",
|
||||||
|
"react-cookie-consent": "^9.0.0",
|
||||||
|
"react-dom": "18.3.0",
|
||||||
|
"react-iframe": "^1.8.5",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
|
"react-polyglot": "^0.7.2",
|
||||||
|
"tss-react": "^4.9.7",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.24.4",
|
||||||
|
"@types/node": "^20.12.7",
|
||||||
|
"@types/react": "18.3.0",
|
||||||
|
"@types/uuid": "^9.0.8",
|
||||||
|
"babel-loader": "^9.1.3",
|
||||||
|
"eslint": "^8.0.0",
|
||||||
|
"eslint-config-next": "^14.2.3",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"eslint-plugin-react": "^7.34.1",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"typescript": "5.4.5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
packages/bridge-ui/tsconfig.json
Normal file
26
packages/bridge-ui/tsconfig.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
1
packages/bridge-ui/tsconfig.tsbuildinfo
Normal file
1
packages/bridge-ui/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -12,6 +12,7 @@ type SelectProps = {
|
||||||
formState: Record<string, any>;
|
formState: Record<string, any>;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
getOptions?: (formState: any) => Promise<SelectOption[]>;
|
getOptions?: (formState: any) => Promise<SelectOption[]>;
|
||||||
|
updateFormState?: (field: string, value: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Select: FC<SelectProps> = ({
|
export const Select: FC<SelectProps> = ({
|
||||||
|
|
@ -20,6 +21,7 @@ export const Select: FC<SelectProps> = ({
|
||||||
formState,
|
formState,
|
||||||
required = false,
|
required = false,
|
||||||
getOptions,
|
getOptions,
|
||||||
|
updateFormState,
|
||||||
}) => {
|
}) => {
|
||||||
const [options, setOptions] = useState([] as SelectOption[]);
|
const [options, setOptions] = useState([] as SelectOption[]);
|
||||||
|
|
||||||
|
|
@ -42,7 +44,8 @@ export const Select: FC<SelectProps> = ({
|
||||||
required={required}
|
required={required}
|
||||||
size="small"
|
size="small"
|
||||||
inputProps={{ id: name }}
|
inputProps={{ id: name }}
|
||||||
defaultValue={formState.values[name] || ""}
|
value={formState.values[name] || ""}
|
||||||
|
onChange={(e: any) => updateFormState?.(name, e.target.value)}
|
||||||
sx={{
|
sx={{
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue