diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 33364d8d..dce02c74 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,3 +46,23 @@ jobs: - uses: cloudflare/wrangler-action@2.0.0 with: apiToken: ${{ secrets.CF_API_TOKEN }} + deploy-reflect: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "npm" + + - name: npm install + run: | + npm install + + - name: publish + env: + REFLECT_AUTH_KEY: ${{ secrets.REFLECT_AUTH_KEY }} + run: | + npx reflect publish --auth-key-from-env=REFLECT_AUTH_KEY diff --git a/app/s/[studio]/s/[space]/[display_name]/join/JoinSpace.tsx b/app/s/[studio]/s/[space]/[display_name]/join/JoinSpace.tsx index 0ad53a05..c4c9fd9c 100644 --- a/app/s/[studio]/s/[space]/[display_name]/join/JoinSpace.tsx +++ b/app/s/[studio]/s/[space]/[display_name]/join/JoinSpace.tsx @@ -13,6 +13,9 @@ import { SpaceCard, SpaceData } from "components/SpacesList"; import { Divider } from "components/Layout"; import { WORKER_URL } from "src/constants"; import { useSearchParams } from "next/dist/client/components/navigation"; +import { makeReflect } from "components/ReplicacheProvider"; +import { ulid } from "src/ulid"; + export function JoinSpace({ space_id }: { space_id: string }) { let id = useSpaceID(); let { session, authToken } = useAuth(); @@ -22,12 +25,23 @@ export function JoinSpace({ space_id }: { space_id: string }) { let { data } = useSpaceData({ space_id }); const onClick = async () => { - if (!authToken || !code || !id) return; + if (!authToken || !code || !id || !session.user) return; let data = await spaceAPI(`${WORKER_URL}/space/${id}`, "join", { authToken, code, }); if (data.success) { + let reflect = makeReflect({ + roomID: id, + authToken, + userID: session.user.id, + }); + if (session.session) + await reflect.mutate.joinSpace({ + memberEntity: ulid(), + username: session.session.username, + studio: session.session.studio, + }); router.push(`/s/${query?.studio}/s/${query?.space}`); } }; diff --git a/app/setup/SetupForm.tsx b/app/setup/SetupForm.tsx index 496ae8ca..bc764ace 100644 --- a/app/setup/SetupForm.tsx +++ b/app/setup/SetupForm.tsx @@ -12,6 +12,8 @@ import { WORKER_URL } from "src/constants"; import { supabaseBrowserClient } from "supabase/clients"; import Image from "next/image"; import spotIllo from "public/img/spotIllustration/welcome.png"; +import { makeReflect } from "components/ReplicacheProvider"; +import { ulid } from "src/ulid"; export function SignupPageForm() { let router = useRouter(); @@ -59,9 +61,20 @@ export function SignupPageForm() { else router.push(`/s/${data.username}`); } } + if (res.success) { let { data } = await supabase.auth.refreshSession(); - if (data.session) { + if (data.user && data.session) { + let reflect = makeReflect({ + roomID: data.user?.user_metadata.studio, + authToken: tokens, + userID: data.user.id, + }); + reflect.mutate.joinSpace({ + memberEntity: ulid(), + username: data.user?.user_metadata.username, + studio: data.user?.user_metadata.studio, + }); await supabase.auth.setSession(data?.session); if (redirectTo) router.push(redirectTo); else router.push(`/s/${data.user?.user_metadata.username}`); diff --git a/app/studio/[studio_id]/space/StudioPresenceHandler.tsx b/app/studio/[studio_id]/space/StudioPresenceHandler.tsx index 663d27ef..c190ab11 100644 --- a/app/studio/[studio_id]/space/StudioPresenceHandler.tsx +++ b/app/studio/[studio_id]/space/StudioPresenceHandler.tsx @@ -1,12 +1,9 @@ "use client"; -import { useMeetingState } from "@daily-co/daily-react"; -import { ref } from "data/Facts"; import { useMutations } from "hooks/useReplicache"; import { useRoom } from "hooks/useUIState"; -import { useAtomValue } from "jotai"; import { useEffect } from "react"; import { ulid } from "src/ulid"; -import { SpaceProvider, socketStateAtom } from "components/ReplicacheProvider"; +import { SpaceProvider } from "components/ReplicacheProvider"; import { SpaceData } from "components/SpacesList"; import { useParams } from "next/navigation"; @@ -46,22 +43,18 @@ export const SpacePageStudioPresenceHandler = (props: { space: SpaceData }) => { ); }; export const PresenceHandler = (props: { space_do_id: string }) => { - let { rep, mutate, authorized, memberEntity, client, permissions } = - useMutations(); + let { rep, mutate, authorized, memberEntity, client } = useMutations(); let room = useRoom(); - let socketState = useAtomValue(socketStateAtom); useEffect(() => { - if (!authorized || !rep || !memberEntity || socketState !== "connected") - return; - rep.clientID.then((clientID) => { - mutate("initializeClient", { - clientID, - clientEntity: ulid(), - memberEntity: memberEntity as string, - }); + if (!authorized || !rep || !memberEntity) return; + + mutate("initializeClient", { + clientID: rep.clientID, + clientEntity: ulid(), + memberEntity: memberEntity as string, }); - }, [rep, authorized, memberEntity, socketState, mutate]); + }, [rep, authorized, memberEntity, mutate]); useEffect(() => { if (!client?.entity || !authorized) return; diff --git a/backend/SpaceDurableObject/fact_store.ts b/backend/SpaceDurableObject/fact_store.ts index 975162ba..62136a90 100644 --- a/backend/SpaceDurableObject/fact_store.ts +++ b/backend/SpaceDurableObject/fact_store.ts @@ -158,7 +158,7 @@ export const store = (storage: BasicStorage, ctx: { id: string }) => { }, }; - let context: Omit = { + let context: Omit = { scanIndex, updateFact: async (id, data) => { return lock.withLock(async () => { diff --git a/backend/SpaceDurableObject/index.ts b/backend/SpaceDurableObject/index.ts index 5f9bbd93..0851a333 100644 --- a/backend/SpaceDurableObject/index.ts +++ b/backend/SpaceDurableObject/index.ts @@ -7,19 +7,15 @@ import { delete_file_upload_route } from "./routes/delete_file_upload"; import { get_share_code_route } from "./routes/get_share_code"; import { join_route } from "./routes/join"; import { pullRoute } from "./routes/pull"; -import { push_route } from "./routes/push"; import { connect } from "./socket"; import { handleFileUpload } from "./upload_file"; import { migrations } from "./migrations"; import { delete_self_route } from "./routes/delete_self"; import { get_daily_token_route } from "./routes/get_daily_token"; -import { post_feed_route } from "./routes/post_feed"; -import { get_card_data_route } from "./routes/get_card_data"; import type { WebSocket as DOWebSocket } from "@cloudflare/workers-types"; import { createClient } from "backend/lib/supabase"; export type Env = { - factStore: ReturnType; storage: DurableObjectStorage; state: DurableObjectState; poke: () => void; @@ -30,16 +26,13 @@ export type Env = { }; let routes = [ - get_card_data_route, pullRoute, - push_route, claimRoute, get_share_code_route, join_route, delete_file_upload_route, delete_self_route, get_daily_token_route, - post_feed_route, ]; export type SpaceRoutes = typeof routes; let router = makeRouter(routes); diff --git a/backend/SpaceDurableObject/lib/isMember.ts b/backend/SpaceDurableObject/lib/isMember.ts index cd069b8c..f173e13d 100644 --- a/backend/SpaceDurableObject/lib/isMember.ts +++ b/backend/SpaceDurableObject/lib/isMember.ts @@ -24,3 +24,7 @@ export async function isUserMember( isMember.members_in_studios.length > 0) ); } + +export function isAuthorized(env: Env, userID: string) { + return env.id === userID; +} diff --git a/backend/SpaceDurableObject/migrations/2023-02-02.ts b/backend/SpaceDurableObject/migrations/2023-02-02.ts index d731b9c4..736f2f4b 100644 --- a/backend/SpaceDurableObject/migrations/2023-02-02.ts +++ b/backend/SpaceDurableObject/migrations/2023-02-02.ts @@ -11,6 +11,7 @@ export default { let promptRoom = rooms.find((r) => r.value === "prompts"); if (promptRoom) { await fact_store.updateFact(promptRoom.id, { + //@ts-ignore attribute: "promptroom/name", value: "Prompt Pool", }); diff --git a/backend/SpaceDurableObject/migrations/2023-09-04.ts b/backend/SpaceDurableObject/migrations/2023-09-04.ts index eb2a2dbf..13c4c1d4 100644 --- a/backend/SpaceDurableObject/migrations/2023-09-04.ts +++ b/backend/SpaceDurableObject/migrations/2023-09-04.ts @@ -8,6 +8,7 @@ export default { let fact_store = store(storage, { id: "" }); let members = await fact_store.scanIndex.aev("space/member"); for (let member of members) { + //@ts-ignore let color = await getMemberColor(fact_store); await fact_store.assertFact({ entity: member.entity, diff --git a/backend/SpaceDurableObject/routes/claim.ts b/backend/SpaceDurableObject/routes/claim.ts index 184b2135..4a026400 100644 --- a/backend/SpaceDurableObject/routes/claim.ts +++ b/backend/SpaceDurableObject/routes/claim.ts @@ -1,25 +1,6 @@ import { makeRoute } from "backend/lib/api"; -import { flag, ref } from "data/Facts"; -import { ulid } from "src/ulid"; import { z } from "zod"; import { Env } from ".."; -import { getMemberColor } from "./join"; - -let defaultReactions = [ - "๐Ÿ˜Š", - "๐Ÿ˜”", - "โค๏ธ", - "๐ŸŽ‰", - "๐Ÿ”ฅ", - "๐Ÿ‘€", - "๐Ÿ’€", - "๐Ÿ“Œ", - "โœ…", - "๐Ÿ‘", - "๐Ÿ‘Ž", - "!!", - "?", -]; export const claimRoute = makeRoute({ route: "claim", @@ -31,184 +12,12 @@ export const claimRoute = makeRoute({ handler: async (msg, env: Env) => { let creator = await env.storage.get("meta-creator"); let space_type = await env.storage.get("meta-space-type"); - let thisEntity = ulid(); if (creator || space_type) return { data: { success: false } }; - let memberEntity = ulid(); - let canvasRoom = ulid(); - let collectionRoom = ulid(); - let chatRoom = ulid(); - let readmeEntity = ulid(); - let readmeCardPositionFact = ulid(); await Promise.all([ - env.factStore.assertFact({ - entity: readmeEntity, - attribute: "card/title", - value: README_Title, - positions: {}, - }), - env.factStore.assertFact({ - entity: readmeEntity, - attribute: "card/content", - value: README.trim(), - positions: {}, - }), - env.factStore.assertFact({ - entity: canvasRoom, - factID: readmeCardPositionFact, - attribute: "desktop/contains", - value: ref(readmeEntity), - positions: {}, - }), - env.factStore.assertFact({ - entity: readmeCardPositionFact, - attribute: "card/position-in", - value: { - x: 32, - y: 32, - rotation: 0, - size: "small", - type: "position", - }, - positions: {}, - }), - env.factStore.assertFact({ - entity: canvasRoom, - attribute: "home", - value: flag(), - positions: {}, - }), - - env.factStore.assertFact({ - entity: canvasRoom, - attribute: "room/name", - value: "Canvas", - positions: { roomList: "a0" }, - }), - env.factStore.assertFact({ - entity: canvasRoom, - attribute: "room/type", - value: "canvas", - positions: {}, - }), - - env.factStore.assertFact({ - entity: collectionRoom, - attribute: "room/name", - value: "Collection", - positions: { roomList: "c1" }, - }), - env.factStore.assertFact({ - entity: collectionRoom, - attribute: "room/type", - value: "collection", - positions: {}, - }), - env.factStore.assertFact({ - entity: chatRoom, - attribute: "room/name", - value: "Chat", - positions: { roomList: "t1" }, - }), - env.factStore.assertFact({ - entity: chatRoom, - attribute: "room/type", - value: "chat", - positions: {}, - }), - env.factStore.assertFact({ - entity: memberEntity, - attribute: "member/color", - value: await getMemberColor(env.factStore), - positions: {}, - }), - - env.factStore.assertFact({ - entity: memberEntity, - attribute: "space/member", - value: msg.ownerID, - positions: { aev: "a0" }, - }), - env.factStore.assertFact({ - entity: memberEntity, - attribute: "member/name", - value: msg.ownerName, - positions: { aev: "a0" }, - }), - ...defaultReactions.map((r) => - env.factStore.assertFact({ - entity: thisEntity, - attribute: "space/reaction", - value: r, - positions: {}, - }) - ), env.storage.put("meta-creator", msg.ownerID), env.storage.put("meta-space-type", msg.type), ]); return { data: { success: true } }; }, }); - -const README_Title = `HYPERLINK README ๐Ÿ“–โœจ๐Ÿ“– click here! ๐ŸŒฑ`; -const README = ` - -Welcome to Hyperlink! This card will: - -1) Show you how Hyperlink works -2) Help you get started making Spaces -3) Inspire you to try some fun experiments! - -==For more info, click "?" in the sidebar== - -# How Hyperlink Works - -The very basics: make cards; organize them in rooms; talk about them together! - -**Quick things to try:** - -- make a room from the sidebar -- make a card (double click the canvas) -- add card content โ€” text, or other media from the toolbar up top -- add a comment at the bottom of a card - -**When you invite others, you'll also find:** - -- audio calls, to hang & explore stuff together -- live presence, to see where others are -- unreads, to see what's new in the space -- notifications, for alerts on new activity - -๐Ÿ‘ฏ You can invite collaborators at any time! But if you'd like to test things out first, we're glad to help with some feedback & experimentation. - -๐Ÿšจ Send us the invite link from the sidebar to contact@hyperlink.academy & one of us will join (you can delete the Space after testing!) - -# Making Spaces - -Quick checklist for setting up a Space: - -1๏ธโƒฃ **set a goal** โ€” what are you aiming to do? what's the ideal outcome? how should it end? - -2๏ธโƒฃ **add things to explore** (readings? questions?) and make cards for each - -3๏ธโƒฃ **organize things** in rooms, e.g. canvas for ideas, collection for tasks, chat forโ€ฆchat! - -4๏ธโƒฃ **decide what to do** e.g. weekly calls; sharing things and commenting on them - -5๏ธโƒฃ **invite a friend** (or a few) to join! - -# Things to Try - -Spaces are super flexibleโ€ฆyou can design many *games* with these basic pieces: - -- **reading club**: add readings & discuss -- **writing group**: share drafts and feedback -- **creative projects**: manage tasks; make things; share work in progress -- **prompts**: e.g. each share a daily drawing -- **interactive workshops** or other sessions - -Click "?" in the sidebar for ==examples of real Spaces we've made== for inspiration โœจ๐Ÿ”ฎ - -โ€ฆand if you have questions or feedback, reach out any time! --> contact@hyperlink.academy - -`; diff --git a/backend/SpaceDurableObject/routes/get_card_data.ts b/backend/SpaceDurableObject/routes/get_card_data.ts deleted file mode 100644 index 663187cc..00000000 --- a/backend/SpaceDurableObject/routes/get_card_data.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Env } from ".."; -import { makeRoute } from "backend/lib/api"; -import { z } from "zod"; - -export const get_card_data_route = makeRoute({ - route: "get_card_data", - input: z.object({ - cardEntity: z.string(), - }), - handler: async (msg, env: Env) => { - let title = await env.factStore.scanIndex.eav(msg.cardEntity, "card/title"); - let content = await env.factStore.scanIndex.eav( - msg.cardEntity, - "card/content" - ); - let creator = await env.factStore.scanIndex.eav( - msg.cardEntity, - "card/created-by" - ); - let creatorName; - if (creator) - creatorName = await env.factStore.scanIndex.eav( - creator.value.value, - "member/name" - ); - - return { - data: { - title: title?.value, - content: content?.value, - creator: creatorName?.value, - }, - }; - }, -}); diff --git a/backend/SpaceDurableObject/routes/get_daily_token.ts b/backend/SpaceDurableObject/routes/get_daily_token.ts index 0afa448a..94d3cec0 100644 --- a/backend/SpaceDurableObject/routes/get_daily_token.ts +++ b/backend/SpaceDurableObject/routes/get_daily_token.ts @@ -2,9 +2,9 @@ import { z } from "zod"; import { makeRoute } from "backend/lib/api"; import { Env } from ".."; import { authTokenVerifier, verifyIdentity } from "backend/lib/auth"; -import { createClient } from "@supabase/supabase-js"; -import { Database } from "backend/lib/database.types"; import { uuidToBase62 } from "src/uuidHelpers"; +import { createClient } from "backend/lib/supabase"; +import { isUserMember } from "../lib/isMember"; export const get_daily_token_route = makeRoute({ route: "get_daily_token", @@ -23,22 +23,14 @@ export const get_daily_token_route = makeRoute({ data: { success: false, error: "Invalid session token" }, } as const; - let isMember = await env.factStore.scanIndex.ave( - "space/member", - session.studio - ); + const supabase = createClient(env.env); - if (!isMember) { + if (!isUserMember(env, session.id)) { return { data: { success: false, error: "user is not a member" }, } as const; } - const supabase = createClient( - env.env.SUPABASE_URL, - env.env.SUPABASE_API_TOKEN - ); - let { data } = await supabase .from("space_data") .select(`name, owner:identity_data!space_data_owner_fkey(*), id`) diff --git a/backend/SpaceDurableObject/routes/join.ts b/backend/SpaceDurableObject/routes/join.ts index 239240c0..62293c23 100644 --- a/backend/SpaceDurableObject/routes/join.ts +++ b/backend/SpaceDurableObject/routes/join.ts @@ -2,7 +2,6 @@ import { app_event } from "backend/lib/analytics"; import { makeRoute } from "backend/lib/api"; import { authTokenVerifier, verifyIdentity } from "backend/lib/auth"; import { createClient } from "backend/lib/supabase"; -import { ulid } from "src/ulid"; import { z } from "zod"; import { memberColors } from "src/colors"; import { Env } from ".."; @@ -51,39 +50,6 @@ export const join_route = makeRoute({ isMember = !!data; } - let color = await getMemberColor(env.factStore); - let memberEntity = ulid(); - console.log("creating members"); - console.log( - await Promise.all([ - env.factStore.assertFact({ - entity: memberEntity, - attribute: "member/color", - value: color, - positions: {}, - }), - env.factStore.assertFact({ - entity: memberEntity, - attribute: "space/member", - value: session.studio, - positions: {}, - }), - msg.bio - ? env.factStore.assertFact({ - entity: memberEntity, - attribute: "card/content", - value: msg.bio, - positions: {}, - }) - : null, - env.factStore.assertFact({ - entity: memberEntity, - attribute: "member/name", - value: session.username, - positions: {}, - }), - ]) - ); if (space_type === "studio") { let { data: studio_ID } = await supabase .from("studios") diff --git a/backend/SpaceDurableObject/routes/post_feed.ts b/backend/SpaceDurableObject/routes/post_feed.ts deleted file mode 100644 index ed9d588a..00000000 --- a/backend/SpaceDurableObject/routes/post_feed.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Env } from ".."; -import { makeRoute } from "backend/lib/api"; -import { z } from "zod"; -import { authTokenVerifier, verifyIdentity } from "backend/lib/auth"; -import { ulid } from "src/ulid"; -import { ref } from "data/Facts"; -import { generateKeyBetween } from "src/fractional-indexing"; -import { createClient } from "backend/lib/supabase"; - -const position = z.object({ x: z.number(), y: z.number() }); -export const post_feed_route = makeRoute({ - route: "post_feed_route", - input: z.object({ - authToken: authTokenVerifier, - cardPosition: position.optional(), - contentPosition: position.optional(), - content: z.string(), - spaceID: z.string(), - cardEntity: z.string(), - }), - handler: async (msg, env: Env) => { - const supabase = createClient(env.env); - let session = await verifyIdentity(env.env, msg.authToken); - let space_type = await env.storage.get("meta-space-type"); - if (space_type !== "studio") - return { data: { success: false, error: "This is not a studio" } }; - if (!session) - return { - data: { success: false, error: "Invalid session token" }, - } as const; - let { data: isMember } = await supabase - .from("members_in_studios") - .select("member, studios!inner(do_id)") - .eq("member", session.id) - .eq("studios.do_id", env.id); - if (!isMember) - return { - data: { success: false, error: "You are not a member of this studio" }, - } as const; - - let creator = await env.factStore.scanIndex.ave( - "space/member", - session.studio - ); - if (!creator) - return { data: { success: false, error: "no member entity found" } }; - - let entity = ulid(); - await env.factStore.assertFact({ - entity, - attribute: "post/attached-card", - value: { space_do_id: msg.spaceID, cardEntity: msg.cardEntity }, - positions: {}, - }); - - await env.factStore.assertFact({ - entity, - attribute: "card/content", - value: msg.content, - positions: {}, - }); - if (msg.contentPosition) - await env.factStore.assertFact({ - entity, - attribute: "post/content/position", - positions: {}, - value: { - type: "position", - x: msg.contentPosition?.x || 0, - y: msg.contentPosition?.y || 0, - rotation: 0, - size: "small", - }, - }); - - if (msg.contentPosition) - await env.factStore.assertFact({ - entity, - attribute: "post/attached-card/position", - positions: {}, - value: { - type: "position", - x: msg.cardPosition?.x || 0, - y: msg.cardPosition?.y || 0, - rotation: 0, - size: "small", - }, - }); - - await env.factStore.assertFact({ - entity, - attribute: "card/created-by", - value: ref(creator.entity), - positions: {}, - }); - let latestPosts = await env.factStore.scanIndex.aev("feed/post"); - await env.factStore.assertFact({ - entity, - attribute: "feed/post", - value: generateKeyBetween( - null, - latestPosts.sort((a, b) => { - let aPosition = a.value, - bPosition = b.value; - if (aPosition === bPosition) return a.id > b.id ? 1 : -1; - return aPosition > bPosition ? 1 : -1; - })[0]?.value || null - ), - positions: {}, - }); - - env.poke(); - return { data: { success: true } } as const; - }, -}); diff --git a/backend/SpaceDurableObject/routes/push.ts b/backend/SpaceDurableObject/routes/push.ts deleted file mode 100644 index 8baad15d..00000000 --- a/backend/SpaceDurableObject/routes/push.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { z } from "zod"; -import { makeRoute } from "backend/lib/api"; -import { Env } from ".."; -import { Mutations, StudioMatePermissions } from "data/mutations"; -import { store } from "../fact_store"; -import { CachedStorage } from "../storage_cache"; -import { authTokenVerifier, verifyIdentity } from "backend/lib/auth"; -import { createClient } from "backend/lib/supabase"; -import { isUserMember } from "../lib/isMember"; - -export const push_route = makeRoute({ - route: "push", - input: z.object({ - authToken: authTokenVerifier, - clientID: z.string(), - mutations: z.array( - z.object({ - id: z.number(), - name: z.string(), - args: z.any(), - timestamp: z.number(), - }) - ), - pushVersion: z.number(), - schemaVersion: z.string(), - }), - handler: async (msg, env: Env) => { - let lastMutationID = - (await env.storage.get(`lastMutationID-${msg.clientID}`)) || 0; - let supabase = createClient(env.env); - - let session = await verifyIdentity(env.env, msg.authToken); - if (!session) - return { - data: { success: false, error: "Invalid session token" }, - } as const; - let cachedStore = new CachedStorage(env.storage); - let fact_store = store(cachedStore, { id: env.id }); - - let isMember = await isUserMember(env, session.id); - - let isStudioMember = false; - if (!isMember) { - let { data } = await supabase - .from("space_data") - .select( - "do_id, spaces_in_studios!inner(studios!inner(members_in_studios!inner(member)))" - ) - .eq("do_id", env.id) - .eq("spaces_in_studios.studios.members_in_studios.member", session.id) - .single(); - isStudioMember = !!data; - if (!isStudioMember) { - env.storage.put( - `lastMutationID-${msg.clientID}`, - msg.mutations[msg.mutations.length - 1].id - ); - return { - data: { success: false, error: "user is not a member" }, - } as const; - } - } - - let release = await env.pushLock.lock(); - - for (let i = 0; i < msg.mutations.length; i++) { - let mutation = msg.mutations[i]; - if (mutation.id <= lastMutationID) continue; - lastMutationID = mutation.id; - let name = mutation.name as keyof typeof Mutations; - if (!Mutations[name]) { - continue; - } - if ( - !isMember && - isStudioMember && - !StudioMatePermissions.includes(name) - ) { - continue; - } - try { - await Mutations[name](mutation.args, { - ...fact_store, - runOnServer: (fn) => - fn({ ...env, factStore: fact_store }, session?.id as string), - }); - } catch (e) { - console.log( - `Error occured while running mutation: ${name}`, - JSON.stringify(e) - ); - } - } - cachedStore.put(`lastMutationID-${msg.clientID}`, lastMutationID); - await cachedStore.flush(); - release(); - - env.poke(); - if ( - msg.mutations.filter((m) => !EphemeralMutations.includes(m.name as any)) - .length > 0 - ) - env.updateLastUpdated(); - - return { data: { success: true, errors: [] } }; - }, -}); - -// We need to filter these out to prevent updating lastUpdated just when a -// client connects and such -const EphemeralMutations: Array = [ - "assertEmphemeralFact", - "initializeClient", - "setClientInCall", -]; diff --git a/backend/SpaceDurableObject/upload_file.ts b/backend/SpaceDurableObject/upload_file.ts index e6934faf..40c2ff9f 100644 --- a/backend/SpaceDurableObject/upload_file.ts +++ b/backend/SpaceDurableObject/upload_file.ts @@ -1,6 +1,7 @@ import { Env } from "."; import { verifyIdentity } from "backend/lib/auth"; import { createClient } from "backend/lib/supabase"; +import { isUserMember } from "./lib/isMember"; async function computeHash(data: ArrayBuffer): Promise { const buf = await crypto.subtle.digest("SHA-256", data); @@ -40,11 +41,7 @@ export const handleFileUpload = async (req: Request, env: Env) => { if (!session) return new Response(JSON.stringify({ success: false }), { headers }); - let isMember = await env.factStore.scanIndex.ave( - "space/member", - session.studio - ); - if (!isMember) + if (!isUserMember(env, session.id)) return new Response( JSON.stringify({ success: false, error: "user is not a member" }), { headers } diff --git a/backend/database.types.ts b/backend/database.types.ts new file mode 100644 index 00000000..92973f52 --- /dev/null +++ b/backend/database.types.ts @@ -0,0 +1,619 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[] + +export interface Database { + graphql_public: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + graphql: { + Args: { + operationName?: string + query?: string + variables?: Json + extensions?: Json + } + Returns: Json + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + public: { + Tables: { + communities: { + Row: { + id: number + name: string + spaceID: string + } + Insert: { + id?: number + name: string + spaceID: string + } + Update: { + id?: number + name?: string + spaceID?: string + } + Relationships: [] + } + debug_logs: { + Row: { + data: Json | null + id: number + time: string | null + } + Insert: { + data?: Json | null + id?: number + time?: string | null + } + Update: { + data?: Json | null + id?: number + time?: string | null + } + Relationships: [] + } + file_uploads: { + Row: { + created_at: string + deleted: boolean + hash: string + id: string + space: string + user_id: string + } + Insert: { + created_at?: string + deleted?: boolean + hash: string + id?: string + space: string + user_id: string + } + Update: { + created_at?: string + deleted?: boolean + hash?: string + id?: string + space?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "file_uploads_user_id_fkey" + columns: ["user_id"] + referencedRelation: "users" + referencedColumns: ["id"] + } + ] + } + identity_data: { + Row: { + id: string + studio: string + username: string + } + Insert: { + id: string + studio: string + username: string + } + Update: { + id?: string + studio?: string + username?: string + } + Relationships: [ + { + foreignKeyName: "identity_data_id_fkey" + columns: ["id"] + referencedRelation: "users" + referencedColumns: ["id"] + } + ] + } + members_in_spaces: { + Row: { + joined_at: string | null + member: string + space_id: string + } + Insert: { + joined_at?: string | null + member: string + space_id: string + } + Update: { + joined_at?: string | null + member?: string + space_id?: string + } + Relationships: [ + { + foreignKeyName: "members_in_spaces_member_fkey" + columns: ["member"] + referencedRelation: "identity_data" + referencedColumns: ["id"] + }, + { + foreignKeyName: "members_in_spaces_space_id_fkey" + columns: ["space_id"] + referencedRelation: "space_data" + referencedColumns: ["id"] + } + ] + } + members_in_studios: { + Row: { + member: string + studio: string + } + Insert: { + member: string + studio: string + } + Update: { + member?: string + studio?: string + } + Relationships: [ + { + foreignKeyName: "members_in_studios_member_fkey" + columns: ["member"] + referencedRelation: "identity_data" + referencedColumns: ["id"] + }, + { + foreignKeyName: "members_in_studios_studio_fkey" + columns: ["studio"] + referencedRelation: "studios" + referencedColumns: ["id"] + } + ] + } + old_identities: { + Row: { + email: string + hashed_password: string + studio: string + username: string + } + Insert: { + email: string + hashed_password: string + studio: string + username: string + } + Update: { + email?: string + hashed_password?: string + studio?: string + username?: string + } + Relationships: [] + } + push_subscriptions: { + Row: { + endpoint: string + id: number + push_subscription: Json + user_id: string + } + Insert: { + endpoint: string + id?: number + push_subscription: Json + user_id: string + } + Update: { + endpoint?: string + id?: number + push_subscription?: Json + user_id?: string + } + Relationships: [ + { + foreignKeyName: "push_subscriptions_user_id_fkey" + columns: ["user_id"] + referencedRelation: "identity_data" + referencedColumns: ["id"] + } + ] + } + space_data: { + Row: { + archived: boolean + created_at: string | null + default_space_image: string | null + description: string | null + display_name: string | null + do_id: string + end_date: string | null + id: string + image: string | null + lastUpdated: string | null + name: string | null + owner: string + start_date: string | null + } + Insert: { + archived?: boolean + created_at?: string | null + default_space_image?: string | null + description?: string | null + display_name?: string | null + do_id: string + end_date?: string | null + id?: string + image?: string | null + lastUpdated?: string | null + name?: string | null + owner: string + start_date?: string | null + } + Update: { + archived?: boolean + created_at?: string | null + default_space_image?: string | null + description?: string | null + display_name?: string | null + do_id?: string + end_date?: string | null + id?: string + image?: string | null + lastUpdated?: string | null + name?: string | null + owner?: string + start_date?: string | null + } + Relationships: [ + { + foreignKeyName: "space_data_owner_fkey" + columns: ["owner"] + referencedRelation: "identity_data" + referencedColumns: ["id"] + } + ] + } + space_events: { + Row: { + at: string + event: string + id: number + space_id: string + user: string + } + Insert: { + at?: string + event: string + id?: number + space_id: string + user: string + } + Update: { + at?: string + event?: string + id?: number + space_id?: string + user?: string + } + Relationships: [ + { + foreignKeyName: "space_events_space_id_fkey" + columns: ["space_id"] + referencedRelation: "space_data" + referencedColumns: ["id"] + }, + { + foreignKeyName: "space_events_user_fkey" + columns: ["user"] + referencedRelation: "identity_data" + referencedColumns: ["id"] + } + ] + } + spaces_in_studios: { + Row: { + space_id: string + studio: string + } + Insert: { + space_id: string + studio: string + } + Update: { + space_id?: string + studio?: string + } + Relationships: [ + { + foreignKeyName: "spaces_in_studios_space_id_fkey" + columns: ["space_id"] + referencedRelation: "space_data" + referencedColumns: ["id"] + }, + { + foreignKeyName: "spaces_in_studios_studio_fkey" + columns: ["studio"] + referencedRelation: "studios" + referencedColumns: ["id"] + } + ] + } + studios: { + Row: { + creator: string + description: string | null + do_id: string + id: string + name: string + welcome_message: string + } + Insert: { + creator: string + description?: string | null + do_id: string + id?: string + name: string + welcome_message?: string + } + Update: { + creator?: string + description?: string | null + do_id?: string + id?: string + name?: string + welcome_message?: string + } + Relationships: [ + { + foreignKeyName: "studios_creator_fkey" + columns: ["creator"] + referencedRelation: "identity_data" + referencedColumns: ["id"] + } + ] + } + user_space_unreads: { + Row: { + space_id: string + unreads: number + user: string + } + Insert: { + space_id: string + unreads: number + user: string + } + Update: { + space_id?: string + unreads?: number + user?: string + } + Relationships: [ + { + foreignKeyName: "user_space_unreads_space_id_fkey" + columns: ["space_id"] + referencedRelation: "space_data" + referencedColumns: ["id"] + }, + { + foreignKeyName: "user_space_unreads_user_fkey" + columns: ["user"] + referencedRelation: "identity_data" + referencedColumns: ["id"] + } + ] + } + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + storage: { + Tables: { + buckets: { + Row: { + allowed_mime_types: string[] | null + avif_autodetection: boolean | null + created_at: string | null + file_size_limit: number | null + id: string + name: string + owner: string | null + public: boolean | null + updated_at: string | null + } + Insert: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id: string + name: string + owner?: string | null + public?: boolean | null + updated_at?: string | null + } + Update: { + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id?: string + name?: string + owner?: string | null + public?: boolean | null + updated_at?: string | null + } + Relationships: [ + { + foreignKeyName: "buckets_owner_fkey" + columns: ["owner"] + referencedRelation: "users" + referencedColumns: ["id"] + } + ] + } + migrations: { + Row: { + executed_at: string | null + hash: string + id: number + name: string + } + Insert: { + executed_at?: string | null + hash: string + id: number + name: string + } + Update: { + executed_at?: string | null + hash?: string + id?: number + name?: string + } + Relationships: [] + } + objects: { + Row: { + bucket_id: string | null + created_at: string | null + id: string + last_accessed_at: string | null + metadata: Json | null + name: string | null + owner: string | null + path_tokens: string[] | null + updated_at: string | null + version: string | null + } + Insert: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + Update: { + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } + Relationships: [ + { + foreignKeyName: "objects_bucketId_fkey" + columns: ["bucket_id"] + referencedRelation: "buckets" + referencedColumns: ["id"] + } + ] + } + } + Views: { + [_ in never]: never + } + Functions: { + can_insert_object: { + Args: { + bucketid: string + name: string + owner: string + metadata: Json + } + Returns: undefined + } + extension: { + Args: { + name: string + } + Returns: string + } + filename: { + Args: { + name: string + } + Returns: string + } + foldername: { + Args: { + name: string + } + Returns: unknown + } + get_size_by_bucket: { + Args: Record + Returns: { + size: number + bucket_id: string + }[] + } + search: { + Args: { + prefix: string + bucketname: string + limits?: number + levels?: number + offsets?: number + search?: string + sortcolumn?: string + sortorder?: string + } + Returns: { + name: string + id: string + updated_at: string + created_at: string + last_accessed_at: string + metadata: Json + }[] + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} + diff --git a/backend/lib/analytics.ts b/backend/lib/analytics.ts index d00d191a..a7cd0394 100644 --- a/backend/lib/analytics.ts +++ b/backend/lib/analytics.ts @@ -1,5 +1,4 @@ -import { Bindings } from "backend"; -import { internalWorkerAPI } from "./api"; +import { createClient } from "./supabase"; type AppEvents = | "signup" @@ -11,8 +10,19 @@ type AppEvents = | "created_room"; export const app_event = async ( - env: Bindings, + env: { SUPABASE_API_TOKEN: string; SUPABASE_URL: string }, event: { event: AppEvents; user: string; space_do_id: string } ) => { - internalWorkerAPI(env)("http://internal/v0", "app_event", event); + const supabase = createClient(env); + let { data: space_id } = await supabase + .from("space_data") + .select("id") + .eq("do_id", event.space_do_id) + .single(); + if (!space_id) + return { data: { success: false, error: "no space found" } } as const; + + await supabase + .from("space_events") + .insert({ event: event.event, user: event.user, space_id: space_id.id }); }; diff --git a/backend/lib/database.types.ts b/backend/lib/database.types.ts index 0df97a44..92973f52 100644 --- a/backend/lib/database.types.ts +++ b/backend/lib/database.types.ts @@ -399,7 +399,7 @@ export interface Database { } Insert: { space_id: string - unreads?: number + unreads: number user: string } Update: { diff --git a/components/CardPreview/index.tsx b/components/CardPreview/index.tsx index 04fe2852..b10c88e0 100644 --- a/components/CardPreview/index.tsx +++ b/components/CardPreview/index.tsx @@ -61,7 +61,6 @@ export type Props = { data: CardPreviewData; }; -const WORKER_URL = process.env.NEXT_PUBLIC_WORKER_URL as string; export const CardPreview = ( props: { entityID: string; diff --git a/components/CardStack.tsx b/components/CardStack.tsx index 7c48c4dc..cde92ae0 100644 --- a/components/CardStack.tsx +++ b/components/CardStack.tsx @@ -2,7 +2,6 @@ import React, { useContext, useState } from "react"; import { ReferenceAttributes } from "data/Attributes"; import { ReplicacheContext, - ReplicacheMutators, scanIndex, db, useMutations, @@ -12,9 +11,10 @@ import { ulid } from "src/ulid"; import { sortByPosition } from "src/position_helpers"; import { generateKeyBetween } from "src/fractional-indexing"; import { useLongPress } from "hooks/useLongPress"; -import { Replicache } from "replicache"; import { useCardViewer } from "./CardViewerContext"; import { CardSearch } from "./Icons"; +import { Reflect } from "@rocicorp/reflect/client"; +import { ReplicacheMutators } from "reflect"; import { NewCardPreview, useOnDragEndCollection } from "./CardCollection"; import { useDroppableZone } from "./DragContext"; import { CardPreview, PlaceholderNewCard } from "./CardPreview"; @@ -53,10 +53,7 @@ export const CardAdder = ( return null; } else return ( -
+
{over && (over.type === "card" || over.type === "search-card" ? (
@@ -74,9 +71,7 @@ export const CardAdder = (
) : null)} -
+