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 => { + +