From 3e79c8cf28ed58a6c54712ead3638a6270d9e32b Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Thu, 23 May 2024 18:12:41 +0530 Subject: [PATCH 01/32] [mob][photos] Decrypt remote embeddings in computer --- .../file_ml/remote_fileml_service.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart b/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart index eafbc6323d..bb1c9f999b 100644 --- a/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart +++ b/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart @@ -1,6 +1,7 @@ import "dart:async"; import "dart:convert"; +import "package:computer/computer.dart"; import "package:logging/logging.dart"; import "package:photos/core/network/network.dart"; import "package:photos/db/files_db.dart"; @@ -16,6 +17,8 @@ import "package:shared_preferences/shared_preferences.dart"; class RemoteFileMLService { RemoteFileMLService._privateConstructor(); + static final Computer _computer = Computer.shared(); + static final RemoteFileMLService instance = RemoteFileMLService._privateConstructor(); @@ -107,15 +110,17 @@ class RemoteFileMLService { final input = EmbeddingsDecoderInput(embedding, fileKey); inputs.add(input); } - // todo: use compute or isolate - return decryptFileMLComputer( - { + return _computer.compute, Map>( + _decryptFileMLComputer, + param: { "inputs": inputs, }, ); } - Future> decryptFileMLComputer( +} + +Future> _decryptFileMLComputer( Map args, ) async { final result = {}; @@ -134,5 +139,4 @@ class RemoteFileMLService { result[input.embedding.fileID] = decodedEmbedding; } return result; - } -} + } \ No newline at end of file From ccac5e73a3eb11252d4a647b2e66a23c6e9d1eb1 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Thu, 23 May 2024 18:13:47 +0530 Subject: [PATCH 02/32] [mob][photos] Remove found faces from status --- .../settings/machine_learning_settings_page.dart | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/mobile/lib/ui/settings/machine_learning_settings_page.dart b/mobile/lib/ui/settings/machine_learning_settings_page.dart index 47e2166282..83bc159a53 100644 --- a/mobile/lib/ui/settings/machine_learning_settings_page.dart +++ b/mobile/lib/ui/settings/machine_learning_settings_page.dart @@ -479,7 +479,6 @@ class FaceRecognitionStatusWidgetState if (snapshot.hasData) { final int indexedFiles = snapshot.data!.$1; final int pendingFiles = snapshot.data!.$2; - final int foundFaces = snapshot.data!.$3; final double clusteringDoneRatio = snapshot.data!.$4; final double clusteringPercentage = (clusteringDoneRatio * 100).clamp(0, 100); @@ -512,19 +511,6 @@ class FaceRecognitionStatusWidgetState isGestureDetectorDisabled: true, key: ValueKey("pending_items_" + pendingFiles.toString()), ), - MenuItemWidget( - captionedTextWidget: CaptionedTextWidget( - title: S.of(context).foundFaces, - ), - trailingWidget: Text( - NumberFormat().format(foundFaces), - style: Theme.of(context).textTheme.bodySmall, - ), - singleBorderRadius: 8, - alignCaptionedTextToLeft: true, - isGestureDetectorDisabled: true, - key: ValueKey("found_faces_" + foundFaces.toString()), - ), MenuItemWidget( captionedTextWidget: CaptionedTextWidget( title: S.of(context).clusteringProgress, From d814b6cdf0ffccc4fc9390d740b88ccd8308d533 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 23 May 2024 21:01:18 +0530 Subject: [PATCH 03/32] Use standard URL parsing - WIP 1 --- web/apps/auth/src/services/code.ts | 65 +++++++++--------------------- 1 file changed, 20 insertions(+), 45 deletions(-) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index 064cc78743..09e1ec5673 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -1,5 +1,5 @@ +import { ensure } from "@/utils/ensure"; import { HOTP, TOTP } from "otpauth"; -import { URI } from "vscode-uri"; /** * A parsed representation of an *OTP code URI. @@ -32,7 +32,7 @@ export interface Code { /** The (HMAC) algorithm used by the OTP generator. */ algorithm: "sha1" | "sha256" | "sha512"; /** The original string from which this code was generated. */ - uriString?: string; + uriString: string; } /** @@ -47,50 +47,25 @@ export interface Code { * otpauth://totp/ACME:user@example.org?algorithm=SHA1&digits=6&issuer=acme&period=30&secret=ALPHANUM */ export const codeFromURIString = (id: string, uriString: string): Code => { - const santizedRawData = uriString - .replaceAll("+", "%2B") - .replaceAll(":", "%3A") - .replaceAll("\r", "") - // trim quotes - .replace(/^"|"$/g, ""); - - const uriParams = {}; - const searchParamsString = - decodeURIComponent(santizedRawData).split("?")[1]; - searchParamsString.split("&").forEach((pair) => { - const [key, value] = pair.split("="); - uriParams[key] = value; - }); - - const uri = URI.parse(santizedRawData); - let uriPath = decodeURIComponent(uri.path); - if (uriPath.startsWith("/otpauth://") || uriPath.startsWith("otpauth://")) { - uriPath = uriPath.split("otpauth://")[1]; - } else if (uriPath.startsWith("otpauth%3A//")) { - uriPath = uriPath.split("otpauth%3A//")[1]; - } + const url = new URL(uriString); return { id, - type: _getType(uriPath), + type: parseType(url), account: _getAccount(uriPath), issuer: _getIssuer(uriPath, uriParams), - digits: parseDigits(uriParams), - period: parsePeriod(uriParams), - secret: parseSecret(uriParams), - algorithm: parseAlgorithm(uriParams), + digits: parseDigits(url), + period: parsePeriod(url), + secret: parseSecret(url), + algorithm: parseAlgorithm(url), uriString, }; }; -const _getType = (uriPath: string): Code["type"] => { - const oauthType = uriPath.split("/")[0].substring(0); - if (oauthType.toLowerCase() === "totp") { - return "totp"; - } else if (oauthType.toLowerCase() === "hotp") { - return "hotp"; - } - throw new Error(`Unsupported format with host ${oauthType}`); +const parseType = (url: URL): Code["type"] => { + const t = url.host.toLowerCase(); + if (t == "totp" || t == "hotp") return t; + throw new Error(`Unsupported code with host ${t}`); }; const _getAccount = (uriPath: string): string => { @@ -131,14 +106,14 @@ const _getIssuer = (uriPath: string, uriParams: { get?: any }): string => { } }; -const parseDigits = (uriParams): number => - parseInt(uriParams["digits"] ?? "", 10) || 6; +const parseDigits = (url: URL): number => + parseInt(url.searchParams.get("digits") ?? "", 10) || 6; -const parsePeriod = (uriParams): number => - parseInt(uriParams["period"] ?? "", 10) || 30; +const parsePeriod = (url: URL): number => + parseInt(url.searchParams.get("period") ?? "", 10) || 30; -const parseAlgorithm = (uriParams): Code["algorithm"] => { - switch (uriParams["algorithm"]?.toLowerCase()) { +const parseAlgorithm = (url: URL): Code["algorithm"] => { + switch (url.searchParams.get("algorithm")?.toLowerCase()) { case "sha256": return "sha256"; case "sha512": @@ -148,8 +123,8 @@ const parseAlgorithm = (uriParams): Code["algorithm"] => { } }; -const parseSecret = (uriParams): string => - uriParams["secret"].replaceAll(" ", "").toUpperCase(); +const parseSecret = (url: URL): string => + ensure(url.searchParams.get("secret")).replaceAll(" ", "").toUpperCase(); /** * Generate a pair of OTPs (one time passwords) from the given {@link code}. From 0a01cac57baad9166bb21b30ef3e4e2fffb70553 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 09:27:28 +0530 Subject: [PATCH 04/32] Take 1 (incorrect) --- web/apps/auth/src/pages/auth.tsx | 15 ++++++--------- web/apps/auth/src/services/code.ts | 19 +++++-------------- web/apps/photos/package.json | 1 - web/yarn.lock | 5 ----- 4 files changed, 11 insertions(+), 29 deletions(-) diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index 4006cc9e19..5264897795 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -46,14 +46,11 @@ const AuthenticatorCodesPage = () => { appContext.showNavBar(false); }, []); + const lcSearch = searchTerm.toLowerCase(); const filteredCodes = codes.filter( - (secret) => - (secret.issuer ?? "") - .toLowerCase() - .includes(searchTerm.toLowerCase()) || - (secret.account ?? "") - .toLowerCase() - .includes(searchTerm.toLowerCase()), + (code) => + code.issuer?.toLowerCase().includes(lcSearch) || + code.account?.toLowerCase().includes(lcSearch), ); if (!hasFetched) { @@ -270,7 +267,7 @@ const OTPDisplay: React.FC = ({ code, otp, nextOTP }) => { textAlign: "left", }} > - {code.issuer} + {code.issuer ?? ""}

= ({ code, otp, nextOTP }) => { color: "grey", }} > - {code.account} + {code.account ?? ""}

{ return { id, type: parseType(url), - account: _getAccount(uriPath), + account: parseAccount(url), issuer: _getIssuer(uriPath, uriParams), digits: parseDigits(url), period: parsePeriod(url), @@ -68,18 +68,9 @@ const parseType = (url: URL): Code["type"] => { throw new Error(`Unsupported code with host ${t}`); }; -const _getAccount = (uriPath: string): string => { - try { - const path = decodeURIComponent(uriPath); - if (path.includes(":")) { - return path.split(":")[1]; - } else if (path.includes("/")) { - return path.split("/")[1]; - } - } catch (e) { - return ""; - } -}; +/** Convert the pathname from "/ACME:user@example.org" => "user@example.org" */ +const parseAccount = (url: URL): string => + url.pathname.split(":").at(-1).split("/").at(-1); const _getIssuer = (uriPath: string, uriParams: { get?: any }): string => { try { diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index 1541878c51..0ec924b29b 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -43,7 +43,6 @@ "similarity-transformation": "^0.0.1", "transformation-matrix": "^2.16", "uuid": "^9.0.1", - "vscode-uri": "^3.0.7", "xml-js": "^1.6.11", "zxcvbn": "^4.4.2" }, diff --git a/web/yarn.lock b/web/yarn.lock index 894a44dd02..aaa0d517a8 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -4804,11 +4804,6 @@ void-elements@3.1.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== -vscode-uri@^3.0.7: - version "3.0.8" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" - integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From bfe8fd83acfb7974cf39f827afd2ebee8d9eb255 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 09:29:54 +0530 Subject: [PATCH 05/32] Take 2 --- web/apps/auth/src/services/code.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index ff29c58fdd..0acc537f0c 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -69,8 +69,12 @@ const parseType = (url: URL): Code["type"] => { }; /** Convert the pathname from "/ACME:user@example.org" => "user@example.org" */ -const parseAccount = (url: URL): string => - url.pathname.split(":").at(-1).split("/").at(-1); +const parseAccount = (url: URL): string => { + let p = url.pathname; + if (p.startsWith("/")) p = p.slice(1); + if (p.includes(":")) p = p.split(":").slice(1).join(":"); + return p; +}; const _getIssuer = (uriPath: string, uriParams: { get?: any }): string => { try { From 623b71715d08e8428a19edec97b1304981712e4e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 09:42:23 +0530 Subject: [PATCH 06/32] Wrap --- web/apps/auth/src/services/code.ts | 45 ++++++++++++++---------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index 0acc537f0c..4eb964d31d 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -53,7 +53,7 @@ export const codeFromURIString = (id: string, uriString: string): Code => { id, type: parseType(url), account: parseAccount(url), - issuer: _getIssuer(uriPath, uriParams), + issuer: parseIssuer(url), digits: parseDigits(url), period: parsePeriod(url), secret: parseSecret(url), @@ -68,37 +68,34 @@ const parseType = (url: URL): Code["type"] => { throw new Error(`Unsupported code with host ${t}`); }; -/** Convert the pathname from "/ACME:user@example.org" => "user@example.org" */ -const parseAccount = (url: URL): string => { +const parseAccount = (url: URL): string | undefined => { + // "/ACME:user@example.org" => "user@example.org" let p = url.pathname; if (p.startsWith("/")) p = p.slice(1); if (p.includes(":")) p = p.split(":").slice(1).join(":"); return p; }; -const _getIssuer = (uriPath: string, uriParams: { get?: any }): string => { - try { - if (uriParams["issuer"] !== undefined) { - let issuer = uriParams["issuer"]; - // This is to handle bug in the ente auth app - if (issuer.endsWith("period")) { - issuer = issuer.substring(0, issuer.length - 6); - } - return issuer; +const parseIssuer = (url: URL): string => { + // If there is a "issuer" search param, use that. + let issuer = url.searchParams.get("issuer"); + if (issuer !== undefined) { + // This is to handle bug in old versions of Ente Auth app. + if (issuer.endsWith("period")) { + issuer = issuer.substring(0, issuer.length - 6); } - let path = decodeURIComponent(uriPath); - if (path.startsWith("totp/") || path.startsWith("hotp/")) { - path = path.substring(5); - } - if (path.includes(":")) { - return path.split(":")[0]; - } else if (path.includes("-")) { - return path.split("-")[0]; - } - return path; - } catch (e) { - return ""; + return issuer; } + + // Otherwise use the `prefix:` from the account as the issuer. + // "/ACME:user@example.org" => "ACME" + let p = url.pathname; + if (p.startsWith("/")) p = p.slice(1); + + if (p.includes(":")) p = p.split(":")[0]; + else if (p.includes("-")) p = p.split("-")[0]; + + return p; }; const parseDigits = (url: URL): number => From 59ed89cba172fdb668502d4d79345710e1e67225 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 09:49:20 +0530 Subject: [PATCH 07/32] .get returns null when the property is not present --- web/apps/auth/src/services/code.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index 4eb964d31d..835bb689dd 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -79,7 +79,7 @@ const parseAccount = (url: URL): string | undefined => { const parseIssuer = (url: URL): string => { // If there is a "issuer" search param, use that. let issuer = url.searchParams.get("issuer"); - if (issuer !== undefined) { + if (issuer) { // This is to handle bug in old versions of Ente Auth app. if (issuer.endsWith("period")) { issuer = issuer.substring(0, issuer.length - 6); From 4fa59ce2589de9b7c3df11734ec647039e8f1d0f Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 24 May 2024 09:56:10 +0530 Subject: [PATCH 08/32] [mob][photos] Common ml util for getting indexable files across faces and clip --- .../services/machine_learning/face_ml/face_ml_service.dart | 7 +------ .../semantic_search/semantic_search_service.dart | 7 +++---- mobile/lib/ui/settings/machine_learning_settings_page.dart | 3 ++- mobile/lib/utils/ml_util.dart | 7 +++++++ 4 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 mobile/lib/utils/ml_util.dart diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart index 38079753c2..9bfa54f1ea 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart @@ -12,7 +12,6 @@ import "package:flutter/foundation.dart" show debugPrint, kDebugMode; import "package:logging/logging.dart"; import "package:onnxruntime/onnxruntime.dart"; import "package:package_info_plus/package_info_plus.dart"; -import "package:photos/core/configuration.dart"; import "package:photos/core/event_bus.dart"; import "package:photos/db/files_db.dart"; import "package:photos/events/diff_sync_complete_event.dart"; @@ -48,6 +47,7 @@ import "package:photos/utils/file_util.dart"; import 'package:photos/utils/image_ml_isolate.dart'; import "package:photos/utils/image_ml_util.dart"; import "package:photos/utils/local_settings.dart"; +import "package:photos/utils/ml_util.dart"; import "package:photos/utils/network_util.dart"; import "package:photos/utils/thumbnail_util.dart"; import "package:synchronized/synchronized.dart"; @@ -1184,11 +1184,6 @@ class FaceMlService { return ratio; } - static Future> getIndexableFileIDs() async { - return FilesDB.instance - .getOwnedFileIDs(Configuration.instance.getUserID()!); - } - bool _skipAnalysisEnteFile(EnteFile enteFile, Map indexedFileIds) { if (_isIndexingOrClusteringRunning == false || _mlControllerStatus == false) { diff --git a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart index db1713c2c3..1384750811 100644 --- a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart +++ b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart @@ -23,6 +23,7 @@ import 'package:photos/services/machine_learning/semantic_search/frameworks/onnx import "package:photos/utils/debouncer.dart"; import "package:photos/utils/device_info.dart"; import "package:photos/utils/local_settings.dart"; +import "package:photos/utils/ml_util.dart"; import "package:photos/utils/thumbnail_util.dart"; class SemanticSearchService { @@ -160,8 +161,7 @@ class SemanticSearchService { } Future getIndexStatus() async { - final indexableFileIDs = await FilesDB.instance - .getOwnedFileIDs(Configuration.instance.getUserID()!); + final indexableFileIDs = await getIndexableFileIDs(); return IndexStatus( min(_cachedEmbeddings.length, indexableFileIDs.length), (await _getFileIDsToBeIndexed()).length, @@ -222,8 +222,7 @@ class SemanticSearchService { } Future> _getFileIDsToBeIndexed() async { - final uploadedFileIDs = await FilesDB.instance - .getOwnedFileIDs(Configuration.instance.getUserID()!); + final uploadedFileIDs = await getIndexableFileIDs(); final embeddedFileIDs = await EmbeddingsDB.instance.getFileIDs(_currentModel); diff --git a/mobile/lib/ui/settings/machine_learning_settings_page.dart b/mobile/lib/ui/settings/machine_learning_settings_page.dart index 83bc159a53..cf546015ce 100644 --- a/mobile/lib/ui/settings/machine_learning_settings_page.dart +++ b/mobile/lib/ui/settings/machine_learning_settings_page.dart @@ -26,6 +26,7 @@ import "package:photos/ui/components/title_bar_widget.dart"; import "package:photos/ui/components/toggle_switch_widget.dart"; import "package:photos/utils/data_util.dart"; import "package:photos/utils/local_settings.dart"; +import "package:photos/utils/ml_util.dart"; final _logger = Logger("MachineLearningSettingsPage"); @@ -442,7 +443,7 @@ class FaceRecognitionStatusWidgetState try { final indexedFiles = await FaceMLDataDB.instance .getIndexedFileCount(minimumMlVersion: faceMlVersion); - final indexableFiles = (await FaceMlService.getIndexableFileIDs()).length; + final indexableFiles = (await getIndexableFileIDs()).length; final showIndexedFiles = min(indexedFiles, indexableFiles); final pendingFiles = max(indexableFiles - indexedFiles, 0); final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount(); diff --git a/mobile/lib/utils/ml_util.dart b/mobile/lib/utils/ml_util.dart new file mode 100644 index 0000000000..4033e29349 --- /dev/null +++ b/mobile/lib/utils/ml_util.dart @@ -0,0 +1,7 @@ +import "package:photos/core/configuration.dart"; +import "package:photos/db/files_db.dart"; + +Future> getIndexableFileIDs() async { + return FilesDB.instance + .getOwnedFileIDs(Configuration.instance.getUserID()!); + } \ No newline at end of file From 2ce921245778a031d8a817193d6b20d32474c67c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 09:57:16 +0530 Subject: [PATCH 09/32] We encodeURIComponent the pathname --- web/apps/auth/src/services/code.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index 835bb689dd..a43a3d4730 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -45,6 +45,8 @@ export interface Code { * * - (TOTP) * otpauth://totp/ACME:user@example.org?algorithm=SHA1&digits=6&issuer=acme&period=30&secret=ALPHANUM + * + * See also `auth/test/models/code_test.dart`. */ export const codeFromURIString = (id: string, uriString: string): Code => { const url = new URL(uriString); @@ -70,7 +72,7 @@ const parseType = (url: URL): Code["type"] => { const parseAccount = (url: URL): string | undefined => { // "/ACME:user@example.org" => "user@example.org" - let p = url.pathname; + let p = decodeURIComponent(url.pathname); if (p.startsWith("/")) p = p.slice(1); if (p.includes(":")) p = p.split(":").slice(1).join(":"); return p; @@ -89,7 +91,7 @@ const parseIssuer = (url: URL): string => { // Otherwise use the `prefix:` from the account as the issuer. // "/ACME:user@example.org" => "ACME" - let p = url.pathname; + let p = decodeURIComponent(url.pathname); if (p.startsWith("/")) p = p.slice(1); if (p.includes(":")) p = p.split(":")[0]; From eaf8b9cebc770eee44d418cb7aa7a61124ae82af Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 10:10:59 +0530 Subject: [PATCH 10/32] Also include same workaround as mobile app --- web/apps/auth/src/services/code.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index a43a3d4730..a04409ceea 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -49,6 +49,19 @@ export interface Code { * See also `auth/test/models/code_test.dart`. */ export const codeFromURIString = (id: string, uriString: string): Code => { + try { + return _codeFromURIString(id, uriString); + } catch (e) { + // We might have legacy encodings of account names that contain a "#", + // which causes the rest of the URL to be treated as a fragment, and + // ignored. See if this was potentially such a case, otherwise rethrow. + if (uriString.includes("#")) + return _codeFromURIString(id, uriString.replaceAll("#", "%23")); + throw e; + } +}; + +const _codeFromURIString = (id: string, uriString: string): Code => { const url = new URL(uriString); return { From c3fb47228768a02fcb821d04e7e4eed5d9112240 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 24 May 2024 10:18:17 +0530 Subject: [PATCH 11/32] [mob][photos] Fix clustering progress number --- mobile/lib/face/db.dart | 29 +++++++++++++++---- .../debug/face_debug_section_widget.dart | 2 +- .../machine_learning_settings_page.dart | 11 +++---- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart index c72b197b46..626a25114d 100644 --- a/mobile/lib/face/db.dart +++ b/mobile/lib/face/db.dart @@ -13,6 +13,8 @@ import "package:photos/face/model/face.dart"; import "package:photos/models/file/file.dart"; import "package:photos/services/machine_learning/face_ml/face_clustering/face_info_for_clustering.dart"; import 'package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart'; +import "package:photos/services/machine_learning/face_ml/face_ml_result.dart"; +import "package:photos/utils/ml_util.dart"; import 'package:sqlite_async/sqlite_async.dart'; /// Stores all data for the FacesML-related features. The database can be accessed by `FaceMLDataDB.instance.database`. @@ -401,8 +403,10 @@ class FaceMLDataDB { final personID = map[personIdColumn] as String; final clusterID = map[fcClusterID] as int; final faceID = map[fcFaceId] as String; - result.putIfAbsent(personID, () => {}).putIfAbsent(clusterID, () => {}) - .add(faceID); + result + .putIfAbsent(personID, () => {}) + .putIfAbsent(clusterID, () => {}) + .add(faceID); } return result; } @@ -673,11 +677,24 @@ class FaceMLDataDB { return maps.first['count'] as int; } - Future getClusteredToTotalFacesRatio() async { - final int totalFaces = await getTotalFaceCount(); - final int clusteredFaces = await getClusteredFaceCount(); + Future getClusteredFileCount() async { + final db = await instance.asyncDB; + final List> maps = await db.getAll( + 'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable', + ); + final Set fileIDs = {}; + for (final map in maps) { + final int fileID = getFileIdFromFaceId(map[fcFaceId] as String); + fileIDs.add(fileID); + } + return fileIDs.length; + } - return clusteredFaces / totalFaces; + Future getClusteredToIndexableFilesRatio() async { + final int indexableFiles = (await getIndexableFileIDs()).length; + final int clusteredFiles = await getClusteredFileCount(); + + return clusteredFiles / indexableFiles; } Future getBlurryFaceCount([ diff --git a/mobile/lib/ui/settings/debug/face_debug_section_widget.dart b/mobile/lib/ui/settings/debug/face_debug_section_widget.dart index 726a9f2ceb..376793769f 100644 --- a/mobile/lib/ui/settings/debug/face_debug_section_widget.dart +++ b/mobile/lib/ui/settings/debug/face_debug_section_widget.dart @@ -177,7 +177,7 @@ class _FaceDebugSectionWidgetState extends State { sectionOptionSpacing, MenuItemWidget( captionedTextWidget: FutureBuilder( - future: FaceMLDataDB.instance.getClusteredToTotalFacesRatio(), + future: FaceMLDataDB.instance.getClusteredToIndexableFilesRatio(), builder: (context, snapshot) { if (snapshot.hasData) { return CaptionedTextWidget( diff --git a/mobile/lib/ui/settings/machine_learning_settings_page.dart b/mobile/lib/ui/settings/machine_learning_settings_page.dart index cf546015ce..4e9178a57d 100644 --- a/mobile/lib/ui/settings/machine_learning_settings_page.dart +++ b/mobile/lib/ui/settings/machine_learning_settings_page.dart @@ -439,19 +439,16 @@ class FaceRecognitionStatusWidgetState }); } - Future<(int, int, int, double)> getIndexStatus() async { + Future<(int, int, double)> getIndexStatus() async { try { final indexedFiles = await FaceMLDataDB.instance .getIndexedFileCount(minimumMlVersion: faceMlVersion); final indexableFiles = (await getIndexableFileIDs()).length; final showIndexedFiles = min(indexedFiles, indexableFiles); final pendingFiles = max(indexableFiles - indexedFiles, 0); - final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount(); - final clusteredFaces = - await FaceMLDataDB.instance.getClusteredFaceCount(); - final clusteringDoneRatio = clusteredFaces / foundFaces; + final clusteringDoneRatio = await FaceMLDataDB.instance.getClusteredToIndexableFilesRatio(); - return (showIndexedFiles, pendingFiles, foundFaces, clusteringDoneRatio); + return (showIndexedFiles, pendingFiles, clusteringDoneRatio); } catch (e, s) { _logger.severe('Error getting face recognition status', e, s); rethrow; @@ -480,7 +477,7 @@ class FaceRecognitionStatusWidgetState if (snapshot.hasData) { final int indexedFiles = snapshot.data!.$1; final int pendingFiles = snapshot.data!.$2; - final double clusteringDoneRatio = snapshot.data!.$4; + final double clusteringDoneRatio = snapshot.data!.$3; final double clusteringPercentage = (clusteringDoneRatio * 100).clamp(0, 100); From 86f96a571326e1a39d5da6e748a774d0fb0c5553 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 24 May 2024 10:19:24 +0530 Subject: [PATCH 12/32] [mob][photos] Show intermediate clustering results --- mobile/lib/face/db.dart | 8 -------- mobile/lib/services/search_service.dart | 9 --------- 2 files changed, 17 deletions(-) diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart index 626a25114d..17055234bd 100644 --- a/mobile/lib/face/db.dart +++ b/mobile/lib/face/db.dart @@ -669,14 +669,6 @@ class FaceMLDataDB { return maps.first['count'] as int; } - Future getClusteredFaceCount() async { - final db = await instance.asyncDB; - final List> maps = await db.getAll( - 'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable', - ); - return maps.first['count'] as int; - } - Future getClusteredFileCount() async { final db = await instance.asyncDB; final List> maps = await db.getAll( diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 1ff73dbc89..d15eddb718 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -754,15 +754,6 @@ class SearchService { Future> getAllFace(int? limit) async { try { - // Don't return anything if clustering is not nearly complete yet - final foundFaces = await FaceMLDataDB.instance.getTotalFaceCount(); - final clusteredFaces = - await FaceMLDataDB.instance.getClusteredFaceCount(); - final clusteringDoneRatio = clusteredFaces / foundFaces; - if (clusteringDoneRatio < 0.9) { - return []; - } - debugPrint("getting faces"); final Map> fileIdToClusterID = await FaceMLDataDB.instance.getFileIdToClusterIds(); From fec040e5285b843cad9a28bcf8ff1ec87e37dfe9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 10:19:53 +0530 Subject: [PATCH 13/32] Tweak error report --- web/apps/auth/src/services/code.ts | 2 +- web/apps/auth/src/services/remote.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index a04409ceea..901411dd30 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -80,7 +80,7 @@ const _codeFromURIString = (id: string, uriString: string): Code => { const parseType = (url: URL): Code["type"] => { const t = url.host.toLowerCase(); if (t == "totp" || t == "hotp") return t; - throw new Error(`Unsupported code with host ${t}`); + throw new Error(`Unsupported code with host "${t}"`); }; const parseAccount = (url: URL): string | undefined => { diff --git a/web/apps/auth/src/services/remote.ts b/web/apps/auth/src/services/remote.ts index 07b15d7d71..11d57aa23b 100644 --- a/web/apps/auth/src/services/remote.ts +++ b/web/apps/auth/src/services/remote.ts @@ -35,7 +35,7 @@ export const getAuthCodes = async (): Promise => { ); return codeFromURIString(entity.id, decryptedCode); } catch (e) { - log.error(`failed to parse codeId = ${entity.id}`); + log.error(`Failed to parse codeID ${entity.id}`, e); return null; } }), From edf9f743f44a9b7483e735fa4a5b042228e8993b Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 24 May 2024 10:27:16 +0530 Subject: [PATCH 14/32] [mob][photos] Prefer using getFileIdFromFaceId --- mobile/lib/face/db.dart | 15 +++++++-------- .../machine_learning/face_ml/face_ml_result.dart | 6 +++++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart index 17055234bd..02a49ff4a8 100644 --- a/mobile/lib/face/db.dart +++ b/mobile/lib/face/db.dart @@ -251,7 +251,7 @@ class FaceMLDataDB { final List fileId = [recentFileID]; int? avatarFileId; if (avatarFaceId != null) { - avatarFileId = int.tryParse(avatarFaceId.split('_')[0]); + avatarFileId = tryGetFileIdFromFaceId(avatarFaceId); if (avatarFileId != null) { fileId.add(avatarFileId); } @@ -480,8 +480,7 @@ class FaceMLDataDB { for (final map in maps) { final clusterID = map[fcClusterID] as int; final faceID = map[fcFaceId] as String; - final x = faceID.split('_').first; - final fileID = int.parse(x); + final fileID = getFileIdFromFaceId(faceID); result[fileID] = (result[fileID] ?? {})..add(clusterID); } return result; @@ -804,7 +803,7 @@ class FaceMLDataDB { for (final map in maps) { final clusterID = map[clusterIDColumn] as int; final String faceID = map[fcFaceId] as String; - final fileID = int.parse(faceID.split('_').first); + final fileID = getFileIdFromFaceId(faceID); result[fileID] = (result[fileID] ?? {})..add(clusterID); } return result; @@ -823,8 +822,8 @@ class FaceMLDataDB { final Map> result = {}; for (final map in maps) { final clusterID = map[fcClusterID] as int; - final faceId = map[fcFaceId] as String; - final fileID = int.parse(faceId.split("_").first); + final faceID = map[fcFaceId] as String; + final fileID = getFileIdFromFaceId(faceID); result[fileID] = (result[fileID] ?? {})..add(clusterID); } return result; @@ -973,7 +972,7 @@ class FaceMLDataDB { final Map faceIDToClusterID = {}; for (final row in faceIdsResult) { final faceID = row[fcFaceId] as String; - if (fileIds.contains(faceID.split('_').first)) { + if (fileIds.contains(getFileIdFromFaceId(faceID))) { maxClusterID += 1; faceIDToClusterID[faceID] = maxClusterID; } @@ -999,7 +998,7 @@ class FaceMLDataDB { final Map faceIDToClusterID = {}; for (final row in faceIdsResult) { final faceID = row[fcFaceId] as String; - if (fileIds.contains(faceID.split('_').first)) { + if (fileIds.contains(getFileIdFromFaceId(faceID))) { maxClusterID += 1; faceIDToClusterID[faceID] = maxClusterID; } diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart index 19f954013e..9f87b87220 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_result.dart @@ -310,5 +310,9 @@ class FaceResultBuilder { } int getFileIdFromFaceId(String faceId) { - return int.parse(faceId.split("_")[0]); + return int.parse(faceId.split("_").first); } + +int? tryGetFileIdFromFaceId(String faceId) { + return int.tryParse(faceId.split("_").first); +} \ No newline at end of file From dc38a8bc9f851d6c7a9db3a14d36ccc7b93d211c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 10:51:19 +0530 Subject: [PATCH 15/32] Account for node/browser discrepancy --- web/apps/auth/src/services/code.ts | 39 +++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index 901411dd30..c0be011ea3 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -64,11 +64,27 @@ export const codeFromURIString = (id: string, uriString: string): Code => { const _codeFromURIString = (id: string, uriString: string): Code => { const url = new URL(uriString); + // A URL like + // + // new URL("otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0") + // + // is parsed differently by the browser and Node depending on the scheme. + // When the scheme is http(s), then both of them consider "hotp" as the + // `host`. However, when the scheme is "otpauth", as is our case, the + // browser considers the entire thing as part of the pathname. so we get. + // + // host: "" + // pathname: "//hotp/Test" + // + // Since this code run on browsers only, we parse as per that behaviour. + + const [type, path] = parsePathname(url); + return { id, - type: parseType(url), - account: parseAccount(url), - issuer: parseIssuer(url), + type, + account: parseAccount(path), + issuer: parseIssuer(url, path), digits: parseDigits(url), period: parsePeriod(url), secret: parseSecret(url), @@ -77,21 +93,22 @@ const _codeFromURIString = (id: string, uriString: string): Code => { }; }; -const parseType = (url: URL): Code["type"] => { - const t = url.host.toLowerCase(); - if (t == "totp" || t == "hotp") return t; - throw new Error(`Unsupported code with host "${t}"`); +const parsePathname = (url: URL): [type: Code["type"], path: string] => { + const p = url.pathname.toLowerCase(); + if (p.startsWith("//totp")) return ["totp", url.pathname.slice(6)]; + if (p.startsWith("//hotp")) return ["hotp", url.pathname.slice(6)]; + throw new Error(`Unsupported code or unparseable path "${url.pathname}"`); }; -const parseAccount = (url: URL): string | undefined => { +const parseAccount = (path: string): string | undefined => { // "/ACME:user@example.org" => "user@example.org" - let p = decodeURIComponent(url.pathname); + let p = decodeURIComponent(path); if (p.startsWith("/")) p = p.slice(1); if (p.includes(":")) p = p.split(":").slice(1).join(":"); return p; }; -const parseIssuer = (url: URL): string => { +const parseIssuer = (url: URL, path: string): string => { // If there is a "issuer" search param, use that. let issuer = url.searchParams.get("issuer"); if (issuer) { @@ -104,7 +121,7 @@ const parseIssuer = (url: URL): string => { // Otherwise use the `prefix:` from the account as the issuer. // "/ACME:user@example.org" => "ACME" - let p = decodeURIComponent(url.pathname); + let p = decodeURIComponent(path); if (p.startsWith("/")) p = p.slice(1); if (p.includes(":")) p = p.split(":")[0]; From f1d1a4a9e1cce35eef2857ccfdd9fe004ab6bda9 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 24 May 2024 10:57:27 +0530 Subject: [PATCH 16/32] [mob][photos] Clustering sort to cluster new files first --- .../face_clustering_service.dart | 56 +++++++------------ .../face_ml/face_ml_service.dart | 4 +- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart b/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart index 1b8d9c3bd5..1a635b0f07 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart @@ -498,19 +498,8 @@ class FaceClusteringService { } } - // Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first if (fileIDToCreationTime != null) { - faceInfos.sort((a, b) { - if (a.fileCreationTime == null && b.fileCreationTime == null) { - return 0; - } else if (a.fileCreationTime == null) { - return 1; - } else if (b.fileCreationTime == null) { - return -1; - } else { - return a.fileCreationTime!.compareTo(b.fileCreationTime!); - } - }); + _sortFaceInfosOnCreationTime(faceInfos); } // Sort the faceInfos such that the ones with null clusterId are at the end @@ -796,19 +785,8 @@ class FaceClusteringService { ); } - // Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first if (fileIDToCreationTime != null) { - faceInfos.sort((a, b) { - if (a.fileCreationTime == null && b.fileCreationTime == null) { - return 0; - } else if (a.fileCreationTime == null) { - return 1; - } else if (b.fileCreationTime == null) { - return -1; - } else { - return a.fileCreationTime!.compareTo(b.fileCreationTime!); - } - }); + _sortFaceInfosOnCreationTime(faceInfos); } if (faceInfos.isEmpty) { @@ -996,19 +974,8 @@ class FaceClusteringService { ); } - // Sort the faceInfos based on fileCreationTime, in ascending order, so oldest faces are first if (fileIDToCreationTime != null) { - faceInfos.sort((a, b) { - if (a.fileCreationTime == null && b.fileCreationTime == null) { - return 0; - } else if (a.fileCreationTime == null) { - return 1; - } else if (b.fileCreationTime == null) { - return -1; - } else { - return a.fileCreationTime!.compareTo(b.fileCreationTime!); - } - }); + _sortFaceInfosOnCreationTime(faceInfos); } // Get the embeddings @@ -1027,3 +994,20 @@ class FaceClusteringService { return clusteredFaceIDs; } } + +/// Sort the faceInfos based on fileCreationTime, in descending order, so newest faces are first +void _sortFaceInfosOnCreationTime( + List faceInfos, +) { + faceInfos.sort((b, a) { + if (a.fileCreationTime == null && b.fileCreationTime == null) { + return 0; + } else if (a.fileCreationTime == null) { + return 1; + } else if (b.fileCreationTime == null) { + return -1; + } else { + return a.fileCreationTime!.compareTo(b.fileCreationTime!); + } + }); +} diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart index 9bfa54f1ea..47464b8e9f 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart @@ -590,8 +590,8 @@ class FaceMlService { allFaceInfoForClustering.add(faceInfo); } } - // sort the embeddings based on file creation time, oldest first - allFaceInfoForClustering.sort((a, b) { + // sort the embeddings based on file creation time, newest first + allFaceInfoForClustering.sort((b, a) { return fileIDToCreationTime[a.fileID]! .compareTo(fileIDToCreationTime[b.fileID]!); }); From 5587373b422a6454fdf520b0e15102adc17c6ae3 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 24 May 2024 11:00:05 +0530 Subject: [PATCH 17/32] [mob][photos] Remove clustering restriction based on indexed amount --- .../face_ml/face_ml_service.dart | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart index 47464b8e9f..932c7e39e1 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart @@ -47,7 +47,6 @@ import "package:photos/utils/file_util.dart"; import 'package:photos/utils/image_ml_isolate.dart'; import "package:photos/utils/image_ml_util.dart"; import "package:photos/utils/local_settings.dart"; -import "package:photos/utils/ml_util.dart"; import "package:photos/utils/network_util.dart"; import "package:photos/utils/thumbnail_util.dart"; import "package:synchronized/synchronized.dart"; @@ -359,15 +358,7 @@ class FaceMlService { await sync(forceSync: _shouldSyncPeople); await indexAllImages(); - final indexingCompleteRatio = await _getIndexedDoneRatio(); - if (indexingCompleteRatio < 0.95) { - _logger.info( - "Indexing is not far enough to start clustering, skipping clustering. Indexing is at $indexingCompleteRatio", - ); - return; - } else { - await clusterAllImages(); - } + await clusterAllImages(); } void pauseIndexingAndClustering() { @@ -1171,19 +1162,6 @@ class FaceMlService { } } - Future _getIndexedDoneRatio() async { - final w = (kDebugMode ? EnteWatch('_getIndexedDoneRatio') : null)?..start(); - - final int alreadyIndexedCount = await FaceMLDataDB.instance - .getIndexedFileCount(minimumMlVersion: faceMlVersion); - final int totalIndexableCount = (await getIndexableFileIDs()).length; - final ratio = alreadyIndexedCount / totalIndexableCount; - - w?.log('getIndexedDoneRatio'); - - return ratio; - } - bool _skipAnalysisEnteFile(EnteFile enteFile, Map indexedFileIds) { if (_isIndexingOrClusteringRunning == false || _mlControllerStatus == false) { From cc91cb8012a38df370807f81a3fbb90df39d229a Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 24 May 2024 11:16:40 +0530 Subject: [PATCH 18/32] [mob][photos] Correct mistake --- mobile/lib/face/db.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart index 02a49ff4a8..e9e6079204 100644 --- a/mobile/lib/face/db.dart +++ b/mobile/lib/face/db.dart @@ -671,7 +671,7 @@ class FaceMLDataDB { Future getClusteredFileCount() async { final db = await instance.asyncDB; final List> maps = await db.getAll( - 'SELECT COUNT(DISTINCT $fcFaceId) as count FROM $faceClustersTable', + 'SELECT $fcFaceId FROM $faceClustersTable', ); final Set fileIDs = {}; for (final map in maps) { From 697946f4154977ad16b053cca774e702ac30d620 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 12:12:06 +0530 Subject: [PATCH 19/32] Scaffold --- web/apps/auth/src/services/code.ts | 40 +++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index c0be011ea3..b02e2314b8 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -171,7 +171,7 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => { }); otp = totp.generate(); nextOTP = totp.generate({ - timestamp: new Date().getTime() + code.period * 1000, + timestamp: Date.now() + code.period * 1000, }); break; } @@ -186,6 +186,44 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => { nextOTP = hotp.generate({ counter: 1 }); break; } + + case "steam": { + const steam = new Steam({ + secret: code.secret, + }); + otp = steam.generate(); + nextOTP = steam.generate({ + timestamp: Date.now() + code.period * 1000, + }); + break; + } } return [otp, nextOTP]; }; + +/** + * Steam OTPs. + * + * Steam's algorithm is a custom variant of TOTP that uses a 26-character + * alphabet instead of digits. + * + * A Dart implementation of the algorithm can be found in + * https://github.com/elliotwutingfeng/steam_totp/blob/main/lib/src/steam_totp_base.dart + * (MIT license), and we use that as a reference. Our implementation is written + * in the style of the other TOTP/HOTP classes that are provided by the otpauth + * JS library that we use for the normal TOTP/HOTP generation + * https://github.com/hectorm/otpauth/blob/master/src/hotp.js (MIT license). + */ +class Steam { + secret: string; + period: number; + + constructor({ secret }: { secret: string }) { + this.secret = secret; + this.period = 30; + } + + generate({ timestamp = Date.now() }: { timestamp: number }) { + return `${timestamp}`; + } +} From 1ce90839fe8e43a4f4d931126881aec604e8e65b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 12:18:28 +0530 Subject: [PATCH 20/32] Remove type from auth UI --- web/apps/auth/src/pages/auth.tsx | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index 5264897795..d750c5f7c7 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -187,28 +187,21 @@ const CodeDisplay: React.FC = ({ code }) => { useEffect(() => { // Generate to set the initial otp and nextOTP on component mount. regen(); - const codeType = code.type; - const codePeriodInMs = code.period * 1000; - const timeToNextCode = - codePeriodInMs - (new Date().getTime() % codePeriodInMs); - const interval = null; + + const periodMs = code.period * 1000; + const timeToNextCode = periodMs - (Date.now() % periodMs); + + let interval: ReturnType | undefined; // Wait until we are at the start of the next code period, and then // start the interval loop. setTimeout(() => { // We need to call regen() once before the interval loop to set the // initial otp and nextOTP. regen(); - codeType.toLowerCase() === "totp" || - codeType.toLowerCase() === "hotp" - ? setInterval(() => { - regen(); - }, codePeriodInMs) - : null; + interval = setInterval(() => regen, periodMs); }, timeToNextCode); - return () => { - if (interval) clearInterval(interval); - }; + return () => interval && clearInterval(interval); }, [code]); return ( @@ -346,7 +339,7 @@ const TimerProgress: React.FC = ({ period }) => { useEffect(() => { const advance = () => { - const timeRemaining = us - ((new Date().getTime() * 1000) % us); + const timeRemaining = us - ((Date.now() * 1000) % us); setProgress(timeRemaining / us); }; From 0fdb58eda19095d70fa010a429ba9c0a062a41e8 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 24 May 2024 12:30:22 +0530 Subject: [PATCH 21/32] [mob][photos] Force clustering first if too many unclustered faces --- mobile/lib/face/db.dart | 14 ++++++++++++++ .../machine_learning/face_ml/face_ml_service.dart | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart index e9e6079204..9b5f42f540 100644 --- a/mobile/lib/face/db.dart +++ b/mobile/lib/face/db.dart @@ -688,6 +688,20 @@ class FaceMLDataDB { return clusteredFiles / indexableFiles; } + Future getUnclusteredFaceCount() async { + final db = await instance.asyncDB; + const String query = ''' + SELECT f.$faceIDColumn + FROM $facesTable f + LEFT JOIN $faceClustersTable fc ON f.$faceIDColumn = fc.$fcFaceId + WHERE f.$faceScore > $kMinimumQualityFaceScore + AND f.$faceBlur > $kLaplacianHardThreshold + AND fc.$fcFaceId IS NULL + '''; + final List> maps = await db.getAll(query); + return maps.length; + } + Future getBlurryFaceCount([ int blurThreshold = kLaplacianHardThreshold, ]) async { diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart index 932c7e39e1..a6b38f8e3b 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart @@ -98,6 +98,7 @@ class FaceMlService { final int _fileDownloadLimit = 5; final int _embeddingFetchLimit = 200; + final int _kForceClusteringFaceCount = 4000; Future init({bool initializeImageMlIsolate = false}) async { if (LocalSettings.instance.isFaceIndexingEnabled == false) { @@ -357,6 +358,15 @@ class FaceMlService { if (_cannotRunMLFunction()) return; await sync(forceSync: _shouldSyncPeople); + + final int unclusteredFacesCount = + await FaceMLDataDB.instance.getUnclusteredFaceCount(); + if (unclusteredFacesCount > _kForceClusteringFaceCount) { + _logger.info( + "There are $unclusteredFacesCount unclustered faces, doing clustering first", + ); + await clusterAllImages(); + } await indexAllImages(); await clusterAllImages(); } From 05e737cb11b90dc92f4b6dac46fe7bfdb6885c57 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 12:32:58 +0530 Subject: [PATCH 22/32] Add steam as a type --- web/apps/auth/src/services/code.ts | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index b02e2314b8..74d982849b 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -10,13 +10,19 @@ export interface Code { /** A unique id for the corresponding "auth entity" in our system. */ id?: String; /** The type of the code. */ - type: "totp" | "hotp"; + type: "totp" | "hotp" | "steam"; /** The user's account or email for which this code is used. */ account?: string; /** The name of the entity that issued this code. */ issuer: string; - /** Number of digits in the generated OTP. */ - digits: number; + /** + * Length of the generated OTP. + * + * This is vernacularly called "digits", which is an accurate description + * for the OG TOTP/HOTP codes. However, steam codes are not just digits, so + * we name this as a content-neutral "length". + */ + length: number; /** * The time period (in seconds) for which a single OTP generated from this * code remains valid. @@ -85,7 +91,7 @@ const _codeFromURIString = (id: string, uriString: string): Code => { type, account: parseAccount(path), issuer: parseIssuer(url, path), - digits: parseDigits(url), + length: parseLength(url), period: parsePeriod(url), secret: parseSecret(url), algorithm: parseAlgorithm(url), @@ -130,8 +136,17 @@ const parseIssuer = (url: URL, path: string): string => { return p; }; -const parseDigits = (url: URL): number => - parseInt(url.searchParams.get("digits") ?? "", 10) || 6; +/** + * Parse the length of the generated code. + * + * The URI query param is called digits since originally TOTP/HOTP codes used + * this for generating numeric codes. Now we also support steam, which instead + * shows non-numeric codes, and also with a different default length of 5. + */ +const parseLength = (url: URL, type: Code["type"]): number => { + const defaultLength = type == "steam" ? 5 : 6; + return parseInt(url.searchParams.get("digits") ?? "", 10) || defaultLength; +}; const parsePeriod = (url: URL): number => parseInt(url.searchParams.get("period") ?? "", 10) || 30; From 370b28f9e4d60839cf0b9a1f5367827dd18af91c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 12:38:35 +0530 Subject: [PATCH 23/32] Type --- web/apps/auth/src/services/code.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index 74d982849b..ac71a9adcf 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -91,7 +91,7 @@ const _codeFromURIString = (id: string, uriString: string): Code => { type, account: parseAccount(path), issuer: parseIssuer(url, path), - length: parseLength(url), + length: parseLength(url, type), period: parsePeriod(url), secret: parseSecret(url), algorithm: parseAlgorithm(url), @@ -182,7 +182,7 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => { secret: code.secret, algorithm: code.algorithm, period: code.period, - digits: code.digits, + digits: code.length, }); otp = totp.generate(); nextOTP = totp.generate({ @@ -238,7 +238,7 @@ class Steam { this.period = 30; } - generate({ timestamp = Date.now() }: { timestamp: number }) { + generate({ timestamp }: { timestamp: number } = { timestamp: Date.now() }) { return `${timestamp}`; } } From ef6fe80944e5467e7a07a03a42a115bc1802ee17 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 24 May 2024 12:44:01 +0530 Subject: [PATCH 24/32] [mob][photos] Fix 400 on embedding fetch --- .../services/machine_learning/face_ml/face_ml_service.dart | 2 +- .../machine_learning/file_ml/remote_fileml_service.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart index a6b38f8e3b..5f3d15bdc4 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart @@ -446,7 +446,7 @@ class FaceMlService { if (LocalSettings.instance.remoteFetchEnabled) { try { - final List fileIds = []; + final Set fileIds = {}; // if there are duplicates here server returns 400 // Try to find embeddings on the remote server for (final f in chunk) { fileIds.add(f.uploadedFileID!); diff --git a/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart b/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart index bb1c9f999b..4712916d07 100644 --- a/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart +++ b/mobile/lib/services/machine_learning/file_ml/remote_fileml_service.dart @@ -55,13 +55,13 @@ class RemoteFileMLService { } Future getFilessEmbedding( - List fileIds, + Set fileIds, ) async { try { final res = await _dio.post( "/embeddings/files", data: { - "fileIDs": fileIds, + "fileIDs": fileIds.toList(), "model": 'file-ml-clip-face', }, ); From 7f49f530c5515f64f87b7dfce3dd3bba5f747cb4 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 24 May 2024 12:47:10 +0530 Subject: [PATCH 25/32] [mob][photos] Bump --- mobile/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index a1f6607763..1417d17f3e 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.8.109+633 +version: 0.8.110+634 publish_to: none environment: From 36aa33ed5a00804a1b272bf2a2e401982150a220 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 13:08:41 +0530 Subject: [PATCH 26/32] Move to separate file --- web/apps/auth/package.json | 1 + web/apps/auth/src/services/code.ts | 27 ----------------- web/apps/auth/src/services/steam.ts | 46 +++++++++++++++++++++++++++++ web/docs/dependencies.md | 4 +++ 4 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 web/apps/auth/src/services/steam.ts diff --git a/web/apps/auth/package.json b/web/apps/auth/package.json index 463ff06e8d..268f6f5c68 100644 --- a/web/apps/auth/package.json +++ b/web/apps/auth/package.json @@ -7,6 +7,7 @@ "@ente/accounts": "*", "@ente/eslint-config": "*", "@ente/shared": "*", + "jssha": "~3.3.1", "otpauth": "^9" } } diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index ac71a9adcf..b5497f6726 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -215,30 +215,3 @@ export const generateOTPs = (code: Code): [otp: string, nextOTP: string] => { } return [otp, nextOTP]; }; - -/** - * Steam OTPs. - * - * Steam's algorithm is a custom variant of TOTP that uses a 26-character - * alphabet instead of digits. - * - * A Dart implementation of the algorithm can be found in - * https://github.com/elliotwutingfeng/steam_totp/blob/main/lib/src/steam_totp_base.dart - * (MIT license), and we use that as a reference. Our implementation is written - * in the style of the other TOTP/HOTP classes that are provided by the otpauth - * JS library that we use for the normal TOTP/HOTP generation - * https://github.com/hectorm/otpauth/blob/master/src/hotp.js (MIT license). - */ -class Steam { - secret: string; - period: number; - - constructor({ secret }: { secret: string }) { - this.secret = secret; - this.period = 30; - } - - generate({ timestamp }: { timestamp: number } = { timestamp: Date.now() }) { - return `${timestamp}`; - } -} diff --git a/web/apps/auth/src/services/steam.ts b/web/apps/auth/src/services/steam.ts new file mode 100644 index 0000000000..e6007287f8 --- /dev/null +++ b/web/apps/auth/src/services/steam.ts @@ -0,0 +1,46 @@ +import jsSHA from "jssha"; + +/** + * Steam OTPs. + * + * Steam's algorithm is a custom variant of TOTP that uses a 26-character + * alphabet instead of digits. + * + * A Dart implementation of the algorithm can be found in + * https://github.com/elliotwutingfeng/steam_totp/blob/main/lib/src/steam_totp_base.dart + * (MIT license), and we use that as a reference. Our implementation is written + * in the style of the other TOTP/HOTP classes that are provided by the otpauth + * JS library that we use for the normal TOTP/HOTP generation + * https://github.com/hectorm/otpauth/blob/master/src/hotp.js (MIT license). + */ +export class Steam { + secret: string; + period: number; + + constructor({ secret }: { secret: string }) { + this.secret = secret; + this.period = 30; + } + + async generate( + { timestamp }: { timestamp: number } = { timestamp: Date.now() }, + ) { + const counter = Math.floor(timestamp / 1000 / this.period); + // const digest = new Uint8Array( + // sha1HMACDigest(this.secret, uintToBuf(counter)), + // ); + + return `${timestamp}`; + } +} + +// We don't necessarily need this dependency, we could use SubtleCrypto here +// instead too. However, SubtleCrypto has an async interface, and we already +// have a transitive dependency on jssha via otpauth, so just using it here +// doesn't increase our bundle size any further. +const sha1HMACDiggest = (key: ArrayBuffer, message: ArrayBuffer) => { + const hmac = new jsSHA("SHA-1", "ARRAYBUFFER"); + hmac.setHMACKey(key, "ARRAYBUFFER"); + hmac.update(message); + return hmac.getHMAC("ARRAYBUFFER"); +}; diff --git a/web/docs/dependencies.md b/web/docs/dependencies.md index 83a2b27990..3ea8fb2409 100644 --- a/web/docs/dependencies.md +++ b/web/docs/dependencies.md @@ -198,3 +198,7 @@ some cases. - [otpauth](https://github.com/hectorm/otpauth) is used for the generation of the actual OTP from the user's TOTP/HOTP secret. + +- However, otpauth doesn't support steam OTPs. For these, we need to compute + the SHA-1, and we use the same library, `jssha` that `otpauth` uses (since + it is already part of our bundle). From f6c40ee67d2799390cead23b63f20b698a7bb298 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 13:18:27 +0530 Subject: [PATCH 27/32] fromBase32 is exposed in the library API --- web/apps/auth/src/services/code.ts | 1 + web/apps/auth/src/services/steam.ts | 17 ++++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index b5497f6726..4b0a49d88e 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -1,5 +1,6 @@ import { ensure } from "@/utils/ensure"; import { HOTP, TOTP } from "otpauth"; +import { Steam } from "./steam"; /** * A parsed representation of an *OTP code URI. diff --git a/web/apps/auth/src/services/steam.ts b/web/apps/auth/src/services/steam.ts index e6007287f8..d43ec81a86 100644 --- a/web/apps/auth/src/services/steam.ts +++ b/web/apps/auth/src/services/steam.ts @@ -1,4 +1,5 @@ import jsSHA from "jssha"; +import { Secret } from "otpauth"; /** * Steam OTPs. @@ -14,21 +15,19 @@ import jsSHA from "jssha"; * https://github.com/hectorm/otpauth/blob/master/src/hotp.js (MIT license). */ export class Steam { - secret: string; + secret: Secret; period: number; constructor({ secret }: { secret: string }) { - this.secret = secret; + this.secret = Secret.fromBase32(secret); this.period = 30; } - async generate( - { timestamp }: { timestamp: number } = { timestamp: Date.now() }, - ) { + generate({ timestamp }: { timestamp: number } = { timestamp: Date.now() }) { const counter = Math.floor(timestamp / 1000 / this.period); - // const digest = new Uint8Array( - // sha1HMACDigest(this.secret, uintToBuf(counter)), - // ); + const digest = new Uint8Array( + sha1HMACDigest(this.secret.buffer), uintToBuf(counter)), + ); return `${timestamp}`; } @@ -38,7 +37,7 @@ export class Steam { // instead too. However, SubtleCrypto has an async interface, and we already // have a transitive dependency on jssha via otpauth, so just using it here // doesn't increase our bundle size any further. -const sha1HMACDiggest = (key: ArrayBuffer, message: ArrayBuffer) => { +const sha1HMACDigest = (key: ArrayBuffer, message: ArrayBuffer) => { const hmac = new jsSHA("SHA-1", "ARRAYBUFFER"); hmac.setHMACKey(key, "ARRAYBUFFER"); hmac.update(message); From 6594db9393e3d93eda5cddec9d3c4d807539b480 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 13:26:16 +0530 Subject: [PATCH 28/32] Encode counter --- web/apps/auth/src/services/steam.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/web/apps/auth/src/services/steam.ts b/web/apps/auth/src/services/steam.ts index d43ec81a86..f53259687b 100644 --- a/web/apps/auth/src/services/steam.ts +++ b/web/apps/auth/src/services/steam.ts @@ -26,19 +26,30 @@ export class Steam { generate({ timestamp }: { timestamp: number } = { timestamp: Date.now() }) { const counter = Math.floor(timestamp / 1000 / this.period); const digest = new Uint8Array( - sha1HMACDigest(this.secret.buffer), uintToBuf(counter)), + sha1HMACDigest(this.secret.buffer, uintToArray(counter)), ); return `${timestamp}`; } } +// Equivalent to +// https://github.com/hectorm/otpauth/blob/master/src/utils/encoding/uint.js +const uintToArray = (n: number): Uint8Array => { + const result = new Uint8Array(8); + for (let i = 7; i >= 0; i--) { + result[i] = n & 0xff; + n >>= 8; + } + return result; +}; + // We don't necessarily need this dependency, we could use SubtleCrypto here // instead too. However, SubtleCrypto has an async interface, and we already // have a transitive dependency on jssha via otpauth, so just using it here // doesn't increase our bundle size any further. -const sha1HMACDigest = (key: ArrayBuffer, message: ArrayBuffer) => { - const hmac = new jsSHA("SHA-1", "ARRAYBUFFER"); +const sha1HMACDigest = (key: ArrayBuffer, message: Uint8Array) => { + const hmac = new jsSHA("SHA-1", "UINT8ARRAY"); hmac.setHMACKey(key, "ARRAYBUFFER"); hmac.update(message); return hmac.getHMAC("ARRAYBUFFER"); From cb78c848d664924b0a2445da94a897518ce55b21 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 13:36:23 +0530 Subject: [PATCH 29/32] Impl --- web/apps/auth/src/services/steam.ts | 30 +++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/web/apps/auth/src/services/steam.ts b/web/apps/auth/src/services/steam.ts index f53259687b..8a9fcdf183 100644 --- a/web/apps/auth/src/services/steam.ts +++ b/web/apps/auth/src/services/steam.ts @@ -24,12 +24,30 @@ export class Steam { } generate({ timestamp }: { timestamp: number } = { timestamp: Date.now() }) { + // Same as regular TOTP. const counter = Math.floor(timestamp / 1000 / this.period); - const digest = new Uint8Array( - sha1HMACDigest(this.secret.buffer, uintToArray(counter)), - ); - return `${timestamp}`; + // Same as regular HOTP, but algorithm is fixed to SHA-1. + const digest = sha1HMACDigest(this.secret.buffer, uintToArray(counter)); + + // Same calculation as regular HOTP. + const offset = digest[digest.length - 1] & 15; + let otp = + ((digest[offset] & 127) << 24) | + ((digest[offset + 1] & 255) << 16) | + ((digest[offset + 2] & 255) << 8) | + (digest[offset + 3] & 255); + + // However, instead of using this as the OTP, use it to index into + // the steam OTP alphabet. + const alphabet = "23456789BCDFGHJKMNPQRTVWXY"; + const N = alphabet.length; + const steamOTP = []; + for (let i = 0; i < 5; i++) { + steamOTP.push(alphabet[otp % N]); + otp = Math.trunc(otp / N); + } + return steamOTP.join(""); } } @@ -38,7 +56,7 @@ export class Steam { const uintToArray = (n: number): Uint8Array => { const result = new Uint8Array(8); for (let i = 7; i >= 0; i--) { - result[i] = n & 0xff; + result[i] = n & 255; n >>= 8; } return result; @@ -52,5 +70,5 @@ const sha1HMACDigest = (key: ArrayBuffer, message: Uint8Array) => { const hmac = new jsSHA("SHA-1", "UINT8ARRAY"); hmac.setHMACKey(key, "ARRAYBUFFER"); hmac.update(message); - return hmac.getHMAC("ARRAYBUFFER"); + return hmac.getHMAC("UINT8ARRAY"); }; From 0ec75c2435ab744cced4e8c7e52de1ff0db4b157 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 13:47:11 +0530 Subject: [PATCH 30/32] Parse the type --- web/apps/auth/src/services/code.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index 4b0a49d88e..b5da0ffe55 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -104,6 +104,7 @@ const parsePathname = (url: URL): [type: Code["type"], path: string] => { const p = url.pathname.toLowerCase(); if (p.startsWith("//totp")) return ["totp", url.pathname.slice(6)]; if (p.startsWith("//hotp")) return ["hotp", url.pathname.slice(6)]; + if (p.startsWith("//steam")) return ["steam", url.pathname.slice(7)]; throw new Error(`Unsupported code or unparseable path "${url.pathname}"`); }; From fffe96a4c71bed81ced7dbad329857372d6a6805 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 13:49:21 +0530 Subject: [PATCH 31/32] Tweak --- web/apps/auth/src/services/steam.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/apps/auth/src/services/steam.ts b/web/apps/auth/src/services/steam.ts index 8a9fcdf183..f214640c24 100644 --- a/web/apps/auth/src/services/steam.ts +++ b/web/apps/auth/src/services/steam.ts @@ -62,10 +62,10 @@ const uintToArray = (n: number): Uint8Array => { return result; }; -// We don't necessarily need this dependency, we could use SubtleCrypto here -// instead too. However, SubtleCrypto has an async interface, and we already -// have a transitive dependency on jssha via otpauth, so just using it here -// doesn't increase our bundle size any further. +// We don't necessarily need a dependency on `jssha`, we could use SubtleCrypto +// here too. However, SubtleCrypto has an async interface, and we already have a +// transitive dependency on `jssha` via `otpauth`, so just using it here doesn't +// increase our bundle size any further. const sha1HMACDigest = (key: ArrayBuffer, message: Uint8Array) => { const hmac = new jsSHA("SHA-1", "UINT8ARRAY"); hmac.setHMACKey(key, "ARRAYBUFFER"); From bd2444d353b9517997d41079c8c477a9f95cf12a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 24 May 2024 14:11:05 +0530 Subject: [PATCH 32/32] [web] Fix auth ticker --- web/apps/auth/src/pages/auth.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index d750c5f7c7..f6661b1b13 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -198,7 +198,7 @@ const CodeDisplay: React.FC = ({ code }) => { // We need to call regen() once before the interval loop to set the // initial otp and nextOTP. regen(); - interval = setInterval(() => regen, periodMs); + interval = setInterval(regen, periodMs); }, timeToNextCode); return () => interval && clearInterval(interval);