Skip to content

Commit 9d3924a

Browse files
committed
feat: add revert missing members to pending on sync
1 parent 14863f0 commit 9d3924a

File tree

5 files changed

+156
-87
lines changed

5 files changed

+156
-87
lines changed

src/actions/retry-pending-members.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as R from "remeda";
12
import { and, eq } from "drizzle-orm";
23
import * as schema from "../db/schema";
34
import { db } from "../db";
@@ -13,7 +14,12 @@ export async function retryPendingMembers(groupId?: string) {
1314
: eq(fields.status, "pending"),
1415
});
1516

16-
await Promise.allSettled(pendingMembers.map(retryAddMember));
17+
const batchedPromises = R.chunk(pendingMembers.map(retryAddMember), 10);
18+
19+
for (const batch of batchedPromises) {
20+
// TODO: maybe we should also have a delay?
21+
await Promise.allSettled(batch);
22+
}
1723
}
1824

1925
/**
+147-81
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,183 @@
1-
import { and, eq } from "drizzle-orm";
1+
import * as R from "remeda";
2+
import { and, eq, inArray } from "drizzle-orm";
23
import * as schema from "../db/schema";
34
import { db } from "../db";
45
import { bot } from "../lib/xmtp/client";
56
import type { ChainAwareAddress } from "../db/schema";
67
import { getWalletClient } from "../lib/eth/clients";
78

9+
const { walletClient } = getWalletClient();
10+
811
/**
912
* Syncs the database member state with the on network XMTP group chat member state
1013
*
14+
* This function will:
15+
* - Ensure that the xmtp group chats exist in the database if not found
16+
* - Ensure that all chat members are in the database
17+
* - Ensure database members with approved status who are not in the group chat are reverted to pending status
18+
* - Ensure database member status is approved if the user is in the group on XMTP
19+
*
1120
* @param groupId Optionally provide a groupId to only sync members for that group
1221
*/
1322
export async function syncStoredMembersWithXmtp(groupId?: string) {
14-
const groups = await bot.listGroups();
23+
const groupChats = await bot.listGroups().catch((e) => {
24+
console.error("Failed to list groups", e);
25+
});
1526

16-
if (!groups) {
27+
console.log("groupChats", groupChats);
28+
if (!groupChats) {
1729
return;
1830
}
1931

2032
// - ensure that the database member status is approved if the user is in the group on XMTP
21-
const members = await db.query.groupMembers.findMany({
33+
const membersFromDatabase = await db.query.groupMembers.findMany({
2234
...(groupId && {
2335
where: (fields, { eq }) => eq(fields.groupId, groupId),
2436
}),
2537
});
2638

27-
for (const group of groups) {
28-
// - only sync the members for the provided group if defined
29-
if (groupId && group.group_id !== groupId) {
30-
continue;
31-
}
32-
33-
const storedMembers = members.filter((m) => m.groupId === group.group_id);
34-
35-
if (storedMembers.length === 0) {
36-
// - ensure the group exists in the database
37-
const storedGroup = await db.query.groups.findFirst({
38-
where: (fields, { eq }) => eq(fields.id, group.group_id),
39-
});
40-
41-
// - store the group is not found
42-
if (!storedGroup) {
43-
const { walletClient } = getWalletClient();
44-
if (
45-
group.metdata.creator_account_address.toLowerCase() !==
46-
walletClient.account.address.toLowerCase() ||
47-
group.metdata.policy !== "GroupCreatorIsAdmin"
48-
) {
49-
// - this is not a group that we don't manage so we do nothing
50-
// ? maybe we remove this in the future
51-
return;
52-
}
53-
54-
await db.insert(schema.groups).values({
55-
id: group.group_id,
56-
});
39+
// - first separate the groups into missing, stored and unsupported
40+
// - missing groups are groups that are not in the database
41+
// - stored groups are groups that are in the database
42+
// - unsupported groups are groups that are not supported by this function
43+
// - then map the values down onto each of the members so we end up with
44+
// - an array of members with the group id & metadata attached
45+
const { missing: missingGroups, stored: storedGroups } = R.pipe(
46+
groupChats,
47+
R.groupBy((group) => {
48+
console.log(
49+
"groupChats -> ",
50+
group,
51+
"unsupported",
52+
group.metadata.creator_account_address.toLowerCase() !==
53+
walletClient.account.address.toLowerCase() ||
54+
group.metadata.policy !== "GroupCreatorIsAdmin",
55+
);
56+
if (
57+
group.metadata.creator_account_address.toLowerCase() !==
58+
walletClient.account.address.toLowerCase() ||
59+
group.metadata.policy !== "GroupCreatorIsAdmin"
60+
) {
61+
// - this is not a group that we don't manage so we do nothing
62+
// ? maybe we remove this in the future
63+
return "unsupported";
5764
}
5865

59-
// - the user has been added to the chat but is not in the database ... probably only possible if there is a bug
60-
// - but best to add them to the database to be safe
61-
62-
// ? for now we assume that they are an EOA
66+
return !membersFromDatabase.some((m) => m.groupId === group.group_id)
67+
? "missing"
68+
: "stored";
69+
}),
70+
);
6371

64-
await db.insert(schema.groupMembers).values(
65-
group.members.map((address) => ({
66-
status: "approved" as const,
67-
chainAwareAddress: `eth:${address}` satisfies ChainAwareAddress,
68-
groupId: group.group_id,
69-
})),
70-
);
72+
console.log("missingGroups", missingGroups);
73+
console.log("storedGroups", storedGroups);
7174

72-
continue;
73-
}
75+
// - find the set of missing groups and store them in the database
76+
for (const missingGroupId of R.unique(
77+
(missingGroups ?? []).map((group) => group.group_id),
78+
)) {
79+
await db.insert(schema.groups).values({ id: missingGroupId });
80+
}
7481

75-
// - check if the user is in the group chat & has the correct status
76-
for (const memberAddress of group.members) {
77-
const storedMember = storedMembers.find((m) =>
78-
m.chainAwareAddress.endsWith(memberAddress),
79-
);
82+
const missingMembers = R.pipe(
83+
missingGroups ?? [],
84+
R.flatMap((group) =>
85+
group.members.map((address) => ({
86+
groupId: group.group_id,
87+
address,
88+
})),
89+
),
90+
);
91+
92+
if (missingMembers.length !== 0) {
93+
// - store the missing groups and their members in the database with no further checks needed on these groups
94+
// ? for now we assume that they are an EOA
95+
await db.insert(schema.groupMembers).values(
96+
missingMembers.map((member) => ({
97+
status: "approved" as const,
98+
chainAwareAddress: `eth:${member.address}` satisfies ChainAwareAddress,
99+
groupId: member.groupId,
100+
})),
101+
);
102+
}
80103

81-
// - if the user is not in the database then add them
82-
if (!storedMember) {
83-
await db.insert(schema.groupMembers).values(
84-
group.members.map((address) => ({
85-
status: "approved" as const,
86-
chainAwareAddress: `eth:${address}` satisfies ChainAwareAddress,
87-
groupId: group.group_id,
88-
})),
104+
const storedMembersInGroupChat = R.pipe(
105+
storedGroups ?? [],
106+
R.flatMap((group) =>
107+
group.members.map((address) => {
108+
const member = membersFromDatabase.find(
109+
(m) =>
110+
m.groupId === group.group_id &&
111+
m.chainAwareAddress.toLowerCase().endsWith(address.toLowerCase()),
89112
);
113+
const { status, chainAwareAddress } = member ?? {};
114+
return {
115+
groupId: group.group_id,
116+
address,
117+
// biome-ignore lint/style/noNonNullAssertion: we filtered on these members to get here
118+
chainAwareAddress: chainAwareAddress!,
119+
// biome-ignore lint/style/noNonNullAssertion: we filtered on these members to get here
120+
status: status!,
121+
};
122+
}),
123+
),
124+
);
125+
126+
for (const storedMember of storedMembersInGroupChat) {
127+
// - only sync the already stored members for the provided group if defined
128+
if (groupId && storedMember.groupId !== groupId) {
129+
continue;
130+
}
90131

132+
// - if the user is in the group chat but is not approved then approve them
133+
switch (storedMember.status) {
134+
case "approved": {
135+
// - do nothing they are already approved
91136
continue;
92137
}
93-
94-
// - if the user is in the group chat but is not approved then approve them
95-
switch (storedMember.status) {
96-
case "approved": {
97-
// - do nothing they are already approved
98-
continue;
99-
}
100-
default:
101-
await db
102-
.update(schema.groupMembers)
103-
.set({ status: "approved" as const })
104-
.where(
105-
and(
106-
eq(schema.groupMembers.groupId, group.group_id),
107-
eq(
108-
schema.groupMembers.chainAwareAddress,
109-
storedMember.chainAwareAddress,
110-
),
138+
default:
139+
await db
140+
.update(schema.groupMembers)
141+
.set({ status: "approved" as const })
142+
.where(
143+
and(
144+
eq(schema.groupMembers.groupId, storedMember.groupId),
145+
eq(
146+
schema.groupMembers.chainAwareAddress,
147+
storedMember.chainAwareAddress,
111148
),
112-
);
113-
break;
114-
}
149+
),
150+
);
151+
break;
115152
}
153+
154+
// - for members that are in the database but not in the group chat we should revert them to pending status
155+
156+
const storedMembersThatAreNotInGroupChat = R.pipe(
157+
membersFromDatabase,
158+
R.filter((m) => {
159+
// - if the user is in the chat then we don't want to revert them to pending
160+
const groupChat = groupChats.find(
161+
({ group_id: id, members }) =>
162+
id === m.groupId &&
163+
members.some((address) =>
164+
m.chainAwareAddress.toLowerCase().endsWith(address.toLowerCase()),
165+
),
166+
);
167+
if (groupChat) return false;
168+
return true;
169+
}),
170+
);
171+
172+
await db
173+
.update(schema.groupMembers)
174+
.set({ status: "pending" as const })
175+
.where(
176+
inArray(
177+
schema.groupMembers.id,
178+
// biome-ignore lint/style/noNonNullAssertion: these are assigned on insert and so shouldn't be null
179+
storedMembersThatAreNotInGroupChat.map((m) => m.id!),
180+
),
181+
);
116182
}
117183
}

src/index.ts

-3
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,6 @@ export default new Elysia()
8383
.get("/", async () => {
8484
if (process.env.NODE_ENV === "development") {
8585
console.log("groups", await bot.listGroups());
86-
await bot
87-
.send("5eb5b1fa27adc585a75cdedd6a1d4d5d", "Hello")
88-
.catch(console.error);
8986
}
9087

9188
return "Onit XMTP bot 🤖";

src/lib/xmtp/cli.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export async function createClient(dbPath: string) {
7272
groups: {
7373
group_id: string;
7474
members: Address[];
75-
metdata: {
75+
metadata: {
7676
creator_account_address: Address;
7777
policy: "GroupCreatorIsAdmin" | "EveryoneIsAdmin";
7878
};

test/local-api-test.http

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ GET http://localhost:8080/group/4cf971dcb124e9e57fad653cdc6242f1/wallets HTTP/1.
7272

7373

7474
###
75-
GET http://localhost:8080/0x524bF2086D4b5BBdA06f4c16Ec36f06AAd4E1Cad HTTP/1.1
75+
GET http://localhost:8080/wallet/0x524bF2086D4b5BBdA06f4c16Ec36f06AAd4E1Cad HTTP/1.1
7676

7777
###
7878
GET http://localhost:8080/group/ed4ac987e6556dc8802bb7d0fce39d1d/link-wallet/basesep:0xaC03aD602D6786e7E87566192b48e30666e327Ad HTTP/1.1

0 commit comments

Comments
 (0)