Skip to content

Commit 0bffb72

Browse files
committed
Message.delete() method
1 parent e3bd907 commit 0bffb72

File tree

3 files changed

+98
-4
lines changed

3 files changed

+98
-4
lines changed

examples/greet.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,16 @@ const bot = createBot<void>({
3232
behindProxy: true,
3333
});
3434

35-
const session = bot.getSession(new URL("https://yourdomain"));
35+
const session = bot.getSession(
36+
new URL(Deno.env.get("ORIGIN") ?? "http://localhost"),
37+
);
3638
setInterval(async () => {
37-
await session.publish(text`Hi, forks! It's a minutely greeting.`);
39+
const message = await session.publish(
40+
text`Hi, forks! It's a minutely greeting. It will be deleted in 30 seconds.`,
41+
);
42+
setTimeout(async () => {
43+
await message.delete();
44+
}, 1000 * 30);
3845
}, 1000 * 60);
3946

4047
export default bot;

src/message-impl.ts

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,21 @@
1515
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1616
import {
1717
type Actor,
18+
Article,
19+
ChatMessage,
20+
Create,
21+
Delete,
1822
Document,
1923
Hashtag,
2024
isActor,
25+
type KvKey,
2126
LanguageString,
2227
Mention,
23-
type Note,
28+
Note,
29+
type Object,
2430
PUBLIC_COLLECTION,
31+
Question,
32+
Tombstone,
2533
} from "@fedify/fedify";
2634
import type { LanguageTag } from "@phensley/language-tag";
2735
import { unescape } from "@std/html/entities";
@@ -34,6 +42,8 @@ import type {
3442
} from "./session.ts";
3543
import type { Text } from "./text.ts";
3644

45+
const messageClasses = [Article, ChatMessage, Note, Question];
46+
3747
export class MessageImpl<T extends MessageClass, TContextData>
3848
implements Message<T, TContextData> {
3949
readonly session: SessionImpl<TContextData>;
@@ -50,7 +60,7 @@ export class MessageImpl<T extends MessageClass, TContextData>
5060

5161
constructor(
5262
session: SessionImpl<TContextData>,
53-
message: Omit<Message<T, TContextData>, "reply">,
63+
message: Omit<Message<T, TContextData>, "delete" | "reply">,
5464
) {
5565
this.session = session;
5666
this.raw = message.raw;
@@ -65,6 +75,74 @@ export class MessageImpl<T extends MessageClass, TContextData>
6575
this.attachments = message.attachments;
6676
}
6777

78+
async delete(): Promise<void> {
79+
const parsed = this.session.context.parseUri(this.id);
80+
if (
81+
parsed?.type !== "object" ||
82+
!messageClasses.some((cls) => parsed.class === cls)
83+
) {
84+
return;
85+
}
86+
const { id } = parsed.values;
87+
const kv = this.session.bot.kv;
88+
const listKey: KvKey = this.session.bot.kvPrefixes.messages;
89+
const lockKey: KvKey = [...listKey, "lock"];
90+
const lockId = `${id}:delete`;
91+
do {
92+
await kv.set(lockKey, lockId);
93+
const set = new Set(await kv.get<string[]>(listKey) ?? []);
94+
set.delete(id);
95+
const list = [...set];
96+
list.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
97+
await kv.set(listKey, list);
98+
} while (await kv.get(lockKey) !== lockId);
99+
const messageKey: KvKey = [...listKey, id];
100+
const createJson = await kv.get(messageKey);
101+
if (createJson == null) return;
102+
await kv.delete(messageKey);
103+
const create = await Create.fromJsonLd(createJson, this.session.context);
104+
const message = await create.getObject(this.session.context);
105+
if (message == null) return;
106+
const mentionedActorIds: Set<string> = new Set();
107+
for await (const tag of message.getTags(this.session.context)) {
108+
if (tag instanceof Mention && tag.href != null) {
109+
mentionedActorIds.add(tag.href.href);
110+
}
111+
}
112+
const promises: Promise<Object | null>[] = [];
113+
const documentLoader = await this.session.context.getDocumentLoader(
114+
this.session.bot,
115+
);
116+
for (const uri of mentionedActorIds) {
117+
promises.push(this.session.context.lookupObject(uri, { documentLoader }));
118+
}
119+
const mentionedActors = (await Promise.all(promises)).filter(isActor);
120+
const activity = new Delete({
121+
id: new URL("#delete", this.id),
122+
actor: this.session.context.getActorUri(this.session.bot.identifier),
123+
tos: create.toIds,
124+
ccs: create.ccIds,
125+
object: new Tombstone({
126+
id: this.id,
127+
}),
128+
});
129+
const excludeBaseUris = [new URL(this.session.context.origin)];
130+
await this.session.context.sendActivity(
131+
this.session.bot,
132+
"followers",
133+
activity,
134+
{ preferSharedInbox: true, excludeBaseUris },
135+
);
136+
for (const actor of mentionedActors) {
137+
await this.session.context.sendActivity(
138+
this.session.bot,
139+
actor,
140+
activity,
141+
{ preferSharedInbox: true, excludeBaseUris },
142+
);
143+
}
144+
}
145+
68146
reply(
69147
text: Text<TContextData>,
70148
options?: SessionPublishOptions,

src/message.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ export interface Message<T extends MessageClass, TContextData> {
118118
*/
119119
readonly attachments: readonly Document[];
120120

121+
/**
122+
* Deletes the message, if possible.
123+
*
124+
* If the message is not published by the bot, it will silently fail.
125+
*
126+
* If the message is already deleted, it will be a no-op.
127+
*/
128+
delete(): Promise<void>;
129+
121130
/**
122131
* Publishes a reply to the message.
123132
* @param text The content of the message.

0 commit comments

Comments
 (0)