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
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ To be released.
existing accounts can move their followers to a BotKit bot. Actor URIs
listed in the option are published as `alsoKnownAs`, and are available
through `Bot.aliases` and `Session.bot.aliases`. [[#48], [#55]]
- Added automatic re-following when an account a bot follows moves to a
verified new account, with an `onFolloweeMove` event after submitting the
new follow request and unfollowing the old account. [[#49], [#56]]
- Added names, inline summaries, and custom URLs when publishing or updating
messages. Updated messages can remove these fields by setting them to
`null`, and the new `inline` template composes paragraphless rich text for
Expand All @@ -27,6 +30,9 @@ To be released.
- Fixed a bug where a remote server could approve a quote with a quote
authorization stamp other than the one named in its `Accept` activity,
as long as the substituted stamp was on the same origin. [[#52], [#53]]
- Fixed delivery of follow requests, acceptances, rejections, and unfollows
between bots hosted on the same instance, including follow requests to
account migration targets on that instance. [[#49], [#56]]
- Upgraded Fedify to 2.4.0, which adds support for [FEP-ef61] portable
objects and hardens HTTP Signature verification and document loading.

Expand All @@ -38,9 +44,11 @@ To be released.
[#42]: https://github.com/fedify-dev/botkit/pull/42
[#43]: https://github.com/fedify-dev/botkit/pull/43
[#48]: https://github.com/fedify-dev/botkit/issues/48
[#49]: https://github.com/fedify-dev/botkit/issues/49
[#52]: https://github.com/fedify-dev/botkit/issues/52
[#53]: https://github.com/fedify-dev/botkit/pull/53
[#55]: https://github.com/fedify-dev/botkit/pull/55
[#56]: https://github.com/fedify-dev/botkit/pull/56


Version 0.5.6
Expand Down
8 changes: 8 additions & 0 deletions changes.d/botkit/followee-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
links:
'#49': https://github.com/fedify-dev/botkit/issues/49
'#56': https://github.com/fedify-dev/botkit/pull/56
---
- Added automatic re-following when an account a bot follows moves to a
verified new account, with an `onFolloweeMove` event after submitting the
new follow request and unfollowing the old account. [[#49], [#56]]
8 changes: 8 additions & 0 deletions changes.d/botkit/local-follows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
links:
'#49': https://github.com/fedify-dev/botkit/issues/49
'#56': https://github.com/fedify-dev/botkit/pull/56
---
- Fixed delivery of follow requests, acceptances, rejections, and unfollows
between bots hosted on the same instance, including follow requests to
account migration targets on that instance. [[#49], [#56]]
60 changes: 59 additions & 1 deletion docs/concepts/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ bot.onMention = async (session, message) => {
~~~~

Every event handler receives a [session](./session.md) object as the first
argument, and the event-specific object as the second argument.
argument, followed by the event-specific objects.

BotKit invokes these event handlers only for activities whose signatures were
verified by Fedify. As of Fedify 2.1.0, BotKit also acknowledges certain
Expand Down Expand Up @@ -152,6 +152,64 @@ bot.onRejectFollow = async (session, rejecter) => {
~~~~


Followee move
-------------

*This event is available since BotKit 0.6.0.*

When an account your bot follows moves, BotKit automatically submits a follow
request to its new account and unfollows the old one. It accepts push-mode
`Move` activities sent by the old account only, and verifies that the new
account lists the old actor URI in its `alsoKnownAs`. A target embedded in
an activity is checked against the target account's own actor document.

The `~Bot.onFolloweeMove` handler receives the bot's session, the old `Actor`,
and the new `Actor`, in that order. It runs after the new request is submitted
and the old account is unfollowed. The new request may still await acceptance;
`~Bot.onAcceptFollow` or `~Bot.onRejectFollow` reports the eventual response.
With an outgoing queue, submitting the request means enqueueing it, rather
than completing delivery.

~~~~ typescript twoslash
import type { Bot } from "@fedify/botkit";
declare const bot: Bot<void>;
// ---cut-before---
bot.onFolloweeMove = (session, oldActor, newActor) => {
console.info(
session.bot.identifier,
"followed account moved",
oldActor.id?.href,
newActor.id?.href,
);
};
~~~~

You can also supply this handler as the `onFolloweeMove` option to
`createBot()`, or assign it to a
[dynamic bot group](./instance.md#dynamic-bots). The `FolloweeMoveEventHandler`
type is exported by *@fedify/botkit*.

Only accepted follows of the old account are migrated. If the bot already
follows the target, it keeps that follow and only unfollows the old account.
A repeat delivery does nothing once the old follow has been removed. A pending
request to the target may receive another follow request, since it is not yet
an accepted follow.

There is no migration policy option. A handler can unfollow an already accepted
target with `~Session.unfollow()`; to decline a target whose request is still
pending, unfollow it from `~Bot.onAcceptFollow`. `~Session.unfollow()` does not
cancel pending requests. A rejected target leaves the bot following neither
account.

> [!NOTE]
> These changes are not a transaction across servers. Failure to submit the
> new request preserves the old follow, but an outgoing queue's later delivery
> failure cannot restore it. The old follow is removed before its `Undo` is
> submitted; if that submission fails, the event does not run, and a repeated
> `Move` does not retry the `Undo`. Event handlers are likewise not replayed
> after the old follow has been removed.


Mention
-------

Expand Down
5 changes: 5 additions & 0 deletions docs/concepts/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ bot.onFollow = async (session, followRequest) => {
> If you try to follow an actor that is already followed, the method will just
> do nothing.

When an account the bot follows moves to another account, BotKit automatically
submits a follow request to the verified target and unfollows the old account.
See [the followee move event](./events.md#followee-move) for validation rules,
request timing, and the `onFolloweeMove` callback.


Unfollowing an actor
--------------------
Expand Down
11 changes: 11 additions & 0 deletions packages/botkit/src/bot-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
} from "./emoji.ts";
import type {
AcceptEventHandler,
FolloweeMoveEventHandler,
FollowEventHandler,
LikeEventHandler,
MentionEventHandler,
Expand Down Expand Up @@ -162,6 +163,7 @@ export interface BotImplOptions<TContextData>
*/
export const botEventHandlerNames = [
"onFollow",
"onFolloweeMove",
"onUnfollow",
"onAcceptFollow",
"onRejectFollow",
Expand Down Expand Up @@ -239,6 +241,7 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
}

onFollow?: FollowEventHandler<TContextData>;
onFolloweeMove?: FolloweeMoveEventHandler<TContextData>;
onUnfollow?: UnfollowEventHandler<TContextData>;
onAcceptFollow?: AcceptEventHandler<TContextData>;
onRejectFollow?: RejectEventHandler<TContextData>;
Expand All @@ -258,6 +261,7 @@ export class BotImpl<TContextData> implements Bot<TContextData> {
onVote?: VoteEventHandler<TContextData>;

constructor(options: BotImplOptions<TContextData>) {
this.onFolloweeMove = options.onFolloweeMove;
this.identifier = options.identifier ?? "bot";
this.class = options.class ?? Service;
this.username = options.username;
Expand Down Expand Up @@ -2225,6 +2229,12 @@ export function wrapBotImpl<TContextData>(
set onFollow(value) {
bot.onFollow = value;
},
get onFolloweeMove() {
return bot.onFolloweeMove;
},
set onFolloweeMove(value) {
bot.onFolloweeMove = value;
},
get onUnfollow() {
return bot.onUnfollow;
},
Expand Down Expand Up @@ -2679,6 +2689,7 @@ export class BotGroupImpl<TContextData> implements BotGroup<TContextData> {
) => string | null | Promise<string | null>;

onFollow?: FollowEventHandler<TContextData>;
onFolloweeMove?: FolloweeMoveEventHandler<TContextData>;
onUnfollow?: UnfollowEventHandler<TContextData>;
onAcceptFollow?: AcceptEventHandler<TContextData>;
onRejectFollow?: RejectEventHandler<TContextData>;
Expand Down
16 changes: 16 additions & 0 deletions packages/botkit/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { BotImpl, wrapBotImpl } from "./bot-impl.ts";
import type { CustomEmoji, DeferredCustomEmoji } from "./emoji.ts";
import type {
AcceptEventHandler,
FolloweeMoveEventHandler,
FollowEventHandler,
LikeEventHandler,
MentionEventHandler,
Expand Down Expand Up @@ -63,6 +64,14 @@ export interface BotEventHandlers<TContextData> {
*/
onFollow?: FollowEventHandler<TContextData>;

/**
* Invoked after submitting a follow request to a followed actor's verified
* migration target and unfollowing the old actor. The new request may
* still await acceptance.
* @since 0.6.0
*/
onFolloweeMove?: FolloweeMoveEventHandler<TContextData>;

/**
* An event handler for an unfollow event from the bot.
*/
Expand Down Expand Up @@ -339,6 +348,13 @@ export interface BotWithVoidContextData extends Bot<void> {
* Options for creating a bot.
*/
export interface CreateBotOptions<TContextData> {
/**
* The handler invoked after a followed actor moves to a verified target.
* It can also be assigned through {@link Bot.onFolloweeMove} afterwards.
* @since 0.6.0
*/
readonly onFolloweeMove?: FolloweeMoveEventHandler<TContextData>;

/**
* The internal identifier of the bot. Since it is used for the actor URI,
* it *should not* be changed after the bot is federated.
Expand Down
16 changes: 16 additions & 0 deletions packages/botkit/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ export type FollowEventHandler<TContextData> = (
followRequest: FollowRequest,
) => void | Promise<void>;

/**
* An event handler invoked after a followed actor moves to another account.
* The new follow request has been submitted, but may still await acceptance.
* @typeParam TContextData The type of the context data.
* @param session The session of the bot.
* @param oldActor The actor the bot previously followed.
* @param newActor The actor to which the account moved.
* @returns Nothing, or a promise that resolves when handling completes.
* @since 0.6.0
*/
export type FolloweeMoveEventHandler<TContextData> = (
session: Session<TContextData>,
oldActor: Actor,
newActor: Actor,
) => void | Promise<void>;

/**
* An event handler for an unfollow event from the bot.
* @typeParam TContextData The type of the context data.
Expand Down
5 changes: 3 additions & 2 deletions packages/botkit/src/follow-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import { Accept, type Actor, type Follow, Reject } from "@fedify/vocab";
import type { FollowRequest } from "./follow.ts";
import type { SessionImpl } from "./session-impl.ts";
import { getFollowDeliveryOptions } from "./uri.ts";

export class FollowRequestImpl<TContextData> implements FollowRequest {
readonly session: SessionImpl<TContextData>;
Expand Down Expand Up @@ -58,7 +59,7 @@ export class FollowRequestImpl<TContextData> implements FollowRequest {
to: this.follower.id,
object: this.raw,
}),
{ excludeBaseUris: [new URL(this.session.context.origin)] },
getFollowDeliveryOptions(this.session.context, this.follower.id),
);
await this.session.bot.repository.addFollower(this.id, this.follower);
this.#state = "accepted";
Expand All @@ -77,7 +78,7 @@ export class FollowRequestImpl<TContextData> implements FollowRequest {
to: this.follower.id,
object: this.raw,
}),
{ excludeBaseUris: [new URL(this.session.context.origin)] },
getFollowDeliveryOptions(this.session.context, this.follower.id),
);
this.#state = "rejected";
}
Expand Down
Loading
Loading