Skip to content

Commit

Permalink
Make mechanism of moving users to the beta feature, separate between …
Browse files Browse the repository at this point in the history
…games saved on frontend, games saved on backend and games shared (#2469)

* Make mechanism of moving users to the beta feature, but it's not active yet, except for development and cosmin

* remove console log

* refactor code and use sha256Hash instead of custom hashing function

* add function to move game to backend saving

* add icons for backend saving and sharing

* refactor code to remove most duplication

* remove console log
  • Loading branch information
Cosmin-Mare authored Oct 28, 2024
1 parent 4821090 commit 9349ade
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 166 deletions.
12 changes: 1 addition & 11 deletions src/components/big-interactive-pages/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const onRun = async () => {

interface EditorProps {
persistenceState: Signal<PersistenceState>;
roomState?: Signal<RoomState>;
roomState?: Signal<RoomState> | undefined;
cookies: {
outputAreaSize: number | null;
helpAreaSize: number | null;
Expand Down Expand Up @@ -262,16 +262,6 @@ export default function Editor({ persistenceState, cookies, roomState }: EditorP

const [sessionId] = useState(nanoid());


useEffect(() => {
if(roomState){
isNewSaveStrat.value = true;
} else {
isNewSaveStrat.value = false;
}
}, [])


useEffect(() => {
const channel = new BroadcastChannel('session_channel');
channel.onmessage = (event) => {
Expand Down
6 changes: 4 additions & 2 deletions src/components/codemirror.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export default function CodeMirror(props: CodeMirrorProps) {
});
};

useEffect(() => {
isNewSaveStrat.value = props.roomState ? true : false; // If a roomState was passed, move to new save strat
}, [])

// Alert the parent to code changes (not reactive)
const onCodeChangeRef = useRef(props.onCodeChange)
useEffect(() => { onCodeChangeRef.current = props.onCodeChange }, [props.onCodeChange])
Expand Down Expand Up @@ -104,7 +108,6 @@ export default function CodeMirror(props: CodeMirrorProps) {
});
useEffect(() => {
if (!parent.current) throw new Error('Oh golly! The editor parent ref is null')

if(!isNewSaveStrat.value){
const editor = new EditorView({
state: createEditorState(props.initialCode ? props.initialCode : '', () => {
Expand All @@ -118,7 +121,6 @@ export default function CodeMirror(props: CodeMirrorProps) {
props.onEditorView?.(editor)
return
}

if(!props.roomState) return
if(!props.persistenceState) return
try{
Expand Down
34 changes: 31 additions & 3 deletions src/lib/game-saving/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import { lazy } from '../utils/lazy'
import { generateGameName } from '../words'
import metrics from '../../../metrics'
import { RoomParticipant } from '../state'
import { sha256Hash } from "../../lib/codemirror/util";

const numberid = customAlphabet('0123456789')

const whitelistedBetaCollabAndSavingStratEmails = ["[email protected]", "[email protected]", "[email protected]"]

const app = lazy(() => {
if (admin.apps.length === 0) {
return initializeApp({
Expand Down Expand Up @@ -56,6 +59,7 @@ export interface Game {
code: string
tutorialName?: string
tutorialIndex?: number
isSavedOnBackend?: boolean
roomParticipants?: RoomParticipant[]
isRoomOpen?: boolean
password?: string
Expand Down Expand Up @@ -253,7 +257,7 @@ export const getGame = async (id: string | undefined): Promise<Game | null> => {
return { id: _game.id, ..._game.data() } as Game
}

export const makeGame = async (ownerId: string, unprotected: boolean, name?: string, code?: string, tutorialName?: string, tutorialIndex?: number): Promise<Game> => {
export const makeGame = async (ownerId: string, unprotected: boolean, name?: string, code?: string, tutorialName?: string, tutorialIndex?: number, isSavedOnBackend?: boolean): Promise<Game> => {

const createdDate = Timestamp.now()
const data = {
Expand All @@ -264,7 +268,8 @@ export const makeGame = async (ownerId: string, unprotected: boolean, name?: str
name: name ?? generateGameName(),
code: code ?? '',
tutorialName: tutorialName ?? null,
tutorialIndex: tutorialIndex ?? null
tutorialIndex: tutorialIndex ?? null,
isSavedOnBackend: isSavedOnBackend ?? false,
}
const _game = await addDocument('games', data);
return { id: _game.id, ...data } as Game
Expand Down Expand Up @@ -331,4 +336,27 @@ export const getSnapshotData = async (id: string): Promise<SnapshotData | null>
ownerName: user?.username ?? snapshot.ownerName,
code: snapshot.code
}
}
}

export const updateUserGitHubToken = async (userId: string, githubAccessToken: string, githubId: string, githubUsername: string): Promise<void> => {
await updateDocument('users', userId, { githubAccessToken, githubId, githubUsername });
}

async function hashCodeToBigInt(string : string) : Promise<bigint>{
return BigInt(`0x${ await sha256Hash(string)}`);
}

export async function isAccountWhitelistedToUseCollabAndSavingBetaFeatures(id: string, email: string) : Promise<boolean>{
if(import.meta.env.PERCENT_OF_USERS_WHITELISTED_FOR_BETA_FEATURE == 0 || import.meta.env.PERCENT_OF_USERS_WHITELISTED_FOR_BETA_FEATURE == undefined) return false;
let hashedId = await hashCodeToBigInt(id);

if(hashedId % BigInt(100) < import.meta.env.PERCENT_OF_USERS_WHITELISTED_FOR_BETA_FEATURE ||
whitelistedBetaCollabAndSavingStratEmails.includes(email)){
return true
}
return false;
}

export const moveGameToBackendSaving = async (game: Game): Promise<Game> => {
return makeGame(game.ownerId, game.unprotected, game.name, game.code, game.tutorialName, game.tutorialIndex, true);
}
118 changes: 80 additions & 38 deletions src/pages/~/[id].astro
Original file line number Diff line number Diff line change
@@ -1,75 +1,115 @@
---
import '../../global.css'
import { firestore, getGame, getSession } from '../../lib/game-saving/account'
import { firestore, Game, getGame, getSession, moveGameToBackendSaving } from '../../lib/game-saving/account'
import Editor from '../../components/big-interactive-pages/editor'
import StandardHead from '../../components/standard-head.astro'
import { signal } from '@preact/signals'
import type { PersistenceState } from '../../lib/state'
import { ConnectionStatus, PersistenceStateKind, RoomState, type PersistenceState } from '../../lib/state'
import MobileUnsupported from '../../components/popups-etc/mobile-unsupported'
import { mobileUserAgent } from '../../lib/utils/mobile'
import { isAccountWhitelistedToUseCollabAndSavingBetaFeatures } from '../../lib/game-saving/account'
const session = await getSession(Astro.cookies)
if (!session) return Astro.redirect(`/login?to=${Astro.request.url}`, 302)
const game = await getGame(Astro.params.id!)
if (!game || game.ownerId !== session.user.id) return Astro.redirect('/404', 302)
let persistenceState;
let isMobile;
let roomState;
let _persistenceState: PersistenceState | undefined
const fileRegexp = /^.*\/(.+)-(\d+)\.md$/
async function handleTutorial(game: Game) : Promise<[string[] | undefined, number | undefined]>{
const fileRegexp = /^.*\/(.+)-(\d+)\.md$/
let tutorial: string[] | undefined;
let tutorialIndex: number | undefined;
if (game.tutorialName) {
const files = await Astro.glob('/games/*.md')
tutorial = files.filter(file => {
if (game.tutorialName) {
const files = await Astro.glob('/games/*.md')
return [files.filter(file => {
const regexedFile = file.file.match(fileRegexp)
return regexedFile && regexedFile[1] === game.tutorialName
})
?.map(md => md.compiledContent())
tutorialIndex = game.tutorialIndex
?.map(md => md.compiledContent()), game.tutorialIndex]
}
return [undefined, undefined]
}
let _persistenceState: PersistenceState
if (session.session.full) {
if (game.unprotected) {
await firestore.collection('games').doc(game.id).update({ unprotected: false })
game.unprotected = false
async function handlePersistenceState(isSessionFull : boolean, game : Game, tutorial : string[] | undefined, tutorialIndex : number | undefined) : Promise<PersistenceState | undefined>{
if (isSessionFull) {
if (game.unprotected) {
await firestore.collection('games').doc(game.id).update({ unprotected: false })
game.unprotected = false
}
if (Astro.cookies.get('sprigTempGame').value === game.id)
Astro.cookies.delete('sprigTempGame', { path: '/' })
} else {
if (!game.unprotected) return undefined;
}
if (Astro.cookies.get('sprigTempGame').value === game.id)
Astro.cookies.delete('sprigTempGame', { path: '/' })
_persistenceState = {
kind: 'PERSISTED',
return {
kind: PersistenceStateKind.PERSISTED,
session,
cloudSaveState: 'SAVED',
game,
tutorial,
tutorialIndex,
stale: false
} as PersistenceState
}
function checkRoomAccess(game : Game, userEmail : string){
if(game.roomParticipants?.filter(participant => participant.userEmail === userEmail)[0]?.isBanned) return Astro.redirect('/404', 302)
if(game.password == undefined) return Astro.redirect('/404', 302)
return undefined;
}
const game = await getGame(Astro.params.id!)
if (!game) return Astro.redirect('/404', 302)
let shouldGameUseBetaCollabAndSavingFeatures =
await isAccountWhitelistedToUseCollabAndSavingBetaFeatures(session.user.id, session.user.email) || game.isSavedOnBackend
if(shouldGameUseBetaCollabAndSavingFeatures){
if(!game.isSavedOnBackend){
return Astro.redirect(`/~/${(await moveGameToBackendSaving(game)).id}`)
}
let checkRoom = false; // Whether or not to check whether the user has access to the room he's trying to enter
if(game.ownerId !== session.user.id) checkRoom = true;
let [tutorial, tutorialIndex] = await handleTutorial(game);
if(checkRoom){
_persistenceState = {
kind: PersistenceStateKind.COLLAB,
game: game.id,
password: undefined,
cloudSaveState: "SAVED",
session: session,
stale: false
} as PersistenceState
let roomAccess = checkRoomAccess(game, session.user.email)
if(roomAccess != undefined) return roomAccess
} else {
_persistenceState = await handlePersistenceState(session.session.full, game, tutorial, tutorialIndex)
if(!_persistenceState) return Astro.redirect(`/login?to=${Astro.request.url}`, 302)
}
roomState = signal<RoomState>({
connectionStatus: ConnectionStatus.DISCONNECTED,
roomId: game.id,
participants: [],
})
} else {
if (!game.unprotected) return Astro.redirect(`/login?to=${Astro.request.url}`, 302)
if (game.ownerId !== session.user.id) return Astro.redirect('/404', 302)
_persistenceState = {
kind: 'PERSISTED',
session,
cloudSaveState: 'SAVED',
game,
tutorial,
tutorialIndex,
stale: false
}
let [tutorial, tutorialIndex] = await handleTutorial(game)
_persistenceState = await handlePersistenceState(session.session.full, game, tutorial, tutorialIndex)
if(!_persistenceState) return Astro.redirect(`/login?to=${Astro.request.url}`, 302)
}
const persistenceState = signal<PersistenceState>(_persistenceState)
const isMobile = mobileUserAgent(Astro.request.headers.get('user-agent') ?? '')
persistenceState = signal<PersistenceState>(_persistenceState)
isMobile = mobileUserAgent(Astro.request.headers.get('user-agent') ?? '')
---
<script>
import { isNewSaveStrat } from "../../lib/state";
isNewSaveStrat.value = false; // This code is executed on the client side and it disables the new features implemented (save strat and collab)
</script>
<html lang='en'>
<head>
<StandardHead title='Editor' />
Expand All @@ -78,8 +118,10 @@ const isMobile = mobileUserAgent(Astro.request.headers.get('user-agent') ?? '')
<Editor
client:load
persistenceState={persistenceState}
roomState={roomState}
cookies={{
outputAreaSize: Astro.cookies.get('outputAreaSize').number(),
helpAreaSize: Astro.cookies.get('helpAreaSize').number(),
hideHelp: Astro.cookies.get('hideHelp').boolean()
}}
/>
Expand Down
Loading

0 comments on commit 9349ade

Please sign in to comment.