Skip to content

Commit 5d32c24

Browse files
committed
feat: improved findmy capabilities & cache
1 parent 9ed618d commit 5d32c24

27 files changed

+457
-267
lines changed
17.9 KB
Binary file not shown.

packages/server/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@types/node-forge": "^1.0.4",
5555
"@types/numeral": "^0.0.26",
5656
"@types/plist": "^3.0.2",
57+
"@types/uuid": "^9.0.8",
5758
"@types/validatorjs": "^3.15.0",
5859
"@types/vcf": "^2.0.3",
5960
"@types/webpack-env": "^1.15.1",
@@ -119,6 +120,7 @@
119120
"slugify": "^1.6.0",
120121
"socket.io": "3.1.2",
121122
"typeorm": "0.3.6",
123+
"uuid": "^9.0.1",
122124
"validatorjs": "^3.22.1",
123125
"vcf": "^2.1.1",
124126
"zx": "^4.3.0"

packages/server/src/server/api/http/api/v1/httpRoutes.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { ScheduledMessageValidator } from "./validators/scheduledMessageValidato
3636
import { ScheduledMessageRouter } from "./routers/scheduledMessageRouter";
3737
import { ThemeValidator } from "./validators/themeValidator";
3838
import type { Context, Next } from "koa";
39+
import { FindMyRouter } from "./routers/findmyRouter";
3940

4041
export class HttpRoutes {
4142
static version = 1;
@@ -110,23 +111,23 @@ export class HttpRoutes {
110111
{
111112
method: HttpMethod.GET,
112113
path: "findmy/devices",
113-
controller: iCloudRouter.devices
114+
controller: FindMyRouter.devices
114115
},
115116
{
116117
method: HttpMethod.POST,
117118
path: "findmy/devices/refresh",
118-
controller: iCloudRouter.refreshDevices
119+
controller: FindMyRouter.refreshDevices
119120
},
120121
{
121122
method: HttpMethod.GET,
122123
path: "findmy/friends",
123124
middleware: [...HttpRoutes.protected, PrivateApiMiddleware],
124-
controller: iCloudRouter.friends
125+
controller: FindMyRouter.friends
125126
},
126127
{
127128
method: HttpMethod.POST,
128129
path: "findmy/friends/refresh",
129-
controller: iCloudRouter.refreshFriends
130+
controller: FindMyRouter.refreshFriends
130131
}
131132
]
132133
},
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Next } from "koa";
2+
import { RouterContext } from "koa-router";
3+
import { Success } from "../responses/success";
4+
import { ServerError } from "../responses/errors";
5+
import { FindMyInterface } from "@server/api/interfaces/findMyInterface";
6+
7+
export class FindMyRouter {
8+
static async refreshDevices(ctx: RouterContext, _: Next) {
9+
try {
10+
await FindMyInterface.refreshDevices();
11+
return new Success(ctx, { message: "Successfully refreshed Find My device locations!" }).send();
12+
} catch (ex: any) {
13+
throw new ServerError({
14+
message: "Failed to refresh Find My device locations!",
15+
error: ex?.message ?? ex.toString()
16+
});
17+
}
18+
}
19+
20+
static async refreshFriends(ctx: RouterContext, _: Next) {
21+
try {
22+
const locations = await FindMyInterface.refreshFriends();
23+
return new Success(ctx, {
24+
message: "Successfully refreshed Find My friends locations!",
25+
data: locations
26+
}).send();
27+
} catch (ex: any) {
28+
throw new ServerError({
29+
message: "Failed to refresh Find My friends locations!",
30+
error: ex?.message ?? ex.toString()
31+
});
32+
}
33+
}
34+
35+
static async devices(ctx: RouterContext, _: Next) {
36+
try {
37+
const data = await FindMyInterface.getDevices();
38+
return new Success(ctx, { message: "Successfully fetched Find My device locations!", data }).send();
39+
} catch (ex: any) {
40+
throw new ServerError({
41+
message: "Failed to fetch Find My device locations!",
42+
error: ex?.message ?? ex.toString()
43+
});
44+
}
45+
}
46+
47+
static async friends(ctx: RouterContext, _: Next) {
48+
try {
49+
const data: any = await FindMyInterface.getFriends();
50+
return new Success(ctx, { message: "Successfully fetched Find My friends locations!", data }).send();
51+
} catch (ex: any) {
52+
throw new ServerError({
53+
message: "Failed to fetch Find My friends locations!",
54+
error: ex?.message ?? ex.toString()
55+
});
56+
}
57+
}
58+
}

packages/server/src/server/api/http/api/v1/routers/icloudRouter.ts

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,9 @@ import { Next } from "koa";
22
import { RouterContext } from "koa-router";
33
import { Success } from "../responses/success";
44
import { ServerError } from "../responses/errors";
5-
import { FindMyService } from "@server/services/findMyService";
65
import { iCloudInterface } from "@server/api/interfaces/iCloudInterface";
7-
import { findMyInterface } from "@server/api/interfaces/findMyInterface";
86

97
export class iCloudRouter {
10-
static async refreshDevices(ctx: RouterContext, _: Next) {
11-
try {
12-
const data = await FindMyService.refreshDevices();
13-
return new Success(ctx, { message: "Successfully refreshed Find My device locations!", data }).send();
14-
} catch (ex: any) {
15-
throw new ServerError(
16-
{ message: "Failed to refresh Find My device locations!", error: ex?.message ?? ex.toString() });
17-
}
18-
}
19-
20-
static async refreshFriends(ctx: RouterContext, _: Next) {
21-
try {
22-
await FindMyService.refreshFriends();
23-
const data = findMyInterface.getFriends();
24-
return new Success(ctx, { message: "Successfully refreshed Find My friends locations!", data }).send();
25-
} catch (ex: any) {
26-
throw new ServerError(
27-
{ message: "Failed to refresh Find My friends locations!", error: ex?.message ?? ex.toString() });
28-
}
29-
}
30-
31-
static async devices(ctx: RouterContext, _: Next) {
32-
try {
33-
const data = await FindMyService.getDevices();
34-
return new Success(ctx, { message: "Successfully fetched Find My device locations!", data }).send();
35-
} catch (ex: any) {
36-
throw new ServerError(
37-
{ message: "Failed to fetch Find My device locations!", error: ex?.message ?? ex.toString() });
38-
}
39-
}
40-
41-
static async friends(ctx: RouterContext, _: Next) {
42-
try {
43-
const data: any = await findMyInterface.getFriends();
44-
return new Success(ctx, { message: "Successfully fetched Find My friends locations!", data }).send();
45-
} catch (ex: any) {
46-
throw new ServerError(
47-
{ message: "Failed to fetch Find My friends locations!", error: ex?.message ?? ex.toString() });
48-
}
49-
}
50-
518
static async getAccountInfo(ctx: RouterContext, _: Next) {
529
try {
5310
const data: any = await iCloudInterface.getAccountInfo();
Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,90 @@
11
import { Server } from "@server";
2-
import { isMinBigSur } from "@server/env";
3-
import { checkPrivateApiStatus } from "@server/helpers/utils";
4-
import { FindMyService } from "@server/services";
2+
import { FileSystem } from "@server/fileSystem";
3+
import { isMinBigSur, isMinSonoma } from "@server/env";
4+
import { checkPrivateApiStatus, isEmpty, waitMs } from "@server/helpers/utils";
5+
import { quitFindMyFriends, startFindMyFriends, showFindMyFriends, hideFindMyFriends } from "../apple/scripts";
6+
import { FindMyDevice, FindMyItem, FindMyLocationItem } from "@server/api/lib/findmy/types";
7+
import { transformFindMyItemToDevice } from "@server/api/lib/findmy/utils";
58

6-
export class findMyInterface {
9+
export class FindMyInterface {
710
static async getFriends() {
11+
return Server().findMyCache.getAll();
12+
}
13+
14+
static async getDevices(): Promise<Array<FindMyDevice> | null> {
15+
try {
16+
const [devices, items] = await Promise.all([
17+
FindMyInterface.readDataFile("Devices"),
18+
FindMyInterface.readDataFile("Items")
19+
]);
20+
21+
// Return null if neither of the files exist
22+
if (!devices && !items) return null;
23+
24+
// Transform the items to match the same shape as devices
25+
const transformedItems = (items ?? []).map(transformFindMyItemToDevice);
26+
27+
return [...(devices ?? []), ...transformedItems];
28+
} catch {
29+
return null;
30+
}
31+
}
32+
33+
static async refreshDevices() {
34+
// Can't use the Private API to refresh devices yet
35+
await this.refreshLocationsAccessibility();
36+
}
37+
38+
static async refreshFriends(): Promise<FindMyLocationItem[]> {
839
const papiEnabled = Server().repo.getConfig("enable_private_api") as boolean;
9-
let data = null;
10-
if (papiEnabled && isMinBigSur) {
40+
if (papiEnabled && isMinBigSur && !isMinSonoma) {
1141
checkPrivateApiStatus();
12-
const result = await Server().privateApi.findmy.getFriendsLocations();
13-
data = result?.data;
42+
const result = await Server().privateApi.findmy.refreshFriends();
43+
const refreshLocations = result?.data?.locations ?? [];
44+
45+
// Save the data to the cache
46+
// The cache will handle properly updating the data.
47+
Server().findMyCache.addAll(refreshLocations);
1448
} else {
15-
data = await FindMyService.getFriends();
49+
await this.refreshLocationsAccessibility();
1650
}
1751

18-
return data;
52+
return Server().findMyCache.getAll();
53+
}
54+
55+
static async refreshLocationsAccessibility() {
56+
await FileSystem.executeAppleScript(quitFindMyFriends());
57+
await waitMs(3000);
58+
59+
// Make sure the Find My app is open.
60+
// Give it 5 seconds to open
61+
await FileSystem.executeAppleScript(startFindMyFriends());
62+
await waitMs(5000);
63+
64+
// Bring the Find My app to the foreground so it refreshes the devices
65+
// Give it 15 seconods to refresh
66+
await FileSystem.executeAppleScript(showFindMyFriends());
67+
await waitMs(15000);
68+
69+
// Re-hide the Find My App
70+
await FileSystem.executeAppleScript(hideFindMyFriends());
71+
}
72+
73+
private static readDataFile<T extends "Devices" | "Items">(
74+
type: T
75+
): Promise<Array<T extends "Devices" ? FindMyDevice : FindMyItem> | null> {
76+
const devicesPath = path.join(FileSystem.findMyDir, `${type}.data`);
77+
return new Promise((resolve, reject) => {
78+
fs.readFile(devicesPath, { encoding: "utf-8" }, (err, data) => {
79+
// Couldn't read the file
80+
if (err) return resolve(null);
81+
82+
try {
83+
return resolve(JSON.parse(data.toString("utf-8")));
84+
} catch {
85+
reject(new Error(`Failed to read FindMy ${type} cache file! It is not in the correct format!`));
86+
}
87+
});
88+
});
1989
}
2090
}

packages/server/src/server/api/lib/facetime/FaceTimeSession.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { NotificationCenterDB } from "@server/databases/notificationCenter/Notii
44
import { convertDateToCocoaTime } from "@server/databases/imessage/helpers/dateUtil";
55
import { waitMs } from "@server/helpers/utils";
66
import { EventEmitter } from "events";
7-
import { uuidv4 } from "@firebase/util";
7+
import { v4 } from "uuid";
88

99
export enum FaceTimeSessionStatus {
1010
UNKNOWN = 0,
@@ -50,7 +50,7 @@ export class FaceTimeSession extends EventEmitter {
5050
} = {}) {
5151
super();
5252

53-
this.uuid = uuidv4();
53+
this.uuid = v4();
5454

5555
this.createdAt = new Date();
5656
this.callUuid = callUuid;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { isEmpty } from "@server/helpers/utils";
2+
import { FindMyLocationItem } from "./types";
3+
import { Server } from "@server";
4+
5+
export class FindMyFriendsCache {
6+
cache: Record<string, FindMyLocationItem> = {};
7+
8+
/**
9+
* Adds a list of location data to the cache.
10+
* Location data may be dropped if it doesn't update/change the cache at all.
11+
*
12+
* @param locationData
13+
* @returns The location data that was updated in the cache
14+
*/
15+
addAll(locationData: FindMyLocationItem[]): FindMyLocationItem[] {
16+
const output: FindMyLocationItem[] = [];
17+
for (const i of locationData) {
18+
const success = this.add(i);
19+
if (success) {
20+
output.push(i);
21+
}
22+
}
23+
24+
return output;
25+
}
26+
27+
/**
28+
* Adds a single location data to the cache
29+
*
30+
* @param locationData
31+
* @returns Whether the location data updated the cache at all
32+
*/
33+
add(locationData: FindMyLocationItem): boolean {
34+
const handle = locationData?.handle;
35+
if (isEmpty(handle)) return false;
36+
37+
const updateCache = (): boolean => {
38+
this.cache[handle] = locationData;
39+
return true;
40+
};
41+
42+
// If we don't have a cache item, add it to the cache as-is
43+
const currentData = this.cache[handle];
44+
if (!currentData) {
45+
return updateCache();
46+
}
47+
48+
// If the update is a "legacy" update, and the current location isn't, ignore it.
49+
// We don't want to override a live/shallow location with a legacy one
50+
if (locationData?.status === "legacy" && currentData?.status !== "legacy") return false;
51+
52+
// We don't want to overwrite a non [0, 0] location with a [0, 0] one
53+
const currentCoords = currentData?.coordinates ?? [0, 0];
54+
const updatedCoords = locationData?.coordinates ?? [0, 0];
55+
const noLocationType = currentData?.status === "legacy" && locationData?.status === "legacy";
56+
if (
57+
noLocationType &&
58+
currentCoords[0] !== 0 &&
59+
currentCoords[1] !== 0 &&
60+
updatedCoords[0] === 0 &&
61+
updatedCoords[1] === 0
62+
)
63+
return false;
64+
65+
// If the latest update has an older timestamp, ignore it.
66+
const updateTimestamp = locationData?.last_updated ?? 0;
67+
const currentTimestamp = currentData?.last_updated ?? 0;
68+
if (updateTimestamp < currentTimestamp) return false;
69+
70+
return updateCache();
71+
}
72+
73+
get(handle: string): FindMyLocationItem | null {
74+
return this.cache[handle] ?? null;
75+
}
76+
77+
getAll(): FindMyLocationItem[] {
78+
return Object.values(this.cache);
79+
}
80+
}

packages/server/src/server/services/findMyService/types.ts renamed to packages/server/src/server/api/lib/findmy/types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export interface FindMyDevice {
4949
// Extra properties from FindMyItem
5050
role?: FindMyItem["role"];
5151
serialNumber?: string;
52-
lostModeMetadata?: FindMyItem["lostModeMetadata"]
52+
lostModeMetadata?: FindMyItem["lostModeMetadata"];
5353
}
5454

5555
export interface FindMyItem {
@@ -121,3 +121,15 @@ interface FindMyLocation {
121121
altitude?: number;
122122
locationFinished?: boolean;
123123
}
124+
125+
export type FindMyLocationItem = {
126+
handle: string | null;
127+
coordinates: [number, number];
128+
long_address: string | null;
129+
short_address: string | null;
130+
subtitle: string | null;
131+
title: string | null;
132+
last_updated: number;
133+
is_locating_in_progress: 0 | 1;
134+
status: "legacy" | "live" | "shallow";
135+
};

packages/server/src/server/services/findMyService/utils.ts renamed to packages/server/src/server/api/lib/findmy/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FindMyItem, FindMyDevice } from "@server/services/findMyService/types";
1+
import { FindMyItem, FindMyDevice } from "@server/api/lib/findmy/types";
22

33
export const getFindMyItemModelDisplayName = (item: FindMyItem): string => {
44
if (item.productType.type === "b389") return "AirTag";

0 commit comments

Comments
 (0)