Skip to content

Commit 2d332e0

Browse files
committed
Merge remote-tracking branch 'origin/staging' into production
2 parents 728a2b4 + c17fe4a commit 2d332e0

File tree

14 files changed

+658
-5
lines changed

14 files changed

+658
-5
lines changed

functions/package-lock.json

Lines changed: 392 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

functions/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,14 @@
6060
"firebase-admin": "^11.11.0",
6161
"firebase-functions": "^4.4.1",
6262
"fuse.js": "^7.0.0",
63+
"get-urls": "^12.1.0",
6364
"google-auth-library": "^9.0.0",
6465
"google-libphonenumber": "^3.2.33",
6566
"json2csv": "^5.0.7",
6667
"jsonwebtoken": "^9.0.2",
6768
"jszip": "^3.10.1",
6869
"lodash": "^4.17.21",
70+
"open-graph-scraper": "^6.5.1",
6971
"openai": "^4.33.0",
7072
"p-limit": "3.1.0",
7173
"pino": "^8.14.1",

functions/src/chat/controllers/sendChatMessage.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { InferType, array, object, string } from "yup";
1+
import { InferType, array, boolean, number, object, string } from "yup";
22
import { responseExecutor } from "../../util";
33
import { chatService } from "../services";
44
import { NextFunction, Request, Response } from "express";
@@ -27,6 +27,23 @@ export const sendChatMessageValidationSchema = object({
2727
ownerAvatar: link.required(),
2828
parentId: string().uuid().optional(),
2929
id: string().uuid().required(),
30+
linkPreviews: array(
31+
object({
32+
hidden: boolean().optional(),
33+
title: string().optional(),
34+
description: string().optional(),
35+
image: object({
36+
height: number().optional(),
37+
type: string().optional(),
38+
url: string().required(),
39+
width: number().optional(),
40+
alt: string().optional(),
41+
}).optional(),
42+
url: string().required(),
43+
})
44+
)
45+
.optional()
46+
.default(undefined),
3047
});
3148

3249
export type SendChatMessageValidated = InferType<

functions/src/chat/db/chatMessage/update.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { SUBCOLLECTIONS } from "../../../shared/enums";
77
export type ChatMessageUpdate = Partial<
88
Pick<
99
Omit<ChatMessage, keyof BaseEntity>,
10-
"text" | "images" | "files" | "editedAt" | "mentions"
10+
"text" | "images" | "files" | "editedAt" | "mentions" | "linkPreviews"
1111
>
1212
>;
1313

functions/src/chat/eventHandlers/onChatMessageCreated.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { EVENT_TYPES } from "../../shared/enums";
2-
import { IEvent } from "../../shared/interfaces";
2+
import { ChatMessage, IEvent } from "../../shared/interfaces";
3+
import ogs from "open-graph-scraper";
34
import { isString } from "lodash";
45
import { chatChannelDb } from "../db/chatChannel";
56
import { chatMessageDb } from "../db/chatMessage";
67
import { updateNewMessageChatChannelUserUniqueStatus } from "../business/newMessageChatChannelUserUniqueStatus";
8+
import { richTextToPlainText } from "../../discussion/business/discussionMessage/utils/textExtractor";
9+
import { isDefined } from "../../util";
710

811
export const onChatMessageCreated = async (
912
event: Omit<IEvent, "type"> & { type: EVENT_TYPES.CHAT_MESSAGE_CREATED }
@@ -17,6 +20,8 @@ export const onChatMessageCreated = async (
1720
true
1821
);
1922

23+
const addLinkPreviewsPromise = addLinkPreviews(chatMessage);
24+
2025
await Promise.all(
2126
chatChannel.participants.map(async (participant) => {
2227
if (participant !== chatMessage.ownerId) {
@@ -27,5 +32,48 @@ export const onChatMessageCreated = async (
2732
}
2833
})
2934
);
35+
await addLinkPreviewsPromise;
3036
}
3137
};
38+
39+
async function addLinkPreviews(message: ChatMessage) {
40+
if (message.linkPreviews !== undefined) return;
41+
let discussionMessageText: string = message.text ?? "";
42+
try {
43+
const parsedMessage = JSON.parse(message.text);
44+
if (Array.isArray(parsedMessage)) {
45+
discussionMessageText = richTextToPlainText(parsedMessage);
46+
}
47+
} catch {
48+
return;
49+
}
50+
51+
const getUrls = (await import("get-urls")).default;
52+
const links = getUrls(discussionMessageText);
53+
if (links.size === 0) return;
54+
55+
const linkPreviews = (
56+
await Promise.all(
57+
Array.from(links).map(async (url) => {
58+
try {
59+
const { result } = await ogs({ url });
60+
return {
61+
title: result.ogTitle,
62+
description: result.ogDescription,
63+
image: result.ogImage?.[0],
64+
url,
65+
};
66+
} catch {
67+
return undefined;
68+
}
69+
})
70+
)
71+
).filter(isDefined);
72+
73+
await chatMessageDb.update(
74+
{
75+
linkPreviews,
76+
},
77+
message.id
78+
);
79+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { discussionDb } from "../../../discussion/db";
2+
import { proposalsDb } from "../../../proposals/db/proposals";
3+
import { EVENT_TYPES } from "../../../shared/enums";
4+
import { FeedObjectTypes } from "../../../shared/interfaces";
5+
import { DbOperationConfig, createEvent } from "../../../util/db";
6+
import { commonFeedDb } from "../../db/commonFeed";
7+
8+
export const createMembershipAdmittanceCommonFeedObject = async (
9+
proposalId: string,
10+
dbConfig?: DbOperationConfig
11+
) => {
12+
const {
13+
data: {
14+
args: { proposerId, commonId, images, files },
15+
},
16+
id,
17+
createdAt,
18+
discussionId,
19+
} = await proposalsDb.get(proposalId, true, dbConfig);
20+
const discussion = await discussionDb.getDiscussion(
21+
discussionId,
22+
true,
23+
dbConfig
24+
);
25+
const feedItem = await commonFeedDb.add(
26+
{
27+
userId: proposerId,
28+
createdAt,
29+
data: {
30+
type: FeedObjectTypes.Proposal,
31+
id,
32+
discussionId,
33+
hasFiles: !!files?.length,
34+
hasImages: !!images?.length,
35+
},
36+
circleVisibility: discussion.circleVisibilityByCommon?.[commonId] || [],
37+
},
38+
commonId,
39+
dbConfig
40+
);
41+
42+
await createEvent(
43+
{
44+
type: EVENT_TYPES.NEW_FEED_ITEM_CREATED,
45+
userId: proposerId,
46+
objectIds: [commonId, feedItem?.id || ""],
47+
data: {
48+
sourceAction: "create",
49+
},
50+
},
51+
dbConfig
52+
);
53+
};

functions/src/commons/business/createCommonFeedObject/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createCreateCircleCommonFeedObject } from "./createCreateCircleCommonFe
66
import { createDeleteCommonCommonFeedObject } from "./createDeleteCommonCommonFeedObject";
77
import { createDiscussionCommonFeedObject } from "./createDiscussionCommonFeedObject";
88
import { createFundsAllocationCommonFeedObject } from "./createFundsAllocationCommonFeedObject";
9+
import { createMembershipAdmittanceCommonFeedObject } from "./createMembershipAdmittanceCommonFeedObject";
910
import { createPayInCommonFeedObject } from "./createPayInCommonFeedObject";
1011
import { createProjectCreatedCommonFeedObject } from "./createProjectCreatedCommonFeedObject";
1112
import { createRemoveCircleCommonFeedObject } from "./createRemoveCircleCommonFeedObject";
@@ -54,6 +55,9 @@ export const createCommonFeedObject = async (
5455
if (type === EVENT_TYPES.PAYMENT_CONFIRMED)
5556
return await createPayInCommonFeedObject(objectId, dbConfig);
5657

58+
if (type === EVENT_TYPES.REQUEST_TO_JOIN_CREATED)
59+
return await createMembershipAdmittanceCommonFeedObject(objectId, dbConfig);
60+
5761
if (type === EVENT_TYPES.DISCUSSION_CREATED) {
5862
const discussion = await discussionDb.getDiscussion(
5963
objectId,

functions/src/discussion/eventHandlers/onMessageCreated.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import OpenAI from "openai";
2+
import ogs from "open-graph-scraper";
23
import { EVENT_TYPES } from "../../shared/enums";
34
import {
45
Common,
@@ -48,8 +49,48 @@ export const onMessageCreated = async (
4849
});
4950

5051
await aiReplyPromise;
52+
await addLinkPreviews(discussionMessage);
5153
};
5254

55+
async function addLinkPreviews(message: DiscussionMessage) {
56+
if (message.linkPreviews !== undefined) return;
57+
let discussionMessageText: string = message.text ?? "";
58+
try {
59+
const parsedMessage = JSON.parse(message.text);
60+
if (Array.isArray(parsedMessage)) {
61+
discussionMessageText = richTextToPlainText(parsedMessage);
62+
}
63+
} catch {
64+
return;
65+
}
66+
67+
const getUrls = (await import("get-urls")).default;
68+
const links = getUrls(discussionMessageText);
69+
if (links.size === 0) return;
70+
71+
const linkPreviews = (
72+
await Promise.all(
73+
Array.from(links).map(async (url) => {
74+
try {
75+
const { result } = await ogs({ url });
76+
return {
77+
title: result.ogTitle,
78+
description: result.ogDescription,
79+
image: result.ogImage?.[0],
80+
url,
81+
};
82+
} catch {
83+
return undefined;
84+
}
85+
})
86+
)
87+
).filter(isDefined);
88+
89+
await discussionMessageDb.updateDiscussionMessage(message.id, {
90+
linkPreviews,
91+
});
92+
}
93+
5394
async function createAiReply(message: DiscussionMessage) {
5495
if (
5596
message.ownerType === DiscussionMessageOwnerType.System ||

functions/src/discussion/services/discussionMessage/create.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const createDiscussionMessageService = async (
3131
ownerType: DiscussionMessageOwnerType.User,
3232
ownerId: user.uid,
3333
id: payload.id || v4(),
34+
linkPreviews: payload.linkPreviews,
3435
});
3536

3637
return discussionMessage;

functions/src/metadata/index.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { env } from "../shared/configs";
2-
import { commonRouter } from "../util";
2+
import {
3+
CommonError,
4+
UnauthorizedError,
5+
authenticate,
6+
commonRouter,
7+
responseExecutor,
8+
} from "../util";
39
import { ENVIRONMENTS } from "../shared/enums";
410
import { helper } from "./helper";
511
import { addCommonUsers } from "./addCommonUsers";
612
import { ExpressAppConfig } from "../shared/interfaces";
13+
import ogs from "open-graph-scraper";
714

815
const metadataRouter = commonRouter();
916

@@ -18,6 +25,41 @@ metadataRouter.get("/headers", async (req, res) => {
1825
res.send(req.ipAddress);
1926
});
2027

28+
// Authenticated routes
29+
metadataRouter.use(authenticate);
30+
31+
metadataRouter.get(
32+
"/og-link",
33+
async (req, res, next) =>
34+
await responseExecutor(
35+
async () => {
36+
if (!req.user) throw new UnauthorizedError();
37+
const url = req.query.url as string;
38+
if (!url) {
39+
throw new CommonError("URL is required");
40+
}
41+
try {
42+
const { result } = await ogs({ url });
43+
return {
44+
title: result.ogTitle,
45+
description: result.ogDescription,
46+
image: result.ogImage?.[0],
47+
url,
48+
};
49+
} catch (error) {
50+
logger.error("Failed to fetch link preview", error);
51+
throw new CommonError("Failed to fetch link preview");
52+
}
53+
},
54+
{
55+
req,
56+
res,
57+
next,
58+
successMessage: "",
59+
}
60+
)
61+
);
62+
2163
if (
2264
env.environment === ENVIRONMENTS.DEV ||
2365
ENVIRONMENTS.LOCAL ||

0 commit comments

Comments
 (0)