Fixed 267U.pre2
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
import { Blockings, DriveFiles, Followings, FollowRequests, Mutings, NoteFavorites, NoteReactions, Notes, NoteWatchings, RegistryItems, UserNotePinings, UserProfiles, Users } from "../../../../models/index.js";
|
||||
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
|
||||
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
|
||||
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
|
||||
import AsyncLock from "async-lock";
|
||||
import { getUser } from "../../common/getters.js";
|
||||
import { PaginationHelpers } from "./pagination.js";
|
||||
import { awaitAll } from "../../../../prelude/await-all.js";
|
||||
import createFollowing from "../../../../services/following/create.js";
|
||||
import deleteFollowing from "../../../../services/following/delete.js";
|
||||
import cancelFollowRequest from "../../../../services/following/requests/cancel.js";
|
||||
import createBlocking from "../../../../services/blocking/create.js";
|
||||
import deleteBlocking from "../../../../services/blocking/delete.js";
|
||||
import { genId } from "../../../../misc/gen-id.js";
|
||||
import { publishUserEvent } from "../../../../services/stream.js";
|
||||
import { UserConverter } from "../converters/user.js";
|
||||
import acceptFollowRequest from "../../../../services/following/requests/accept.js";
|
||||
import { rejectFollowRequest } from "../../../../services/following/reject.js";
|
||||
import { Brackets, IsNull } from "typeorm";
|
||||
import { VisibilityConverter } from "../converters/visibility.js";
|
||||
import { toSingleLast } from "../../../../prelude/array.js";
|
||||
import { MediaHelpers } from "./media.js";
|
||||
import { verifyLink } from "../../../../services/fetch-rel-me.js";
|
||||
import { MastoApiError } from "../middleware/catch-errors.js";
|
||||
import { resolveUser } from "../../../../remote/resolve-user.js";
|
||||
import { updatePerson } from "../../../../remote/activitypub/models/person.js";
|
||||
import { promiseEarlyReturn } from "../../../../prelude/promise.js";
|
||||
import { updateUserProfileData } from "../../../../services/i/update.js";
|
||||
export class UserHelpers {
|
||||
static async followUser(target, reblogs, notify, ctx) {
|
||||
//FIXME: implement reblogs & notify params
|
||||
const localUser = ctx.user;
|
||||
const following = await Followings.exist({
|
||||
where: {
|
||||
followerId: localUser.id,
|
||||
followeeId: target.id
|
||||
}
|
||||
});
|
||||
const requested = await FollowRequests.exist({
|
||||
where: {
|
||||
followerId: localUser.id,
|
||||
followeeId: target.id
|
||||
}
|
||||
});
|
||||
if (!following && !requested) await createFollowing(localUser, target);
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
static async unfollowUser(target, ctx) {
|
||||
const localUser = ctx.user;
|
||||
const following = await Followings.exist({
|
||||
where: {
|
||||
followerId: localUser.id,
|
||||
followeeId: target.id
|
||||
}
|
||||
});
|
||||
const requested = await FollowRequests.exist({
|
||||
where: {
|
||||
followerId: localUser.id,
|
||||
followeeId: target.id
|
||||
}
|
||||
});
|
||||
if (following) await deleteFollowing(localUser, target);
|
||||
if (requested) await cancelFollowRequest(target, localUser);
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
static async blockUser(target, ctx) {
|
||||
const localUser = ctx.user;
|
||||
const blocked = await Blockings.exist({
|
||||
where: {
|
||||
blockerId: localUser.id,
|
||||
blockeeId: target.id
|
||||
}
|
||||
});
|
||||
if (!blocked) await createBlocking(localUser, target);
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
static async unblockUser(target, ctx) {
|
||||
const localUser = ctx.user;
|
||||
const blocked = await Blockings.exist({
|
||||
where: {
|
||||
blockerId: localUser.id,
|
||||
blockeeId: target.id
|
||||
}
|
||||
});
|
||||
if (blocked) await deleteBlocking(localUser, target);
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
static async muteUser(target, notifications = true, duration = 0, ctx) {
|
||||
//FIXME: respect notifications parameter
|
||||
const localUser = ctx.user;
|
||||
const muted = await Mutings.exist({
|
||||
where: {
|
||||
muterId: localUser.id,
|
||||
muteeId: target.id
|
||||
}
|
||||
});
|
||||
if (!muted) {
|
||||
await Mutings.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: duration === 0 ? null : new Date(new Date().getTime() + duration * 1000),
|
||||
muterId: localUser.id,
|
||||
muteeId: target.id
|
||||
});
|
||||
publishUserEvent(localUser.id, "mute", target);
|
||||
NoteWatchings.delete({
|
||||
userId: localUser.id,
|
||||
noteUserId: target.id
|
||||
});
|
||||
}
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
static async unmuteUser(target, ctx) {
|
||||
const localUser = ctx.user;
|
||||
const muting = await Mutings.findOneBy({
|
||||
muterId: localUser.id,
|
||||
muteeId: target.id
|
||||
});
|
||||
if (muting) {
|
||||
await Mutings.delete({
|
||||
id: muting.id
|
||||
});
|
||||
publishUserEvent(localUser.id, "unmute", target);
|
||||
}
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
static async acceptFollowRequest(target, ctx) {
|
||||
const localUser = ctx.user;
|
||||
const pending = await FollowRequests.exist({
|
||||
where: {
|
||||
followerId: target.id,
|
||||
followeeId: localUser.id
|
||||
}
|
||||
});
|
||||
if (pending) await acceptFollowRequest(localUser, target);
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
static async rejectFollowRequest(target, ctx) {
|
||||
const localUser = ctx.user;
|
||||
const pending = await FollowRequests.exist({
|
||||
where: {
|
||||
followerId: target.id,
|
||||
followeeId: localUser.id
|
||||
}
|
||||
});
|
||||
if (pending) await rejectFollowRequest(localUser, target);
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
static async updateCredentials(ctx) {
|
||||
const user = ctx.user;
|
||||
const files = ctx.request.files;
|
||||
const formData = ctx.request.body;
|
||||
const updates = {};
|
||||
const profileUpdates = {};
|
||||
const avatar = toSingleLast(files?.avatar);
|
||||
const header = toSingleLast(files?.header);
|
||||
if (avatar) {
|
||||
const file = await MediaHelpers.uploadMediaBasic(avatar, ctx);
|
||||
updates.avatarId = file.id;
|
||||
updates.avatarBlurhash = file.blurhash;
|
||||
updates.avatarUrl = DriveFiles.getDatabasePrefetchUrl(file, true);
|
||||
}
|
||||
if (header) {
|
||||
const file = await MediaHelpers.uploadMediaBasic(header, ctx);
|
||||
updates.bannerId = file.id;
|
||||
updates.bannerBlurhash = file.blurhash;
|
||||
updates.bannerUrl = DriveFiles.getDatabasePrefetchUrl(file, false);
|
||||
}
|
||||
if (formData.fields_attributes) {
|
||||
profileUpdates.fields = await Promise.all(formData.fields_attributes.map(async (field)=>{
|
||||
if (!(field.name.trim() === "" && field.value.trim() === "")) {
|
||||
if (field.name.trim() === "") throw new MastoApiError(400, "Field name can not be empty");
|
||||
if (field.value.trim() === "") throw new MastoApiError(400, "Field value can not be empty");
|
||||
}
|
||||
const verified = field.value.startsWith("http") ? await promiseEarlyReturn(verifyLink(field.value, user.username), 1500) ?? false : undefined;
|
||||
return {
|
||||
...field,
|
||||
verified
|
||||
};
|
||||
})).then((p)=>p.filter((field)=>field.name.trim().length > 0 && field.value.length > 0));
|
||||
}
|
||||
if (formData.display_name) updates.name = formData.display_name;
|
||||
if (formData.note) profileUpdates.description = formData.note;
|
||||
if (formData.locked) updates.isLocked = formData.locked;
|
||||
if (formData.bot) updates.isBot = formData.bot;
|
||||
if (formData.discoverable) updates.isExplorable = formData.discoverable;
|
||||
await updateUserProfileData(user, null, updates, profileUpdates, false);
|
||||
return this.verifyCredentials(ctx);
|
||||
}
|
||||
static async verifyCredentials(ctx) {
|
||||
const user = ctx.user;
|
||||
const acct = UserConverter.encode(user, ctx);
|
||||
const profile = UserProfiles.findOneByOrFail({
|
||||
userId: user.id
|
||||
});
|
||||
const followRequests = FollowRequests.count({
|
||||
where: {
|
||||
followeeId: user.id
|
||||
}
|
||||
});
|
||||
const privacy = this.getDefaultNoteVisibility(ctx);
|
||||
const fields = profile.then((profile)=>profile.fields.map((field)=>{
|
||||
return {
|
||||
name: field.name,
|
||||
value: field.value
|
||||
};
|
||||
}));
|
||||
return acct.then((acct)=>{
|
||||
const source = {
|
||||
note: profile.then((profile)=>profile.description ?? ''),
|
||||
fields: fields,
|
||||
privacy: privacy.then((p)=>VisibilityConverter.encode(p)),
|
||||
sensitive: profile.then((p)=>p.alwaysMarkNsfw),
|
||||
language: profile.then((p)=>p.lang ?? ''),
|
||||
follow_requests_count: followRequests
|
||||
};
|
||||
const result = {
|
||||
...acct,
|
||||
source: awaitAll(source)
|
||||
};
|
||||
return awaitAll(result);
|
||||
});
|
||||
}
|
||||
static async getUserFromAcct(acct) {
|
||||
const split = acct.toLowerCase().split('@');
|
||||
if (split.length > 2) throw new Error('Invalid acct');
|
||||
return split[1] == null ? Users.findOneBy({
|
||||
usernameLower: split[0],
|
||||
host: split[1] ?? IsNull()
|
||||
}).then((p)=>{
|
||||
if (p) return p;
|
||||
throw new MastoApiError(404);
|
||||
}) : resolveUser(split[0], split[1], 'no-refresh').catch(()=>{
|
||||
throw new MastoApiError(404);
|
||||
});
|
||||
}
|
||||
static async getUserMutes(maxId, sinceId, minId, limit = 40, ctx) {
|
||||
if (limit > 80) limit = 80;
|
||||
const user = ctx.user;
|
||||
const query = PaginationHelpers.makePaginationQuery(Mutings.createQueryBuilder("muting"), sinceId, maxId, minId);
|
||||
query.andWhere("muting.muterId = :userId", {
|
||||
userId: user.id
|
||||
}).innerJoinAndSelect("muting.mutee", "mutee");
|
||||
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then(async (mutes)=>{
|
||||
const users = mutes.map((p)=>p.mutee).filter((p)=>p);
|
||||
return await UserConverter.encodeMany(users, ctx).then((res)=>res.map((m)=>{
|
||||
const muting = mutes.find((acc)=>acc.muteeId === m.id);
|
||||
return {
|
||||
...m,
|
||||
mute_expires_at: muting?.expiresAt?.toISOString() ?? null
|
||||
};
|
||||
}));
|
||||
});
|
||||
}
|
||||
static async getUserBlocks(maxId, sinceId, minId, limit = 40, ctx) {
|
||||
if (limit > 80) limit = 80;
|
||||
const user = ctx.user;
|
||||
const query = PaginationHelpers.makePaginationQuery(Blockings.createQueryBuilder("blocking"), sinceId, maxId, minId);
|
||||
query.andWhere("blocking.blockerId = :userId", {
|
||||
userId: user.id
|
||||
}).innerJoinAndSelect("blocking.blockee", "blockee");
|
||||
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then((blocks)=>{
|
||||
return blocks.map((p)=>p.blockee).filter((p)=>p);
|
||||
});
|
||||
}
|
||||
static async getUserFollowRequests(maxId, sinceId, minId, limit = 40, ctx) {
|
||||
if (limit > 80) limit = 80;
|
||||
const user = ctx.user;
|
||||
const query = PaginationHelpers.makePaginationQuery(FollowRequests.createQueryBuilder("request"), sinceId, maxId, minId);
|
||||
query.andWhere("request.followeeId = :userId", {
|
||||
userId: user.id
|
||||
}).innerJoinAndSelect("request.follower", "follower");
|
||||
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then((requests)=>{
|
||||
return requests.map((p)=>p.follower).filter((p)=>p);
|
||||
});
|
||||
}
|
||||
static async getUserStatuses(user, maxId, sinceId, minId, limit = 20, onlyMedia = false, excludeReplies = false, excludeReblogs = false, pinned = false, tagged, ctx) {
|
||||
if (limit > 40) limit = 40;
|
||||
const localUser = ctx.user;
|
||||
if (tagged !== undefined && tagged.length > 0) {
|
||||
//FIXME respect tagged
|
||||
return [];
|
||||
}
|
||||
const query = PaginationHelpers.makePaginationQuery(Notes.createQueryBuilder("note"), sinceId, maxId, minId).andWhere("note.userId = :userId");
|
||||
if (pinned) {
|
||||
const sq = UserNotePinings.createQueryBuilder("pin").select("pin.noteId").where("pin.userId = :userId");
|
||||
query.andWhere(`note.id IN (${sq.getQuery()})`);
|
||||
}
|
||||
if (excludeReblogs) {
|
||||
query.andWhere(new Brackets((qb)=>{
|
||||
qb.where('note.renoteId IS NULL').orWhere('note.text IS NOT NULL').orWhere('note.hasPoll = TRUE').orWhere("note.fileIds != '{}'");
|
||||
}));
|
||||
}
|
||||
if (excludeReplies) {
|
||||
query.leftJoin("note", "thread", "note.threadId = thread.id").andWhere(new Brackets((qb)=>{
|
||||
qb.where("note.replyId IS NULL").orWhere(new Brackets((qb)=>{
|
||||
qb.where('note.mentions = :mentions', {
|
||||
mentions: []
|
||||
}).andWhere('thread.userId = :userId');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
query.leftJoinAndSelect("note.renote", "renote");
|
||||
generateVisibilityQuery(query, localUser);
|
||||
if (localUser) {
|
||||
generateMutedUserQuery(query, localUser, user);
|
||||
generateBlockedUserQuery(query, localUser);
|
||||
}
|
||||
if (onlyMedia) query.andWhere("note.fileIds != '{}'");
|
||||
query.andWhere("note.visibility != 'hidden'");
|
||||
query.andWhere("note.visibility != 'specified'");
|
||||
query.setParameters({
|
||||
userId: user.id
|
||||
});
|
||||
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx);
|
||||
}
|
||||
static async getUserBookmarks(maxId, sinceId, minId, limit = 20, ctx) {
|
||||
if (limit > 40) limit = 40;
|
||||
const localUser = ctx.user;
|
||||
const query = PaginationHelpers.makePaginationQuery(NoteFavorites.createQueryBuilder("favorite"), sinceId, maxId, minId).andWhere("favorite.userId = :meId", {
|
||||
meId: localUser.id
|
||||
}).leftJoinAndSelect("favorite.note", "note");
|
||||
generateVisibilityQuery(query, localUser);
|
||||
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then((res)=>res.map((p)=>p.note));
|
||||
}
|
||||
static async getUserFavorites(maxId, sinceId, minId, limit = 20, ctx) {
|
||||
if (limit > 40) limit = 40;
|
||||
const localUser = ctx.user;
|
||||
const query = PaginationHelpers.makePaginationQuery(NoteReactions.createQueryBuilder("reaction"), sinceId, maxId, minId).andWhere("reaction.userId = :meId", {
|
||||
meId: localUser.id
|
||||
}).leftJoinAndSelect("reaction.note", "note");
|
||||
generateVisibilityQuery(query, localUser);
|
||||
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then((res)=>res.map((p)=>p.note));
|
||||
}
|
||||
static async getUserRelationships(type, user, maxId, sinceId, minId, limit = 40, ctx) {
|
||||
if (limit > 80) limit = 80;
|
||||
const localUser = ctx.user;
|
||||
const profile = await UserProfiles.findOneByOrFail({
|
||||
userId: user.id
|
||||
});
|
||||
if (profile.ffVisibility === "private") {
|
||||
if (!localUser || user.id !== localUser.id) return [];
|
||||
} else if (profile.ffVisibility === "followers") {
|
||||
if (!localUser) return [];
|
||||
if (user.id !== localUser.id) {
|
||||
const isFollowed = await Followings.exist({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: localUser.id
|
||||
}
|
||||
});
|
||||
if (!isFollowed) return [];
|
||||
}
|
||||
}
|
||||
const query = PaginationHelpers.makePaginationQuery(Followings.createQueryBuilder("following"), sinceId, maxId, minId);
|
||||
if (type === "followers") {
|
||||
query.andWhere("following.followeeId = :userId", {
|
||||
userId: user.id
|
||||
}).innerJoinAndSelect("following.follower", "follower");
|
||||
} else {
|
||||
query.andWhere("following.followerId = :userId", {
|
||||
userId: user.id
|
||||
}).innerJoinAndSelect("following.followee", "followee");
|
||||
}
|
||||
return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then((relations)=>relations.map((p)=>type === "followers" ? p.follower : p.followee).filter((p)=>p));
|
||||
}
|
||||
static async getUserFollowers(user, maxId, sinceId, minId, limit = 40, ctx) {
|
||||
return this.getUserRelationships('followers', user, maxId, sinceId, minId, limit, ctx);
|
||||
}
|
||||
static async getUserFollowing(user, maxId, sinceId, minId, limit = 40, ctx) {
|
||||
return this.getUserRelationships('following', user, maxId, sinceId, minId, limit, ctx);
|
||||
}
|
||||
static async getUserRelationhipToMany(targetIds, localUserId) {
|
||||
return Promise.all(targetIds.map((targetId)=>this.getUserRelationshipTo(targetId, localUserId)));
|
||||
}
|
||||
static async getUserRelationshipTo(targetId, localUserId) {
|
||||
const relation = await Users.getRelation(localUserId, targetId);
|
||||
const response = {
|
||||
id: targetId,
|
||||
following: relation.isFollowing,
|
||||
followed_by: relation.isFollowed,
|
||||
blocking: relation.isBlocking,
|
||||
blocked_by: relation.isBlocked,
|
||||
muting: relation.isMuted,
|
||||
muting_notifications: relation.isMuted,
|
||||
requested: relation.hasPendingFollowRequestFromYou,
|
||||
domain_blocking: false,
|
||||
showing_reblogs: !relation.isRenoteMuted,
|
||||
endorsed: false,
|
||||
notifying: false,
|
||||
note: '' //FIXME
|
||||
};
|
||||
return awaitAll(response);
|
||||
}
|
||||
static async getUserCached(id, ctx) {
|
||||
const cache = ctx.cache;
|
||||
return cache.locks.acquire(id, async ()=>{
|
||||
const cacheHit = cache.users.find((p)=>p.id == id);
|
||||
if (cacheHit) return cacheHit;
|
||||
return getUser(id).then((p)=>{
|
||||
cache.users.push(p);
|
||||
return p;
|
||||
});
|
||||
});
|
||||
}
|
||||
static async getUserCachedOr404(id, ctx) {
|
||||
return this.getUserCached(id, ctx).catch((_)=>{
|
||||
throw new MastoApiError(404);
|
||||
});
|
||||
}
|
||||
static async getUserOr404(id) {
|
||||
return getUser(id).catch((_)=>{
|
||||
throw new MastoApiError(404);
|
||||
});
|
||||
}
|
||||
static async updateUserInBackground(user) {
|
||||
if (Users.isLocalUser(user)) return;
|
||||
if (user.lastFetchedAt != null && Date.now() - user.lastFetchedAt.getTime() < 1000 * 60 * 60 * 24) return;
|
||||
await Users.update(user.id, {
|
||||
lastFetchedAt: new Date()
|
||||
});
|
||||
// noinspection ES6MissingAwait
|
||||
updatePerson(user.uri, undefined, undefined, user);
|
||||
}
|
||||
static getFreshAccountCache() {
|
||||
return {
|
||||
locks: new AsyncLock(),
|
||||
accounts: [],
|
||||
users: []
|
||||
};
|
||||
}
|
||||
static async getDefaultNoteVisibility(ctx) {
|
||||
const user = ctx.user;
|
||||
return RegistryItems.findOneBy({
|
||||
domain: IsNull(),
|
||||
userId: user.id,
|
||||
key: 'defaultNoteVisibility',
|
||||
scope: '{client,base}'
|
||||
}).then((p)=>p?.value ?? 'public');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user