Skip to content

Commit b05f5c1

Browse files
authored
Add API for user comment notification #1245 (#1246)
* Support Comment Notification #1245 * add space info #1245 * improve #1245 * split type #1245 * add title #1245 * add cid #1245
1 parent a8b215c commit b05f5c1

9 files changed

Lines changed: 314 additions & 2 deletions

File tree

backend/packages/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@polkadot/util": "^13.0.2",
1717
"@polkadot/util-crypto": "^13.0.2",
1818
"bignumber.js": "^9.0.1",
19+
"cheerio": "^1.1.2",
1920
"data-uri-to-buffer": "^4.0.1",
2021
"dotenv": "^10.0.0",
2122
"ethers": "5.8.0",

backend/packages/backend/src/constants/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ const NotificationType = Object.freeze({
1717
ProposalStarted: "proposalStarted",
1818
ProposalCloseToEnd: "proposalCloseToEnd",
1919
ProposalEnd: "proposalEnd",
20+
CommentMentionUser: "commentMentionUser",
21+
AppendantMentionUser: "appendantMentionUser",
22+
VoteMentionUser: "voteMentionUser",
2023
});
2124

2225
module.exports = {

backend/packages/backend/src/services/appendant.service.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
const { NotificationType } = require("../constants");
12
const { HttpError } = require("../exc");
23
const { getProposalCollection, getAppendantCollection } = require("../mongo");
34
const { isSameAddress } = require("../utils/address");
5+
const { createMentionNotification } = require("./notification");
46
const { pinData } = require("./proposal.service/common");
57

68
async function addAppendant(
@@ -44,6 +46,18 @@ async function addAppendant(
4446
createdAt: now,
4547
});
4648

49+
await createMentionNotification(
50+
NotificationType.AppendantMentionUser,
51+
content,
52+
contentType,
53+
{
54+
space: proposal.space,
55+
proposalCid,
56+
title: proposal.title,
57+
cid,
58+
},
59+
);
60+
4761
return {
4862
cid,
4963
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const { getNotificationCollection } = require("../../mongo");
2+
const { extractMentionUsers } = require("../../utils/comment");
3+
const { logger } = require("../../utils/logger");
4+
5+
async function createMentionNotification(type, content, contentType, data) {
6+
try {
7+
const { space, proposalCid, title, cid } = data;
8+
const memberPublicKeys = await extractMentionUsers(content, contentType);
9+
10+
const createdAt = new Date().getTime();
11+
12+
const notificationCol = await getNotificationCollection();
13+
await notificationCol.insertMany(
14+
memberPublicKeys.map((memberPublicKey) => ({
15+
owner: memberPublicKey,
16+
type,
17+
read: false,
18+
data: { space, proposalCid, title, content, contentType, cid },
19+
createdAt,
20+
})),
21+
);
22+
} catch (error) {
23+
logger.error(
24+
`Failed to create notification for notificationType: ${type}, error: ${error.message}`,
25+
);
26+
}
27+
}
28+
29+
module.exports = createMentionNotification;

backend/packages/backend/src/services/notification/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
const createNotification = require("./createNotification");
2+
const createMentionNotification = require("./createMentionNotification");
23
const getNotifications = require("./getNotifications");
34
const clearUnreadNotifications = require("./clearUnreadNotifications");
45
const getUnreadNotificationsCount = require("./getUnreadNotificationsCount");
56

67
module.exports = {
78
createNotification,
9+
createMentionNotification,
810
getNotifications,
911
clearUnreadNotifications,
1012
getUnreadNotificationsCount,

backend/packages/backend/src/services/proposal.service/postComment.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
const { safeHtml } = require("../../utils/post");
22
const { getProposalCollection, getCommentCollection } = require("../../mongo");
33
const { HttpError } = require("../../exc");
4-
const { ContentType } = require("../../constants");
4+
const { ContentType, NotificationType } = require("../../constants");
55
const { pinData } = require("./common");
66
const { SpaceType } = require("../../consts/space");
7+
const { createMentionNotification } = require("../notification");
78

89
async function postComment(
910
proposalCid,
@@ -68,6 +69,18 @@ async function postComment(
6869
},
6970
);
7071

72+
await createMentionNotification(
73+
NotificationType.CommentMentionUser,
74+
content,
75+
contentType,
76+
{
77+
space: proposal.space,
78+
proposalCid,
79+
title: proposal.title,
80+
cid,
81+
},
82+
);
83+
7184
return newCommentId;
7285
}
7386

backend/packages/backend/src/services/proposal.service/vote.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const {
2323
hasOnePersonOneVoteStrategy,
2424
} = require("../../utils/strategy");
2525
const { isSameAddress, normalizeAddress } = require("../../utils/address");
26+
const { NotificationType } = require("../../constants");
27+
const { createMentionNotification } = require("../notification");
2628

2729
async function getDelegatorBalances({ proposal, voter, voterNetwork }) {
2830
const snapshotHeight = proposal.snapshotHeights?.[voterNetwork];
@@ -391,6 +393,18 @@ async function saveVote({
391393
},
392394
},
393395
);
396+
397+
await createMentionNotification(
398+
NotificationType.VoteMentionUser,
399+
remark,
400+
remarkType,
401+
{
402+
space: proposal.space,
403+
proposalCid,
404+
title: proposal.title,
405+
cid,
406+
},
407+
);
394408
}
395409

396410
async function handleBalanceVote({
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
const { isAddress, isEthereumAddress } = require("@polkadot/util-crypto");
2+
const { ContentType } = require("../constants");
3+
const cheerio = require("cheerio");
4+
const { toPublicKey } = require(".");
5+
6+
function findDIDMentionsFromMarkdown(content) {
7+
const mentions = [];
8+
const reMention = /\[@[^\]]+\]\((\w+)-(\w+)\)/g;
9+
let match;
10+
while ((match = reMention.exec(content)) !== null) {
11+
const [, address] = match;
12+
if (!isAddress(address) && !isEthereumAddress(address)) {
13+
continue;
14+
}
15+
mentions.push(address);
16+
}
17+
18+
return mentions;
19+
}
20+
21+
function findNonDIDMentionsFromMarkdown(content) {
22+
const mentions = [];
23+
24+
const reMention = /\[@[^\]]+\]\(\/(member|user)\/([-\w]+)\)/g;
25+
let match;
26+
while ((match = reMention.exec(content)) !== null) {
27+
const [, , usernameOrAddr] = match;
28+
mentions.push(usernameOrAddr);
29+
}
30+
31+
return mentions;
32+
}
33+
34+
function extractMentions(content, contentType) {
35+
const mentions = new Set();
36+
if (contentType === ContentType.Markdown) {
37+
const foundMentions = [
38+
...findDIDMentionsFromMarkdown(content),
39+
...findNonDIDMentionsFromMarkdown(content),
40+
];
41+
for (const item of foundMentions) {
42+
mentions.add(item);
43+
}
44+
}
45+
46+
if (contentType === ContentType.Html) {
47+
const $ = cheerio.load(content);
48+
$(".mention").each((i, el) => {
49+
const at = $(el).attr("data-id");
50+
if (at) {
51+
mentions.add(at);
52+
}
53+
});
54+
}
55+
56+
return mentions;
57+
}
58+
59+
async function extractMentionUsers(content, contentType) {
60+
const mentions = extractMentions(content, contentType);
61+
62+
const polkadotAddresses = [];
63+
const ethereumAddresses = [];
64+
for (const item of mentions) {
65+
if (isEthereumAddress(item)) {
66+
ethereumAddresses.push(item);
67+
continue;
68+
}
69+
if (isAddress(item)) {
70+
polkadotAddresses.push(item);
71+
continue;
72+
}
73+
}
74+
75+
return [...polkadotAddresses, ...ethereumAddresses].map((item) =>
76+
toPublicKey(item),
77+
);
78+
}
79+
80+
module.exports = {
81+
extractMentionUsers,
82+
};

0 commit comments

Comments
 (0)