Skip to content

Commit

Permalink
Merge pull request #536 from BlueBubblesApp/development
Browse files Browse the repository at this point in the history
v1.7.3
  • Loading branch information
zlshames authored May 27, 2023
2 parents e5d8af5 + b58bb5d commit 64323b7
Show file tree
Hide file tree
Showing 31 changed files with 387 additions and 159 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bluebubbles-server",
"version": "1.7.2",
"version": "1.7.3",
"description": "BlueBubbles Server is the app that powers the BlueBubbles app ecosystem",
"private": true,
"workspaces": [
Expand Down
Binary file modified packages/server/appResources/macos/daemons/cloudflared
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/server/appResources/macos/daemons/cloudflared.md5
Original file line number Diff line number Diff line change
@@ -1 +1 @@
09aeb674587258c1e2cd4e00ab5f0b66
3219119d3ec2669d8c4f5ef8660ab878
4 changes: 3 additions & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bluebubbles/server",
"version": "1.7.2",
"version": "1.7.3",
"main": "./dist/main.js",
"license": "Apache-2.0",
"author": {
Expand Down Expand Up @@ -41,6 +41,7 @@
"@types/electron": "^1.6.10",
"@types/google-libphonenumber": "^7.4.18",
"@types/history": "^4.7.5",
"@types/js-yaml": "^4.0.5",
"@types/koa": "^2.13.4",
"@types/koa-cors": "^0.0.2",
"@types/koa-json": "^2.0.20",
Expand Down Expand Up @@ -93,6 +94,7 @@
"find-process": "^1.4.4",
"firebase-admin": "^11.1.0",
"google-libphonenumber": "^3.2.10",
"js-yaml": "^4.1.0",
"koa": "^2.13.1",
"koa-body": "^4.2.0",
"koa-cors": "^0.0.16",
Expand Down
29 changes: 24 additions & 5 deletions packages/server/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import "reflect-metadata";
import { app, BrowserWindow, Tray, Menu, nativeTheme, shell, HandlerDetails } from "electron";
import * as process from "process";
import * as path from "path";
import { app, BrowserWindow, Tray, Menu, nativeTheme, shell, HandlerDetails, } from "electron";
import process from "process";
import path from "path";
import fs from "fs";
import yaml from "js-yaml";
import { FileSystem } from "@server/fileSystem";
import { ParseArguments } from "@server/helpers/argParser";

import { Server } from "@server";
import { isEmpty, safeTrim } from "@server/helpers/utils";
Expand All @@ -12,12 +15,23 @@ app.commandLine.appendSwitch("in-process-gpu");
// Patch in original user data directory
app.setPath("userData", app.getPath("userData").replace("@bluebubbles/server", "bluebubbles-server"));

// Load the config file
let cfg = {};
if (fs.existsSync(FileSystem.cfgFile)) {
cfg = yaml.load(fs.readFileSync(FileSystem.cfgFile, "utf8"));
}

// Parse the CLI args and marge with config args
const args = ParseArguments(process.argv);
const parsedArgs: Record<string, any> = { ...cfg, ...args };
const noGui = parsedArgs["no-gui"] || false;

let win: BrowserWindow;
let tray: Tray;
let isHandlingExit = false;

// Instantiate the server
Server(win);
Server(parsedArgs, win);

// Only 1 instance is allowed
const gotTheLock = app.requestSingleInstanceLock();
Expand Down Expand Up @@ -148,6 +162,11 @@ const createTray = () => {
};

const createWindow = async () => {
if (noGui) {
Server().log("GUI disabled, skipping window creation...");
return;
}

win = new BrowserWindow({
title: "BlueBubbles Server",
useContentSize: true,
Expand Down Expand Up @@ -224,7 +243,7 @@ const createWindow = async () => {
});

// Set the new window in the Server()
Server(win);
Server(parsedArgs, win);
};

app.on("ready", () => {
Expand Down
105 changes: 16 additions & 89 deletions packages/server/src/server/api/v1/apple/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import {
exportContacts,
restartMessages,
openChat,
sendMessageFallback,
sendAttachmentAccessibility
sendMessageFallback
} from "@server/api/v1/apple/scripts";
import { ValidRemoveTapback } from "../../../types";

Expand All @@ -30,18 +29,19 @@ import {
safeTrim
} from "../../../helpers/utils";
import { tapbackUIMap } from "./mappings";
import { MessageInterface } from "../interfaces/messageInterface";

/**
* This class handles all actions that require an AppleScript execution.
* Pretty much, using command line to execute a script, passing any required
* variables
*/
export class ActionHandler {
static sendMessageHandler = async (chatGuid: string, message: string, attachment: string) => {
static sendMessage = async (chatGuid: string, message: string, attachment: string, isAudioMessage = false) => {
let messageScript: string;

let theAttachment = attachment;
if (theAttachment !== null && theAttachment.endsWith(".mp3")) {
if (theAttachment !== null && theAttachment.endsWith(".mp3") && isAudioMessage) {
try {
const newPath = `${theAttachment.substring(0, theAttachment.length - 4)}.caf`;
await FileSystem.convertMp3ToCaf(theAttachment, newPath);
Expand Down Expand Up @@ -111,89 +111,6 @@ export class ActionHandler {
}
};

/**
* Sends a message by executing the sendMessage AppleScript
*
* @param chatGuid The GUID for the chat
* @param message The message to send
* @param attachmentName The name of the attachment to send (optional)
* @param attachment The bytes (buffer) for the attachment
*
* @returns The command line response
*/
static sendMessage = async (
tempGuid: string,
chatGuid: string,
message: string,
attachmentGuid?: string,
attachmentName?: string,
attachment?: Uint8Array
): Promise<void> => {
if (!chatGuid) throw new Error("No chat GUID provided");

// Add attachment, if present
if (attachment) {
FileSystem.saveAttachment(attachmentName, attachment);
}

Server().log(`Sending message "${message}" ${attachment ? "with attachment" : ""} to ${chatGuid}`, "debug");

// Make sure messages is open
await FileSystem.startMessages();

// We need offsets here due to iMessage's save times being a bit off for some reason
const now = new Date(new Date().getTime() - 10000).getTime(); // With 10 second offset

// Create the awaiter
let messageAwaiter = null;
if (isNotEmpty(message)) {
messageAwaiter = new MessagePromise({
chatGuid,
text: message,
isAttachment: false,
sentAt: now,
tempGuid
});
Server().log(`Adding await for chat: "${chatGuid}"; text: ${messageAwaiter.text}`);
Server().messageManager.add(messageAwaiter);
}

// Since we convert mp3s to cafs, we need to modify the attachment name here too
// This is mainly just for the awaiter
let aName = attachmentName;
if (aName !== null && aName.endsWith(".mp3")) {
aName = `${aName.substring(0, aName.length - 4)}.caf`;
}

// Create the awaiter
let attachmentAwaiter = null;
if (attachment && isNotEmpty(aName)) {
attachmentAwaiter = new MessagePromise({
chatGuid,
text: aName,
isAttachment: true,
sentAt: now,
tempGuid: attachmentGuid
});
Server().log(`Adding await for chat: "${chatGuid}"; attachment: ${aName}`);
Server().messageManager.add(attachmentAwaiter);
}

// Hande-off params to send handler to actually send
const theAttachment = attachment ? `${FileSystem.attachmentsDir}/${attachmentName}` : null;
await ActionHandler.sendMessageHandler(chatGuid, message, theAttachment);

// Wait for the attachment first
if (attachmentAwaiter) {
await attachmentAwaiter.promise;
}

// Next, wait for the message
if (messageAwaiter) {
await messageAwaiter.promise;
}
};

/**
* Renames a group chat via an AppleScript
*
Expand Down Expand Up @@ -577,7 +494,12 @@ export class ActionHandler {
// If there is a message attached, try to send it
try {
if (isNotEmpty(message) && isNotEmpty(tempGuid)) {
await ActionHandler.sendMessage(tempGuid, ret, message);
await MessageInterface.sendMessageSync({
chatGuid: ret,
message: message,
tempGuid: tempGuid,
method: "apple-script"
});
}
} catch (ex: any) {
throw new Error(`Failed to send message to chat, ${ret}!`);
Expand Down Expand Up @@ -611,7 +533,12 @@ export class ActionHandler {
const chatGuid = `${service};-;${buddy}`;

// Send the message to the chat
await ActionHandler.sendMessage(tempGuid, chatGuid, message);
await MessageInterface.sendMessageSync({
chatGuid: chatGuid,
message: message,
tempGuid: tempGuid,
method: "apple-script"
});

// Return the chat GUID
return chatGuid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ export class AttachmentInterface {
if (isEmpty(fPath)) return null;

// Get the extension
const ext = fPath.split(".").pop();
const ext = fPath.split(".").pop() ?? '';

// If the extension is not an image extension, return null
if (!AttachmentInterface.livePhotoExts.includes(ext)) return null;
if (!AttachmentInterface.livePhotoExts.includes(ext.toLowerCase())) return null;

// Get the path to the live photo by replacing the extension with .mov
// fs.existsSync is case-insensitive on macOS
const livePath = ext !== fPath ? fPath.replace(`.${ext}`, ".mov") : `${fPath}.mov`;
const realPath = FileSystem.getRealPath(livePath);

Expand Down
13 changes: 7 additions & 6 deletions packages/server/src/server/api/v1/interfaces/chatInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,21 +267,22 @@ export class ChatInterface {
if (!chat && isEmpty(guid)) throw new Error("No chat or chat GUID provided!");

const theChat = chat ?? (await repo.findOneBy({ guid }));
if (!theChat) return;
if (!theChat) throw new Error(`Failed to delete chat! Chat not found. (GUID: ${guid})`);

// Tell the private API to delete the chat
await Server().privateApiHelper.deleteChat(theChat.guid);

// Wait for the DB changes to propogate
const maxWaitMs = 30000;
const success = !!(await resultAwaiter({
const success = await resultAwaiter({
maxWaitMs,
getData: async () => {
return await repo.findOneBy({ guid: theChat.guid });
const res = await repo.findOneBy({ guid: theChat.guid });
return !res ? true : false;
},
// Keep looping if we keep finding the chat
dataLoopCondition: data => !!data
}));
// Keep looping if we keep finding the chat (getData returns false)
dataLoopCondition: data => !data
});

if (!success) {
throw new Error(`Failed to delete chat! Chat still exists. (GUID: ${theChat.guid})`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export class MessageInterface {
let sentMessage = null;
if (method === "apple-script") {
// Attempt to send the message
await ActionHandler.sendMessageHandler(chatGuid, message ?? "", null);
await ActionHandler.sendMessage(chatGuid, message ?? "", null);
sentMessage = await awaiter.promise;
} else if (method === "private-api") {
sentMessage = await MessageInterface.sendMessagePrivateApi({
Expand Down Expand Up @@ -151,7 +151,7 @@ export class MessageInterface {

// Since we convert mp3s to cafs we need to correct the name for the awaiter
let aName = attachmentName;
if (aName !== null && aName.endsWith(".mp3")) {
if (aName !== null && aName.endsWith(".mp3") && isAudioMessage) {
aName = `${aName.substring(0, aName.length - 4)}.caf`;
}

Expand All @@ -174,7 +174,7 @@ export class MessageInterface {
let sentMessage = null;
if (method === "apple-script") {
// Attempt to send the attachment
await ActionHandler.sendMessageHandler(chatGuid, "", newPath);
await ActionHandler.sendMessage(chatGuid, "", newPath, isAudioMessage);
sentMessage = await awaiter.promise;
} else if (method === "private-api") {
sentMessage = await MessageInterface.sendAttachmentPrivateApi({
Expand Down Expand Up @@ -271,7 +271,7 @@ export class MessageInterface {
}: SendAttachmentPrivateApiParams): Promise<Message> {
checkPrivateApiStatus();

if (filePath.endsWith(".mp3")) {
if (filePath.endsWith(".mp3") && isAudioMessage) {
try {
const newPath = `${filePath.substring(0, filePath.length - 4)}.caf`;
await FileSystem.convertMp3ToCaf(filePath, newPath);
Expand Down
30 changes: 25 additions & 5 deletions packages/server/src/server/databases/imessage/entity/Attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isMinSierra, isMinHighSierra, isEmpty } from "@server/helpers/utils";
import { conditional } from "conditional-decorator";
import * as mime from "mime-types";
import { FileSystem } from "@server/fileSystem";
import { AttributedBodyTransformer } from "@server/databases/transformers/AttributedBodyTransformer";

@Entity("attachment")
export class Attachment {
Expand Down Expand Up @@ -59,8 +60,13 @@ export class Attachment {
})
isOutgoing: boolean;

@Column({ type: "blob", name: "user_info", nullable: true })
userInfo: Blob;
@Column({
type: "blob",
name: "user_info",
nullable: true,
transformer: AttributedBodyTransformer
})
userInfo: NodeJS.Dict<any>[] | null;

@Column({ type: "text", name: "transfer_name", nullable: false })
transferName: string;
Expand Down Expand Up @@ -94,10 +100,11 @@ export class Attachment {
Column({
type: "blob",
name: "attribution_info",
nullable: true
nullable: true,
transformer: AttributedBodyTransformer
})
)
attributionInfo: Blob;
attributionInfo: NodeJS.Dict<any>[] | null;

@conditional(
isMinSierra,
Expand All @@ -120,9 +127,22 @@ export class Attachment {
)
originalGuid: string;

private getMimeTypeFromUserInfo(): string | null {
if (isEmpty(this.userInfo)) return null;
return this.userInfo[0]['mime-type'] ?? null;
}

getDimensions(): { height: number, width: number } | null {
if (isEmpty(this.attributionInfo)) return null;
const height = this.attributionInfo[0]?.pgensh;
const width = this.attributionInfo[0]?.pgensw;
if (!height || !width) return null;
return { height, width };
}

getMimeType(): string {
const fPath = FileSystem.getRealPath(this.filePath);
let mType = this.mimeType ?? mime.lookup(fPath);
let mType = this.mimeType ?? this.getMimeTypeFromUserInfo() ?? mime.lookup(fPath);
if (!mType || isEmpty(mType as any)) mType = "application/octet-stream";
return mType;
}
Expand Down
Loading

0 comments on commit 64323b7

Please # to comment.