From 161d077ab929a34f6b5c298c1b9199165385e2e7 Mon Sep 17 00:00:00 2001
From: regalijan <r@regalijan.com>
Date: Thu, 19 Oct 2023 16:49:14 -0400
Subject: [PATCH] Add firestore helper functions

---
 functions/gcloud.ts | 175 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 172 insertions(+), 3 deletions(-)

diff --git a/functions/gcloud.ts b/functions/gcloud.ts
index 24913b6..e5d4c5b 100644
--- a/functions/gcloud.ts
+++ b/functions/gcloud.ts
@@ -79,9 +79,7 @@ async function GetAccessToken(env: Env): Promise<string> {
 
   const signingKey = await crypto.subtle.importKey(
     "pkcs8",
-    stringToBuffer(
-      atob(env.STORAGE_ACCOUNT_KEY.replace(/(\r\n|\n|\r)/gm, ""))
-    ),
+    stringToBuffer(atob(env.STORAGE_ACCOUNT_KEY.replace(/(\r\n|\n|\r)/gm, ""))),
     { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
     false,
     ["sign"]
@@ -107,3 +105,174 @@ async function GetAccessToken(env: Env): Promise<string> {
 
   return ((await tokenRequest.json()) as { [k: string]: any }).access_token;
 }
+
+async function getKeyIDs(
+  access_token: string,
+  projectId: string,
+  keys: { partitionId: { projectId: string }; path: { kind: string }[] }[]
+) {
+  const keyRequest = await fetch(
+    `https://datastore.googleapis.com/v1/projects/${projectId}:allocateIds`,
+    {
+      body: JSON.stringify({ keys }),
+      headers: {
+        authorization: `Bearer ${access_token}`,
+        "content-type": "application/json",
+      },
+      method: "POST",
+    }
+  );
+
+  if (!keyRequest.ok) {
+    console.log(await keyRequest.json());
+    throw new Error("Failed to allocate key IDs");
+  }
+
+  return ((await keyRequest.json()) as { keys: { [k: string]: any }[] }).keys;
+}
+
+export async function insertLogs(
+  userActionMap: { [k: string]: number },
+  reportId: string,
+  context: RequestContext
+) {
+  const accessToken = await GetAccessToken(context.env);
+  const actionBaseURLs: { [k: number]: string } = {
+    1: "https://portal.carcrushers.cc/view-closed-report/",
+    2: "https://portal.carcrushers.cc/view-closed-report/",
+    3: "",
+    4: "https://portal.carcrushers.cc/game-appeals/",
+  };
+  const actionIntegers: { [k: number]: string } = {
+    1: "blacklist",
+    2: "ban",
+    3: "revoke",
+    4: "accept_appeal",
+  };
+  const incompleteLogKey = {
+    partitionId: {
+      projectId: context.env.DATASTORE_PROJECT,
+    },
+    path: [
+      {
+        kind: "log",
+      },
+    ],
+  };
+  const payload: { mode: string; mutations: { [k: string]: any }[] } = {
+    mode: "NON_TRANSACTIONAL",
+    mutations: [],
+  };
+  const preAllocatedLogKeys = [];
+
+  while (preAllocatedLogKeys.length < Object.keys(userActionMap).length)
+    preAllocatedLogKeys.push(incompleteLogKey);
+
+  const keys = await getKeyIDs(
+    accessToken,
+    context.env.DATASTORE_PROJECT,
+    preAllocatedLogKeys
+  );
+
+  for (const [user, action] of Object.entries(userActionMap)) {
+    payload.mutations.push({
+      insert: {
+        key: keys.pop(),
+        properties: {
+          action: {
+            stringValue: actionIntegers[action],
+          },
+          evidence: {
+            stringValue: actionBaseURLs[action] + reportId,
+          },
+          executed_at: {
+            integerValue: Date.now(),
+          },
+          executor: {
+            stringValue: context.data.current_user.id,
+          },
+          target: {
+            integerValue: user,
+          },
+        },
+      },
+    });
+  }
+
+  const mutationRequest = await fetch(
+    `https://datastore.googleapis.com/v1/projects/${context.env.DATASTORE_PROJECT}:commit`,
+    {
+      body: JSON.stringify(payload),
+      headers: {
+        authorization: `Bearer ${accessToken}`,
+        "content-type": "application/json",
+      },
+      method: "POST",
+    }
+  );
+
+  if (!mutationRequest.ok) {
+    console.log(await mutationRequest.json());
+    throw new Error("Failed to commit mutation");
+  }
+
+  return await mutationRequest.json();
+}
+
+export async function queryLogs(user: number, context: RequestContext) {
+  const accessToken = await GetAccessToken(context.env);
+
+  const queryRequest = await fetch(
+    `https://datastore.googleapis.com/v1/projects/${context.env.DATASTORE_PROJECT}:runQuery`,
+    {
+      body: JSON.stringify({
+        partitionId: {
+          projectId: context.env.DATASTORE_PROJECT,
+        },
+        query: {
+          filter: {
+            propertyFilter: {
+              op: "EQUAL",
+              property: {
+                name: "target",
+              },
+              value: {
+                integerValue: user.toString(),
+              },
+            },
+          },
+          kind: [
+            {
+              name: "log",
+            },
+          ],
+        },
+      }),
+      headers: {
+        authorization: `Bearer ${accessToken}`,
+        "content-type": "application/json",
+      },
+      method: "POST",
+    }
+  );
+
+  if (!queryRequest.ok) {
+    console.log(await queryRequest.json());
+    throw new Error("Failed to query logs");
+  }
+
+  const entityResult =
+    (
+      (await queryRequest.json()) as {
+        batch: { entityResults: { [k: string]: { [k: string]: any } }[] };
+      }
+    ).batch?.entityResults ?? [];
+
+  if (!entityResult.length) return {};
+
+  const entity = entityResult[0].entity.properties;
+  delete entity.executor;
+  delete entity.target;
+
+  return entity;
+}