Skip to content

Commit

Permalink
Merge pull request #500 from BlueBubblesApp/development
Browse files Browse the repository at this point in the history
v1.6.0
  • Loading branch information
zlshames authored Mar 15, 2023
2 parents e8ac4f9 + 1a8d6b4 commit e7d8b9d
Show file tree
Hide file tree
Showing 49 changed files with 1,152 additions and 371 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ This is the back-end server for the BlueBubbles App. It allows you to forward yo
4. Run the dev server (this will start both the renderer and server)
- `yarn start`

### macOS Warning

If you are using macOS 10.x and are having issues building/running the server, please downgrade the `node-mac-permissions` dependency to `v2.2.0`. The reason it's on a newer version is to fix a production crashing issue on Big Sur+. Please downgrade it manually for testing on macOS v10.x.

```bash
cd packages/server
yarn add node-mac-permissions@2.2.0
```

## Structure / Directory Map

### Back-end
Expand Down
File renamed without changes.
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.5.5",
"version": "1.6.0",
"description": "BlueBubbles Server is the app that powers the BlueBubbles app ecosystem",
"private": true,
"workspaces": [
Expand Down
2 changes: 1 addition & 1 deletion packages/server/appResources/private-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.11
**GitHub Release Reference**: https://github.com/BlueBubblesApp/BlueBubbles-Server-Helper/releases/tag/0.0.13

You can also check the current versions of these bundles by opening the `version.txt` file.
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.11</string>
<string>0.0.13</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>13</string>
<string>15</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/server/appResources/private-api/macos10bundle.md5
Original file line number Diff line number Diff line change
@@ -1 +1 @@
40fdae0ec7bfbf605f1df380b65002c3
694950ddaa3db139e73cd0cb914c8558
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.12</string>
<string>0.0.13</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>14</string>
<string>15</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/server/appResources/private-api/macos11bundle.md5
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3075e81385d42bdf7504160edef0ad5f
8155a6c77c0c4b9fd5abcbabc3473c81
2 changes: 1 addition & 1 deletion packages/server/appResources/private-api/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.12
0.0.13
6 changes: 3 additions & 3 deletions packages/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bluebubbles/server",
"version": "1.5.5",
"version": "1.6.0",
"main": "./dist/main.js",
"license": "Apache-2.0",
"author": {
Expand Down Expand Up @@ -105,7 +105,7 @@
"ngrok": "^4.3.3",
"node-forge": "^1.3.1",
"node-mac-contacts": "1.5.0",
"node-mac-permissions": "2.2.0",
"node-mac-permissions": "^2.3.0",
"node-typedstream": "^1.4.0",
"numeral": "^2.0.6",
"plist": "^3.0.6",
Expand All @@ -114,7 +114,7 @@
"reflect-metadata": "^0.1.13",
"slugify": "^1.6.0",
"socket.io": "3.1.2",
"typeorm": "^0.3.6",
"typeorm": "0.3.6",
"validatorjs": "^3.22.1",
"vcf": "^2.1.1",
"zx": "^4.3.0"
Expand Down
37 changes: 36 additions & 1 deletion packages/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,42 @@ const createWindow = async () => {

// Hook onto when we load the UI
win.webContents.on("dom-ready", async () => {
win.webContents.send("config-update", Server().repo.config);
Server().uiLoaded = true;

if (!win.webContents.isDestroyed()) {
win.webContents.send("config-update", Server().repo.config);
}
});

// Hook onto when the UI finishes loading
win.webContents.on("did-finish-load", async () => {
Server().uiLoaded = true;
});

// Hook onto when the UI fails to load
win.webContents.on(
"did-fail-load",
async (event, errorCode, errorDescription, validatedURL, frameProcessId, frameRoutingId) => {
Server().uiLoaded = false;
Server().log(`Failed to load UI! Error: [${errorCode}] ${errorDescription}`, "error");
}
);

// Hook onto when the renderer process crashes
win.webContents.on("render-process-gone", async (event, details) => {
Server().uiLoaded = false;
Server().log(`Renderer process crashed! Error: [${details.exitCode}] ${details.reason}`, "error");
});

// Hook onto when the webcontents are destroyed
win.webContents.on("destroyed", async () => {
Server().uiLoaded = false;
Server().log(`Webcontents were destroyed.`, "debug");
});

// Hook onto when there is a preload error
win.webContents.on("preload-error", async (event, preloadPath, error) => {
Server().log(`A preload error occurred: Error: ${error.message}.`, "error");
});

// Set the new window in the Server()
Expand Down
6 changes: 2 additions & 4 deletions packages/server/src/server/api/v1/apple/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable max-len */
import * as fs from "fs";
import { Server } from "@server";
import { FileSystem } from "@server/fileSystem";
import { MessagePromise } from "@server/managers/outgoingMessageManager/messagePromise";
Expand Down Expand Up @@ -28,8 +27,7 @@ import {
slugifyAddress,
isNotEmpty,
isEmpty,
safeTrim,
isMinMonterey
safeTrim
} from "../../../helpers/utils";
import { tapbackUIMap } from "./mappings";

Expand Down Expand Up @@ -344,7 +342,7 @@ export class ActionHandler {
static openChat = async (chatGuid: string): Promise<string> => {
Server().log(`Executing Action: Open Chat (Chat: ${chatGuid})`, "debug");

const chats = await Server().iMessageRepo.getChats({ chatGuid, withParticipants: true });
const [chats, _] = await Server().iMessageRepo.getChats({ chatGuid, withParticipants: true });
if (isEmpty(chats)) throw new Error("Chat does not exist");
if (chats[0].participants.length > 1) throw new Error("Chat is a group chat");

Expand Down
132 changes: 96 additions & 36 deletions packages/server/src/server/api/v1/interfaces/chatInterface.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as fs from "fs";
import { Chat } from "@server/databases/imessage/entity/Chat";
import { Handle } from "@server/databases/imessage/entity/Handle";
import {
checkPrivateApiStatus,
getiMessageAddressFormat,
isEmpty,
isMinBigSur,
isMinVentura,
Expand All @@ -11,12 +12,14 @@ import {
} from "@server/helpers/utils";
import { Server } from "@server";
import { FileSystem } from "@server/fileSystem";
import { ChatResponse, HandleResponse } from "@server/types";
import { ChatResponse } from "@server/types";
import { startChat } from "../apple/scripts";
import { MessageInterface } from "./messageInterface";
import { CHAT_READ_STATUS_CHANGED } from "@server/events";
import { ChatSerializer } from "../serializers/ChatSerializer";
import { HandleSerializer } from "../serializers/HandleSerializer";
import { Attachment } from "@server/databases/imessage/entity/Attachment";
import { Message } from "@server/databases/imessage/entity/Message";
import { MessageSerializer } from "../serializers/MessageSerializer";

export class ChatInterface {
static async get({
Expand All @@ -26,28 +29,32 @@ export class ChatInterface {
offset = 0,
limit = null,
sort = "lastmessage"
}: any = {}): Promise<ChatResponse[]> {
const chats = await Server().iMessageRepo.getChats({
}: any = {}): Promise<[ChatResponse[], number]> {
// First fetch chats without the last message.
// This is because fetching the last message will make the participants list 1 for each chat.
// It will also only return chats that have a last message.
const [chats, totalChats] = await Server().iMessageRepo.getChats({
chatGuid: guid as string,
withParticipants: false,
withLastMessage,
withLastMessage: false,
withArchived,
offset,
limit
});

// If the query is with the last message, it makes the participants list 1 for each chat
// We need to fetch all the chats with their participants, then cache the participants
// so we can merge the participant list with the chats
const chatCache: { [key: string]: Handle[] } = {};
const tmpChats = await Server().iMessageRepo.getChats({
chatGuid: guid as string,
withParticipants: true,
withArchived
});
const lastMessageCache: { [key: string]: Message | null } = {};
if (withLastMessage) {
const [tmpChats, _] = await Server().iMessageRepo.getChats({
chatGuid: guid as string,
withLastMessage: true,
withParticipants: false,
withArchived,
offset,
limit
});

for (const chat of tmpChats) {
chatCache[chat.guid] = chat.participants;
for (const chat of tmpChats) {
lastMessageCache[chat.guid] = chat.messages.length > 0 ? chat.messages[0] : null;
}
}

const results = [];
Expand All @@ -62,22 +69,12 @@ export class ChatInterface {
}
});

// Insert the cached participants from the original request
if (Object.keys(chatCache).includes(chat.guid)) {
chatRes.participants = await Promise.all(
chatCache[chat.guid].map(
async (e): Promise<HandleResponse> => await HandleSerializer.serialize({ handle: e })
)
);
}

// Insert the lastmessage from the cache into the chat
if (withLastMessage) {
// Set the last message, if applicable
if (isNotEmpty(chatRes.messages)) {
[chatRes.lastMessage] = chatRes.messages;

// Remove the last message from the result
delete chatRes.messages;
if (Object.keys(lastMessageCache).includes(chat.guid) && lastMessageCache[chat.guid]) {
chatRes.lastMessage = await MessageSerializer.serialize({
message: lastMessageCache[chat.guid] as Message
});
}
}

Expand All @@ -97,7 +94,7 @@ export class ChatInterface {
}
}

return results;
return [results, totalChats];
}

static async setDisplayName(chat: Chat, displayName: string): Promise<Chat> {
Expand All @@ -116,7 +113,10 @@ export class ChatInterface {
maxWaitMs,
dataLoopCondition: data => !!data,
getData: async (previousData: any | null) => {
const chats = await Server().iMessageRepo.getChats({ chatGuid: chat.guid, withParticipants: false });
const [chats, _] = await Server().iMessageRepo.getChats({
chatGuid: chat.guid,
withParticipants: false
});
return chats[0] ?? previousData;
},
extraLoopCondition: data => {
Expand Down Expand Up @@ -175,6 +175,8 @@ export class ChatInterface {
method: "apple-script",
tempGuid
});

chatGuid = `${service};-;${getiMessageAddressFormat(theAddrs[0], true)}`;
} else {
const result = await FileSystem.executeAppleScript(startChat(theAddrs, service, null));
Server().log(`StartChat AppleScript Returned: ${result}`, "debug");
Expand All @@ -192,7 +194,8 @@ export class ChatInterface {
const chats = await resultAwaiter({
maxWaitMs,
getData: async _ => {
return await Server().iMessageRepo.getChats({ chatGuid, withParticipants: true });
const [chats, __] = await Server().iMessageRepo.getChats({ chatGuid, withParticipants: true });
return chats;
},
dataLoopCondition: data => {
return isEmpty(data);
Expand Down Expand Up @@ -277,6 +280,63 @@ export class ChatInterface {
}
}

static async setGroupChatIcon(chat: Chat, iconPath: string | null): Promise<void> {
checkPrivateApiStatus();
if (!isMinBigSur) throw new Error("Setting group chat icons are only supported on macOS Big Sur or newer!");

// The icon path can be null when unsetting the icon
if (isNotEmpty(iconPath)) {
if (!fs.existsSync(iconPath)) {
throw new Error("Icon path does not exist!");
}

// Extract filename from path
const filename = iconPath.split("/").slice(-1)[0];

// Copy the file to the Messages Attachments folder
iconPath = FileSystem.copyAttachment(iconPath, `${chat.chatIdentifier}-${filename}`, "private-api");
}

// Make sure we are executing this on a group chat
if (chat.participants.length === 1) {
throw new Error("Chat is not a group chat!");
}

// Change the chat icon
await Server().privateApiHelper.setGroupChatIcon(chat.guid, iconPath);
}

static async getGroupChatIcon(chat: Chat): Promise<Attachment | null> {
let iconGuid = null;
for (const item of chat.properties ?? []) {
if (isNotEmpty(item.groupPhotoGuid)) {
iconGuid = item.groupPhotoGuid;
}
}

if (isEmpty(iconGuid)) return null;

// Find the corresponding attachment
const attachment = await Server().iMessageRepo.getAttachment(iconGuid);
if (!attachment) return null;

// Return the attachment path
return attachment;
}

static async leave({ chat, guid }: { chat?: Chat; guid?: string } = {}): Promise<void> {
checkPrivateApiStatus();

const repo = Server().iMessageRepo.db.getRepository(Chat);
if (!chat && isEmpty(guid)) throw new Error("No chat or chat GUID provided!");

const theChat = chat ?? (await repo.findOneBy({ guid }));
if (!theChat) return;

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

static async markRead(chatGuid: string): Promise<void> {
await Server().privateApiHelper.markChatRead(chatGuid);
await Server().emitMessage(CHAT_READ_STATUS_CHANGED, {
Expand Down
Loading

0 comments on commit e7d8b9d

Please sign in to comment.