Skip to content

Commit d9c043a

Browse files
committed
Merge branch 'user-tokens-wallet' into develop
2 parents 53b200f + 219bb0c commit d9c043a

19 files changed

+456
-16
lines changed

cloud/functions/package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cloud/functions/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"lint": "eslint --ext .js,.ts .",
55
"build": "tsc",
66
"build:watch": "tsc --watch",
7-
"serve": "npm run build && firebase emulators:start --import data --export-on-exit --inspect-functions",
7+
"serve": "npm run build && MIGRATION_TOKEN=42 firebase emulators:start --import data --export-on-exit --inspect-functions",
88
"shell": "npm run build && firebase functions:shell",
99
"start": "npm run shell",
1010
"deploy": "firebase deploy --only functions",
@@ -24,9 +24,11 @@
2424
"firebase-functions": "^4.2.0",
2525
"lodash": "^4.17.21",
2626
"ts-pattern": "4.3.0",
27+
"uuid": "9.0.0",
2728
"zod": "3.21.4"
2829
},
2930
"devDependencies": {
31+
"@types/uuid": "9.0.2",
3032
"@typescript-eslint/eslint-plugin": "^5.12.0",
3133
"@typescript-eslint/parser": "^5.12.0",
3234
"eslint": "^8.9.0",

cloud/functions/src/crawlers/crawl.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import {WEB2DAY_CRAWLER} from "./web2day/crawler";
99
import {Temporal} from "@js-temporal/polyfill";
1010
import {CAMPING_DES_SPEAKERS_CRAWLER} from "./camping-des-speakers/crawler";
1111
import {DEVOXX_SCALA_CRAWLER} from "./devoxx-scala/crawler";
12+
import {match} from "ts-pattern";
13+
import {v4 as uuidv4} from "uuid"
14+
import {ConferenceOrganizerSpace} from "../../../../shared/conference-organizer-space.firestore";
1215
const axios = require('axios');
1316

1417
export type CrawlerKind<ZOD_TYPE extends z.ZodType> = {
@@ -106,6 +109,28 @@ const saveEvent = async function(event: FullEvent) {
106109
await db.collection("events").doc(event.id).set(event.info)
107110

108111
const firestoreEvent = await db.collection("events").doc(event.id);
112+
const organizerSpaceEntries = await firestoreEvent
113+
.collection('organizer-space')
114+
.listDocuments();
115+
116+
const { organizerSpaceContent, organizerSecretToken } = await match(organizerSpaceEntries.length)
117+
.with(0, async () => {
118+
const organizerSecretToken = uuidv4();
119+
const organizerSpaceContent: ConferenceOrganizerSpace = {
120+
organizerSecretToken,
121+
talkFeedbackViewerTokens: []
122+
}
123+
124+
await firestoreEvent.collection('organizer-space').doc(organizerSecretToken).set(organizerSpaceContent)
125+
return {organizerSecretToken, organizerSpaceContent};
126+
}).with(1, async () => {
127+
const organizerSecretToken = await organizerSpaceEntries[0].id;
128+
const organizerSpaceContent = (await firestoreEvent.collection('organizer-space').doc(organizerSecretToken).get()).data() as ConferenceOrganizerSpace;
129+
return {organizerSecretToken, organizerSpaceContent};
130+
}).otherwise(async () => {
131+
throw new Error(`More than 1 organizer-space entries detected (${organizerSpaceEntries.length}) for event ${event.id}`);
132+
})
133+
109134
await Promise.all(event.daySchedules.map(async daySchedule => {
110135
try {
111136
await firestoreEvent
@@ -121,6 +146,26 @@ const saveEvent = async function(event: FullEvent) {
121146
await firestoreEvent
122147
.collection("talks").doc(talk.id)
123148
.set(talk)
149+
150+
const existingTalkFeedbackViewerToken = organizerSpaceContent.talkFeedbackViewerTokens
151+
.find(tfvt => tfvt.eventId === event.id && tfvt.talkId === talk.id)
152+
153+
// If token already exists for the talk, let's not add it
154+
if(!existingTalkFeedbackViewerToken) {
155+
const talkFeedbackViewerSecretToken = uuidv4();
156+
157+
await firestoreEvent
158+
.collection("talks").doc(talk.id)
159+
.collection("feedbacks-access").doc(talkFeedbackViewerSecretToken)
160+
// Creating a fake entry so that feedbacks-access/{secretToken} node is created
161+
.collection('_empty').add({ _: 42 })
162+
163+
organizerSpaceContent.talkFeedbackViewerTokens.push({
164+
eventId: event.id,
165+
talkId: talk.id,
166+
secretToken: talkFeedbackViewerSecretToken
167+
});
168+
}
124169
}catch(e) {
125170
error(`Error while saving talk ${talk.id}: ${e?.toString()}`)
126171
}
@@ -133,6 +178,12 @@ const saveEvent = async function(event: FullEvent) {
133178
}catch(e) {
134179
error(`Error while storing conference descriptor ${event.conferenceDescriptor.id}: ${e?.toString()}`)
135180
}
181+
182+
try {
183+
await firestoreEvent.collection('organizer-space').doc(organizerSecretToken).set(organizerSpaceContent)
184+
}catch(e) {
185+
error(`Error while storing event's organizer-space content`)
186+
}
136187
}
137188

138189
export default crawlAll;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {db} from "../../firebase";
2+
3+
export async function getSecretTokenDoc<T>(path: string) {
4+
const list = await db.collection(path).listDocuments()
5+
if(list.length !== 1) {
6+
throw new Error(`Unexpected size=${list.length} for path [${path}] (expected=1)`)
7+
}
8+
9+
return (await db.doc(`${path}/${list[0].id}`).get()).data() as T;
10+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {createUserInfos} from "../onUserCreated";
2+
import {db} from "../../../firebase";
3+
4+
export async function createExistingUsersInfos(): Promise<"OK"|"Error"> {
5+
const existingUsers = await db.collection("users").listDocuments()
6+
7+
await Promise.all(existingUsers.map(async existingUser => {
8+
const userInfos = await db.collection('users').doc(existingUser.id).get()
9+
10+
if(!userInfos.exists) {
11+
await createUserInfos(existingUser.id);
12+
}
13+
}))
14+
15+
return "OK";
16+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {db} from "../../../firebase";
2+
import {createEmptyUserTokenWallet} from "../onUserCreated";
3+
4+
export async function createExistingUsersTokensWallet(): Promise<"OK"|"Error"> {
5+
const existingUsers = await db.collection("users").listDocuments()
6+
7+
await Promise.all(existingUsers.map(async existingUser => {
8+
const tokensWallet = await db
9+
.collection("users").doc(existingUser.id)
10+
.collection("tokens-wallet").doc("self")
11+
.get()
12+
13+
if(!tokensWallet.exists) {
14+
await createEmptyUserTokenWallet(existingUser.id);
15+
}
16+
}))
17+
18+
return "OK";
19+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import * as functions from "firebase-functions";
2+
import {UserDailyFeedbacks, UserFeedback} from "../../../../../shared/feedbacks.firestore";
3+
import {db} from "../../firebase";
4+
import {getSecretTokenDoc} from "./firestore-utils";
5+
import {ConferenceOrganizerSpace} from "../../../../../shared/conference-organizer-space.firestore";
6+
import {TalkAttendeeFeedback} from "../../../../../shared/talk-feedbacks.firestore";
7+
import {UserTokensWallet} from "../../../../../shared/user-tokens-wallet.firestore";
8+
9+
10+
export const onTalkFeedbackUpdated = functions.firestore
11+
.document(`users/{userId}/events/{eventId}/days/{dayId}/feedbacks/self`)
12+
.onUpdate(async (change, context) => {
13+
const eventId = context.params.eventId;
14+
const userId = context.params.userId;
15+
16+
const feedbacksBefore = change.before.data() as UserDailyFeedbacks;
17+
const feedbacksAfter = change.after.data() as UserDailyFeedbacks;
18+
19+
const lastModificationTimestampBefore = Math.max(...feedbacksBefore.feedbacks.map(f => Date.parse(f.lastUpdatedOn)));
20+
const feedbacksModifiedAfter = feedbacksAfter.feedbacks.filter(f => Date.parse(f.lastUpdatedOn) > lastModificationTimestampBefore);
21+
22+
await updateTalkFeedbacksFromUserFeedbacks(userId, eventId, feedbacksModifiedAfter);
23+
})
24+
25+
export const onTalkFeedbackCreated = functions.firestore
26+
.document(`users/{userId}/events/{eventId}/days/{dayId}/feedbacks/self`)
27+
.onCreate(async (snapshot, context) => {
28+
const eventId = context.params.eventId;
29+
const userId = context.params.userId;
30+
31+
const userDailyFeedbacks = snapshot.data() as UserDailyFeedbacks
32+
33+
await updateTalkFeedbacksFromUserFeedbacks(userId, eventId, userDailyFeedbacks.feedbacks);
34+
})
35+
36+
async function updateTalkFeedbacksFromUserFeedbacks(userId: string, eventId: string, userFeedbacks: UserFeedback[]) {
37+
const organizerSpace: ConferenceOrganizerSpace = await getSecretTokenDoc(`events/${eventId}/organizer-space`);
38+
await Promise.all(userFeedbacks.map(async feedback => {
39+
if(feedback.status === 'provided') {
40+
const talkFeedbackViewerToken = organizerSpace.talkFeedbackViewerTokens
41+
.find(tfvt => tfvt.eventId === eventId && tfvt.talkId === feedback.talkId);
42+
43+
if(!talkFeedbackViewerToken) {
44+
throw new Error(`No organizer talk token found for eventId=${eventId}, talkId=${feedback.talkId}`);
45+
}
46+
47+
const userTokensWallet = (await db.doc(`users/${userId}/tokens-wallet/self`).get()).data() as UserTokensWallet|undefined;
48+
if(!userTokensWallet) {
49+
throw new Error(`Unexpected unexistant user token wallet for userId=${userId}`);
50+
}
51+
52+
const userPublicTokensHavingProvidedFeedback = ((await db.collection(`events/${eventId}/talks/${feedback.talkId}/feedbacks-access/${talkFeedbackViewerToken.secretToken}/feedbacks`).listDocuments())
53+
|| []).map(d => d.id)
54+
55+
const attendeeFeedback: TalkAttendeeFeedback = {
56+
talkId: feedback.talkId,
57+
comment: feedback.comment,
58+
createdOn: feedback.createdOn,
59+
lastUpdatedOn: feedback.lastUpdatedOn,
60+
ratings: feedback.ratings,
61+
attendeePublicToken: userTokensWallet.publicUserToken
62+
}
63+
64+
await db.doc(`events/${eventId}/talks/${feedback.talkId}/feedbacks-access/${talkFeedbackViewerToken.secretToken}/feedbacks/${userTokensWallet.publicUserToken}`).set(attendeeFeedback)
65+
}
66+
}))
67+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as functions from "firebase-functions";
2+
import {db, info} from "../../firebase"
3+
import {UserTokensWallet} from "../../../../../shared/user-tokens-wallet.firestore";
4+
import {v4 as uuidv4} from "uuid";
5+
import {User} from "../../../../../shared/user.firestore";
6+
import {ISODatetime} from "../../../../../shared/type-utils";
7+
8+
9+
/**
10+
* Purpose of this function is to initialize a public user token for every users entries
11+
*/
12+
export const onUserCreated = functions.auth.user().onCreate(async (user, context) => {
13+
info(`User created triggered: ${user.uid}`)
14+
await createEmptyUserTokenWallet(user.uid);
15+
await createUserInfos(user.uid);
16+
});
17+
18+
export async function createEmptyUserTokenWallet(userId: string) {
19+
const publicUserToken = uuidv4();
20+
const userTokensWallet: UserTokensWallet = {
21+
publicUserToken,
22+
secretTokens: {
23+
eventOrganizerTokens: [],
24+
talkFeedbacksViewerTokens: []
25+
}
26+
};
27+
28+
await db
29+
.collection('users').doc(userId)
30+
.collection('tokens-wallet').doc('self')
31+
.set(userTokensWallet)
32+
}
33+
34+
export async function createUserInfos(userId: string) {
35+
const user: User = {
36+
userCreation: new Date().toISOString() as ISODatetime,
37+
username: `Anonymous${generateRandom15DigitInteger()}`
38+
}
39+
40+
await db.collection('users').doc(userId).set(user);
41+
}
42+
43+
function generateRandom15DigitInteger() {
44+
const nineDigits = Math.floor(Math.random() * 1e9).toString().padStart(9, '0');
45+
const sixDigits = Math.floor(Math.random() * 1e6).toString().padStart(6, '0');
46+
47+
return nineDigits + sixDigits;
48+
}

cloud/functions/src/functions/http/crawl.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
11
import * as functions from "firebase-functions";
22

33
import crawlAll from "../../crawlers/crawl"
4-
5-
function extractSingleQueryParam(request: functions.https.Request, paramName: string) {
6-
const value = request.query[paramName];
7-
return Array.isArray(value)?value[0]?.toString():value?.toString();
8-
}
9-
10-
function extractMultiQueryParam(request: functions.https.Request, paramName: string): string[] {
11-
const value = request.query[paramName];
12-
return (Array.isArray(value)?value.map(v => v.toString()):[ value?.toString() ].filter(v => !!v)) as string[];
13-
}
4+
import {extractMultiQueryParam, extractSingleQueryParam} from "./utils";
145

156
const crawl = functions.https.onRequest(async (request, response) => {
167
const crawlingToken = extractSingleQueryParam(request, 'crawlingToken')

0 commit comments

Comments
 (0)