|
1 |
| -import { and, eq } from "drizzle-orm"; |
| 1 | +import * as R from "remeda"; |
| 2 | +import { and, eq, inArray } from "drizzle-orm"; |
2 | 3 | import * as schema from "../db/schema";
|
3 | 4 | import { db } from "../db";
|
4 | 5 | import { bot } from "../lib/xmtp/client";
|
5 | 6 | import type { ChainAwareAddress } from "../db/schema";
|
6 | 7 | import { getWalletClient } from "../lib/eth/clients";
|
7 | 8 |
|
| 9 | +const { walletClient } = getWalletClient(); |
| 10 | + |
8 | 11 | /**
|
9 | 12 | * Syncs the database member state with the on network XMTP group chat member state
|
10 | 13 | *
|
| 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 | + * |
11 | 20 | * @param groupId Optionally provide a groupId to only sync members for that group
|
12 | 21 | */
|
13 | 22 | 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 | + }); |
15 | 26 |
|
16 |
| - if (!groups) { |
| 27 | + console.log("groupChats", groupChats); |
| 28 | + if (!groupChats) { |
17 | 29 | return;
|
18 | 30 | }
|
19 | 31 |
|
20 | 32 | // - 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({ |
22 | 34 | ...(groupId && {
|
23 | 35 | where: (fields, { eq }) => eq(fields.groupId, groupId),
|
24 | 36 | }),
|
25 | 37 | });
|
26 | 38 |
|
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"; |
57 | 64 | }
|
58 | 65 |
|
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 | + ); |
63 | 71 |
|
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); |
71 | 74 |
|
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 | + } |
74 | 81 |
|
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 | + } |
80 | 103 |
|
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()), |
89 | 112 | );
|
| 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 | + } |
90 | 131 |
|
| 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 |
91 | 136 | continue;
|
92 | 137 | }
|
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, |
111 | 148 | ),
|
112 |
| - ); |
113 |
| - break; |
114 |
| - } |
| 149 | + ), |
| 150 | + ); |
| 151 | + break; |
115 | 152 | }
|
| 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 | + ); |
116 | 182 | }
|
117 | 183 | }
|
0 commit comments