birdclaw 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +55 -5
  3. package/bin/birdclaw.mjs +50 -11
  4. package/package.json +9 -7
  5. package/public/birdclaw-mark.png +0 -0
  6. package/scripts/browser-perf.mjs +400 -0
  7. package/scripts/build-docs-site.mjs +940 -0
  8. package/scripts/docs-site-assets.mjs +311 -0
  9. package/scripts/run-vitest.mjs +21 -0
  10. package/scripts/sanitize-node-options.mjs +23 -0
  11. package/scripts/start-test-server.mjs +42 -0
  12. package/src/cli.ts +422 -14
  13. package/src/components/AccountSwitcher.tsx +186 -0
  14. package/src/components/AppNav.tsx +29 -9
  15. package/src/components/AvatarChip.tsx +9 -3
  16. package/src/components/BrandMark.tsx +67 -0
  17. package/src/components/ConversationThread.tsx +11 -10
  18. package/src/components/DmWorkspace.tsx +39 -14
  19. package/src/components/FeedState.tsx +147 -0
  20. package/src/components/InboxCard.tsx +14 -2
  21. package/src/components/LinkPreviewCard.tsx +40 -18
  22. package/src/components/MarkdownViewer.tsx +452 -0
  23. package/src/components/SavedTimelineView.tsx +64 -56
  24. package/src/components/SyncNowButton.tsx +137 -0
  25. package/src/components/ThemeSlider.tsx +49 -93
  26. package/src/components/TimelineCard.tsx +364 -136
  27. package/src/components/TimelineRouteFrame.tsx +170 -0
  28. package/src/components/TweetMediaGrid.tsx +170 -24
  29. package/src/components/TweetRichText.tsx +28 -13
  30. package/src/components/account-selection.ts +64 -0
  31. package/src/components/useTimelineRouteData.ts +153 -0
  32. package/src/lib/account-sync-job.ts +654 -0
  33. package/src/lib/actions-transport.ts +216 -146
  34. package/src/lib/api-client.ts +304 -0
  35. package/src/lib/archive-finder.ts +72 -53
  36. package/src/lib/archive-import.ts +1377 -1298
  37. package/src/lib/authored-live.ts +261 -204
  38. package/src/lib/avatar-cache.ts +159 -44
  39. package/src/lib/backup.ts +1532 -951
  40. package/src/lib/bird-actions.ts +139 -57
  41. package/src/lib/bird-command.ts +101 -28
  42. package/src/lib/bird.ts +549 -194
  43. package/src/lib/blocklist.ts +40 -23
  44. package/src/lib/blocks-write.ts +129 -80
  45. package/src/lib/blocks.ts +165 -97
  46. package/src/lib/bookmark-sync-job.ts +250 -160
  47. package/src/lib/conversation-surface.ts +205 -0
  48. package/src/lib/db.ts +35 -3
  49. package/src/lib/dms-live.ts +720 -66
  50. package/src/lib/effect-runtime.ts +45 -0
  51. package/src/lib/follow-graph.ts +224 -180
  52. package/src/lib/http-effect.ts +222 -0
  53. package/src/lib/inbox.ts +74 -43
  54. package/src/lib/link-index.ts +88 -76
  55. package/src/lib/link-insights.ts +24 -0
  56. package/src/lib/link-preview-metadata.ts +472 -52
  57. package/src/lib/media-fetch.ts +286 -213
  58. package/src/lib/mention-threads-live.ts +352 -288
  59. package/src/lib/mentions-live.ts +390 -342
  60. package/src/lib/moderation-target.ts +102 -65
  61. package/src/lib/moderation-write.ts +77 -18
  62. package/src/lib/mutes-write.ts +129 -80
  63. package/src/lib/mutes.ts +8 -1
  64. package/src/lib/openai.ts +84 -53
  65. package/src/lib/period-digest.ts +953 -0
  66. package/src/lib/profile-affiliation-hydration.ts +93 -54
  67. package/src/lib/profile-hydration.ts +124 -72
  68. package/src/lib/profile-replies.ts +60 -43
  69. package/src/lib/profile-resolver.ts +402 -294
  70. package/src/lib/queries.ts +1024 -189
  71. package/src/lib/research.ts +165 -120
  72. package/src/lib/sqlite.ts +1 -0
  73. package/src/lib/timeline-collections-live.ts +204 -167
  74. package/src/lib/timeline-live.ts +60 -39
  75. package/src/lib/tweet-lookup.ts +30 -19
  76. package/src/lib/types.ts +38 -1
  77. package/src/lib/ui.ts +41 -10
  78. package/src/lib/url-expansion.ts +226 -55
  79. package/src/lib/url-safety.ts +220 -0
  80. package/src/lib/web-sync.ts +511 -0
  81. package/src/lib/whois.ts +166 -147
  82. package/src/lib/x-web.ts +102 -71
  83. package/src/lib/xurl.ts +681 -411
  84. package/src/routeTree.gen.ts +63 -0
  85. package/src/routes/__root.tsx +25 -5
  86. package/src/routes/api/action.tsx +127 -78
  87. package/src/routes/api/avatar.tsx +39 -30
  88. package/src/routes/api/blocks.tsx +26 -23
  89. package/src/routes/api/conversation.tsx +25 -14
  90. package/src/routes/api/inbox.tsx +27 -21
  91. package/src/routes/api/link-insights.tsx +31 -25
  92. package/src/routes/api/link-preview.tsx +25 -21
  93. package/src/routes/api/period-digest.tsx +123 -0
  94. package/src/routes/api/profile-hydrate.tsx +31 -28
  95. package/src/routes/api/query.tsx +79 -55
  96. package/src/routes/api/status.tsx +18 -10
  97. package/src/routes/api/sync.tsx +105 -0
  98. package/src/routes/dms.tsx +195 -55
  99. package/src/routes/inbox.tsx +32 -19
  100. package/src/routes/index.tsx +21 -127
  101. package/src/routes/links.tsx +50 -5
  102. package/src/routes/mentions.tsx +21 -127
  103. package/src/routes/today.tsx +441 -0
  104. package/src/styles.css +74 -11
  105. package/vite.config.ts +8 -0
@@ -1,6 +1,8 @@
1
1
  import type { Database } from "./sqlite";
2
+ import { Effect } from "effect";
2
3
  import { lookupProfileViaBird } from "./bird-actions";
3
4
  import { getNativeDb } from "./db";
5
+ import { runEffectPromise, tryPromise } from "./effect-runtime";
4
6
  import type { ProfileRecord, XurlMentionUser } from "./types";
5
7
  import { getExternalUserId, upsertProfileFromXUser } from "./x-profile";
6
8
  import {
@@ -14,6 +16,17 @@ export interface ResolvedModerationProfile {
14
16
  externalUserId: string | null;
15
17
  }
16
18
 
19
+ function toError(error: unknown) {
20
+ return error instanceof Error ? error : new Error(String(error));
21
+ }
22
+
23
+ function trySync<T>(try_: () => T) {
24
+ return Effect.try({
25
+ try: try_,
26
+ catch: toError,
27
+ });
28
+ }
29
+
17
30
  export function toProfile(row: Record<string, unknown>): ProfileRecord {
18
31
  const followingCount = Number(row.following_count ?? 0);
19
32
  return {
@@ -90,90 +103,114 @@ export function resolveLocalProfile(
90
103
  };
91
104
  }
92
105
 
93
- export async function resolveProfile(
106
+ export function resolveProfileEffect(
94
107
  query: string,
95
- ): Promise<ResolvedModerationProfile> {
96
- const db = getNativeDb();
97
- const normalizedQuery = normalizeProfileQuery(query);
98
- if (!normalizedQuery) {
99
- throw new Error("Missing profile handle or id");
100
- }
108
+ ): Effect.Effect<ResolvedModerationProfile, unknown> {
109
+ return Effect.gen(function* () {
110
+ const db = yield* trySync(() => getNativeDb());
111
+ const normalizedQuery = normalizeProfileQuery(query);
112
+ if (!normalizedQuery) {
113
+ return yield* Effect.fail(new Error("Missing profile handle or id"));
114
+ }
101
115
 
102
- const local = resolveLocalProfile(db, normalizedQuery);
103
- if (process.env.BIRDCLAW_DISABLE_LIVE_PROFILE_LOOKUP === "1") {
104
- if (local) {
116
+ const local = yield* trySync(() =>
117
+ resolveLocalProfile(db, normalizedQuery),
118
+ );
119
+ if (process.env.BIRDCLAW_DISABLE_LIVE_PROFILE_LOOKUP === "1") {
120
+ if (local) {
121
+ return local;
122
+ }
123
+ return yield* Effect.fail(
124
+ new Error(`Profile not found locally: ${query}`),
125
+ );
126
+ }
127
+ if (
128
+ local &&
129
+ !local.profile.id.startsWith("profile_group_") &&
130
+ local.externalUserId
131
+ ) {
105
132
  return local;
106
133
  }
107
- throw new Error(`Profile not found locally: ${query}`);
108
- }
109
- if (
110
- local &&
111
- !local.profile.id.startsWith("profile_group_") &&
112
- local.externalUserId
113
- ) {
114
- return local;
115
- }
116
-
117
- let user: XurlMentionUser | undefined;
118
- let lastError: unknown;
119
134
 
120
- try {
121
- user =
122
- (await lookupProfileViaBird(local?.profile.handle ?? normalizedQuery)) ??
123
- undefined;
124
- } catch (error) {
125
- lastError = error;
126
- }
135
+ let user: XurlMentionUser | undefined;
136
+ let lastError: unknown;
137
+
138
+ const birdResult = yield* tryPromise(() =>
139
+ lookupProfileViaBird(local?.profile.handle ?? normalizedQuery),
140
+ ).pipe(
141
+ Effect.map((value) => ({ ok: true as const, value })),
142
+ Effect.catchAll((error) => Effect.succeed({ ok: false as const, error })),
143
+ );
144
+ if (birdResult.ok) {
145
+ user = birdResult.value ?? undefined;
146
+ } else {
147
+ lastError = birdResult.error;
148
+ }
127
149
 
128
- if (!user) {
129
- try {
130
- if (/^\d+$/.test(normalizedQuery)) {
131
- [user] = await lookupUsersByIds([normalizedQuery]);
150
+ if (!user) {
151
+ const xurlResult = yield* tryPromise(() =>
152
+ /^\d+$/.test(normalizedQuery)
153
+ ? lookupUsersByIds([normalizedQuery])
154
+ : lookupUsersByHandles([local?.profile.handle ?? normalizedQuery]),
155
+ ).pipe(
156
+ Effect.map((value) => ({ ok: true as const, value })),
157
+ Effect.catchAll((error) =>
158
+ Effect.succeed({ ok: false as const, error }),
159
+ ),
160
+ );
161
+ if (xurlResult.ok) {
162
+ [user] = xurlResult.value;
132
163
  } else {
133
- [user] = await lookupUsersByHandles([
134
- local?.profile.handle ?? normalizedQuery,
135
- ]);
164
+ lastError = xurlResult.error;
136
165
  }
137
- } catch (error) {
138
- lastError = error;
139
166
  }
140
- }
141
167
 
142
- if (!user && lastError) {
143
- if (local) {
144
- return local;
168
+ if (!user && lastError) {
169
+ if (local) {
170
+ return local;
171
+ }
172
+ return yield* Effect.fail(lastError);
145
173
  }
146
- throw lastError;
147
- }
148
174
 
149
- if (!user) {
150
- if (local) {
151
- return local;
175
+ if (!user) {
176
+ if (local) {
177
+ return local;
178
+ }
179
+ return yield* Effect.fail(new Error(`Profile not found: ${query}`));
152
180
  }
153
- throw new Error(`Profile not found: ${query}`);
154
- }
155
181
 
156
- if (local) {
157
- return upsertProfileFromXUser(db, user);
158
- }
182
+ if (local) {
183
+ return yield* trySync(() => upsertProfileFromXUser(db, user));
184
+ }
159
185
 
160
- const username = String(user.username ?? "").replace(/^@/, "");
161
- if (username) {
162
- const localByHandle = resolveLocalProfile(db, username);
163
- if (localByHandle) {
164
- return upsertProfileFromXUser(db, user);
186
+ const username = String(user.username ?? "").replace(/^@/, "");
187
+ if (username) {
188
+ const localByHandle = yield* trySync(() =>
189
+ resolveLocalProfile(db, username),
190
+ );
191
+ if (localByHandle) {
192
+ return yield* trySync(() => upsertProfileFromXUser(db, user));
193
+ }
165
194
  }
166
- }
167
195
 
168
- return upsertProfileFromXUser(db, user);
196
+ return yield* trySync(() => upsertProfileFromXUser(db, user));
197
+ });
198
+ }
199
+
200
+ export function resolveProfile(
201
+ query: string,
202
+ ): Promise<ResolvedModerationProfile> {
203
+ return runEffectPromise(resolveProfileEffect(query));
169
204
  }
170
205
 
171
- export async function getAuthenticatedUserId() {
172
- try {
173
- const me = await lookupAuthenticatedUser();
206
+ export function getAuthenticatedUserIdEffect() {
207
+ return Effect.gen(function* () {
208
+ const me = yield* tryPromise(() => lookupAuthenticatedUser());
174
209
  const id = me?.id;
175
210
  return typeof id === "string" && id.length > 0 ? id : null;
176
- } catch {
177
- return null;
178
- }
211
+ }).pipe(Effect.catchAll(() => Effect.succeed(null)));
212
+ }
213
+
214
+ export function getAuthenticatedUserId() {
215
+ return runEffectPromise(getAuthenticatedUserIdEffect());
179
216
  }
@@ -1,12 +1,15 @@
1
1
  import type { Database } from "./sqlite";
2
+ import { Effect } from "effect";
2
3
  import type { ActionsTransport } from "./config";
3
4
  import { getNativeDb } from "./db";
5
+ import { runEffectPromise } from "./effect-runtime";
4
6
  import {
5
7
  getAccountHandle,
6
8
  getDefaultAccountId,
7
9
  normalizeProfileQuery,
8
- resolveProfile,
10
+ resolveProfileEffect,
9
11
  } from "./moderation-target";
12
+ import { getExternalUserId } from "./x-profile";
10
13
 
11
14
  export interface ModerationActionOptions {
12
15
  transport?: ActionsTransport;
@@ -18,28 +21,84 @@ interface ResolveModerationTargetParams {
18
21
  selfActionError: string;
19
22
  }
20
23
 
21
- export async function resolveModerationTarget({
24
+ function toError(error: unknown) {
25
+ return error instanceof Error ? error : new Error(String(error));
26
+ }
27
+
28
+ function trySync<T>(try_: () => T) {
29
+ return Effect.try({
30
+ try: try_,
31
+ catch: toError,
32
+ });
33
+ }
34
+
35
+ export function resolveModerationTargetEffect({
22
36
  accountId,
23
37
  query,
24
38
  selfActionError,
25
39
  }: ResolveModerationTargetParams) {
26
- const db = getNativeDb();
27
- const resolvedAccountId = accountId || getDefaultAccountId(db);
28
- const accountHandle = getAccountHandle(db, resolvedAccountId);
29
- if (normalizeProfileQuery(query) === accountHandle) {
30
- throw new Error(selfActionError);
31
- }
40
+ return Effect.gen(function* () {
41
+ const db = yield* trySync(() => getNativeDb());
42
+ const resolvedAccountId = accountId || getDefaultAccountId(db);
43
+ const accountHandle = yield* trySync(() =>
44
+ getAccountHandle(db, resolvedAccountId),
45
+ );
46
+ if (!accountHandle) {
47
+ return yield* Effect.fail(
48
+ new Error(`Unknown account: ${resolvedAccountId}`),
49
+ );
50
+ }
51
+ const normalizedQuery = normalizeProfileQuery(query);
52
+ if (normalizedQuery.toLowerCase() === accountHandle.toLowerCase()) {
53
+ return yield* Effect.fail(new Error(selfActionError));
54
+ }
55
+
56
+ const resolved = yield* resolveProfileEffect(query);
57
+ const account = yield* trySync(
58
+ () =>
59
+ db
60
+ .prepare("select handle, external_user_id from accounts where id = ?")
61
+ .get(resolvedAccountId) as
62
+ | { handle: string; external_user_id: string | null }
63
+ | undefined,
64
+ );
65
+ const accountProfile = yield* trySync(() =>
66
+ account
67
+ ? (db
68
+ .prepare("select id from profiles where handle = ? limit 1")
69
+ .get(account.handle.replace(/^@/, "")) as
70
+ | { id: string }
71
+ | undefined)
72
+ : undefined,
73
+ );
74
+ const accountExternalUserId =
75
+ account?.external_user_id ?? getExternalUserId(accountProfile?.id ?? "");
76
+ if (
77
+ resolved.profile.handle.toLowerCase() === accountHandle.toLowerCase() ||
78
+ (accountProfile?.id && resolved.profile.id === accountProfile.id) ||
79
+ (accountExternalUserId &&
80
+ resolved.externalUserId === accountExternalUserId)
81
+ ) {
82
+ return yield* Effect.fail(new Error(selfActionError));
83
+ }
84
+
85
+ return {
86
+ db,
87
+ resolved,
88
+ resolvedAccountId,
89
+ accountIdentity: {
90
+ id: resolvedAccountId,
91
+ handle: account?.handle ?? accountHandle,
92
+ externalUserId: accountExternalUserId || null,
93
+ },
94
+ actionQuery:
95
+ resolved.externalUserId ?? resolved.profile.handle ?? normalizedQuery,
96
+ };
97
+ });
98
+ }
32
99
 
33
- const resolved = await resolveProfile(query);
34
- return {
35
- db,
36
- resolved,
37
- resolvedAccountId,
38
- actionQuery:
39
- resolved.externalUserId ??
40
- resolved.profile.handle ??
41
- normalizeProfileQuery(query),
42
- };
100
+ export function resolveModerationTarget(params: ResolveModerationTargetParams) {
101
+ return runEffectPromise(resolveModerationTargetEffect(params));
43
102
  }
44
103
 
45
104
  export function writeModerationRow(
@@ -1,118 +1,167 @@
1
+ import { Effect } from "effect";
1
2
  import { runModerationAction } from "./actions-transport";
3
+ import { runEffectPromise, tryPromise } from "./effect-runtime";
2
4
  import {
3
5
  deleteModerationRow,
4
6
  type ModerationActionOptions,
5
- resolveModerationTarget,
7
+ resolveModerationTargetEffect,
6
8
  writeModerationRow,
7
9
  } from "./moderation-write";
8
10
 
9
- export async function addMute(
11
+ function trySync<T>(try_: () => T) {
12
+ return Effect.try({
13
+ try: try_,
14
+ catch: (error) =>
15
+ error instanceof Error ? error : new Error(String(error)),
16
+ });
17
+ }
18
+
19
+ export function addMuteEffect(
10
20
  accountId: string,
11
21
  query: string,
12
22
  options: ModerationActionOptions = {},
13
23
  ) {
14
- const { db, resolved, resolvedAccountId, actionQuery } =
15
- await resolveModerationTarget({
16
- accountId,
17
- query,
18
- selfActionError: "Cannot mute the current account",
19
- });
20
- const transport = await runModerationAction({
21
- action: "mute",
22
- query: actionQuery,
23
- targetUserId: resolved.externalUserId ?? undefined,
24
- transport: options.transport,
25
- });
24
+ return Effect.gen(function* () {
25
+ const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } =
26
+ yield* resolveModerationTargetEffect({
27
+ accountId,
28
+ query,
29
+ selfActionError: "Cannot mute the current account",
30
+ });
31
+ const transport = yield* tryPromise(() =>
32
+ runModerationAction({
33
+ action: "mute",
34
+ query: actionQuery,
35
+ targetUserId: resolved.externalUserId ?? undefined,
36
+ transport: options.transport,
37
+ expectedAccount: accountIdentity,
38
+ }),
39
+ );
40
+
41
+ if (!transport.ok) {
42
+ return {
43
+ ok: false,
44
+ action: "mute",
45
+ accountId: resolvedAccountId,
46
+ profile: resolved.profile,
47
+ transport,
48
+ };
49
+ }
50
+
51
+ const mutedAt = new Date().toISOString();
52
+ yield* trySync(() =>
53
+ writeModerationRow(
54
+ db,
55
+ "mutes",
56
+ resolvedAccountId,
57
+ resolved.profile.id,
58
+ mutedAt,
59
+ ),
60
+ );
26
61
 
27
- if (!transport.ok) {
28
62
  return {
29
- ok: false,
63
+ ok: true,
30
64
  action: "mute",
31
65
  accountId: resolvedAccountId,
66
+ mutedAt,
32
67
  profile: resolved.profile,
33
68
  transport,
34
69
  };
35
- }
36
-
37
- const mutedAt = new Date().toISOString();
38
- writeModerationRow(
39
- db,
40
- "mutes",
41
- resolvedAccountId,
42
- resolved.profile.id,
43
- mutedAt,
44
- );
70
+ });
71
+ }
45
72
 
46
- return {
47
- ok: true,
48
- action: "mute",
49
- accountId: resolvedAccountId,
50
- mutedAt,
51
- profile: resolved.profile,
52
- transport,
53
- };
73
+ export function addMute(
74
+ accountId: string,
75
+ query: string,
76
+ options: ModerationActionOptions = {},
77
+ ) {
78
+ return runEffectPromise(addMuteEffect(accountId, query, options));
54
79
  }
55
80
 
56
- export async function recordMute(accountId: string, query: string) {
57
- const { db, resolved, resolvedAccountId } = await resolveModerationTarget({
58
- accountId,
59
- query,
60
- selfActionError: "Cannot mute the current account",
61
- });
81
+ export function recordMuteEffect(accountId: string, query: string) {
82
+ return Effect.gen(function* () {
83
+ const { db, resolved, resolvedAccountId } =
84
+ yield* resolveModerationTargetEffect({
85
+ accountId,
86
+ query,
87
+ selfActionError: "Cannot mute the current account",
88
+ });
62
89
 
63
- const mutedAt = new Date().toISOString();
64
- writeModerationRow(
65
- db,
66
- "mutes",
67
- resolvedAccountId,
68
- resolved.profile.id,
69
- mutedAt,
70
- );
90
+ const mutedAt = new Date().toISOString();
91
+ yield* trySync(() =>
92
+ writeModerationRow(
93
+ db,
94
+ "mutes",
95
+ resolvedAccountId,
96
+ resolved.profile.id,
97
+ mutedAt,
98
+ ),
99
+ );
71
100
 
72
- return {
73
- ok: true,
74
- action: "record-mute",
75
- accountId: resolvedAccountId,
76
- mutedAt,
77
- profile: resolved.profile,
78
- };
101
+ return {
102
+ ok: true,
103
+ action: "record-mute",
104
+ accountId: resolvedAccountId,
105
+ mutedAt,
106
+ profile: resolved.profile,
107
+ };
108
+ });
109
+ }
110
+
111
+ export function recordMute(accountId: string, query: string) {
112
+ return runEffectPromise(recordMuteEffect(accountId, query));
79
113
  }
80
114
 
81
- export async function removeMute(
115
+ export function removeMuteEffect(
82
116
  accountId: string,
83
117
  query: string,
84
118
  options: ModerationActionOptions = {},
85
119
  ) {
86
- const { db, resolved, resolvedAccountId, actionQuery } =
87
- await resolveModerationTarget({
88
- accountId,
89
- query,
90
- selfActionError: "Cannot mute the current account",
91
- });
92
- const transport = await runModerationAction({
93
- action: "unmute",
94
- query: actionQuery,
95
- targetUserId: resolved.externalUserId ?? undefined,
96
- transport: options.transport,
97
- });
120
+ return Effect.gen(function* () {
121
+ const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } =
122
+ yield* resolveModerationTargetEffect({
123
+ accountId,
124
+ query,
125
+ selfActionError: "Cannot mute the current account",
126
+ });
127
+ const transport = yield* tryPromise(() =>
128
+ runModerationAction({
129
+ action: "unmute",
130
+ query: actionQuery,
131
+ targetUserId: resolved.externalUserId ?? undefined,
132
+ transport: options.transport,
133
+ expectedAccount: accountIdentity,
134
+ }),
135
+ );
136
+
137
+ if (!transport.ok) {
138
+ return {
139
+ ok: false,
140
+ action: "unmute",
141
+ accountId: resolvedAccountId,
142
+ profile: resolved.profile,
143
+ transport,
144
+ };
145
+ }
146
+
147
+ yield* trySync(() =>
148
+ deleteModerationRow(db, "mutes", resolvedAccountId, resolved.profile.id),
149
+ );
98
150
 
99
- if (!transport.ok) {
100
151
  return {
101
- ok: false,
152
+ ok: true,
102
153
  action: "unmute",
103
154
  accountId: resolvedAccountId,
104
155
  profile: resolved.profile,
105
156
  transport,
106
157
  };
107
- }
108
-
109
- deleteModerationRow(db, "mutes", resolvedAccountId, resolved.profile.id);
158
+ });
159
+ }
110
160
 
111
- return {
112
- ok: true,
113
- action: "unmute",
114
- accountId: resolvedAccountId,
115
- profile: resolved.profile,
116
- transport,
117
- };
161
+ export function removeMute(
162
+ accountId: string,
163
+ query: string,
164
+ options: ModerationActionOptions = {},
165
+ ) {
166
+ return runEffectPromise(removeMuteEffect(accountId, query, options));
118
167
  }
package/src/lib/mutes.ts CHANGED
@@ -2,7 +2,14 @@ import { getNativeDb } from "./db";
2
2
  import { toProfile } from "./moderation-target";
3
3
  import type { BlockItem } from "./types";
4
4
 
5
- export { addMute, recordMute, removeMute } from "./mutes-write";
5
+ export {
6
+ addMute,
7
+ addMuteEffect,
8
+ recordMute,
9
+ recordMuteEffect,
10
+ removeMute,
11
+ removeMuteEffect,
12
+ } from "./mutes-write";
6
13
 
7
14
  export interface MuteItem {
8
15
  accountId: string;