Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ To be released.
at the root.

- Added support for consent-respecting quote posts using [FEP-044f].
[[#27], [#28], [#29], [#31], [#32]]
[[#27], [#28], [#29], [#30], [#31], [#32], [#33]]

BotKit now serializes quote policies on outgoing messages, handles
incoming `QuoteRequest` activities, automatically accepts or rejects them
Expand All @@ -62,6 +62,9 @@ To be released.
sends a `QuoteRequest` to the quoted message's author, applies accepted
`QuoteAuthorization` stamps to the stored message, and strips rejected
quote targets from the stored message before delivering an `Update`.
BotKit also verifies `QuoteAuthorization` stamps on received third-party
quote posts and handles deleted stamps by forwarding the `Delete` activity
before stripping the quote from the bot's own post.

- Added `QuotePolicy`, `QuotePolicyOption`, `QuoteRequest`, and
`QuoteRequestEventHandler` types. [[#27], [#28], [#31]]
Expand All @@ -70,13 +73,18 @@ To be released.
types. [[#27], [#29], [#32]]
- Added `Bot.onQuoteAccepted` and `Bot.onQuoteRejected` event handlers.
[[#27], [#29], [#32]]
- Added `QuoteRevokedEventHandler` type and `Bot.onQuoteRevoked` event
handler. [[#27], [#30], [#33]]
- Added `ReadonlyBot.quotePolicy`, `CreateBotOptions.quotePolicy`, and
`BotProfile.quotePolicy` properties. [[#27], [#28], [#31]]
- Added `SessionPublishOptions.quotePolicy` and
`AuthorizedMessageUpdateOptions.quotePolicy` options.
[[#27], [#28], [#31]]
- Added `Message.quotePolicy` and `AuthorizedMessage.quoteApprovalState`
properties. [[#27], [#29], [#32]]
- Added `Message.quoteApproved` property for inspecting whether a
received quote post has valid FEP-044f approval.
[[#27], [#30], [#33]]
- Added `AuthorizedMessage.unauthorizeQuote()` method for revoking an
existing quote authorization stamp by the quoted message or its URI.
[[#27], [#28], [#31]]
Expand All @@ -98,9 +106,11 @@ To be released.
`Repository.removeQuoteAuthorization()`.
- Added quote authorization reference methods:
`Repository.addQuoteAuthorizationReference()`,
`Repository.findQuoteAuthorizationReference()`, and
`Repository.findQuoteAuthorizationReference()`,
`Repository.findQuoteAuthorizationReferenceIdentifiers()`,
`Repository.findQuoteAuthorizationReferenceAttribution()`, and
`Repository.removeQuoteAuthorizationReference()`.
[[#27], [#29], [#32]]
[[#27], [#29], [#30], [#32], [#33]]
- Added optional `Repository.migrate()` method for adopting data
stored by BotKit 0.4 or earlier.
- Added `Repository.forIdentifier()` method and `ActorScopedRepository`
Expand Down Expand Up @@ -136,8 +146,10 @@ To be released.
[#27]: https://github.com/fedify-dev/botkit/issues/27
[#28]: https://github.com/fedify-dev/botkit/issues/28
[#29]: https://github.com/fedify-dev/botkit/issues/29
[#30]: https://github.com/fedify-dev/botkit/issues/30
[#31]: https://github.com/fedify-dev/botkit/pull/31
[#32]: https://github.com/fedify-dev/botkit/pull/32
[#33]: https://github.com/fedify-dev/botkit/pull/33

### @fedify/botkit-sqlite

Expand Down
1 change: 1 addition & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 21 additions & 6 deletions docs/concepts/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,8 @@ the *Message* concept document.
[FEP-044f]: https://w3id.org/fep/044f


Quote accepted and rejected
---------------------------
Quote accepted, rejected, and revoked
-------------------------------------

*This API is available since BotKit 0.5.0.*

Expand All @@ -336,10 +336,9 @@ bot.onQuoteAccepted = (_session, message, approver) => {
};
~~~~

When the request is rejected, or when a previously accepted
`QuoteAuthorization` stamp is later deleted by the quoted author, BotKit
removes the quote target and fallback quote link from the stored message,
sends an `Update` activity, and then calls `~Bot.onQuoteRejected`:
When the request is rejected, BotKit removes the quote target and fallback
quote link from the stored message, sends an `Update` activity, and then calls
`~Bot.onQuoteRejected`:

~~~~ typescript twoslash
import { type Bot } from "@fedify/botkit";
Expand All @@ -351,6 +350,22 @@ bot.onQuoteRejected = (_session, message, rejecter) => {
};
~~~~

If a previously accepted `QuoteAuthorization` stamp is later deleted by the
quoted author's server, BotKit forwards the `Delete` activity to the quote
post's audience, removes the quote target and fallback quote link from the
stored message, sends an `Update` activity, and then calls
`~Bot.onQuoteRevoked`:

~~~~ typescript twoslash
import { type Bot } from "@fedify/botkit";
const bot = {} as unknown as Bot<void>;
// ---cut-before---
bot.onQuoteRevoked = (_session, message, revoker) => {
console.log(`${revoker.id} revoked approval for ${message.id}`);
console.log(message.quoteTarget); // undefined
};
~~~~


Message
-------
Expand Down
16 changes: 13 additions & 3 deletions docs/concepts/message.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,9 +342,9 @@ console.log(message.quoteApprovalState); // "pending"

If the author accepts the quote request, BotKit stores the received
authorization stamp on the message and sends an `Update` activity. If the
author rejects it, BotKit removes the quote target and the fallback quote link
from the stored message and sends an `Update` activity with the stripped
content.
author rejects it, or the author's server later revokes the authorization
stamp, BotKit removes the quote target and the fallback quote link from the
stored message and sends an `Update` activity with the stripped content.

### Polls

Expand Down Expand Up @@ -619,6 +619,16 @@ You can get the message that is quoted in the message through
the `~Message.quoteTarget` property. It is either another `Message` object
or `undefined` if the message is not a quote.

For messages received from the fediverse, `~Message.quoteApproved` reports
whether the quote has FEP-044f approval. It is `undefined` when the message
does not quote anything, `true` for self-quotes and quotes with a valid
`QuoteAuthorization` stamp, and `false` when the stamp is missing, invalid, or
could not be fetched. Legacy quote links such as Misskey quote tags and
`quoteUrl` without a `QuoteAuthorization` stamp are reported as unapproved.

BotKit checks `~Message.quoteApproved` when it materializes the message, so a
message can reflect a later stamp revocation when it is loaded again.

For authorized messages created by your bot,
`~AuthorizedMessage.quoteApprovalState` describes whether the quote target
still awaits FEP-044f approval. It is `"pending"` for remote quote targets
Expand Down
23 changes: 23 additions & 0 deletions packages/botkit-postgres/src/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1103,10 +1103,27 @@ if (postgresUrl == null) {
await repo.findQuoteAuthorizationReference("other", authorization),
undefined,
);
assert.deepStrictEqual(
await Array.fromAsync(
repo.findQuoteAuthorizationReferenceIdentifiers(authorization),
),
["bot"],
);
assert.deepStrictEqual(
await repo.findQuoteAuthorizationReference("bot", authorization),
firstMessageId,
);
await repo.addQuoteAuthorizationReference(
"other",
authorization,
firstMessageId,
);
assert.deepStrictEqual(
await Array.fromAsync(
repo.findQuoteAuthorizationReferenceIdentifiers(authorization),
),
["bot", "other"],
);

await repo.addQuoteAuthorizationReference(
"bot",
Expand All @@ -1125,6 +1142,12 @@ if (postgresUrl == null) {
await repo.findQuoteAuthorizationReference("bot", authorization),
undefined,
);
assert.deepStrictEqual(
await Array.fromAsync(
repo.findQuoteAuthorizationReferenceIdentifiers(authorization),
),
["other"],
);
} finally {
await harness.cleanup();
}
Expand Down
22 changes: 22 additions & 0 deletions packages/botkit-postgres/src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,13 @@ async function initializePostgresRepositorySchemaInTransaction(
[],
prepare,
);
await execute(
sql,
`CREATE INDEX IF NOT EXISTS "idx_quote_authorization_refs_authorization"
ON "${validatedSchema}"."quote_authorization_refs" (authorization_uri)`,
[],
prepare,
);
await execute(
sql,
`ALTER TABLE "${validatedSchema}"."quote_authorization_refs"
Expand Down Expand Up @@ -1147,6 +1154,21 @@ export class PostgresRepository implements Repository, AsyncDisposable {
return rows[0]?.message_id;
}

async *findQuoteAuthorizationReferenceIdentifiers(
authorization: URL,
): AsyncIterable<string> {
await this.ensureReady();
const rows = await this.query<{ readonly bot_id: string }>(
this.sql,
`SELECT bot_id
FROM ${this.table("quote_authorization_refs")}
WHERE authorization_uri = $1
ORDER BY bot_id`,
[authorization.href],
);
for (const row of rows) yield row.bot_id;
}

async findQuoteAuthorizationReferenceAttribution(
identifier: string,
authorization: URL,
Expand Down
23 changes: 23 additions & 0 deletions packages/botkit-sqlite/src/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,10 +387,27 @@ describe("SqliteRepository", () => {
await repo.findQuoteAuthorizationReference("other", authorization),
undefined,
);
assert.deepStrictEqual(
await Array.fromAsync(
repo.findQuoteAuthorizationReferenceIdentifiers(authorization),
),
["bot"],
);
assert.deepStrictEqual(
await repo.findQuoteAuthorizationReference("bot", authorization),
firstMessageId,
);
await repo.addQuoteAuthorizationReference(
"other",
authorization,
firstMessageId,
);
assert.deepStrictEqual(
await Array.fromAsync(
repo.findQuoteAuthorizationReferenceIdentifiers(authorization),
),
["bot", "other"],
);

await repo.addQuoteAuthorizationReference(
"bot",
Expand All @@ -409,6 +426,12 @@ describe("SqliteRepository", () => {
await repo.findQuoteAuthorizationReference("bot", authorization),
undefined,
);
assert.deepStrictEqual(
await Array.fromAsync(
repo.findQuoteAuthorizationReferenceIdentifiers(authorization),
),
["other"],
);
} finally {
repo.close();
}
Expand Down
15 changes: 15 additions & 0 deletions packages/botkit-sqlite/src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,10 @@ export class SqliteRepository implements Repository, Disposable {
PRIMARY KEY (bot_id, authorization)
)
`);
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_quote_authorization_refs_authorization
ON quote_authorization_refs(authorization)
`);
if (!this.hasColumn("quote_authorization_refs", "attribution")) {
this.db.exec(`
ALTER TABLE quote_authorization_refs ADD COLUMN attribution TEXT
Expand Down Expand Up @@ -1127,6 +1131,17 @@ export class SqliteRepository implements Repository, Disposable {
return Promise.resolve(row?.message_id);
}

async *findQuoteAuthorizationReferenceIdentifiers(
authorization: URL,
): AsyncIterable<string> {
const stmt = this.db.prepare(`
SELECT bot_id FROM quote_authorization_refs
WHERE authorization = ? ORDER BY bot_id
`);
const rows = stmt.all(authorization.href) as { bot_id: string }[];
for (const row of rows) yield row.bot_id;
}

findQuoteAuthorizationReferenceAttribution(
identifier: string,
authorization: URL,
Expand Down
Loading
Loading