forked from FoundKeyGang/FoundKey
Compare commits
7 commits
main
...
move-autob
Author | SHA1 | Date | |
---|---|---|---|
8f0fff5c90 | |||
4d59896cc4 | |||
7258ef597b | |||
2f0ad5ee2d | |||
7fc328acef | |||
ca137e0055 | |||
9606aeff18 |
12 changed files with 189 additions and 98 deletions
|
@ -8,7 +8,7 @@ import { Users, DriveFiles } from '@/models/index.js';
|
|||
import { DbUserImportJobData } from '@/queue/types.js';
|
||||
import { queueLogger } from '@/queue/logger.js';
|
||||
import { resolveUser } from '@/remote/resolve-user.js';
|
||||
import block from '@/services/blocking/create.js';
|
||||
import { createBlock } from '@/services/blocking/create.js';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-blocking');
|
||||
|
||||
|
@ -63,7 +63,7 @@ export async function importBlocking(job: Bull.Job<DbUserImportJobData>, done: a
|
|||
|
||||
logger.info(`Block[${linenum}] ${target.id} ...`);
|
||||
|
||||
await block(user, target);
|
||||
await createBlock(user, target);
|
||||
} catch (e) {
|
||||
logger.warn(`Error in line:${linenum} ${e}`);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import block from '@/services/blocking/create.js';
|
||||
import { createBlock } from '@/services/blocking/create.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import { DbResolver } from '@/remote/activitypub/db-resolver.js';
|
||||
|
@ -18,6 +18,6 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<string> =>
|
|||
return 'skip: blockee is not local';
|
||||
}
|
||||
|
||||
await block(await Users.findOneByOrFail({ id: actor.id }), await Users.findOneByOrFail({ id: blockee.id }));
|
||||
await createBlock(await Users.findOneByOrFail({ id: actor.id }), await Users.findOneByOrFail({ id: blockee.id }));
|
||||
return 'ok';
|
||||
};
|
||||
|
|
|
@ -1,62 +1,22 @@
|
|||
import { IsNull } from 'typeorm';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { resolvePerson } from '@/remote/activitypub/models/person.js';
|
||||
import { Followings, Users } from '@/models/index.js';
|
||||
import { createNotification } from '@/services/create-notification.js';
|
||||
import { verifyMove } from '@/remote/activitypub/models/person.js';
|
||||
import { move } from '@/services/move.js';
|
||||
import Resolver from '../../resolver.js';
|
||||
import { IMove, isActor, getApId } from '../../type.js';
|
||||
import { IMove, getApId } from '../../type.js';
|
||||
|
||||
export async function move(actor: IRemoteUser, activity: IMove, resolver: Resolver): Promise<void> {
|
||||
// actor is not move origin
|
||||
if (activity.object == null || getApId(activity.object) !== actor.uri) return;
|
||||
|
||||
// actor already moved
|
||||
if (actor.movedTo != null) return;
|
||||
|
||||
// no move target
|
||||
if (activity.target == null) return;
|
||||
|
||||
/* the database resolver can not be used here, because:
|
||||
* 1. It must be ensured that the latest data is used.
|
||||
* 2. The AP representation is needed, because `alsoKnownAs`
|
||||
* is not stored in the database.
|
||||
* This also checks whether the move target is blocked
|
||||
*/
|
||||
const movedToAp = await resolver.resolve(getApId(activity.target));
|
||||
const movedTo = await verifyMove(actor, activity.target, resolver);
|
||||
|
||||
// move target is not an actor
|
||||
if (!isActor(movedToAp)) return;
|
||||
if (movedTo == null) {
|
||||
// invalid or unnaccepted move
|
||||
return;
|
||||
}
|
||||
|
||||
// move destination has not accepted
|
||||
if (!Array.isArray(movedToAp.alsoKnownAs) || !movedToAp.alsoKnownAs.includes(actor.id)) return;
|
||||
|
||||
// ensure the user exists
|
||||
const movedTo = await resolvePerson(getApId(activity.target), resolver, movedToAp);
|
||||
// move target is already suspended
|
||||
if (movedTo.isSuspended) return;
|
||||
|
||||
// process move for local followers
|
||||
const followings = Followings.find({
|
||||
select: {
|
||||
followerId: true,
|
||||
},
|
||||
where: {
|
||||
followeeId: actor.id,
|
||||
followerHost: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
Users.update(actor.id, {
|
||||
movedToId: movedTo.id,
|
||||
}),
|
||||
...followings.map(async (following) => {
|
||||
// TODO: autoAcceptMove?
|
||||
|
||||
await createNotification(following.followerId, 'move', {
|
||||
notifierId: actor.id,
|
||||
moveTargetId: movedTo.id,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
await move(actor, movedTo);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import unblock from '@/services/blocking/delete.js';
|
||||
import { deleteBlock } from '@/services/blocking/delete.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import { IBlock } from '@/remote/activitypub/type.js';
|
||||
|
@ -16,6 +16,6 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<string> =>
|
|||
return 'skip: ブロック解除しようとしているユーザーはローカルユーザーではありません';
|
||||
}
|
||||
|
||||
await unblock(await Users.findOneByOrFail({ id: actor.id }), blockee);
|
||||
await deleteBlock(await Users.findOneByOrFail({ id: actor.id }), blockee);
|
||||
return 'ok';
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ import { truncate } from '@/misc/truncate.js';
|
|||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { uriPersonCache } from '@/services/user-cache.js';
|
||||
import { publishInternalEvent } from '@/services/stream.js';
|
||||
import { move } from '@/services/move.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { fromHtml } from '@/mfm/from-html.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
|
@ -34,6 +35,39 @@ import { resolveImage } from './image.js';
|
|||
const nameLength = 128;
|
||||
const summaryLength = 2048;
|
||||
|
||||
/**
|
||||
* Checks that a move of the given origin actor is accepted by the target actor, using the given resolver.
|
||||
* @returns null if move is invalid, or the internal representation of the move target if the move is valid.
|
||||
*/
|
||||
export async function verifyMove(origin: IRemoteUser, target: string | IObject, resolver: Resolver): Promise<User | null> {
|
||||
// actor already moved
|
||||
if (origin.movedToId != null) return null;
|
||||
|
||||
/* the database resolver can not be used here, because:
|
||||
* 1. It must be ensured that the latest data is used.
|
||||
* 2. The AP representation is needed, because `alsoKnownAs`
|
||||
* is not stored in the database.
|
||||
* This also checks whether the move target is blocked
|
||||
*/
|
||||
const resolvedTarget = resolver.resolve(target);
|
||||
|
||||
// move resolvedTarget is not an actor
|
||||
if (!isActor(resolvedTarget)) return null;
|
||||
|
||||
// moved to self, this check is necessary to avoid infinite loops during verification
|
||||
if (origin.uri === resolvedTarget.id) return null;
|
||||
|
||||
// move destination has not accepted
|
||||
if (!Array.isArray(resolvedTarget.alsoKnownAs) || !resolvedTarget.alsoKnownAs.includes(actor.uri)) return null;
|
||||
|
||||
// ensure the user exists (or is fetched)
|
||||
const movedTo = await resolvePerson(resolvedTarget.id, resolver, resolvedTarget);
|
||||
// move target is already suspended
|
||||
if (movedTo.isSuspended) return null;
|
||||
|
||||
return movedTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and convert to actor object
|
||||
* @param x Fetched object
|
||||
|
@ -62,19 +96,22 @@ async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
|
|||
}
|
||||
|
||||
if (x.movedTo !== undefined) {
|
||||
if (!(typeof x.movedTo === 'string' && x.movedTo.length > 0)) {
|
||||
throw new Error('invalid Actor: wrong movedTo');
|
||||
try {
|
||||
const target = await verifyMove(x, x.movedTo, resolver);
|
||||
if (null == target) {
|
||||
throw new Error("invalid move");
|
||||
} else {
|
||||
await resolvePerson(x.movedTo, resolver)
|
||||
.then(moveTarget => {
|
||||
x.movedTo = moveTarget.id
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
apLogger.warn(`cannot find move target "${x.movedTo}", error: ${err.toString()}`);
|
||||
// This move is invalid.
|
||||
// Don't treat the whole actor as invalid, just ignore/remove the movedTo.
|
||||
delete x.movedTo;
|
||||
}
|
||||
if (x.movedTo === uri) {
|
||||
throw new Error('invalid Actor: moved to self');
|
||||
}
|
||||
// This may throw an exception if we cannot resolve the move target.
|
||||
// If we are processing an incoming activity, this is desired behaviour
|
||||
// because that will cause the activity to be retried.
|
||||
await resolvePerson(x.movedTo, resolver)
|
||||
.then(moveTarget => {
|
||||
x.movedTo = moveTarget.id
|
||||
});
|
||||
}
|
||||
|
||||
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
||||
|
@ -306,6 +343,12 @@ export async function updatePerson(value: IObject | string, resolver: Resolver):
|
|||
|
||||
const person = await validateActor(object, resolver);
|
||||
|
||||
if (person.movedTo != null && exist.movedToId == null) {
|
||||
// The person was not moved before, but is now. Therefore the person has moved.
|
||||
// The move was already verified in validateActor.
|
||||
move(exist.id, person.movedTo.id);
|
||||
}
|
||||
|
||||
apLogger.info(`Updating the Person: ${person.id}`);
|
||||
|
||||
// Fetch avatar and banner image
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import create from '@/services/blocking/create.js';
|
||||
import { createBlock } from '@/services/blocking/create.js';
|
||||
import { Blockings, NoteWatchings, Users } from '@/models/index.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
import define from '@/server/api/define.js';
|
||||
|
@ -52,16 +52,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
if (blocked) throw new ApiError('ALREADY_BLOCKING');
|
||||
|
||||
await create(blocker, blockee);
|
||||
|
||||
NoteWatchings.delete({
|
||||
userId: blocker.id,
|
||||
noteUserId: blockee.id,
|
||||
});
|
||||
await createBlock(blocker, blockee);
|
||||
|
||||
return await Users.pack(blockee.id, blocker, {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
publishUserEvent(user.id, 'block', blockee);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import deleteBlocking from '@/services/blocking/delete.js';
|
||||
import { deleteBlock } from '@/services/blocking/delete.js';
|
||||
import { Blockings, Users } from '@/models/index.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
import define from '@/server/api/define.js';
|
||||
|
@ -53,7 +53,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
if (!exist) throw new ApiError('NOT_BLOCKING');
|
||||
|
||||
// Delete blocking
|
||||
await deleteBlocking(blocker, blockee);
|
||||
await deleteBlock(blocker, blockee);
|
||||
|
||||
return await Users.pack(blockee.id, blocker, {
|
||||
detail: true,
|
||||
|
|
|
@ -5,6 +5,7 @@ import { publishUserEvent } from '@/services/stream.js';
|
|||
import define from '@/server/api/define.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { getUser } from '@/server/api/common/getters.js';
|
||||
import { createMute } from '@/services/mute/create.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
@ -51,19 +52,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Create mute
|
||||
await Mutings.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
} as Muting);
|
||||
const expiresAt = ps.expiresAt ? new Date(ps.expiresAt) : null;
|
||||
|
||||
publishUserEvent(user.id, 'mute', mutee);
|
||||
|
||||
NoteWatchings.delete({
|
||||
userId: muter.id,
|
||||
noteUserId: mutee.id,
|
||||
});
|
||||
await createMute(muter, mutee, expiresAt);
|
||||
});
|
||||
|
|
|
@ -7,18 +7,20 @@ import { deliver, webhookDeliver } from '@/queue/index.js';
|
|||
import renderReject from '@/remote/activitypub/renderer/reject.js';
|
||||
import { Blocking } from '@/models/entities/blocking.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js';
|
||||
import { Blockings, Users, FollowRequests, Followings, NoteWatchings, UserListJoinings, UserLists } from '@/models/index.js';
|
||||
import { perUserFollowingChart } from '@/services/chart/index.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { getActiveWebhooks } from '@/misc/webhook-cache.js';
|
||||
|
||||
export default async function(blocker: User, blockee: User): Promise<void> {
|
||||
export async function createBlock(blocker: User, blockee: User): Promise<void> {
|
||||
await Promise.all([
|
||||
cancelRequest(blocker, blockee),
|
||||
cancelRequest(blockee, blocker),
|
||||
unFollow(blocker, blockee),
|
||||
unFollow(blockee, blocker),
|
||||
removeFromList(blockee, blocker),
|
||||
removeWatchings(blocker, blockee),
|
||||
removeWatchings(blockee, blocker),
|
||||
]);
|
||||
|
||||
const blocking = {
|
||||
|
@ -32,9 +34,13 @@ export default async function(blocker: User, blockee: User): Promise<void> {
|
|||
|
||||
await Blockings.insert(blocking);
|
||||
|
||||
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee) && blocker.federateBlocks) {
|
||||
const content = renderActivity(renderBlock(blocking));
|
||||
deliver(blocker, content, blockee.inbox);
|
||||
if (Users.isLocalUser(blocker)) {
|
||||
publishUserEvent(blocker.id, 'block', blockee);
|
||||
|
||||
if (Users.isRemoteUser(blockee) && blocker.federateBlocks) {
|
||||
const content = renderActivity(renderBlock(blocking));
|
||||
deliver(blocker, content, blockee.inbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,3 +147,10 @@ async function removeFromList(listOwner: User, user: User): Promise<void> {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function removeWatchings(watcher: User, watched: User): Promise<void> {
|
||||
await NoteWatchings.delete({
|
||||
userId: watcher.id,
|
||||
noteUserId: watched.id,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import Logger from '../logger.js';
|
|||
|
||||
const logger = new Logger('blocking/delete');
|
||||
|
||||
export default async function(blocker: User, blockee: User) {
|
||||
export async function deleteBlock(blocker: User, blockee: User) {
|
||||
const blocking = await Blockings.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
|
|
60
packages/backend/src/services/move.ts
Normal file
60
packages/backend/src/services/move.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { IsNull } from 'typeorm';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Blockings, Followings, Mutings, Users } from '@/models/index.js';
|
||||
import { createNotification } from '@/services/create-notification.js';
|
||||
import { createBlock } from '@/services/blocking/create.js';
|
||||
import { createMute } from '@/services/mute/create.js';
|
||||
|
||||
/**
|
||||
* Triggers notifications and other side effects after the move of an actor to another actor.
|
||||
*/
|
||||
export async function move(origin: User, movedTo: User): Promise<void> {
|
||||
// process move for local followers
|
||||
const followings = await Followings.find({
|
||||
select: {
|
||||
followerId: true,
|
||||
},
|
||||
where: {
|
||||
followeeId: origin.id,
|
||||
followerHost: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
// create blocks/mutes for the new account analogous to the old account
|
||||
const blockings = await Blockings.createQueryBuilder('blocking')
|
||||
.leftJoinAndSelect('blocking.blocker', 'blocker')
|
||||
// accounts that blocked the previous account
|
||||
.where('blockeeId = :blockee', { blockee: origin.id })
|
||||
// ... and are not already blocking the new account
|
||||
.andWhere('"blocking"."blockerId" NOT IN (SELECT "blockerId" FROM "blocking" WHERE "blockeeId" = :movedTo)', { movedTo: movedTo.id })
|
||||
.getRawMany();
|
||||
const mutes = await Mutings.createQueryBuilder('muting')
|
||||
.leftJoinAndSelect('muting.muter', 'muter')
|
||||
// accounts that muted the previous account
|
||||
.where('muteeId = :mutee', { mutee: origin.id })
|
||||
// ... and are not already muting the new account
|
||||
.andWhere('"muting"."muterId" NOT IN (SELECT "muterId" FROM "muting" WHERE "muteeId" = :movedTo)', { movedTo: movedTo.id })
|
||||
.getRawMany();
|
||||
|
||||
await Promise.all([
|
||||
Users.update(origin.id, {
|
||||
movedToId: movedTo.id,
|
||||
}),
|
||||
...followings.map(async (following) => {
|
||||
// TODO: autoAcceptMove?
|
||||
|
||||
await createNotification(following.followerId, 'move', {
|
||||
notifierId: origin.id,
|
||||
moveTargetId: movedTo.id,
|
||||
});
|
||||
}),
|
||||
...blockings.map(async ({ blocker }) => {
|
||||
// create block with all side effects
|
||||
await createBlock(blocker, origin);
|
||||
}),
|
||||
...mutes.map(async ({ muter, expiresAt }) => {
|
||||
// create mute with all side effects
|
||||
await createMute(muter, origin, expiresAt);
|
||||
});
|
||||
]);
|
||||
}
|
33
packages/backend/src/services/mute/create.ts
Normal file
33
packages/backend/src/services/mute/create.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { genId } from '@/misc/gen-id.js';
|
||||
import { Mutings, NoteWatchings } from '@/models/index.js';
|
||||
import { Muting } from '@/models/entities/muting.js';
|
||||
import { publishUserEvent } from '@/services/stream.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
|
||||
export async function createMute(muter: User, mutee: User, expiresAt: Date | null): Promise<void> {
|
||||
if (expiresAt && ps.expiresAt <= Date.now()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
// Create mute
|
||||
Mutings.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt,
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
} as Muting),
|
||||
removeWatchings(muter, mutee),
|
||||
removeWatchings(mutee, muter),
|
||||
]);
|
||||
|
||||
publishUserEvent(user.id, 'mute', mutee);
|
||||
});
|
||||
|
||||
async function removeWatchings(watcher: User, watched: User): Promise<void> {
|
||||
await NoteWatchings.delete({
|
||||
userId: watcher.id,
|
||||
noteUserId: watched.id,
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue