diff --git a/package-lock.json b/package-lock.json index f5dcb943d..dcce054a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,11 @@ "workspaces": [ "packages/*" ], + "engines": { + "node": "20.11.x", + "npm": ">=10.x", + "python": ">=3.10.x" + }, "devDependencies": { "concurrently": "^7.0.0" } diff --git a/package.json b/package.json index ec60a70d0..66a69e727 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "url": "https://www.github.com/BlueBubblesApp/BlueBubbles-Server/issues" }, "homepage": "https://www.bluebubbles.app", - "devEngines": { + "engines": { "node": "20.11.x", "npm": ">=10.x", "python": ">=3.10.x" diff --git a/packages/server/appResources/private-api/macos11/BlueBubblesHelper.dylib b/packages/server/appResources/private-api/macos11/BlueBubblesHelper.dylib index 7f932fe0c..2aaa37594 100755 Binary files a/packages/server/appResources/private-api/macos11/BlueBubblesHelper.dylib and b/packages/server/appResources/private-api/macos11/BlueBubblesHelper.dylib differ diff --git a/packages/server/src/server/api/apple/scripts.ts b/packages/server/src/server/api/apple/scripts.ts index 6fe641f95..da18cadb8 100644 --- a/packages/server/src/server/api/apple/scripts.ts +++ b/packages/server/src/server/api/apple/scripts.ts @@ -65,7 +65,8 @@ const getServiceFromInput = (value: string) => { if (valSplit.length <= 1) return "iMessage"; // Otherwise, return the "first" index in the array, - return valSplit[0]; + const service = valSplit[0]; + return service === "any" ? "iMessage" : service; }; /** @@ -183,7 +184,7 @@ export const sendMessage = (chatGuid: string, message: string, attachment: strin // If the chat is to an individual, we need to make sure the number is formatted correctly if (chatGuid.includes(";-;")) { const strSplit = chatGuid.split(";-;"); - const service = strSplit[0]; + const service = strSplit[0] === "any" ? "iMessage" : strSplit[0]; const addr = strSplit[1]; chatGuid = `${service};-;${getiMessageAddressFormat(addr)}`; } diff --git a/packages/server/src/server/api/http/api/v1/routers/attachmentRouter.ts b/packages/server/src/server/api/http/api/v1/routers/attachmentRouter.ts index cff32ad0f..99f94b99a 100644 --- a/packages/server/src/server/api/http/api/v1/routers/attachmentRouter.ts +++ b/packages/server/src/server/api/http/api/v1/routers/attachmentRouter.ts @@ -84,7 +84,9 @@ export class AttachmentRouter { const qualities = ["good", "better", "best"]; if (!qualities.includes(quality as string)) { - throw new BadRequest({ error: `Invalid quality specified! Must be one of: ${qualities.join(', ')}` }); + throw new BadRequest({ + error: `Invalid quality specified! Must be one of: ${qualities.join(', ')}` + }); } opts.quality = quality as "good" | "better" | "best"; diff --git a/packages/server/src/server/api/http/api/v1/routers/contactRouter.ts b/packages/server/src/server/api/http/api/v1/routers/contactRouter.ts index 02e247d26..8cdb4c1f5 100644 --- a/packages/server/src/server/api/http/api/v1/routers/contactRouter.ts +++ b/packages/server/src/server/api/http/api/v1/routers/contactRouter.ts @@ -7,8 +7,11 @@ import { Success } from "../responses/success"; import { Contact } from "@server/databases/server/entity"; import { parseWithQuery } from "../utils"; import { BadRequest } from "../responses/errors"; +import { getLogger } from "@server/lib/logging/Loggable"; export class ContactRouter { + private static log = getLogger("ContactRouter"); + private static isAddressObject(data: any): boolean { return ( data && @@ -77,7 +80,7 @@ export class ContactRouter { }) ); } catch (ex: any) { - console.log(ex); + ContactRouter.log.error(ex); errors.push({ entry: item, error: ex?.message ?? String(ex) diff --git a/packages/server/src/server/api/http/api/v1/validators/webhookValidator.ts b/packages/server/src/server/api/http/api/v1/validators/webhookValidator.ts index 7d040a532..e41e93695 100644 --- a/packages/server/src/server/api/http/api/v1/validators/webhookValidator.ts +++ b/packages/server/src/server/api/http/api/v1/validators/webhookValidator.ts @@ -44,7 +44,9 @@ export class WebhookValidator { // Find the webhook value in the webhook events const webhookEvent = webhookEventOptions.find(e => e.value === event); if (!webhookEvent) { - throw new BadRequest({ error: `Invalid webhook event: ${event}! Webhook must be one of: ${WebhookValidator.webhookValues}` }); + throw new BadRequest({ + error: `Invalid webhook event: ${event}! Webhook must be one of: ${WebhookValidator.webhookValues}` + }); } // Update the event to the label diff --git a/packages/server/src/server/api/privateApi/eventHandlers/PrivateApiFindMyEventHandler.ts b/packages/server/src/server/api/privateApi/eventHandlers/PrivateApiFindMyEventHandler.ts index 57c7fbd28..92a589a12 100644 --- a/packages/server/src/server/api/privateApi/eventHandlers/PrivateApiFindMyEventHandler.ts +++ b/packages/server/src/server/api/privateApi/eventHandlers/PrivateApiFindMyEventHandler.ts @@ -33,7 +33,9 @@ export class PrivateApiFindMyEventHandler extends Loggable implements PrivateApi for (const item of added) { const handle = obfuscatedHandle(item?.handle); if (item?.coordinates[0] === 0 && item?.coordinates[1] === 0) { - this.log.debug(`Received FindMy ${titleCase(item.status)} (0, 0) Location Update for Handle: ${handle}`); + this.log.debug( + `Received FindMy ${titleCase(item.status)} (0, 0) Location Update for Handle: ${handle}` + ); } else { this.log.debug(`Received FindMy ${titleCase(item.status)} Location Update for Handle: ${handle}`); } diff --git a/packages/server/src/server/api/privateApi/modes/dylibPlugins/index.ts b/packages/server/src/server/api/privateApi/modes/dylibPlugins/index.ts index 483367ea2..f2d3838df 100644 --- a/packages/server/src/server/api/privateApi/modes/dylibPlugins/index.ts +++ b/packages/server/src/server/api/privateApi/modes/dylibPlugins/index.ts @@ -168,7 +168,7 @@ export abstract class DylibPlugin extends Loggable { try { await FileSystem.executeAppleScript(hideApp(this.parentApp)); } catch (ex) { - console.log(ex); + this.log.error(ex instanceof Error ? ex.message : String(ex)); // Don't do anything } } diff --git a/packages/server/src/server/databases/imessage/entity/Message.ts b/packages/server/src/server/databases/imessage/entity/Message.ts index 54f3260ed..13554a118 100644 --- a/packages/server/src/server/databases/imessage/entity/Message.ts +++ b/packages/server/src/server/databases/imessage/entity/Message.ts @@ -87,7 +87,7 @@ export class Message { return this.dateRetracted ?? this.dateEdited ?? this.dateRead ?? this.dateDelivered ?? this.dateCreated; } - get messageStatus(): String { + get messageStatus(): string { return this.dateRetracted ? "Unsent" : this.isFullyUnsent diff --git a/packages/server/src/server/databases/imessage/helpers/utils.ts b/packages/server/src/server/databases/imessage/helpers/utils.ts index fa4d88b89..153f1045f 100644 --- a/packages/server/src/server/databases/imessage/helpers/utils.ts +++ b/packages/server/src/server/databases/imessage/helpers/utils.ts @@ -19,7 +19,7 @@ export const getConversionPath = (attachment: Attachment, extension: string): st const newName = isEmpty(attachment.transferName) ? guid : attachment.transferName; // If the path already has the extension, return it - let newPath = `${newDir}/${newName}`; + const newPath = `${newDir}/${newName}`; if (newPath.endsWith(`.${extension}`)) return newPath; // Otherwise, return the path with the extension diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index d1ffa1bca..c4e315e4f 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -1497,9 +1497,18 @@ class BlueBubblesServer extends EventEmitter { } private async handleNewMessage(item: Message) { + // If this is a dummy message with a UUID body (often emitted when an audio message is played), ignore it. + const isUUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/i; + if (item.text && isUUID.test(item.text) && !item.cacheHasAttachments) { + this.logger.info(`Ignoring audio note play receipt with UUID body: ${item.text}`); + return; + } + const newMessage = await insertChatParticipants(item); this.logger.info( - `New Message from ${newMessage.isFromMe ? 'You' : obfuscatedHandle(newMessage.handle?.id)}, ${newMessage.contentString()}`); + `New Message from ${newMessage.isFromMe ? 'You' : obfuscatedHandle(newMessage.handle?.id)}, ` + + newMessage.contentString() + ); // Manually send the message to the socket so we can serialize it with // all the extra data diff --git a/packages/server/src/server/managers/cloudflareManager/index.ts b/packages/server/src/server/managers/cloudflareManager/index.ts index f00a34e1a..b3dec4a84 100644 --- a/packages/server/src/server/managers/cloudflareManager/index.ts +++ b/packages/server/src/server/managers/cloudflareManager/index.ts @@ -9,7 +9,13 @@ export class CloudflareManager extends Loggable { tag = "CloudflareManager"; daemonPath = path.join( - FileSystem.resources, "macos", "daemons", "cloudflare", (process.arch === "arm64") ? "arm64" : "x86", "cloudflared"); + FileSystem.resources, + "macos", + "daemons", + "cloudflare", + (process.arch === "arm64") ? "arm64" : "x86", + "cloudflared" + ); // Use a default (empty) config file so we don't interfere with the default CF install (if any) cfgPath = path.join(FileSystem.resources, "macos", "daemons", "cloudflare", "cloudflared-config.yml"); @@ -28,7 +34,10 @@ export class CloudflareManager extends Loggable { async start(): Promise { if (this.isRateLimited) { - throw new Error("Cloudflare is rate limiting your requests. Waiting 1 hour... If you do not wawnt to wait 1 hour, fully restart the server."); + throw new Error( + "Cloudflare is rate limiting your requests. Waiting 1 hour... " + + "If you do not wawnt to wait 1 hour, fully restart the server." + ); } try { @@ -43,51 +52,48 @@ export class CloudflareManager extends Loggable { } private async connectHandler(): Promise { - return new Promise(async (resolve, reject) => { - try { - const port = Server().repo.getConfig("socket_port") as string; - if (this.proc && !this.proc?.process?.killed) { - this.log.debug("Cloudflare Tunnel already running. Stopping..."); - await this.stop(); - } + const port = Server().repo.getConfig("socket_port") as string; + if (this.proc && !this.proc?.process?.killed) { + this.log.debug("Cloudflare Tunnel already running. Stopping..."); + await this.stop(); + } - this.log.debug("Starting Cloudflare Tunnel..."); - this.proc = new ProcessSpawner({ - command: this.daemonPath, - args: [ - 'tunnel', - '--url', `localhost:${port}`, - '--config', this.cfgPath, - '--pidfile', this.pidPath - ], - verbose: true, - logTag: "CloudflareDaemon", - onOutput: (data) => this.handleData(data), - restartOnNonZeroExit: true, - restartOnNonZeroExitCondition: (_) => !this.isRateLimited, - waitForExit: false, - storeOutput: false - }); - - this.on("new-url", url => resolve(url)); - this.on("error", err => { - // Ignore certain errors - if (typeof err === "string") { - if (err.includes("Thank you for trying Cloudflare Tunnel.")) return; - } + const connectPromise = new Promise((resolve, reject) => { + this.on("new-url", url => resolve(url)); + this.on("error", err => { + // Ignore certain errors + if (typeof err === "string") { + if (err.includes("Thank you for trying Cloudflare Tunnel.")) return; + } - reject(err); - }); + reject(err); + }); - setTimeout(() => { - reject(new Error("Failed to connect to Cloudflare after 2 minutes...")); - }, 1000 * 60 * 2); // 2 minutes + setTimeout(() => { + reject(new Error("Failed to connect to Cloudflare after 2 minutes...")); + }, 1000 * 60 * 2); // 2 minutes + }); - await this.proc.execute(); - } catch (ex) { - reject(ex); - } + this.log.debug("Starting Cloudflare Tunnel..."); + this.proc = new ProcessSpawner({ + command: this.daemonPath, + args: [ + 'tunnel', + '--url', `localhost:${port}`, + '--config', this.cfgPath, + '--pidfile', this.pidPath + ], + verbose: true, + logTag: "CloudflareDaemon", + onOutput: (data) => this.handleData(data), + restartOnNonZeroExit: true, + restartOnNonZeroExitCondition: (_) => !this.isRateLimited, + waitForExit: false, + storeOutput: false }); + + await this.proc.execute(); + return connectPromise; } async stop() { @@ -119,9 +125,15 @@ export class CloudflareManager extends Loggable { private detectError(data: string): string | null { if (data.includes('no such host')) { - return 'Unable to resolve api.trycloudflare.com! Ensure that your Mac has internet access and that any networking tools you use are not blocking the hostname.'; + return ( + 'Unable to resolve api.trycloudflare.com! Ensure that your Mac has internet access and that any ' + + 'networking tools you use are not blocking the hostname.' + ); } else if (data.includes("context deadline exceeded")) { - return "Failed to connect to Cloudflare's servers! Connection timed out. Please check your internet connection and try again."; + return ( + "Failed to connect to Cloudflare's servers! Connection timed out. " + + "Please check your internet connection and try again." + ); } else if (data.includes("connect: bad file descriptor")) { return "Failed to connect to Cloudflare's servers! Please make sure your Mac is up to date"; } else if (data.includes('failed to request quick Tunnel: ')) { diff --git a/packages/server/src/server/managers/zrokManager/index.ts b/packages/server/src/server/managers/zrokManager/index.ts index d91cf93da..59655ac83 100644 --- a/packages/server/src/server/managers/zrokManager/index.ts +++ b/packages/server/src/server/managers/zrokManager/index.ts @@ -12,7 +12,14 @@ export class ZrokManager extends Loggable { tag = "ZrokManager"; static get daemonPath() { - return path.join(FileSystem.resources, "macos", "daemons", "zrok", (process.arch === "arm64") ? "arm64" : "x86", "zrok"); + return path.join( + FileSystem.resources, + "macos", + "daemons", + "zrok", + (process.arch === "arm64") ? "arm64" : "x86", + "zrok" + ); } proc: ChildProcess; @@ -48,25 +55,21 @@ export class ZrokManager extends Loggable { const reservedTunnel = (Server().repo.getConfig("zrok_reserve_tunnel") as boolean) ?? false; const tunnelToken = await ZrokManager.reserve(null); - return new Promise(async (resolve, reject) => { - // Didn't use zx here because I couldn't figure out how to pipe the stdout - // properly, without taking over the terminal outputs. - // Conditionally change the command based on if we are reserving a tunnel or not - const commndFlags = [ - "share", - ...(reservedTunnel ? ["reserved", "--headless"] : ["public", "--backend-mode", "proxy", "--headless"]), - ...(reservedTunnel ? [tunnelToken] : [`0.0.0.0:${port}`]) - ]; - - if (this.proc && !this.proc?.killed) { - this.log.debug("Zrok Tunnel already running. Stopping..."); - await this.stop(); - } - - this.proc = spawn(ZrokManager.daemonPath, commndFlags); - this.proc.stdout.on("data", chunk => this.handleData(chunk)); - this.proc.stderr.on("data", chunk => this.handleData(chunk)); + // Didn't use zx here because I couldn't figure out how to pipe the stdout + // properly, without taking over the terminal outputs. + // Conditionally change the command based on if we are reserving a tunnel or not + const commndFlags = [ + "share", + ...(reservedTunnel ? ["reserved", "--headless"] : ["public", "--backend-mode", "proxy", "--headless"]), + ...(reservedTunnel ? [tunnelToken] : [`0.0.0.0:${port}`]) + ]; + + if (this.proc && !this.proc?.killed) { + this.log.debug("Zrok Tunnel already running. Stopping..."); + await this.stop(); + } + const connectPromise = new Promise((resolve, reject) => { this.on("new-url", url => resolve(url)); this.on("error", err => reject(err)); @@ -74,6 +77,12 @@ export class ZrokManager extends Loggable { reject(new Error("Failed to connect to Zrok after 2 minutes...")); }, 1000 * 60 * 2); // 2 minutes }); + + this.proc = spawn(ZrokManager.daemonPath, commndFlags); + this.proc.stdout.on("data", chunk => this.handleData(chunk)); + this.proc.stderr.on("data", chunk => this.handleData(chunk)); + + return connectPromise; } async stop() { @@ -192,7 +201,10 @@ export class ZrokManager extends Loggable { // If there is an existing token, release it if (isNotEmpty(existingToken)) { - logger.info(`Releasing existing Zrok share (${existingToken}) because we no longer want to use a reserved tunnel.`); + logger.info( + `Releasing existing Zrok share (${existingToken}) because we no longer want to use ` + + `a reserved tunnel.` + ); await this.safeRelease(existingToken); } @@ -208,7 +220,11 @@ export class ZrokManager extends Loggable { await ZrokManager.safeRelease(existingToken, { clearToken: true }); // If the tokens match, but the name doesn't match the token (which will be the name), // then release the existing tunnel - } else if (existingToken === reservedToken && isNotEmpty(reservedName) && reservedName !== existingToken) { + } else if ( + existingToken === reservedToken && + isNotEmpty(reservedName) && + reservedName !== existingToken + ) { logger.info(`Releasing existing Zrok share (${existingToken}) because the reserved name has changed.`); await ZrokManager.safeRelease(existingToken, { clearToken: true }); // If we have an existing token and the name hasn't changed, return the existing token @@ -228,7 +244,12 @@ export class ZrokManager extends Loggable { } logger.info(`Reserving new tunnel with flags: ${flags.join(" ")}`); - const output = await ProcessSpawner.executeCommand(this.daemonPath, ["reserve", "public", ...flags], {}, "ZrokManager"); + const output = await ProcessSpawner.executeCommand( + this.daemonPath, + ["reserve", "public", ...flags], + {}, + "ZrokManager" + ); const urlMatches = output.match(ZrokManager.proxyUrlRegex); if (isEmpty(urlMatches)) { logger.info(`Failed to reserve Zrok tunnel! Unable to find URL in output. Output: ${output}`); diff --git a/packages/server/src/server/services/certificateService/index.ts b/packages/server/src/server/services/certificateService/index.ts index 20af7b3e7..9fb19ef18 100644 --- a/packages/server/src/server/services/certificateService/index.ts +++ b/packages/server/src/server/services/certificateService/index.ts @@ -107,7 +107,8 @@ export class CertificateService extends Loggable { if ( prevConfig.password === nextConfig.password && - onlyAlphaNumeric(nextConfig.proxy_service as string).toLowerCase() !== onlyAlphaNumeric(ProxyServices.DynamicDNS) && + onlyAlphaNumeric(nextConfig.proxy_service as string).toLowerCase() !== + onlyAlphaNumeric(ProxyServices.DynamicDNS) && onlyAlphaNumeric(prevConfig.proxy_service as string).toLowerCase() !== onlyAlphaNumeric(nextConfig.proxy_service as string).toLowerCase() ) @@ -120,7 +121,8 @@ export class CertificateService extends Loggable { } else if ( onlyAlphaNumeric(prevConfig.proxy_service as string).toLowerCase() !== onlyAlphaNumeric(nextConfig.proxy_service as string).toLowerCase() && - onlyAlphaNumeric(nextConfig.proxy_service as string).toLowerCase() === onlyAlphaNumeric(ProxyServices.DynamicDNS) + onlyAlphaNumeric(nextConfig.proxy_service as string).toLowerCase() === + onlyAlphaNumeric(ProxyServices.DynamicDNS) ) { log.info("Proxy service changed to Dynamic DNS. Refreshing certificate"); CertificateService.refreshCertificate(); diff --git a/packages/server/src/server/services/oauthService/index.ts b/packages/server/src/server/services/oauthService/index.ts index bec691667..c2be4ae0c 100644 --- a/packages/server/src/server/services/oauthService/index.ts +++ b/packages/server/src/server/services/oauthService/index.ts @@ -682,7 +682,13 @@ export class OauthService extends Loggable { * @param data The data to send (optional) * @param key The key to check for in the response data (optional) */ - async tryUntilNoError(method: "GET" | "POST", url: string, data: Record = null, maxAttempts = 30, waitTime = 2000) { + async tryUntilNoError( + method: "GET" | "POST", + url: string, + data: Record = null, + maxAttempts = 30, + waitTime = 2000 + ) { let attempts = 0; // eslint-disable-next-line no-constant-condition @@ -738,7 +744,7 @@ export class OauthService extends Loggable { // Paginate through all the data let pageToken = null; - let contacts = []; + const contacts = []; do { const res: AxiosResponse = await this.sendRequest("GET", getUrl, null, { ...params, pageToken }); contacts.push(...res.data.connections); @@ -783,7 +789,12 @@ export class OauthService extends Loggable { * @param data The data to send (optional) * @returns The response object */ - async sendRequest(method: "GET" | "POST" | "DELETE", url: string, data: Record = null, params: Record = null) { + async sendRequest( + method: "GET" | "POST" | "DELETE", + url: string, + data: Record = null, + params: Record = null + ) { if (!this.authToken) throw new Error("Missing auth token"); const headers: Record = { diff --git a/packages/server/src/server/services/proxyServices/ngrokService/index.ts b/packages/server/src/server/services/proxyServices/ngrokService/index.ts index d164ad817..51212f992 100644 --- a/packages/server/src/server/services/proxyServices/ngrokService/index.ts +++ b/packages/server/src/server/services/proxyServices/ngrokService/index.ts @@ -2,11 +2,10 @@ import { isEmpty, safeTrim } from "@server/helpers/utils"; import path from "path"; import fs from "fs"; import { Server } from "@server"; -import { FileSystem } from "@server/fileSystem"; +import { FileSystem, userHomeDir } from "@server/fileSystem"; import { connect, disconnect, kill, authtoken, Ngrok, upgradeConfig } from "ngrok"; import { Proxy } from "../proxy"; import { app } from "electron"; -import { userHomeDir } from "@server/fileSystem"; // const sevenHours = 1000 * 60 * 60 * 7; // This is the old ngrok timeout const oneHour45 = 1000 * 60 * (60 + 45); // This is the new ngrok timeout @@ -15,7 +14,13 @@ export class NgrokService extends Proxy { tag = "NgrokService"; static get daemonDir() { - return path.join(FileSystem.resources, "macos", "daemons", "ngrok", (process.arch === "arm64") ? "arm64" : "x86"); + return path.join( + FileSystem.resources, + "macos", + "daemons", + "ngrok", + (process.arch === "arm64") ? "arm64" : "x86" + ); } constructor() {