diff --git a/README.md b/README.md
index e84318c0..bf4dacb9 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ This is the back-end server for the BlueBubbles App. It allows you to forward yo
3. Install the server dependencies
- `yarn`
4. Run the dev server (this will start both the renderer and server)
- - `yarn run start-dev`
+ - `yarn start`
## Structure / Directory Map
diff --git a/package.json b/package.json
index 060596ff..d33b6e23 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bluebubbles-server",
- "version": "1.5.2",
+ "version": "1.5.3",
"description": "BlueBubbles Server is the app that powers the BlueBubbles app ecosystem",
"private": true,
"workspaces": [
diff --git a/packages/server/appResources/macos/daemons/cloudflared b/packages/server/appResources/macos/daemons/cloudflared
index c55a0bc3..af5e141d 100755
Binary files a/packages/server/appResources/macos/daemons/cloudflared and b/packages/server/appResources/macos/daemons/cloudflared differ
diff --git a/packages/server/appResources/macos/daemons/cloudflared.md5 b/packages/server/appResources/macos/daemons/cloudflared.md5
index c30fd00e..9a59b1b7 100644
--- a/packages/server/appResources/macos/daemons/cloudflared.md5
+++ b/packages/server/appResources/macos/daemons/cloudflared.md5
@@ -1 +1 @@
-98f78f5ce6b7c5d51e7351e8280b78d6
\ No newline at end of file
+09aeb674587258c1e2cd4e00ab5f0b66
\ No newline at end of file
diff --git a/packages/server/appResources/private-api/README.md b/packages/server/appResources/private-api/README.md
index 86835200..f3e09320 100644
--- a/packages/server/appResources/private-api/README.md
+++ b/packages/server/appResources/private-api/README.md
@@ -4,6 +4,6 @@ The `macosXbundle.md5` files within this directory contain a single string that
`find -s /path/to/private/api/folder/BlueBubblesHelper.bundle -type f -exec md5 {} \; | md5`
-**GitHub Release Reference**: https://github.com/BlueBubblesApp/BlueBubbles-Server-Helper/releases/tag/0.0.9
+**GitHub Release Reference**: https://github.com/BlueBubblesApp/BlueBubbles-Server-Helper/releases/tag/0.0.11
You can also check the current versions of these bundles by opening the `version.txt` file.
\ No newline at end of file
diff --git a/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Info.plist b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Info.plist
index 3e2e0e27..0d9a75c5 100644
--- a/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Info.plist
+++ b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 0.0.9
+ 0.0.11
CFBundleSignature
????
CFBundleSupportedPlatforms
@@ -25,7 +25,7 @@
MacOSX
CFBundleVersion
- 11
+ 13
DTCompiler
com.apple.compilers.llvm.clang.1_0
DTPlatformBuild
diff --git a/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/MacOS/BlueBubblesHelper b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/MacOS/BlueBubblesHelper
index 2479a235..c444730f 100755
Binary files a/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/MacOS/BlueBubblesHelper and b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/MacOS/BlueBubblesHelper differ
diff --git a/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/IMAccount-AliasAdditions.h b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/IMAccount-AliasAdditions.h
new file mode 100644
index 00000000..1e081da8
--- /dev/null
+++ b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/IMAccount-AliasAdditions.h
@@ -0,0 +1,19 @@
+//
+// Generated by class-dump 3.5 (64 bit) (Debug version compiled Jun 9 2015 22:53:21).
+//
+// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2014 by Steve Nygard.
+//
+
+#import "IMAccount.h"
+
+@interface IMAccount (AliasAdditions)
+- (id)loginName;
+- (id)phoneNumberAlias;
+- (id)allAliases;
+- (long long)numberOfActiveAliases;
+- (BOOL)isAliasActivated:(id)arg1;
+- (double)timeIntervalSinceEmailWasSentForAlias:(id)arg1;
+- (void)removeCreationMarker:(id)arg1;
+- (void)setCreationMarker:(id)arg1;
+@end
+
diff --git a/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/SOAccountAlias.h b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/SOAccountAlias.h
new file mode 100644
index 00000000..a3747747
--- /dev/null
+++ b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/SOAccountAlias.h
@@ -0,0 +1,40 @@
+//
+// Generated by class-dump 3.5 (64 bit) (Debug version compiled Jun 9 2015 22:53:21).
+//
+// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2014 by Steve Nygard.
+//
+
+
+@class NSDictionary, NSString, SOAccountAliasController;
+
+@interface SOAccountAlias : NSObject
+{
+ BOOL _active;
+ BOOL _initialized;
+ int _verificationState;
+ NSString *_name;
+ long long _type;
+ NSDictionary *_failureInfo;
+ SOAccountAliasController *_controller;
+}
+
+@property(nonatomic) __weak SOAccountAliasController *controller; // @synthesize controller=_controller;
+@property(nonatomic) BOOL initialized; // @synthesize initialized=_initialized;
+@property(copy, nonatomic) NSDictionary *failureInfo; // @synthesize failureInfo=_failureInfo;
+@property(nonatomic) BOOL active; // @synthesize active=_active;
+@property(nonatomic) int verificationState; // @synthesize verificationState=_verificationState;
+@property(readonly, nonatomic) long long type; // @synthesize type=_type;
+@property(readonly, nonatomic) NSString *name; // @synthesize name=_name;
+
+- (long long)validationErrorReason;
+- (id)localizedValidationFalure;
+- (void)deactivate;
+- (void)activate;
+- (id)description;
+- (id)copyWithZone:(struct _NSZone *)arg1;
+- (BOOL)isEqual:(id)arg1;
+- (id)initWithName:(id)arg1 type:(long long)arg2 controller:(id)arg3;
+- (id)initWithName:(id)arg1 type:(long long)arg2;
+
+@end
+
diff --git a/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/SOAccountAliasController.h b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/SOAccountAliasController.h
new file mode 100644
index 00000000..342829fc
--- /dev/null
+++ b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/SOAccountAliasController.h
@@ -0,0 +1,60 @@
+//
+// Generated by class-dump 3.5 (64 bit) (Debug version compiled Jun 9 2015 22:53:21).
+//
+// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2014 by Steve Nygard.
+//
+
+#import
+#import "IMAccount-AliasAdditions.h"
+@class IMAccount, NSMutableArray, NSMutableDictionary, NSMutableOrderedSet;
+
+@interface SOAccountAliasController : NSObject
+{
+ IMAccount *_account;
+ NSMutableOrderedSet *_aliasSet;
+ NSMutableArray *_aliasTimers;
+ NSMutableDictionary *_aliasLookup;
+}
+
++ (id)stringForAliasValidationFailure:(long long)arg1 aliasName:(id)arg2;
+@property(retain) NSMutableDictionary *aliasLookup; // @synthesize aliasLookup=_aliasLookup;
+@property(retain) NSMutableArray *aliasTimers; // @synthesize aliasTimers=_aliasTimers;
+@property(retain) NSMutableOrderedSet *aliasSet; // @synthesize aliasSet=_aliasSet;
+@property(readonly) __weak IMAccount *account; // @synthesize account=_account;
+- (void)_validationTimerEnded:(id)arg1;
+- (void)_removeTimerForAliasName:(id)arg1;
+- (void)_addAliasTimer:(id)arg1 length:(double)arg2;
+- (void)confirmNewAlias:(id)arg1;
+- (void)_clearTimers;
+- (void)configureAliasState:(id)arg1;
+- (void)_removeAliasesFromSet:(id)arg1;
+- (void)_removeAliasFromSet:(id)arg1;
+- (void)_addAliasesToSet:(id)arg1;
+- (void)_addAliasToSet:(id)arg1;
+- (void)_initializeAliases;
+- (void)_stopListeningToNotifications;
+- (void)_listenToNotifications;
+- (void)_vettedAliasesChanged:(id)arg1;
+- (void)_aliasValidationChanged:(id)arg1;
+- (void)_aliasesChanged:(id)arg1;
+- (void)deleteAlias:(id)arg1;
+- (void)deactivateAliases:(id)arg1;
+- (void)addAliases:(id)arg1;
+- (void)addAliasesWithNames:(id)arg1;
+- (BOOL)addAliasWithName:(id)arg1;
+- (void)removeAliases:(id)arg1;
+- (void)removeAliasesWithNames:(id)arg1;
+- (void)removeAliasWithName:(id)arg1;
+- (void)setAccount:(id)arg1;
+- (id)aliasForName:(id)arg1;
+- (long long)activeAliasCount;
+- (id)activeAliases;
+- (long long)vettedAliasCount;
+- (id)vettedAliases;
+- (id)aliases;
+- (long long)validationErrorReasonForAlias:(id)arg1;
+- (void)dealloc;
+- (id)initWithAccount:(id)arg1;
+
+@end
+
diff --git a/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/SOAccountRegistrationController.h b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/SOAccountRegistrationController.h
new file mode 100644
index 00000000..48893eec
--- /dev/null
+++ b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/Resources/SocialAppsCore/SOAccountRegistrationController.h
@@ -0,0 +1,73 @@
+//
+// Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39).
+//
+// Copyright (C) 1997-2019 Steve Nygard.
+//
+
+
+
+
+
+@class IMAccount, IMServiceImpl, NSTimer, SOAccountAliasController;
+
+@interface SOAccountRegistrationController : NSObject
+{
+ BOOL _isSigningOut;
+ BOOL _isSignedIn;
+ long long _registrationState;
+ long long _enabledState;
+ IMAccount *_account;
+ SOAccountAliasController *_aliasController;
+ IMServiceImpl *_serviceType;
+ NSTimer *_authenticationTimer;
+}
+
++ (id)stringForAccountRegistrationFailure:(long long)arg1;
++ (id)errorWithCode:(long long)arg1;
++ (id)registrationControllerForAccount:(id)arg1;
++ (id)faceTimeRegistrationController;
++ (id)registrationController;
++ (void)resetSharedInstance;
+@property(retain) NSTimer *authenticationTimer; // @synthesize authenticationTimer=_authenticationTimer;
+@property(nonatomic) BOOL isSignedIn; // @synthesize isSignedIn=_isSignedIn;
+@property(nonatomic) BOOL isSigningOut; // @synthesize isSigningOut=_isSigningOut;
+@property(retain, nonatomic) IMServiceImpl *serviceType; // @synthesize serviceType=_serviceType;
+@property(retain, nonatomic) SOAccountAliasController *aliasController; // @synthesize aliasController=_aliasController;
+@property(retain, nonatomic) IMAccount *account; // @synthesize account=_account;
+@property(nonatomic) long long enabledState; // @synthesize enabledState=_enabledState;
+@property(nonatomic) long long registrationState; // @synthesize registrationState=_registrationState;
+- (BOOL)cloudKitEventNotificationManagerAccountHasiMessageEnabled:(id)arg1;
+- (void)_clearAliasController;
+- (void)_setupAliasController;
+- (void)_authenticationTimerTimedOut:(id)arg1;
+- (void)_clearAuthenticationTimeout;
+- (void)accountLoggedOut:(id)arg1;
+- (void)activationStatusChanged:(id)arg1;
+- (void)registrationStatusChanged:(id)arg1;
+- (void)_accountAliasActivationStateChanged:(id)arg1;
+- (void)_SOAccountAliasVerificationStateChanged:(id)arg1;
+- (void)_accountAliasCountChanged:(id)arg1;
+- (BOOL)accountOnlineAndRegistered;
+- (BOOL)accountIsSignedIn;
+- (id)loginName;
+- (id)localizedRegistrationErrorMessage;
+- (BOOL)readReceiptsEnabled;
+- (void)enableReadReceipts:(BOOL)arg1;
+- (void)confirmNewAlias:(id)arg1;
+- (BOOL)addAndConfirmNewAlias:(id)arg1;
+- (void)enableAccount:(BOOL)arg1;
+- (BOOL)deleteAccount;
+- (void)signOutAndUpdateDaemon:(BOOL)arg1;
+- (void)signOut;
+- (void)_registerAccountIfNecessary;
+- (void)registerAccount;
+- (void)authenticateWithUsername:(id)arg1 password:(id)arg2 authID:(id)arg3 authToken:(id)arg4;
+- (void)authenticateWithUsername:(id)arg1 authID:(id)arg2 authToken:(id)arg3;
+- (void)authenticateWithUsername:(id)arg1 password:(id)arg2;
+- (void)updateStateForAccountStatusAndPostNotification:(BOOL)arg1;
+- (void)setEnabledState:(long long)arg1 andPostNotification:(BOOL)arg2;
+- (void)setRegistrationState:(long long)arg1 andPostNotification:(BOOL)arg2;
+- (void)dealloc;
+
+@end
+
diff --git a/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/_CodeSignature/CodeResources b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/_CodeSignature/CodeResources
index 7a2d0607..4d3c6930 100644
--- a/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/_CodeSignature/CodeResources
+++ b/packages/server/appResources/private-api/macos10/BlueBubblesHelper.bundle/Contents/_CodeSignature/CodeResources
@@ -8,6 +8,22 @@
GOS5kxzLZzPuPU7AMvjis6Oltjk=
+ Resources/SocialAppsCore/IMAccount-AliasAdditions.h
+
+ k3hVO7/+cKZSupuS+4makuvqQ3o=
+
+ Resources/SocialAppsCore/SOAccountAlias.h
+
+ hsOAN73xrV8SAvF4lLs3OVEtSyc=
+
+ Resources/SocialAppsCore/SOAccountAliasController.h
+
+ 72KEKFyBqsA7Kd2a3HObxvQzhPo=
+
+ Resources/SocialAppsCore/SOAccountRegistrationController.h
+
+ SLtcWTI/+Ghzd1OC6oRMTFSt7OE=
+
files2
@@ -18,6 +34,34 @@
OiPCSXEfNginSgTFrUsF7VuW3q3fk6E2Ih7+t73hGVY=
+ Resources/SocialAppsCore/IMAccount-AliasAdditions.h
+
+ hash2
+
+ egihpTZAMD/MEW4PSvmHINNYRzHJCKUmGYsWEFAiFcQ=
+
+
+ Resources/SocialAppsCore/SOAccountAlias.h
+
+ hash2
+
+ WSKWp1BVcMxEmCriwLZKZ5/VVZA7guKISbvt6Uph148=
+
+
+ Resources/SocialAppsCore/SOAccountAliasController.h
+
+ hash2
+
+ iADBnpDPFuRVFA9fr2+8AgtsA74Ws25vWftS+LowmDk=
+
+
+ Resources/SocialAppsCore/SOAccountRegistrationController.h
+
+ hash2
+
+ aS+oUmRnPXxh/QDBEJ5qpBEq7qTwwl08SxpahZbRJn0=
+
+
rules
diff --git a/packages/server/appResources/private-api/macos10bundle.md5 b/packages/server/appResources/private-api/macos10bundle.md5
index ff1ca928..f3c998b3 100644
--- a/packages/server/appResources/private-api/macos10bundle.md5
+++ b/packages/server/appResources/private-api/macos10bundle.md5
@@ -1 +1 @@
-175343768ea164dc7ff92d37b4cd8cfc
\ No newline at end of file
+40fdae0ec7bfbf605f1df380b65002c3
\ No newline at end of file
diff --git a/packages/server/appResources/private-api/macos11/BlueBubblesHelper.bundle/Contents/Info.plist b/packages/server/appResources/private-api/macos11/BlueBubblesHelper.bundle/Contents/Info.plist
index c365c791..727598c2 100644
--- a/packages/server/appResources/private-api/macos11/BlueBubblesHelper.bundle/Contents/Info.plist
+++ b/packages/server/appResources/private-api/macos11/BlueBubblesHelper.bundle/Contents/Info.plist
@@ -17,7 +17,7 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 0.0.9
+ 0.0.11
CFBundleSignature
????
CFBundleSupportedPlatforms
@@ -25,7 +25,7 @@
MacOSX
CFBundleVersion
- 11
+ 13
DTCompiler
com.apple.compilers.llvm.clang.1_0
DTPlatformBuild
diff --git a/packages/server/appResources/private-api/macos11/BlueBubblesHelper.bundle/Contents/MacOS/BlueBubblesHelper b/packages/server/appResources/private-api/macos11/BlueBubblesHelper.bundle/Contents/MacOS/BlueBubblesHelper
index 06ab518f..0c3fad69 100755
Binary files a/packages/server/appResources/private-api/macos11/BlueBubblesHelper.bundle/Contents/MacOS/BlueBubblesHelper and b/packages/server/appResources/private-api/macos11/BlueBubblesHelper.bundle/Contents/MacOS/BlueBubblesHelper differ
diff --git a/packages/server/appResources/private-api/macos11bundle.md5 b/packages/server/appResources/private-api/macos11bundle.md5
index b67c2655..f2cba98a 100644
--- a/packages/server/appResources/private-api/macos11bundle.md5
+++ b/packages/server/appResources/private-api/macos11bundle.md5
@@ -1 +1 @@
-393f7cf77c6a53cc9ef4a2782503a5b8
\ No newline at end of file
+592098c9bb73d8556df04fa32aba6f50
\ No newline at end of file
diff --git a/packages/server/appResources/private-api/version.txt b/packages/server/appResources/private-api/version.txt
index 429d94ae..58682af4 100644
--- a/packages/server/appResources/private-api/version.txt
+++ b/packages/server/appResources/private-api/version.txt
@@ -1 +1 @@
-0.0.9
\ No newline at end of file
+0.0.11
\ No newline at end of file
diff --git a/packages/server/package.json b/packages/server/package.json
index 113ca210..19f797bc 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,6 +1,6 @@
{
"name": "@bluebubbles/server",
- "version": "1.5.2",
+ "version": "1.5.3",
"main": "./dist/main.js",
"license": "Apache-2.0",
"author": {
diff --git a/packages/server/src/server/api/v1/apple/actions.ts b/packages/server/src/server/api/v1/apple/actions.ts
index b8b999b6..c023940a 100644
--- a/packages/server/src/server/api/v1/apple/actions.ts
+++ b/packages/server/src/server/api/v1/apple/actions.ts
@@ -40,7 +40,7 @@ import { tapbackUIMap } from "./mappings";
*/
export class ActionHandler {
static sendMessageHandler = async (chatGuid: string, message: string, attachment: string) => {
- let messageScript;
+ let messageScript: string;
let theAttachment = attachment;
if (theAttachment !== null && theAttachment.endsWith(".mp3")) {
diff --git a/packages/server/src/server/api/v1/apple/scripts.ts b/packages/server/src/server/api/v1/apple/scripts.ts
index 3e2c6c60..b65aa037 100644
--- a/packages/server/src/server/api/v1/apple/scripts.ts
+++ b/packages/server/src/server/api/v1/apple/scripts.ts
@@ -89,23 +89,25 @@ export const startMessages = () => {
};
/**
- * The AppleScript used to send a message with or without an attachment
+ * The AppleScript used to hide an app
*/
-export const startFindMyFriends = () => {
- return startApp("FindMy");
+export const hideApp = (appName: string) => {
+ return `tell application "System Events" to tell application process "${appName}"
+ set visible to false
+ end tell`;
};
/**
- * The AppleScript used to send a message with or without an attachment
+ * The AppleScript used to quit an app
*/
-export const hideApp = (appName: string) => {
- return `tell application "System Events" to tell application process "${appName}"
- set visible to false
+export const quitApp = (appName: string) => {
+ return `tell application "${appName}"
+ quit
end tell`;
};
/**
- * The AppleScript used to send a message with or without an attachment
+ * The AppleScript used to show an app
*/
export const showApp = (appName: string) => {
return `tell application "System Events" to tell application process "${appName}"
@@ -113,8 +115,31 @@ export const showApp = (appName: string) => {
end tell`;
};
+// === FindMy ===
+
/**
- * The AppleScript used to send a message with or without an attachment
+ * The AppleScript used to start the FindMy app
+ */
+export const startFindMyFriends = () => {
+ return startApp("FindMy");
+};
+
+/**
+ * The AppleScript used to show the FindMy app
+ */
+export const showFindMyFriends = () => {
+ return showApp("FindMy");
+};
+
+/**
+ * The AppleScript used to quit the FindMy app
+ */
+export const quitFindMyFriends = () => {
+ return quitApp("FindMy");
+};
+
+/**
+ * The AppleScript used to hide the FindMy app
*/
export const hideFindMyFriends = () => {
return hideApp("FindMy");
@@ -132,6 +157,8 @@ export const startApp = (appName: string) => {
end if`;
};
+// === Messages ===
+
/**
* The AppleScript used to send a message with or without an attachment
*/
@@ -805,6 +832,8 @@ export const checkTypingIndicator = (chatName: string) => {
end splitText`;
};
+// === Contacts ===
+
/**
* Export contacts to a VCF file
*/
@@ -840,6 +869,8 @@ export const exportContacts = () => {
end tell`;
};
+// === System ===
+
export const runTerminalScript = (path: string) => {
return `tell application "Terminal" to do script "${escapeOsaExp(path)}"`;
};
diff --git a/packages/server/src/server/api/v1/caches/apiContactsCache.ts b/packages/server/src/server/api/v1/caches/apiContactsCache.ts
index 497958e4..83fee154 100644
--- a/packages/server/src/server/api/v1/caches/apiContactsCache.ts
+++ b/packages/server/src/server/api/v1/caches/apiContactsCache.ts
@@ -6,8 +6,6 @@ const contacts = require("node-mac-contacts");
export class ApiContactsCache {
contacts: any[] | null = null;
- recentlyUpdated = false;
-
getApiContacts() {
// If we aren't authorized, return an empty array without setting this.contacts.
// This way, if a permission is changed from Denied -> Authorized, we can still
diff --git a/packages/server/src/server/api/v1/interfaces/backupsInterface.ts b/packages/server/src/server/api/v1/interfaces/backupsInterface.ts
index bf18cc26..2448432e 100644
--- a/packages/server/src/server/api/v1/interfaces/backupsInterface.ts
+++ b/packages/server/src/server/api/v1/interfaces/backupsInterface.ts
@@ -18,6 +18,16 @@ export class BackupsInterface {
fs.writeFileSync(themePath, JSON.stringify(data));
}
+ static async deleteTheme(name: string): Promise {
+ const saniName = `${slugify(name)}.json`;
+ const themePath = path.join(FileSystem.themesDir, saniName);
+
+ // Delete the file if it exists
+ if (fs.existsSync(themePath)) {
+ fs.unlinkSync(themePath);
+ }
+ }
+
static async getThemeByName(name: string): Promise {
const saniName = `${slugify(name)}.json`;
const themePath = path.join(FileSystem.themesDir, saniName);
@@ -62,6 +72,16 @@ export class BackupsInterface {
fs.writeFileSync(settingsPath, JSON.stringify(data));
}
+ static async deleteSettings(name: string): Promise {
+ const saniName = `${slugify(name)}.json`;
+ const settingsPath = path.join(FileSystem.settingsDir, saniName);
+
+ // Delete the file if it exists
+ if (fs.existsSync(settingsPath)) {
+ fs.unlinkSync(settingsPath);
+ }
+ }
+
static async getSettingsByName(name: string): Promise {
const saniName = `${slugify(name)}.json`;
const settingsPath = path.join(FileSystem.settingsDir, saniName);
diff --git a/packages/server/src/server/api/v1/interfaces/contactInterface.ts b/packages/server/src/server/api/v1/interfaces/contactInterface.ts
index 635009ee..b6b840ce 100644
--- a/packages/server/src/server/api/v1/interfaces/contactInterface.ts
+++ b/packages/server/src/server/api/v1/interfaces/contactInterface.ts
@@ -166,7 +166,7 @@ export class ContactInterface {
// Compensate for if `avatar` is passed instead of contactImage
if (extraProps.includes("avatar")) {
if (!extraProps.includes("contactImage") && !extraProps.includes("contactThumbnailImage")) {
- extraProps.push("contactThumbnailImage");
+ extraProps.push("contactImage");
}
extraProps = extraProps.filter(e => e !== "avatar");
diff --git a/packages/server/src/server/api/v1/interfaces/messageInterface.ts b/packages/server/src/server/api/v1/interfaces/messageInterface.ts
index dcb0fb74..49245c3f 100644
--- a/packages/server/src/server/api/v1/interfaces/messageInterface.ts
+++ b/packages/server/src/server/api/v1/interfaces/messageInterface.ts
@@ -13,7 +13,8 @@ import type {
SendMessagePrivateApiParams,
SendReactionParams,
UnsendMessageParams,
- EditMessageParams
+ EditMessageParams,
+ SendAttachmentPrivateApiParams
} from "@server/api/v1/types";
export class MessageInterface {
@@ -109,17 +110,26 @@ export class MessageInterface {
chatGuid,
attachmentPath,
attachmentName = null,
- attachmentGuid = null
+ attachmentGuid = null,
+ method = 'apple-script',
+ attributedBody = null,
+ subject = null,
+ effectId = null,
+ selectedMessageGuid = null,
+ partIndex = 0,
+ isAudioMessage = false
}: SendAttachmentParams): Promise {
if (!chatGuid) throw new Error("No chat GUID provided");
// Copy the attachment to a more permanent storage
- const newPath = FileSystem.copyAttachment(attachmentPath, attachmentName);
+ const newPath = FileSystem.copyAttachment(attachmentPath, attachmentName, method);
Server().log(`Sending attachment "${attachmentName}" to ${chatGuid}`, "debug");
// Make sure messages is open
- await FileSystem.startMessages();
+ if (method === 'apple-script') {
+ await FileSystem.startMessages();
+ }
// Since we convert mp3s to cafs we need to correct the name for the awaiter
let aName = attachmentName;
@@ -143,20 +153,52 @@ export class MessageInterface {
);
Server().messageManager.add(awaiter);
- // Send the message
- await ActionHandler.sendMessageHandler(chatGuid, "", newPath);
- const ret = await awaiter.promise;
+ let sentMessage = null;
+ if (method === "apple-script") {
+ // Attempt to send the attachment
+ await ActionHandler.sendMessageHandler(chatGuid, "", newPath);
+ sentMessage = await awaiter.promise;
+ } else if (method === "private-api") {
+ sentMessage = await MessageInterface.sendAttachmentPrivateApi({
+ chatGuid,
+ filePath: newPath,
+ attributedBody,
+ subject,
+ effectId,
+ selectedMessageGuid,
+ partIndex,
+ isAudioMessage
+ });
+
+ try {
+ // Wait for the promise so that we can confirm the message was sent.
+ // Wrapped in a try/catch because if the private API returned a sentMessage,
+ // we know it sent, and maybe it just took a while (longer than the timeout).
+ // Only wait for the promise if it's not sent yet.
+ if (sentMessage && !sentMessage.isSent) {
+ sentMessage = await awaiter.promise;
+ }
+ } catch (e) {
+ if (sentMessage) {
+ Server().log('Attachment sent via Private API, but message match failed', 'debug');
+ } else {
+ throw e;
+ }
+ }
+ } else {
+ throw new Error(`Invalid send method: ${method}`);
+ }
// Delete the attachment.
// Only if below Monterey. On Monterey, we store attachments
// within the iMessage App Support directory. When AppleScript sees this
// it _does not_ copy the attachment to a permanent location.
// This means that if we delete the attachment, it won't be downloadable anymore.
- if (!isMinMonterey) {
+ if (!isMinMonterey && method === "apple-script") {
fs.unlink(newPath, _ => null);
}
- return ret;
+ return sentMessage;
}
static async sendMessagePrivateApi({
@@ -199,6 +241,60 @@ export class MessageInterface {
return retMessage;
}
+ static async sendAttachmentPrivateApi({
+ chatGuid,
+ filePath,
+ attributedBody = null,
+ subject = null,
+ effectId = null,
+ selectedMessageGuid = null,
+ partIndex = 0,
+ isAudioMessage = false
+ }: SendAttachmentPrivateApiParams): Promise {
+ checkPrivateApiStatus();
+
+ if (filePath.endsWith(".mp3")) {
+ try {
+ const newPath = `${filePath.substring(0, filePath.length - 4)}.caf`;
+ await FileSystem.convertMp3ToCaf(filePath, newPath);
+ filePath = newPath;
+ } catch (ex) {
+ Server().log("Failed to convert MP3 to CAF!", "warn");
+ }
+ }
+
+ const result = await Server().privateApiHelper.sendAttachment({
+ chatGuid,
+ filePath,
+ attributedBody,
+ subject,
+ effectId,
+ selectedMessageGuid,
+ partIndex,
+ isAudioMessage
+ });
+
+ if (!result?.identifier) {
+ throw new Error("Failed to send attachment!");
+ }
+
+ const maxWaitMs = 30000;
+ const retMessage = await resultAwaiter({
+ maxWaitMs,
+ getData: async _ => {
+ return await Server().iMessageRepo.getMessage(result.identifier, true, false);
+ }
+ });
+
+ // Check if the name changed
+ if (!retMessage) {
+ throw new Error(
+ `Failed to send attachment! Attachment not found in database after ${maxWaitMs / 1000} seconds!`);
+ }
+
+ return retMessage;
+ }
+
static async unsendMessage({ chatGuid, messageGuid, partIndex = 0 }: UnsendMessageParams) {
checkPrivateApiStatus();
const msg = await Server().iMessageRepo.getMessage(messageGuid, false, false);
diff --git a/packages/server/src/server/api/v1/serializers/MessageSerializer.ts b/packages/server/src/server/api/v1/serializers/MessageSerializer.ts
index 776524ac..a73af211 100644
--- a/packages/server/src/server/api/v1/serializers/MessageSerializer.ts
+++ b/packages/server/src/server/api/v1/serializers/MessageSerializer.ts
@@ -103,6 +103,10 @@ export class MessageSerializer {
// If we've reached out max size, we need to clear the participants
if (len > config.maxSizeBytes) {
+ Server().log(
+ `MessageSerializer: Max size reached (${config.maxSizeBytes} bytes). Clearing participants.`,
+ 'debug'
+ );
for (let i = 0; i < messageResponses.length; i++) {
for (let c = 0; c < (messageResponses[i]?.chats ?? []).length; c++) {
if (isEmpty(messageResponses[i].chats[c].participants)) continue;
diff --git a/packages/server/src/server/api/v1/types/index.ts b/packages/server/src/server/api/v1/types/index.ts
index fb2839fc..ef749a60 100644
--- a/packages/server/src/server/api/v1/types/index.ts
+++ b/packages/server/src/server/api/v1/types/index.ts
@@ -23,6 +23,17 @@ export type SendMessagePrivateApiParams = {
partIndex?: number;
};
+export type SendAttachmentPrivateApiParams = {
+ chatGuid: string;
+ filePath: string;
+ isAudioMessage?: boolean;
+ attributedBody?: Record | null;
+ subject?: string;
+ effectId?: string;
+ selectedMessageGuid?: string;
+ partIndex?: number;
+};
+
export type UnsendMessageParams = {
chatGuid: string;
messageGuid: string;
@@ -39,9 +50,16 @@ export type EditMessageParams = {
export type SendAttachmentParams = {
chatGuid: string;
+ method?: string;
attachmentPath: string;
attachmentName?: string;
attachmentGuid?: string;
+ isAudioMessage?: boolean;
+ attributedBody?: Record | null;
+ subject?: string;
+ effectId?: string;
+ selectedMessageGuid?: string;
+ partIndex?: number;
};
export type SendReactionParams = {
diff --git a/packages/server/src/server/databases/imessage/index.ts b/packages/server/src/server/databases/imessage/index.ts
index 804bce06..b5e76b2e 100644
--- a/packages/server/src/server/databases/imessage/index.ts
+++ b/packages/server/src/server/databases/imessage/index.ts
@@ -1,5 +1,5 @@
/* eslint-disable no-param-reassign */
-import { Brackets, DataSource } from "typeorm";
+import { Brackets, DataSource, SelectQueryBuilder } from "typeorm";
import { DBMessageParams, ChatParams, HandleParams } from "@server/databases/imessage/types";
import { convertDateTo2001Time } from "@server/databases/imessage/helpers/dateUtil";
@@ -256,18 +256,7 @@ export class MessageRepository {
}
if (after || before) {
- query.andWhere(
- new Brackets(qb => {
- if (after)
- qb.andWhere("message.date >= :after", {
- after: convertDateTo2001Time(after as Date)
- });
- if (before)
- qb.andWhere("message.date <= :before", {
- before: convertDateTo2001Time(before as Date)
- });
- })
- );
+ this.applyMessageDateQuery(query, after as Date, before as Date);
}
// Add pagination params
@@ -347,65 +336,7 @@ export class MessageRepository {
// Add date_delivered constraints
if (after || before) {
- query.andWhere(
- new Brackets(qb => {
- qb.orWhere(
- new Brackets(qb2 => {
- if (after)
- qb2.andWhere("message.date_delivered >= :after", {
- after: convertDateTo2001Time(after as Date)
- });
- if (before)
- qb2.andWhere("message.date_delivered <= :before", {
- before: convertDateTo2001Time(before as Date)
- });
- })
- );
-
- qb.orWhere(
- new Brackets(qb2 => {
- if (after)
- qb2.andWhere("message.date_read >= :after", {
- after: convertDateTo2001Time(after as Date)
- });
- if (before)
- qb2.andWhere("message.date_read <= :before", {
- before: convertDateTo2001Time(before as Date)
- });
- })
- );
-
- if (isMinVentura) {
- qb.orWhere(
- new Brackets(qb2 => {
- if (after)
- qb2.andWhere("message.date_edited >= :after", {
- after: convertDateTo2001Time(after as Date)
- });
- if (before)
- qb2.andWhere("message.date_edited <= :before", {
- before: convertDateTo2001Time(before as Date)
- });
- })
- );
- }
-
- if (isMinVentura) {
- qb.orWhere(
- new Brackets(qb2 => {
- if (after)
- qb2.andWhere("message.date_retracted >= :after", {
- after: convertDateTo2001Time(after as Date)
- });
- if (before)
- qb2.andWhere("message.date_retracted <= :before", {
- before: convertDateTo2001Time(before as Date)
- });
- })
- );
- }
- })
- );
+ this.applyMessageUpdateDateQuery(query, after as Date, before as Date);
}
// Add pagination params
@@ -440,35 +371,13 @@ export class MessageRepository {
if (isFromMe) query.andWhere("message.is_from_me = 1");
- // Add date restraints
- if (updated) {
- if (after)
- query.andWhere("message.date_delivered >= :after", {
- after: convertDateTo2001Time(after as Date)
- });
- if (before)
- query.andWhere("message.date_delivered < :before", {
- before: convertDateTo2001Time(before as Date)
- });
-
- // Add date_read constraints
- if (after)
- query.orWhere("message.date_read >= :after", {
- after: convertDateTo2001Time(after as Date)
- });
- if (before)
- query.andWhere("message.date_read < :before", {
- before: convertDateTo2001Time(before as Date)
- });
- } else {
- if (after)
- query.andWhere("message.date >= :after", {
- after: convertDateTo2001Time(after)
- });
- if (before)
- query.andWhere("message.date < :before", {
- before: convertDateTo2001Time(before)
- });
+ // Add date constraints
+ if (after || before) {
+ if (updated) {
+ this.applyMessageUpdateDateQuery(query, after, before);
+ } else {
+ this.applyMessageDateQuery(query, after, before);
+ }
}
// Add pagination params
@@ -579,6 +488,83 @@ export class MessageRepository {
return isEmpty(result) ? null : result[0].filename;
}
+ applyMessageDateQuery(query: SelectQueryBuilder, after?: Date, before?: Date) {
+ query.andWhere(
+ new Brackets(qb => {
+ if (after)
+ qb.andWhere("message.date >= :after", {
+ after: convertDateTo2001Time(after)
+ });
+ if (before)
+ qb.andWhere("message.date <= :before", {
+ before: convertDateTo2001Time(before)
+ });
+ })
+ );
+ }
+
+ applyMessageUpdateDateQuery(query: SelectQueryBuilder, after?: Date, before?: Date) {
+ query.andWhere(
+ new Brackets(qb => {
+ qb.orWhere(
+ new Brackets(qb2 => {
+ if (after)
+ qb2.andWhere("message.date_delivered >= :after", {
+ after: convertDateTo2001Time(after)
+ });
+ if (before)
+ qb2.andWhere("message.date_delivered <= :before", {
+ before: convertDateTo2001Time(before)
+ });
+ })
+ );
+
+ qb.orWhere(
+ new Brackets(qb2 => {
+ if (after)
+ qb2.andWhere("message.date_read >= :after", {
+ after: convertDateTo2001Time(after)
+ });
+ if (before)
+ qb2.andWhere("message.date_read <= :before", {
+ before: convertDateTo2001Time(before)
+ });
+ })
+ );
+
+ if (isMinVentura) {
+ qb.orWhere(
+ new Brackets(qb2 => {
+ if (after)
+ qb2.andWhere("message.date_edited >= :after", {
+ after: convertDateTo2001Time(after)
+ });
+ if (before)
+ qb2.andWhere("message.date_edited <= :before", {
+ before: convertDateTo2001Time(before)
+ });
+ })
+ );
+ }
+
+ if (isMinVentura) {
+ qb.orWhere(
+ new Brackets(qb2 => {
+ if (after)
+ qb2.andWhere("message.date_retracted >= :after", {
+ after: convertDateTo2001Time(after)
+ });
+ if (before)
+ qb2.andWhere("message.date_retracted <= :before", {
+ before: convertDateTo2001Time(before)
+ });
+ })
+ );
+ }
+ })
+ );
+ }
+
/**
* Gets message counts associated with a chat
*
diff --git a/packages/server/src/server/databases/imessage/listeners/incomingMessageListener.ts b/packages/server/src/server/databases/imessage/listeners/incomingMessageListener.ts
index 7db8bee1..f8ed01cd 100644
--- a/packages/server/src/server/databases/imessage/listeners/incomingMessageListener.ts
+++ b/packages/server/src/server/databases/imessage/listeners/incomingMessageListener.ts
@@ -17,18 +17,15 @@ export class IncomingMessageListener extends MessageChangeListener {
async getEntries(after: Date, before: Date): Promise {
// Offset 15 seconds to account for the "Apple" delay
- const offsetDate = new Date(after.getTime() - 15000);
- const query = [
- {
- statement: "message.is_from_me = :fromMe",
- args: { fromMe: 0 }
- }
- ];
-
const entries = await this.repo.getMessages({
- after: offsetDate,
+ after: new Date(after.getTime() - 15000),
withChats: true,
- where: query
+ where: [
+ {
+ statement: "message.is_from_me = :fromMe",
+ args: { fromMe: 0 }
+ }
+ ]
});
// Emit the new message
diff --git a/packages/server/src/server/databases/imessage/listeners/outgoingMessageListener.ts b/packages/server/src/server/databases/imessage/listeners/outgoingMessageListener.ts
index d96ba78a..86f3453e 100644
--- a/packages/server/src/server/databases/imessage/listeners/outgoingMessageListener.ts
+++ b/packages/server/src/server/databases/imessage/listeners/outgoingMessageListener.ts
@@ -37,11 +37,11 @@ export class OutgoingMessageListener extends MessageChangeListener {
* @param before The time right before get Entries run
*/
async getEntries(after: Date, before: Date): Promise {
- // Second, emit the outgoing messages (lookback 15 seconds to make up for the "Apple" delay)
+ // First, emit the outgoing messages (lookback 15 seconds to make up for the "Apple" delay)
const afterOffsetDate = new Date(after.getTime() - 15000);
await this.emitOutgoingMessages(afterOffsetDate);
- // Third, check for updated messages
+ // Second, check for updated messages
const afterUpdateOffsetDate = new Date(after.getTime() - this.pollFrequency - 15000);
await this.emitUpdatedMessages(afterUpdateOffsetDate);
}
diff --git a/packages/server/src/server/databases/server/constants.ts b/packages/server/src/server/databases/server/constants.ts
index 02f16a56..1744c703 100644
--- a/packages/server/src/server/databases/server/constants.ts
+++ b/packages/server/src/server/databases/server/constants.ts
@@ -23,5 +23,6 @@ export const DEFAULT_DB_ITEMS: { [key: string]: () => any } = {
use_oled_dark_mode: () => 0,
db_poll_interval: () => 1000,
dock_badge: () => 1,
- facetime_detection: () => (Server().hasAccessibilityAccess ? 1 : 0)
+ facetime_detection: () => (Server().hasAccessibilityAccess ? 1 : 0),
+ start_minimized: () => 0
};
diff --git a/packages/server/src/server/fileSystem/index.ts b/packages/server/src/server/fileSystem/index.ts
index 5cedd764..e4691e36 100644
--- a/packages/server/src/server/fileSystem/index.ts
+++ b/packages/server/src/server/fileSystem/index.ts
@@ -171,9 +171,9 @@ export class FileSystem {
* @param name Name for the attachment
* @param buffer The attachment bytes (buffer)
*/
- static copyAttachment(originalPath: string, name: string): string {
+ static copyAttachment(originalPath: string, name: string, method = 'apple-script'): string {
let newPath = path.join(FileSystem.attachmentsDir, name);
- if (isMinMonterey) {
+ if (isMinMonterey || method === 'private-api') {
if (!fs.existsSync(FileSystem.messagesAttachmentsDir)) fs.mkdirSync(FileSystem.messagesAttachmentsDir);
newPath = path.join(FileSystem.messagesAttachmentsDir, name);
} else {
diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts
index e6978258..7a4d5311 100644
--- a/packages/server/src/server/index.ts
+++ b/packages/server/src/server/index.ts
@@ -52,7 +52,8 @@ import {
isMinMojave,
isMinMonterey,
isMinSierra,
- isNotEmpty
+ isNotEmpty,
+ waitMs
} from "./helpers/utils";
import { Proxy } from "./services/proxyServices/proxy";
import { BlueBubblesHelperService } from "./services/privateApi";
@@ -421,7 +422,7 @@ class BlueBubblesServer extends EventEmitter {
try {
await this.startProxyServices();
} catch (ex: any) {
- this.log(`Failed to connect to Ngrok! ${ex.message}`, "error");
+ this.log(`Failed to connect to proxy service! ${ex.message}`, "error");
}
try {
@@ -581,6 +582,9 @@ class BlueBubblesServer extends EventEmitter {
// After setup is complete, start the update checker
try {
+ // Wait 5 seconds before starting the update checker.
+ // This is just to not use up too much CPU on startup
+ await waitMs(5000);
this.log("Initializing Update Service..");
this.updater = new UpdateService(this.window);
@@ -683,6 +687,20 @@ class BlueBubblesServer extends EventEmitter {
// Set the dock icon according to the config
this.setDockIcon();
+ // Start minimized if enabled
+ const startMinimized = Server().repo.getConfig("start_minimized") as boolean;
+ if (startMinimized) {
+ this.window.minimize();
+ }
+
+ // Disable the encryp coms setting if it's enabled.
+ // This is a temporary fix until the android client supports it again.
+ const encryptComs = Server().repo.getConfig("encrypt_coms") as boolean;
+ if (encryptComs) {
+ this.log("Disabling encrypt coms setting...");
+ Server().repo.setConfig("encrypt_coms", false);
+ }
+
try {
// Restart via terminal if configured
const restartViaTerminal = Server().repo.getConfig("start_via_terminal") as boolean;
diff --git a/packages/server/src/server/services/facetimeService/index.ts b/packages/server/src/server/services/facetimeService/index.ts
index cc7eb133..9913ee56 100644
--- a/packages/server/src/server/services/facetimeService/index.ts
+++ b/packages/server/src/server/services/facetimeService/index.ts
@@ -5,7 +5,7 @@ import {
} from "@server/api/v1/apple/scripts";
import { Server } from "@server/index";
import { FileSystem } from "@server/fileSystem";
-import { isMinBigSur, isMinVentura, isNotEmpty } from "@server/helpers/utils";
+import { isMinBigSur, isMinVentura, isNotEmpty, waitMs } from "@server/helpers/utils";
import { INCOMING_FACETIME } from "@server/events";
export class FacetimeService {
@@ -32,12 +32,17 @@ export class FacetimeService {
this.notificationSent = false;
}
- async listen(): Promise {
+ async listen({ delay = 30000 } = {}): Promise {
if (this.isRunning) {
Server().log("Facetime listener already running");
return this.serviceAwaiter;
}
+ if (delay && delay > 0) {
+ Server().log(`Delaying FaceTime listener start by ${delay} ms...`);
+ await waitMs(delay);
+ }
+
Server().log("Starting FaceTime listener...");
this.flush();
@@ -120,7 +125,7 @@ export class FacetimeService {
this.hadPreviousCall = this.isGettingCall;
}
- setTimeout(serviceLoop, 4000);
+ setTimeout(serviceLoop, 5000);
};
setTimeout(serviceLoop, 0);
diff --git a/packages/server/src/server/services/findMyService/index.ts b/packages/server/src/server/services/findMyService/index.ts
index 86287bc1..630b1eda 100644
--- a/packages/server/src/server/services/findMyService/index.ts
+++ b/packages/server/src/server/services/findMyService/index.ts
@@ -1,15 +1,24 @@
import path from "path";
import fs from "fs";
import { FileSystem } from "@server/fileSystem";
-import { hideFindMyFriends, showApp, startFindMyFriends } from "@server/api/v1/apple/scripts";
+import {
+ hideFindMyFriends,
+ startFindMyFriends,
+ showFindMyFriends,
+ quitFindMyFriends
+} from "@server/api/v1/apple/scripts";
import { waitMs } from "@server/helpers/utils";
import { Server } from "@server";
+import { FindMyDevice, FindMyItem } from "@server/services/findMyService/types";
+import { transformFindMyItemToDevice } from "@server/services/findMyService/utils";
/**
* This services manages the connection to the connected
* Google FCM server. This is used to handle/manage notifications
*/
export class FindMyService {
+ // Unix timestamp in milliseconds
+ static quitAppTime = 0;
private static cacheFileExists(guid: string): boolean {
const cPath = path.join(FileSystem.findMyFriendsDir, "fsCachedData", guid);
@@ -19,12 +28,12 @@ export class FindMyService {
private static readCacheFile(guid: string): NodeJS.Dict | null {
if (!FindMyService.cacheFileExists(guid)) return null;
const cPath = path.join(FileSystem.findMyFriendsDir, "fsCachedData", guid);
- const data = fs.readFileSync(cPath, { encoding: 'utf-8' });
+ const data = fs.readFileSync(cPath, { encoding: "utf-8" });
try {
return JSON.parse(data);
} catch {
- throw new Error('Failed to read FindMy cache file! It is not in the correct format!');
+ throw new Error("Failed to read FindMy cache file! It is not in the correct format!");
}
}
@@ -36,7 +45,7 @@ export class FindMyService {
// return null because we want to indicate it's not capable
if (!db) return null;
- // Get the references
+ // Get the references
const cacheRef = await Server().findMyRepo.getLatestCacheReference();
if (!cacheRef) return null;
@@ -50,23 +59,55 @@ export class FindMyService {
return await FindMyService.getFriends();
}
- static getDevices(): NodeJS.Dict | null {
- const devicesPath = path.join(FileSystem.findMyDir, 'Devices.data');
- if (!fs.existsSync(devicesPath)) return null;
-
- const data = fs.readFileSync(devicesPath, { encoding: 'utf-8' });
+ private static readDataFile(
+ type: T
+ ): Promise | null> {
+ const devicesPath = path.join(FileSystem.findMyDir, `${type}.data`);
+ return new Promise((resolve, reject) => {
+ fs.readFile(devicesPath, { encoding: "utf-8" }, (err, data) => {
+ // Couldn't read the file
+ if (err) return resolve(null);
+
+ try {
+ return resolve(JSON.parse(data));
+ } catch {
+ reject(new Error(`Failed to read FindMy ${type} cache file! It is not in the correct format!`));
+ }
+ });
+ });
+ }
+ static async getDevices(): Promise | null> {
try {
- return JSON.parse(data);
+ const [devices, items] = await Promise.all([
+ FindMyService.readDataFile("Devices"),
+ FindMyService.readDataFile("Items")
+ ]);
+
+ // Return null if neither of the files exist
+ if (!devices && !items) return null;
+
+ // Transform the items to match the same shape as devices
+ const transformedItems = (items ?? []).map(transformFindMyItemToDevice);
+
+ return [...(devices ?? []), ...transformedItems];
} catch {
- throw new Error('Failed to read FindMy cache file! It is not in the correct format!');
+ return null;
}
}
static async refresh(): Promise {
- const devicesPath = path.join(FileSystem.findMyDir, 'Devices.data');
+ const devicesPath = path.join(FileSystem.findMyDir, "Devices.data");
if (!fs.existsSync(devicesPath)) return null;
+ // Quit the FindMy app if it's been more than 2 minutes since the last refresh
+ const now = new Date().getTime();
+ if (now - FindMyService.quitAppTime > 120_000) {
+ FindMyService.quitAppTime = now;
+ await FileSystem.executeAppleScript(quitFindMyFriends());
+ await waitMs(3000);
+ }
+
// Make sure the Find My app is open.
// Give it 3 seconds to open
await FileSystem.executeAppleScript(startFindMyFriends());
@@ -74,22 +115,20 @@ export class FindMyService {
// Bring the Find My app to the foreground so it refreshes the devices
// Give it 5 seconods to refresh
- await FileSystem.executeAppleScript(showApp('FindMy'));
+ await FileSystem.executeAppleScript(showFindMyFriends());
await waitMs(5000);
// Re-hide the Find My App
- await FileSystem.executeAppleScript(hideFindMyFriends());
+ await FileSystem.executeAppleScript(hideFindMyFriends());
}
static async refreshDevices(): Promise | null> {
- const devicesPath = path.join(FileSystem.findMyDir, 'Devices.data');
+ const devicesPath = path.join(FileSystem.findMyDir, "Devices.data");
if (!fs.existsSync(devicesPath)) return null;
await FindMyService.refresh();
// Get the new locations
- return FindMyService.getDevices();
+ return await FindMyService.getDevices();
}
-
-
}
diff --git a/packages/server/src/server/services/findMyService/types.ts b/packages/server/src/server/services/findMyService/types.ts
new file mode 100644
index 00000000..26c9f4a6
--- /dev/null
+++ b/packages/server/src/server/services/findMyService/types.ts
@@ -0,0 +1,123 @@
+export interface FindMyDevice {
+ deviceModel?: string;
+ lowPowerMode?: unknown;
+ passcodeLength?: number;
+ itemGroup?: unknown;
+ id?: string;
+ batteryStatus?: string;
+ audioChannels?: Array;
+ lostModeCapable?: unknown;
+ snd?: unknown;
+ batteryLevel?: number;
+ locationEnabled?: unknown;
+ isConsideredAccessory?: unknown;
+ address?: FindMyAddress;
+ location?: FindMyLocation;
+ modelDisplayName?: string;
+ deviceColor?: unknown;
+ activationLocked?: unknown;
+ rm2State?: number;
+ locFoundEnabled?: unknown;
+ nwd?: unknown;
+ deviceStatus?: string;
+ remoteWipe?: unknown;
+ fmlyShare?: unknown;
+ thisDevice?: unknown;
+ lostDevice?: unknown;
+ lostModeEnabled?: unknown;
+ deviceDisplayName?: string;
+ safeLocations?: Array;
+ name?: string;
+ canWipeAfterLock?: unknown;
+ isMac?: unknown;
+ rawDeviceModel?: string;
+ baUuid?: string;
+ trackingInfo?: unknown;
+ features?: Record;
+ deviceDiscoveryId?: string;
+ prsId?: string;
+ scd?: unknown;
+ locationCapable?: unknown;
+ remoteLock?: unknown;
+ wipeInProgress?: unknown;
+ darkWake?: unknown;
+ deviceWithYou?: unknown;
+ maxMsgChar?: number;
+ deviceClass?: string;
+ crowdSourcedLocation: FindMyLocation;
+
+ // Extra properties from FindMyItem
+ role?: FindMyItem["role"];
+ serialNumber?: string;
+ lostModeMetadata?: FindMyItem["lostModeMetadata"]
+}
+
+export interface FindMyItem {
+ partInfo?: unknown;
+ isFirmwareUpdateMandatory: boolean;
+ productType: {
+ type: string;
+ productInformation: {
+ manufacturerName: string;
+ modelName: string;
+ productIdentifier: number;
+ vendorIdentifier: number;
+ antennaPower: number;
+ };
+ };
+ safeLocations?: Array;
+ owner: string;
+ batteryStatus: number;
+ serialNumber: string;
+ lostModeMetadata?: null | {
+ email: string;
+ message: string;
+ ownerNumber: string;
+ timestamp: number;
+ };
+ capabilities: number;
+ identifier: string;
+ address: FindMyAddress;
+ location: FindMyLocation;
+ productIdentifier: string;
+ isAppleAudioAccessory: false;
+ crowdSourcedLocation: FindMyLocation;
+ groupIdentifier: null;
+ role: {
+ name: string;
+ emoji: string;
+ identifier: number;
+ };
+ systemVersion: string;
+ name: string;
+}
+
+interface FindMyAddress {
+ subAdministrativeArea?: string;
+ label?: string;
+ streetAddress?: string;
+ countryCode?: string;
+ stateCode?: string;
+ administrativeArea?: string;
+ streetName?: string;
+ formattedAddressLines: Array;
+ mapItemFullAddress?: string;
+ fullThroroughfare?: string;
+ areaOfInterest?: Array;
+ locality?: string;
+ country?: string;
+}
+
+interface FindMyLocation {
+ positionType?: string;
+ verticalAccuracy?: number;
+ longitude?: number;
+ floorLevel?: number;
+ isInaccurate?: boolean;
+ isOld?: boolean;
+ horizontalAccuracy?: number;
+ latitude?: number;
+ timeStamp?: number;
+ altitude?: number;
+ locationFinished?: boolean;
+}
diff --git a/packages/server/src/server/services/findMyService/utils.ts b/packages/server/src/server/services/findMyService/utils.ts
new file mode 100644
index 00000000..9ac096e8
--- /dev/null
+++ b/packages/server/src/server/services/findMyService/utils.ts
@@ -0,0 +1,38 @@
+import { FindMyItem, FindMyDevice } from "@server/services/findMyService/types";
+
+export const getFindMyItemModelDisplayName = (item: FindMyItem): string => {
+ if (item.productType.type === "b389") return "AirTag";
+
+ return item.productType.productInformation.modelName || item.productType.type;
+};
+
+export const transformFindMyItemToDevice = (item: FindMyItem): FindMyDevice => ({
+ deviceModel: item.productType.type,
+ id: item.identifier,
+ batteryStatus: "Unknown",
+ audioChannels: [],
+ lostModeCapable: true,
+ batteryLevel: item.batteryStatus,
+ locationEnabled: true,
+ isConsideredAccessory: true,
+ address: item.address,
+ location: item.location,
+ modelDisplayName: getFindMyItemModelDisplayName(item),
+ fmlyShare: false,
+ thisDevice: false,
+ lostModeEnabled: Boolean(item.lostModeMetadata),
+ deviceDisplayName: item.role.emoji,
+ safeLocations: item.safeLocations,
+ name: item.name,
+ isMac: false,
+ rawDeviceModel: item.productType.type,
+ prsId: "owner",
+ locationCapable: true,
+ deviceClass: item.productType.type,
+ crowdSourcedLocation: item.crowdSourcedLocation,
+
+ // Extras from FindMyItem
+ role: item.role,
+ serialNumber: item.serialNumber,
+ lostModeMetadata: item.lostModeMetadata
+});
diff --git a/packages/server/src/server/services/httpService/api/v1/httpRoutes.ts b/packages/server/src/server/services/httpService/api/v1/httpRoutes.ts
index 80594f5f..71bf7f79 100644
--- a/packages/server/src/server/services/httpService/api/v1/httpRoutes.ts
+++ b/packages/server/src/server/services/httpService/api/v1/httpRoutes.ts
@@ -34,6 +34,7 @@ import { ChatValidator } from "./validators/chatValidator";
import { AlertsValidator } from "./validators/alertsValidator";
import { ScheduledMessageValidator } from "./validators/scheduledMessageValidator";
import { ScheduledMessageRouter } from "./routers/scheduledMessageRouter";
+import { ThemeValidator } from "./validators/themeValidator";
export class HttpRoutes {
static version = 1;
@@ -444,8 +445,15 @@ export class HttpRoutes {
{
method: HttpMethod.POST,
path: "theme",
+ validators: [ThemeValidator.validate],
controller: ThemeRouter.create
},
+ {
+ method: HttpMethod.DELETE,
+ path: "theme",
+ validators: [ThemeValidator.validateDelete],
+ controller: ThemeRouter.delete
+ },
{
method: HttpMethod.GET,
path: "settings",
@@ -456,6 +464,12 @@ export class HttpRoutes {
path: "settings",
validators: [SettingsValidator.validate],
controller: SettingsRouter.create
+ },
+ {
+ method: HttpMethod.DELETE,
+ path: "settings",
+ validators: [SettingsValidator.validateDelete],
+ controller: SettingsRouter.delete
}
]
}
diff --git a/packages/server/src/server/services/httpService/api/v1/routers/messageRouter.ts b/packages/server/src/server/services/httpService/api/v1/routers/messageRouter.ts
index f4a98a17..cdbd874b 100644
--- a/packages/server/src/server/services/httpService/api/v1/routers/messageRouter.ts
+++ b/packages/server/src/server/services/httpService/api/v1/routers/messageRouter.ts
@@ -240,7 +240,17 @@ export class MessageRouter {
static async sendAttachment(ctx: RouterContext, _: Next) {
const { files } = ctx.request;
- const { tempGuid, chatGuid, name } = ctx.request?.body ?? {};
+ const {
+ tempGuid,
+ chatGuid,
+ name,
+ method,
+ subject,
+ selectedMessageGuid,
+ partIndex,
+ effectId,
+ isAudioMessage
+ } = ctx.request?.body ?? {};
const attachment = files?.attachment as File;
// Add to send cache
@@ -252,7 +262,13 @@ export class MessageRouter {
chatGuid,
attachmentPath: attachment.path,
attachmentName: name,
- attachmentGuid: tempGuid
+ attachmentGuid: tempGuid,
+ method,
+ isAudioMessage,
+ subject,
+ effectId,
+ selectedMessageGuid,
+ partIndex
});
// Remove from cache
diff --git a/packages/server/src/server/services/httpService/api/v1/routers/settingsRouter.ts b/packages/server/src/server/services/httpService/api/v1/routers/settingsRouter.ts
index 4f3c09cd..a90bd5eb 100644
--- a/packages/server/src/server/services/httpService/api/v1/routers/settingsRouter.ts
+++ b/packages/server/src/server/services/httpService/api/v1/routers/settingsRouter.ts
@@ -3,15 +3,11 @@ import { Next } from "koa";
import { isNotEmpty } from "@server/helpers/utils";
import { BackupsInterface } from "@server/api/v1/interfaces/backupsInterface";
-import { ValidateInput, ValidateJSON } from "../validators";
-import { SettingsValidator } from "../validators/settingsValidator";
import { Success } from "../responses/success";
export class SettingsRouter {
static async create(ctx: RouterContext, _: Next) {
- // Validation
- const { name, data } = ValidateInput(ctx.request.body, SettingsValidator.rules);
- ValidateJSON(data, "Settings");
+ const { name, data } = ctx.request.body;
// Safety to always have a name in the JSON dict if not provided
if (!Object.keys(data).includes("name")) {
@@ -23,6 +19,15 @@ export class SettingsRouter {
return new Success(ctx, { message: "Successfully saved settings!" }).send();
}
+ static async delete(ctx: RouterContext, _: Next) {
+ const { name } = ctx.request.body;
+
+ // Save the settings to a file
+ await BackupsInterface.deleteSettings(name);
+ return new Success(ctx, { message: "Successfully deleted settings!" }).send();
+ }
+
+
static async get(ctx: RouterContext, _: Next) {
const name = ctx.query.name as string;
let res: any;
diff --git a/packages/server/src/server/services/httpService/api/v1/routers/themeRouter.ts b/packages/server/src/server/services/httpService/api/v1/routers/themeRouter.ts
index 2efda1b1..18abf456 100644
--- a/packages/server/src/server/services/httpService/api/v1/routers/themeRouter.ts
+++ b/packages/server/src/server/services/httpService/api/v1/routers/themeRouter.ts
@@ -19,6 +19,14 @@ export class ThemeRouter {
return new Success(ctx, { message: "Successfully saved theme!" }).send();
}
+ static async delete(ctx: RouterContext, _: Next) {
+ const { name } = ctx.request.body;
+
+ // Save the theme to a file
+ await BackupsInterface.deleteTheme(name);
+ return new Success(ctx, { message: "Successfully deleted theme!" }).send();
+ }
+
static async get(ctx: RouterContext, _: Next) {
const name = ctx.query.name as string;
let res: any;
diff --git a/packages/server/src/server/services/httpService/api/v1/validators/messageValidator.ts b/packages/server/src/server/services/httpService/api/v1/validators/messageValidator.ts
index 05426e91..b6d86fb8 100644
--- a/packages/server/src/server/services/httpService/api/v1/validators/messageValidator.ts
+++ b/packages/server/src/server/services/httpService/api/v1/validators/messageValidator.ts
@@ -112,13 +112,43 @@ export class MessageValidator {
static sendAttachmentRules = {
chatGuid: "required|string",
- tempGuid: "required|string",
- name: "required|string"
+ tempGuid: "string",
+ method: "string|in:apple-script,private-api",
+ name: "required|string",
+ isAudioMessage: "boolean",
+ effectId: "string",
+ subject: "string",
+ selectedMessageGuid: "string",
+ partIndex: "numeric|min:0"
};
static async validateAttachment(ctx: RouterContext, next: Next) {
const { files } = ctx.request;
- const { tempGuid } = ValidateInput(ctx.request?.body, MessageValidator.sendAttachmentRules);
+ const {
+ tempGuid,
+ method,
+ isAudioMessage,
+ effectId,
+ subject,
+ selectedMessageGuid
+ } = ValidateInput(ctx.request?.body, MessageValidator.sendAttachmentRules);
+
+ let saniMethod = method;
+ if (isAudioMessage || effectId || subject || selectedMessageGuid || ctx.request.body.attributedBody) {
+ saniMethod = "private-api";
+ }
+
+ // Default the method to AppleScript
+ saniMethod = saniMethod ?? "apple-script";
+
+ // If we are sending via apple-script, we require a tempGuid
+ if (saniMethod === "apple-script" && isEmpty(tempGuid)) {
+ throw new BadRequest({ error: `A 'tempGuid' is required when sending via AppleScript` });
+ }
+
+ // Inject the method (we have to force it to thing it's anything)
+ (ctx.request.body as any).method = saniMethod;
+ (ctx.request.body as any).isAudioMessage = isAudioMessage === 'true' ? true : false;
// Make sure the message isn't already in the queue
if (Server().httpService.sendCache.find(tempGuid)) {
diff --git a/packages/server/src/server/services/httpService/api/v1/validators/settingsValidator.ts b/packages/server/src/server/services/httpService/api/v1/validators/settingsValidator.ts
index a54752f2..66e95b78 100644
--- a/packages/server/src/server/services/httpService/api/v1/validators/settingsValidator.ts
+++ b/packages/server/src/server/services/httpService/api/v1/validators/settingsValidator.ts
@@ -8,9 +8,18 @@ export class SettingsValidator {
data: "required"
};
+ static deleteRules = {
+ name: "required|string|min:3|max:50",
+ };
+
static async validate(ctx: RouterContext, next: Next) {
const { data } = ValidateInput(ctx.request.body, SettingsValidator.rules);
- ValidateJSON(data, "Theme");
+ ValidateJSON(data, "Settings");
+ await next();
+ }
+
+ static async validateDelete(ctx: RouterContext, next: Next) {
+ ValidateInput(ctx.request.body, SettingsValidator.deleteRules);
await next();
}
}
diff --git a/packages/server/src/server/services/httpService/api/v1/validators/themeValidator.ts b/packages/server/src/server/services/httpService/api/v1/validators/themeValidator.ts
index 2703e6cf..d71f735c 100644
--- a/packages/server/src/server/services/httpService/api/v1/validators/themeValidator.ts
+++ b/packages/server/src/server/services/httpService/api/v1/validators/themeValidator.ts
@@ -8,9 +8,18 @@ export class ThemeValidator {
data: "required"
};
+ static deleteRules = {
+ name: "required|string|min:3|max:50"
+ };
+
static async validate(ctx: RouterContext, next: Next) {
const { data } = ValidateInput(ctx.request.body, ThemeValidator.rules);
ValidateJSON(data, "Theme");
await next();
}
+
+ static async validateDelete(ctx: RouterContext, next: Next) {
+ ValidateInput(ctx.request.body, ThemeValidator.deleteRules);
+ await next();
+ }
}
diff --git a/packages/server/src/server/services/privateApi/index.ts b/packages/server/src/server/services/privateApi/index.ts
index 765fc4d9..972b4e92 100644
--- a/packages/server/src/server/services/privateApi/index.ts
+++ b/packages/server/src/server/services/privateApi/index.ts
@@ -411,6 +411,46 @@ export class BlueBubblesHelperService {
);
}
+ async sendAttachment({
+ chatGuid,
+ filePath,
+ isAudioMessage = false,
+ attributedBody = null,
+ subject = null,
+ effectId = null,
+ selectedMessageGuid = null,
+ partIndex = 0
+ }: {
+ chatGuid: string,
+ filePath: string,
+ isAudioMessage?: boolean,
+ attributedBody?: Record | null;
+ subject?: string;
+ effectId?: string;
+ selectedMessageGuid?: string;
+ partIndex?: number;
+ }): Promise {
+ if (!chatGuid || !filePath) {
+ throw new Error("Failed to send attachment. Invalid params!");
+ }
+
+ const request = new TransactionPromise(TransactionType.ATTACHMENT);
+ return this.writeData(
+ "send-attachment",
+ {
+ chatGuid,
+ filePath,
+ isAudioMessage: isAudioMessage ? 1 : 0,
+ attributedBody,
+ subject,
+ effectId,
+ selectedMessageGuid,
+ partIndex
+ },
+ request
+ );
+ }
+
async addParticipant(chatGuid: string, address: string) {
return this.toggleParticipant(chatGuid, address, "add");
}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index ddbe4e51..677258af 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@bluebubbles/ui",
- "version": "1.5.2",
+ "version": "1.5.3",
"homepage": "./",
"license": "Apache-2.0",
"scripts": {
diff --git a/packages/ui/src/app/components/fields/StartMinimizedField.tsx b/packages/ui/src/app/components/fields/StartMinimizedField.tsx
new file mode 100644
index 00000000..73e8e476
--- /dev/null
+++ b/packages/ui/src/app/components/fields/StartMinimizedField.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import {
+ FormControl,
+ FormHelperText,
+ Checkbox,
+ Text
+} from '@chakra-ui/react';
+import { useAppSelector } from '../../hooks';
+import { onCheckboxToggle } from '../../actions/ConfigActions';
+
+export interface StartMinimizedFieldProps {
+ helpText?: string;
+}
+
+export const StartMinimizedField = ({ helpText }: StartMinimizedFieldProps): JSX.Element => {
+ const startMinimized: boolean = (useAppSelector(state => state.config.start_minimized) ?? false);
+
+ return (
+
+ Start Minimized
+
+ {helpText ?? (
+
+ When enabled, the BlueBubbles Server will be minimized after starting up.
+
+ )}
+
+
+ );
+};
+
diff --git a/packages/ui/src/app/layouts/settings/connection/ConnectionSettings.tsx b/packages/ui/src/app/layouts/settings/connection/ConnectionSettings.tsx
index 7df8788c..e5ec6db1 100644
--- a/packages/ui/src/app/layouts/settings/connection/ConnectionSettings.tsx
+++ b/packages/ui/src/app/layouts/settings/connection/ConnectionSettings.tsx
@@ -77,8 +77,8 @@ export const ConnectionSettings = (): JSX.Element => {
-
-
+ {/*
+ */}
{(proxyService === 'dynamic-dns') ? () : null}
diff --git a/packages/ui/src/app/layouts/settings/features/FeatureSettings.tsx b/packages/ui/src/app/layouts/settings/features/FeatureSettings.tsx
index 710b58b7..e3e53066 100644
--- a/packages/ui/src/app/layouts/settings/features/FeatureSettings.tsx
+++ b/packages/ui/src/app/layouts/settings/features/FeatureSettings.tsx
@@ -18,6 +18,7 @@ import { DockBadgeField } from '../../../components/fields/DockBadgeField';
import { HideDockIconField } from '../../../components/fields/HideDockIconField';
import { StartViaTerminalField } from '../../../components/fields/StartViaTerminalField';
import { FacetimeDetectionField } from '../../../components/fields/FacetimeDetectionField';
+import { StartMinimizedField } from '../../../components/fields/StartMinimizedField';
export const FeatureSettings = (): JSX.Element => {
@@ -35,6 +36,8 @@ export const FeatureSettings = (): JSX.Element => {
+
+