40
packages/backend/src/server/api/endpoints/users/clips.ts
Normal file
40
packages/backend/src/server/api/endpoints/users/clips.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { Clips } from '@/models/index';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users', 'clips'],
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = makePaginationQuery(Clips.createQueryBuilder('clip'), ps.sinceId, ps.untilId)
|
||||
.andWhere(`clip.userId = :userId`, { userId: ps.userId })
|
||||
.andWhere('clip.isPublic = true');
|
||||
|
||||
const clips = await query
|
||||
.take(ps.limit!)
|
||||
.getMany();
|
||||
|
||||
return await Clips.packMany(clips);
|
||||
});
|
104
packages/backend/src/server/api/endpoints/users/followers.ts
Normal file
104
packages/backend/src/server/api/endpoints/users/followers.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Users, Followings, UserProfiles } from '@/models/index';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { toPunyNullable } from '@/misc/convert-host';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
username: {
|
||||
validator: $.optional.str
|
||||
},
|
||||
|
||||
host: {
|
||||
validator: $.optional.nullable.str
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Following',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '27fa5435-88ab-43de-9360-387de88727cd'
|
||||
},
|
||||
|
||||
forbidden: {
|
||||
message: 'Forbidden.',
|
||||
code: 'FORBIDDEN',
|
||||
id: '3c6a84db-d619-26af-ca14-06232a21df8a'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const user = await Users.findOne(ps.userId != null
|
||||
? { id: ps.userId }
|
||||
: { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) });
|
||||
|
||||
if (user == null) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
const profile = await UserProfiles.findOneOrFail(user.id);
|
||||
|
||||
if (profile.ffVisibility === 'private') {
|
||||
if (me == null || (me.id !== user.id)) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
} else if (profile.ffVisibility === 'followers') {
|
||||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const following = await Followings.findOne({
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
});
|
||||
if (following == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
|
||||
.andWhere(`following.followeeId = :userId`, { userId: user.id })
|
||||
.innerJoinAndSelect('following.follower', 'follower');
|
||||
|
||||
const followings = await query
|
||||
.take(ps.limit!)
|
||||
.getMany();
|
||||
|
||||
return await Followings.packMany(followings, me, { populateFollower: true });
|
||||
});
|
104
packages/backend/src/server/api/endpoints/users/following.ts
Normal file
104
packages/backend/src/server/api/endpoints/users/following.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Users, Followings, UserProfiles } from '@/models/index';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { toPunyNullable } from '@/misc/convert-host';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
username: {
|
||||
validator: $.optional.str
|
||||
},
|
||||
|
||||
host: {
|
||||
validator: $.optional.nullable.str
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Following',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '63e4aba4-4156-4e53-be25-c9559e42d71b'
|
||||
},
|
||||
|
||||
forbidden: {
|
||||
message: 'Forbidden.',
|
||||
code: 'FORBIDDEN',
|
||||
id: 'f6cdb0df-c19f-ec5c-7dbb-0ba84a1f92ba'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const user = await Users.findOne(ps.userId != null
|
||||
? { id: ps.userId }
|
||||
: { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) });
|
||||
|
||||
if (user == null) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
const profile = await UserProfiles.findOneOrFail(user.id);
|
||||
|
||||
if (profile.ffVisibility === 'private') {
|
||||
if (me == null || (me.id !== user.id)) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
} else if (profile.ffVisibility === 'followers') {
|
||||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const following = await Followings.findOne({
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
});
|
||||
if (following == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
|
||||
.andWhere(`following.followerId = :userId`, { userId: user.id })
|
||||
.innerJoinAndSelect('following.followee', 'followee');
|
||||
|
||||
const followings = await query
|
||||
.take(ps.limit!)
|
||||
.getMany();
|
||||
|
||||
return await Followings.packMany(followings, me, { populateFollowee: true });
|
||||
});
|
@@ -0,0 +1,39 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { GalleryPosts } from '@/models/index';
|
||||
import { makePaginationQuery } from '../../../common/make-pagination-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users', 'gallery'],
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
|
||||
.andWhere(`post.userId = :userId`, { userId: ps.userId });
|
||||
|
||||
const posts = await query
|
||||
.take(ps.limit!)
|
||||
.getMany();
|
||||
|
||||
return await GalleryPosts.packMany(posts, user);
|
||||
});
|
@@ -0,0 +1,105 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { maximum } from '@/prelude/array';
|
||||
import { ApiError } from '../../error';
|
||||
import { getUser } from '../../common/getters';
|
||||
import { Not, In, IsNull } from 'typeorm';
|
||||
import { Notes, Users } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: 'e6965129-7b2a-40a4-bae2-cd84cd434822'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Lookup user
|
||||
const user = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw e;
|
||||
});
|
||||
|
||||
// Fetch recent notes
|
||||
const recentNotes = await Notes.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
replyId: Not(IsNull())
|
||||
},
|
||||
order: {
|
||||
id: -1
|
||||
},
|
||||
take: 1000,
|
||||
select: ['replyId']
|
||||
});
|
||||
|
||||
// 投稿が少なかったら中断
|
||||
if (recentNotes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO ミュートを考慮
|
||||
const replyTargetNotes = await Notes.find({
|
||||
where: {
|
||||
id: In(recentNotes.map(p => p.replyId)),
|
||||
},
|
||||
select: ['userId']
|
||||
});
|
||||
|
||||
const repliedUsers: any = {};
|
||||
|
||||
// Extract replies from recent notes
|
||||
for (const userId of replyTargetNotes.map(x => x.userId.toString())) {
|
||||
if (repliedUsers[userId]) {
|
||||
repliedUsers[userId]++;
|
||||
} else {
|
||||
repliedUsers[userId] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Calc peak
|
||||
const peak = maximum(Object.values(repliedUsers));
|
||||
|
||||
// Sort replies by frequency
|
||||
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
|
||||
|
||||
// Extract top replied users
|
||||
const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit!);
|
||||
|
||||
// Make replies object (includes weights)
|
||||
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
|
||||
user: await Users.pack(user, me, { detail: true }),
|
||||
weight: repliedUsers[user] / peak
|
||||
})));
|
||||
|
||||
return repliesObj;
|
||||
});
|
@@ -0,0 +1,45 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { UserGroups, UserGroupJoinings } from '@/models/index';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
import { UserGroup } from '@/models/entities/user-group';
|
||||
import { UserGroupJoining } from '@/models/entities/user-group-joining';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:user-groups',
|
||||
|
||||
params: {
|
||||
name: {
|
||||
validator: $.str.range(1, 100)
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'UserGroup',
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const userGroup = await UserGroups.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
name: ps.name,
|
||||
} as UserGroup).then(x => UserGroups.findOneOrFail(x.identifiers[0]));
|
||||
|
||||
// Push the owner
|
||||
await UserGroupJoinings.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
userGroupId: userGroup.id
|
||||
} as UserGroupJoining);
|
||||
|
||||
return await UserGroups.pack(userGroup);
|
||||
});
|
@@ -0,0 +1,40 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { UserGroups } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:user-groups',
|
||||
|
||||
params: {
|
||||
groupId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchGroup: {
|
||||
message: 'No such group.',
|
||||
code: 'NO_SUCH_GROUP',
|
||||
id: '63dbd64c-cd77-413f-8e08-61781e210b38'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const userGroup = await UserGroups.findOne({
|
||||
id: ps.groupId,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (userGroup == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroup);
|
||||
}
|
||||
|
||||
await UserGroups.delete(userGroup.id);
|
||||
});
|
@@ -0,0 +1,54 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../../define';
|
||||
import { ApiError } from '../../../../error';
|
||||
import { UserGroupJoinings, UserGroupInvitations } from '@/models/index';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
import { UserGroupJoining } from '@/models/entities/user-group-joining';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups', 'users'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:user-groups',
|
||||
|
||||
params: {
|
||||
invitationId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchInvitation: {
|
||||
message: 'No such invitation.',
|
||||
code: 'NO_SUCH_INVITATION',
|
||||
id: '98c11eca-c890-4f42-9806-c8c8303ebb5e'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
// Fetch the invitation
|
||||
const invitation = await UserGroupInvitations.findOne({
|
||||
id: ps.invitationId,
|
||||
});
|
||||
|
||||
if (invitation == null) {
|
||||
throw new ApiError(meta.errors.noSuchInvitation);
|
||||
}
|
||||
|
||||
if (invitation.userId !== user.id) {
|
||||
throw new ApiError(meta.errors.noSuchInvitation);
|
||||
}
|
||||
|
||||
// Push the user
|
||||
await UserGroupJoinings.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
userGroupId: invitation.userGroupId
|
||||
} as UserGroupJoining);
|
||||
|
||||
UserGroupInvitations.delete(invitation.id);
|
||||
});
|
@@ -0,0 +1,44 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../../define';
|
||||
import { ApiError } from '../../../../error';
|
||||
import { UserGroupInvitations } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups', 'users'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:user-groups',
|
||||
|
||||
params: {
|
||||
invitationId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchInvitation: {
|
||||
message: 'No such invitation.',
|
||||
code: 'NO_SUCH_INVITATION',
|
||||
id: 'ad7471d4-2cd9-44b4-ac68-e7136b4ce656'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
// Fetch the invitation
|
||||
const invitation = await UserGroupInvitations.findOne({
|
||||
id: ps.invitationId,
|
||||
});
|
||||
|
||||
if (invitation == null) {
|
||||
throw new ApiError(meta.errors.noSuchInvitation);
|
||||
}
|
||||
|
||||
if (invitation.userId !== user.id) {
|
||||
throw new ApiError(meta.errors.noSuchInvitation);
|
||||
}
|
||||
|
||||
await UserGroupInvitations.delete(invitation.id);
|
||||
});
|
102
packages/backend/src/server/api/endpoints/users/groups/invite.ts
Normal file
102
packages/backend/src/server/api/endpoints/users/groups/invite.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { getUser } from '../../../common/getters';
|
||||
import { UserGroups, UserGroupJoinings, UserGroupInvitations } from '@/models/index';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
import { UserGroupInvitation } from '@/models/entities/user-group-invitation';
|
||||
import { createNotification } from '@/services/create-notification';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups', 'users'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:user-groups',
|
||||
|
||||
params: {
|
||||
groupId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchGroup: {
|
||||
message: 'No such group.',
|
||||
code: 'NO_SUCH_GROUP',
|
||||
id: '583f8bc0-8eee-4b78-9299-1e14fc91e409'
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: 'da52de61-002c-475b-90e1-ba64f9cf13a8'
|
||||
},
|
||||
|
||||
alreadyAdded: {
|
||||
message: 'That user has already been added to that group.',
|
||||
code: 'ALREADY_ADDED',
|
||||
id: '7e35c6a0-39b2-4488-aea6-6ee20bd5da2c'
|
||||
},
|
||||
|
||||
alreadyInvited: {
|
||||
message: 'That user has already been invited to that group.',
|
||||
code: 'ALREADY_INVITED',
|
||||
id: 'ee0f58b4-b529-4d13-b761-b9a3e69f97e6'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the group
|
||||
const userGroup = await UserGroups.findOne({
|
||||
id: ps.groupId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (userGroup == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroup);
|
||||
}
|
||||
|
||||
// Fetch the user
|
||||
const user = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw e;
|
||||
});
|
||||
|
||||
const joining = await UserGroupJoinings.findOne({
|
||||
userGroupId: userGroup.id,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (joining) {
|
||||
throw new ApiError(meta.errors.alreadyAdded);
|
||||
}
|
||||
|
||||
const existInvitation = await UserGroupInvitations.findOne({
|
||||
userGroupId: userGroup.id,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (existInvitation) {
|
||||
throw new ApiError(meta.errors.alreadyInvited);
|
||||
}
|
||||
|
||||
const invitation = await UserGroupInvitations.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
userGroupId: userGroup.id
|
||||
} as UserGroupInvitation).then(x => UserGroupInvitations.findOneOrFail(x.identifiers[0]));
|
||||
|
||||
// 通知を作成
|
||||
createNotification(user.id, 'groupInvited', {
|
||||
notifierId: me.id,
|
||||
userGroupInvitationId: invitation.id
|
||||
});
|
||||
});
|
@@ -0,0 +1,36 @@
|
||||
import define from '../../../define';
|
||||
import { UserGroups, UserGroupJoinings } from '@/models/index';
|
||||
import { Not, In } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups', 'account'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'read:user-groups',
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'UserGroup',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const ownedGroups = await UserGroups.find({
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
const joinings = await UserGroupJoinings.find({
|
||||
userId: me.id,
|
||||
...(ownedGroups.length > 0 ? {
|
||||
userGroupId: Not(In(ownedGroups.map(x => x.id)))
|
||||
} : {})
|
||||
});
|
||||
|
||||
return await Promise.all(joinings.map(x => UserGroups.pack(x.userGroupId)));
|
||||
});
|
@@ -0,0 +1,50 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { UserGroups, UserGroupJoinings } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups', 'users'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:user-groups',
|
||||
|
||||
params: {
|
||||
groupId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchGroup: {
|
||||
message: 'No such group.',
|
||||
code: 'NO_SUCH_GROUP',
|
||||
id: '62780270-1f67-5dc0-daca-3eb510612e31'
|
||||
},
|
||||
|
||||
youAreOwner: {
|
||||
message: 'Your are the owner.',
|
||||
code: 'YOU_ARE_OWNER',
|
||||
id: 'b6d6e0c2-ef8a-9bb8-653d-79f4a3107c69'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the group
|
||||
const userGroup = await UserGroups.findOne({
|
||||
id: ps.groupId,
|
||||
});
|
||||
|
||||
if (userGroup == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroup);
|
||||
}
|
||||
|
||||
if (me.id === userGroup.userId) {
|
||||
throw new ApiError(meta.errors.youAreOwner);
|
||||
}
|
||||
|
||||
await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: me.id });
|
||||
});
|
@@ -0,0 +1,28 @@
|
||||
import define from '../../../define';
|
||||
import { UserGroups } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups', 'account'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'read:user-groups',
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'UserGroup',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const userGroups = await UserGroups.find({
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
return await Promise.all(userGroups.map(x => UserGroups.pack(x)));
|
||||
});
|
@@ -0,0 +1,69 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { getUser } from '../../../common/getters';
|
||||
import { UserGroups, UserGroupJoinings } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups', 'users'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:user-groups',
|
||||
|
||||
params: {
|
||||
groupId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchGroup: {
|
||||
message: 'No such group.',
|
||||
code: 'NO_SUCH_GROUP',
|
||||
id: '4662487c-05b1-4b78-86e5-fd46998aba74'
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '0b5cc374-3681-41da-861e-8bc1146f7a55'
|
||||
},
|
||||
|
||||
isOwner: {
|
||||
message: 'The user is the owner.',
|
||||
code: 'IS_OWNER',
|
||||
id: '1546eed5-4414-4dea-81c1-b0aec4f6d2af'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the group
|
||||
const userGroup = await UserGroups.findOne({
|
||||
id: ps.groupId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (userGroup == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroup);
|
||||
}
|
||||
|
||||
// Fetch the user
|
||||
const user = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (user.id === userGroup.userId) {
|
||||
throw new ApiError(meta.errors.isOwner);
|
||||
}
|
||||
|
||||
// Pull the user
|
||||
await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: user.id });
|
||||
});
|
@@ -0,0 +1,55 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { UserGroups, UserGroupJoinings } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups', 'account'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'read:user-groups',
|
||||
|
||||
params: {
|
||||
groupId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'UserGroup',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchGroup: {
|
||||
message: 'No such group.',
|
||||
code: 'NO_SUCH_GROUP',
|
||||
id: 'ea04751e-9b7e-487b-a509-330fb6bd6b9b'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the group
|
||||
const userGroup = await UserGroups.findOne({
|
||||
id: ps.groupId,
|
||||
});
|
||||
|
||||
if (userGroup == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroup);
|
||||
}
|
||||
|
||||
const joining = await UserGroupJoinings.findOne({
|
||||
userId: me.id,
|
||||
userGroupId: userGroup.id
|
||||
});
|
||||
|
||||
if (joining == null && userGroup.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.noSuchGroup);
|
||||
}
|
||||
|
||||
return await UserGroups.pack(userGroup);
|
||||
});
|
@@ -0,0 +1,83 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { getUser } from '../../../common/getters';
|
||||
import { UserGroups, UserGroupJoinings } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups', 'users'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:user-groups',
|
||||
|
||||
params: {
|
||||
groupId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'UserGroup',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchGroup: {
|
||||
message: 'No such group.',
|
||||
code: 'NO_SUCH_GROUP',
|
||||
id: '8e31d36b-2f88-4ccd-a438-e2d78a9162db'
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '711f7ebb-bbb9-4dfa-b540-b27809fed5e9'
|
||||
},
|
||||
|
||||
noSuchGroupMember: {
|
||||
message: 'No such group member.',
|
||||
code: 'NO_SUCH_GROUP_MEMBER',
|
||||
id: 'd31bebee-196d-42c2-9a3e-9474d4be6cc4'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the group
|
||||
const userGroup = await UserGroups.findOne({
|
||||
id: ps.groupId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (userGroup == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroup);
|
||||
}
|
||||
|
||||
// Fetch the user
|
||||
const user = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw e;
|
||||
});
|
||||
|
||||
const joining = await UserGroupJoinings.findOne({
|
||||
userGroupId: userGroup.id,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (joining == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroupMember);
|
||||
}
|
||||
|
||||
await UserGroups.update(userGroup.id, {
|
||||
userId: ps.userId
|
||||
});
|
||||
|
||||
return await UserGroups.pack(userGroup.id);
|
||||
});
|
@@ -0,0 +1,55 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { UserGroups } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['groups'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:user-groups',
|
||||
|
||||
params: {
|
||||
groupId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
name: {
|
||||
validator: $.str.range(1, 100),
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'UserGroup',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchGroup: {
|
||||
message: 'No such group.',
|
||||
code: 'NO_SUCH_GROUP',
|
||||
id: '9081cda3-7a9e-4fac-a6ce-908d70f282f6'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the group
|
||||
const userGroup = await UserGroups.findOne({
|
||||
id: ps.groupId,
|
||||
userId: me.id
|
||||
});
|
||||
|
||||
if (userGroup == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroup);
|
||||
}
|
||||
|
||||
await UserGroups.update(userGroup.id, {
|
||||
name: ps.name
|
||||
});
|
||||
|
||||
return await UserGroups.pack(userGroup.id);
|
||||
});
|
@@ -0,0 +1,36 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { UserLists } from '@/models/index';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
import { UserList } from '@/models/entities/user-list';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
name: {
|
||||
validator: $.str.range(1, 100)
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'UserList',
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const userList = await UserLists.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
name: ps.name,
|
||||
} as UserList).then(x => UserLists.findOneOrFail(x.identifiers[0]));
|
||||
|
||||
return await UserLists.pack(userList);
|
||||
});
|
@@ -0,0 +1,40 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { UserLists } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
listId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_LIST',
|
||||
id: '78436795-db79-42f5-b1e2-55ea2cf19166'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const userList = await UserLists.findOne({
|
||||
id: ps.listId,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
await UserLists.delete(userList.id);
|
||||
});
|
@@ -0,0 +1,28 @@
|
||||
import define from '../../../define';
|
||||
import { UserLists } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists', 'account'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'UserList',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const userLists = await UserLists.find({
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
return await Promise.all(userLists.map(x => UserLists.pack(x)));
|
||||
});
|
@@ -0,0 +1,62 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import { publishUserListStream } from '@/services/stream';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { getUser } from '../../../common/getters';
|
||||
import { UserLists, UserListJoinings, Users } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists', 'users'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
listId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_LIST',
|
||||
id: '7f44670e-ab16-43b8-b4c1-ccd2ee89cc02'
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '588e7f72-c744-4a61-b180-d354e912bda2'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the list
|
||||
const userList = await UserLists.findOne({
|
||||
id: ps.listId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
// Fetch the user
|
||||
const user = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw e;
|
||||
});
|
||||
|
||||
// Pull the user
|
||||
await UserListJoinings.delete({ userListId: userList.id, userId: user.id });
|
||||
|
||||
publishUserListStream(userList.id, 'userRemoved', await Users.pack(user));
|
||||
});
|
@@ -0,0 +1,92 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { getUser } from '../../../common/getters';
|
||||
import { pushUserToUserList } from '@/services/user-list/push';
|
||||
import { UserLists, UserListJoinings, Blockings } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists', 'users'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
listId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_LIST',
|
||||
id: '2214501d-ac96-4049-b717-91e42272a711'
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: 'a89abd3d-f0bc-4cce-beb1-2f446f4f1e6a'
|
||||
},
|
||||
|
||||
alreadyAdded: {
|
||||
message: 'That user has already been added to that list.',
|
||||
code: 'ALREADY_ADDED',
|
||||
id: '1de7c884-1595-49e9-857e-61f12f4d4fc5'
|
||||
},
|
||||
|
||||
youHaveBeenBlocked: {
|
||||
message: 'You cannot push this user because you have been blocked by this user.',
|
||||
code: 'YOU_HAVE_BEEN_BLOCKED',
|
||||
id: '990232c5-3f9d-4d83-9f3f-ef27b6332a4b'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the list
|
||||
const userList = await UserLists.findOne({
|
||||
id: ps.listId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
// Fetch the user
|
||||
const user = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw e;
|
||||
});
|
||||
|
||||
// Check blocking
|
||||
if (user.id !== me.id) {
|
||||
const block = await Blockings.findOne({
|
||||
blockerId: user.id,
|
||||
blockeeId: me.id,
|
||||
});
|
||||
if (block) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
||||
const exist = await UserListJoinings.findOne({
|
||||
userListId: userList.id,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyAdded);
|
||||
}
|
||||
|
||||
// Push the user
|
||||
await pushUserToUserList(user, userList);
|
||||
});
|
@@ -0,0 +1,47 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { UserLists } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists', 'account'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
params: {
|
||||
listId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'UserList',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_LIST',
|
||||
id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the list
|
||||
const userList = await UserLists.findOne({
|
||||
id: ps.listId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
return await UserLists.pack(userList);
|
||||
});
|
@@ -0,0 +1,55 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { UserLists } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
listId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
name: {
|
||||
validator: $.str.range(1, 100),
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'UserList',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_LIST',
|
||||
id: '796666fe-3dff-4d39-becb-8a5932c1d5b7'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
// Fetch the list
|
||||
const userList = await UserLists.findOne({
|
||||
id: ps.listId,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
await UserLists.update(userList.id, {
|
||||
name: ps.name
|
||||
});
|
||||
|
||||
return await UserLists.pack(userList.id);
|
||||
});
|
144
packages/backend/src/server/api/endpoints/users/notes.ts
Normal file
144
packages/backend/src/server/api/endpoints/users/notes.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { getUser } from '../../common/getters';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { Notes } from '@/models/index';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users', 'notes'],
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
includeReplies: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10,
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
sinceDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
|
||||
untilDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
|
||||
includeMyRenotes: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
|
||||
withFiles: {
|
||||
validator: $.optional.bool,
|
||||
default: false,
|
||||
},
|
||||
|
||||
fileType: {
|
||||
validator: $.optional.arr($.str),
|
||||
},
|
||||
|
||||
excludeNsfw: {
|
||||
validator: $.optional.bool,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '27e494ba-2ac2-48e8-893b-10d4d8c2387b'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Lookup user
|
||||
const user = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw e;
|
||||
});
|
||||
|
||||
//#region Construct query
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere('note.userId = :userId', { userId: user.id })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
generateVisibilityQuery(query, me);
|
||||
if (me) generateMutedUserQuery(query, me, user);
|
||||
if (me) generateBlockedUserQuery(query, me);
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.fileType != null) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const type of ps.fileType!) {
|
||||
const i = ps.fileType!.indexOf(type);
|
||||
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
|
||||
}
|
||||
}));
|
||||
|
||||
if (ps.excludeNsfw) {
|
||||
query.andWhere('note.cw IS NULL');
|
||||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
|
||||
}
|
||||
}
|
||||
|
||||
if (!ps.includeReplies) {
|
||||
query.andWhere('note.replyId IS NULL');
|
||||
}
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :userId', { userId: user.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.take(ps.limit!).getMany();
|
||||
|
||||
return await Notes.packMany(timeline, me);
|
||||
});
|
40
packages/backend/src/server/api/endpoints/users/pages.ts
Normal file
40
packages/backend/src/server/api/endpoints/users/pages.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { Pages } from '@/models/index';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users', 'pages'],
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId)
|
||||
.andWhere(`page.userId = :userId`, { userId: ps.userId })
|
||||
.andWhere('page.visibility = \'public\'');
|
||||
|
||||
const pages = await query
|
||||
.take(ps.limit!)
|
||||
.getMany();
|
||||
|
||||
return await Pages.packMany(pages);
|
||||
});
|
79
packages/backend/src/server/api/endpoints/users/reactions.ts
Normal file
79
packages/backend/src/server/api/endpoints/users/reactions.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { NoteReactions, UserProfiles } from '@/models/index';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { ApiError } from '../../error';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users', 'reactions'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10,
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
sinceDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
|
||||
untilDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'NoteReaction',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
reactionsNotPublic: {
|
||||
message: 'Reactions of the user is not public.',
|
||||
code: 'REACTIONS_NOT_PUBLIC',
|
||||
id: '673a7dd2-6924-1093-e0c0-e68456ceae5c'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const profile = await UserProfiles.findOneOrFail(ps.userId);
|
||||
|
||||
if (me == null || (me.id !== ps.userId && !profile.publicReactions)) {
|
||||
throw new ApiError(meta.errors.reactionsNotPublic);
|
||||
}
|
||||
|
||||
const query = makePaginationQuery(NoteReactions.createQueryBuilder('reaction'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere(`reaction.userId = :userId`, { userId: ps.userId })
|
||||
.leftJoinAndSelect('reaction.note', 'note');
|
||||
|
||||
generateVisibilityQuery(query, me);
|
||||
|
||||
const reactions = await query
|
||||
.take(ps.limit!)
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, me, { withNote: true })));
|
||||
});
|
@@ -0,0 +1,63 @@
|
||||
import * as ms from 'ms';
|
||||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { Users, Followings } from '@/models/index';
|
||||
import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query';
|
||||
import { generateBlockedUserQuery, generateBlockQueryForUsers } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
offset: {
|
||||
validator: $.optional.num.min(0),
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const query = Users.createQueryBuilder('user')
|
||||
.where('user.isLocked = FALSE')
|
||||
.andWhere('user.isExplorable = TRUE')
|
||||
.andWhere('user.host IS NULL')
|
||||
.andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) })
|
||||
.andWhere('user.id != :meId', { meId: me.id })
|
||||
.orderBy('user.followersCount', 'DESC');
|
||||
|
||||
generateMutedUserQueryForUsers(query, me);
|
||||
generateBlockQueryForUsers(query, me);
|
||||
generateBlockedUserQuery(query, me);
|
||||
|
||||
const followingQuery = Followings.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
|
||||
query
|
||||
.andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`);
|
||||
|
||||
query.setParameters(followingQuery.getParameters());
|
||||
|
||||
const users = await query.take(ps.limit!).skip(ps.offset).getMany();
|
||||
|
||||
return await Users.packMany(users, me, { detail: true });
|
||||
});
|
111
packages/backend/src/server/api/endpoints/users/relation.ts
Normal file
111
packages/backend/src/server/api/endpoints/users/relation.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import { Users } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.either($.type(ID), $.arr($.type(ID)).unique()),
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id'
|
||||
},
|
||||
isFollowing: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
hasPendingFollowRequestFromYou: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
hasPendingFollowRequestToYou: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
isFollowed: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
isBlocking: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
isBlocked: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
isMuted: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id'
|
||||
},
|
||||
isFollowing: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
hasPendingFollowRequestFromYou: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
hasPendingFollowRequestToYou: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
isFollowed: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
isBlocking: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
isBlocked: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
isMuted: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId];
|
||||
|
||||
const relations = await Promise.all(ids.map(id => Users.getRelation(me.id, id)));
|
||||
|
||||
return Array.isArray(ps.userId) ? relations : relations[0];
|
||||
});
|
@@ -0,0 +1,90 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { publishAdminStream } from '@/services/stream';
|
||||
import { ApiError } from '../../error';
|
||||
import { getUser } from '../../common/getters';
|
||||
import { AbuseUserReports, Users } from '@/models/index';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
comment: {
|
||||
validator: $.str.range(1, 2048),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '1acefcb5-0959-43fd-9685-b48305736cb5'
|
||||
},
|
||||
|
||||
cannotReportYourself: {
|
||||
message: 'Cannot report yourself.',
|
||||
code: 'CANNOT_REPORT_YOURSELF',
|
||||
id: '1e13149e-b1e8-43cf-902e-c01dbfcb202f'
|
||||
},
|
||||
|
||||
cannotReportAdmin: {
|
||||
message: 'Cannot report the admin.',
|
||||
code: 'CANNOT_REPORT_THE_ADMIN',
|
||||
id: '35e166f5-05fb-4f87-a2d5-adb42676d48f'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Lookup user
|
||||
const user = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (user.id === me.id) {
|
||||
throw new ApiError(meta.errors.cannotReportYourself);
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
throw new ApiError(meta.errors.cannotReportAdmin);
|
||||
}
|
||||
|
||||
const report = await AbuseUserReports.save({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
targetUserId: user.id,
|
||||
targetUserHost: user.host,
|
||||
reporterId: me.id,
|
||||
reporterHost: null,
|
||||
comment: ps.comment,
|
||||
});
|
||||
|
||||
// Publish event to moderators
|
||||
setTimeout(async () => {
|
||||
const moderators = await Users.find({
|
||||
where: [{
|
||||
isAdmin: true
|
||||
}, {
|
||||
isModerator: true
|
||||
}]
|
||||
});
|
||||
|
||||
for (const moderator of moderators) {
|
||||
publishAdminStream(moderator.id, 'newAbuseUserReport', {
|
||||
id: report.id,
|
||||
targetUserId: report.targetUserId,
|
||||
reporterId: report.reporterId,
|
||||
comment: report.comment
|
||||
});
|
||||
}
|
||||
}, 1);
|
||||
});
|
@@ -0,0 +1,116 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { Followings, Users } from '@/models/index';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { USER_ACTIVE_THRESHOLD } from '@/const';
|
||||
import { User } from '@/models/entities/user';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
username: {
|
||||
validator: $.optional.nullable.str,
|
||||
},
|
||||
|
||||
host: {
|
||||
validator: $.optional.nullable.str,
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10,
|
||||
},
|
||||
|
||||
detail: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
|
||||
|
||||
if (ps.host) {
|
||||
const q = Users.createQueryBuilder('user')
|
||||
.where('user.isSuspended = FALSE')
|
||||
.andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' });
|
||||
|
||||
if (ps.username) {
|
||||
q.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' });
|
||||
}
|
||||
|
||||
q.andWhere('user.updatedAt IS NOT NULL');
|
||||
q.orderBy('user.updatedAt', 'DESC');
|
||||
|
||||
const users = await q.take(ps.limit!).getMany();
|
||||
|
||||
return await Users.packMany(users, me, { detail: ps.detail });
|
||||
} else if (ps.username) {
|
||||
let users: User[] = [];
|
||||
|
||||
if (me) {
|
||||
const followingQuery = Followings.createQueryBuilder('following')
|
||||
.select('following.followeeId')
|
||||
.where('following.followerId = :followerId', { followerId: me.id });
|
||||
|
||||
const query = Users.createQueryBuilder('user')
|
||||
.where(`user.id IN (${ followingQuery.getQuery() })`)
|
||||
.andWhere(`user.id != :meId`, { meId: me.id })
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||
}));
|
||||
|
||||
query.setParameters(followingQuery.getParameters());
|
||||
|
||||
users = await query
|
||||
.orderBy('user.usernameLower', 'ASC')
|
||||
.take(ps.limit!)
|
||||
.getMany();
|
||||
|
||||
if (users.length < ps.limit!) {
|
||||
const otherQuery = await Users.createQueryBuilder('user')
|
||||
.where(`user.id NOT IN (${ followingQuery.getQuery() })`)
|
||||
.andWhere(`user.id != :meId`, { meId: me.id })
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
||||
.andWhere('user.updatedAt IS NOT NULL');
|
||||
|
||||
otherQuery.setParameters(followingQuery.getParameters());
|
||||
|
||||
const otherUsers = await otherQuery
|
||||
.orderBy('user.updatedAt', 'DESC')
|
||||
.take(ps.limit! - users.length)
|
||||
.getMany();
|
||||
|
||||
users = users.concat(otherUsers);
|
||||
}
|
||||
} else {
|
||||
users = await Users.createQueryBuilder('user')
|
||||
.where('user.isSuspended = FALSE')
|
||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
||||
.andWhere('user.updatedAt IS NOT NULL')
|
||||
.orderBy('user.updatedAt', 'DESC')
|
||||
.take(ps.limit! - users.length)
|
||||
.getMany();
|
||||
}
|
||||
|
||||
return await Users.packMany(users, me, { detail: ps.detail });
|
||||
}
|
||||
});
|
127
packages/backend/src/server/api/endpoints/users/search.ts
Normal file
127
packages/backend/src/server/api/endpoints/users/search.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { UserProfiles, Users } from '@/models/index';
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Brackets } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
query: {
|
||||
validator: $.str,
|
||||
},
|
||||
|
||||
offset: {
|
||||
validator: $.optional.num.min(0),
|
||||
default: 0,
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10,
|
||||
},
|
||||
|
||||
origin: {
|
||||
validator: $.optional.str.or(['local', 'remote', 'combined']),
|
||||
default: 'combined',
|
||||
},
|
||||
|
||||
detail: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
|
||||
|
||||
const isUsername = ps.query.startsWith('@');
|
||||
|
||||
let users: User[] = [];
|
||||
|
||||
if (isUsername) {
|
||||
const usernameQuery = Users.createQueryBuilder('user')
|
||||
.where('user.usernameLower LIKE :username', { username: ps.query.replace('@', '').toLowerCase() + '%' })
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||
}))
|
||||
.andWhere('user.isSuspended = FALSE');
|
||||
|
||||
if (ps.origin === 'local') {
|
||||
usernameQuery.andWhere('user.host IS NULL');
|
||||
} else if (ps.origin === 'remote') {
|
||||
usernameQuery.andWhere('user.host IS NOT NULL');
|
||||
}
|
||||
|
||||
users = await usernameQuery
|
||||
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
|
||||
.take(ps.limit!)
|
||||
.skip(ps.offset)
|
||||
.getMany();
|
||||
} else {
|
||||
const nameQuery = Users.createQueryBuilder('user')
|
||||
.where('user.name ILIKE :query', { query: '%' + ps.query + '%' })
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||
}))
|
||||
.andWhere('user.isSuspended = FALSE');
|
||||
|
||||
if (ps.origin === 'local') {
|
||||
nameQuery.andWhere('user.host IS NULL');
|
||||
} else if (ps.origin === 'remote') {
|
||||
nameQuery.andWhere('user.host IS NOT NULL');
|
||||
}
|
||||
|
||||
users = await nameQuery
|
||||
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
|
||||
.take(ps.limit!)
|
||||
.skip(ps.offset)
|
||||
.getMany();
|
||||
|
||||
if (users.length < ps.limit!) {
|
||||
const profQuery = UserProfiles.createQueryBuilder('prof')
|
||||
.select('prof.userId')
|
||||
.where('prof.description ILIKE :query', { query: '%' + ps.query + '%' });
|
||||
|
||||
if (ps.origin === 'local') {
|
||||
profQuery.andWhere('prof.userHost IS NULL');
|
||||
} else if (ps.origin === 'remote') {
|
||||
profQuery.andWhere('prof.userHost IS NOT NULL');
|
||||
}
|
||||
|
||||
const query = Users.createQueryBuilder('user')
|
||||
.where(`user.id IN (${ profQuery.getQuery() })`)
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
|
||||
}))
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.setParameters(profQuery.getParameters());
|
||||
|
||||
users = users.concat(await query
|
||||
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
|
||||
.take(ps.limit!)
|
||||
.skip(ps.offset)
|
||||
.getMany()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return await Users.packMany(users, me, { detail: ps.detail });
|
||||
});
|
105
packages/backend/src/server/api/endpoints/users/show.ts
Normal file
105
packages/backend/src/server/api/endpoints/users/show.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import $ from 'cafy';
|
||||
import { resolveUser } from '@/remote/resolve-user';
|
||||
import define from '../../define';
|
||||
import { apiLogger } from '../../logger';
|
||||
import { ApiError } from '../../error';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import { Users } from '@/models/index';
|
||||
import { In } from 'typeorm';
|
||||
import { User } from '@/models/entities/user';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
userIds: {
|
||||
validator: $.optional.arr($.type(ID)).unique(),
|
||||
},
|
||||
|
||||
username: {
|
||||
validator: $.optional.str
|
||||
},
|
||||
|
||||
host: {
|
||||
validator: $.optional.nullable.str
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
},
|
||||
|
||||
errors: {
|
||||
failedToResolveRemoteUser: {
|
||||
message: 'Failed to resolve remote user.',
|
||||
code: 'FAILED_TO_RESOLVE_REMOTE_USER',
|
||||
id: 'ef7b9be4-9cba-4e6f-ab41-90ed171c7d3c',
|
||||
kind: 'server' as const
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '4362f8dc-731f-4ad8-a694-be5a88922a24'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
let user;
|
||||
|
||||
const isAdminOrModerator = me && (me.isAdmin || me.isModerator);
|
||||
|
||||
if (ps.userIds) {
|
||||
if (ps.userIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const users = await Users.find(isAdminOrModerator ? {
|
||||
id: In(ps.userIds)
|
||||
} : {
|
||||
id: In(ps.userIds),
|
||||
isSuspended: false
|
||||
});
|
||||
|
||||
// リクエストされた通りに並べ替え
|
||||
const _users: User[] = [];
|
||||
for (const id of ps.userIds) {
|
||||
_users.push(users.find(x => x.id === id)!);
|
||||
}
|
||||
|
||||
return await Promise.all(_users.map(u => Users.pack(u, me, {
|
||||
detail: true
|
||||
})));
|
||||
} else {
|
||||
// Lookup user
|
||||
if (typeof ps.host === 'string' && typeof ps.username === 'string') {
|
||||
user = await resolveUser(ps.username, ps.host).catch(e => {
|
||||
apiLogger.warn(`failed to resolve remote user: ${e}`);
|
||||
throw new ApiError(meta.errors.failedToResolveRemoteUser);
|
||||
});
|
||||
} else {
|
||||
const q: any = ps.userId != null
|
||||
? { id: ps.userId }
|
||||
: { usernameLower: ps.username!.toLowerCase(), host: null };
|
||||
|
||||
user = await Users.findOne(q);
|
||||
}
|
||||
|
||||
if (user == null || (!isAdminOrModerator && user.isSuspended)) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
return await Users.pack(user, me, {
|
||||
detail: true
|
||||
});
|
||||
}
|
||||
});
|
144
packages/backend/src/server/api/endpoints/users/stats.ts
Normal file
144
packages/backend/src/server/api/endpoints/users/stats.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, ReversiGames, Users } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '9e638e45-3b25-4ef7-8f95-07e8498f1819'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const user = await Users.findOne(ps.userId);
|
||||
if (user == null) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
const [
|
||||
notesCount,
|
||||
repliesCount,
|
||||
renotesCount,
|
||||
repliedCount,
|
||||
renotedCount,
|
||||
pollVotesCount,
|
||||
pollVotedCount,
|
||||
localFollowingCount,
|
||||
remoteFollowingCount,
|
||||
localFollowersCount,
|
||||
remoteFollowersCount,
|
||||
sentReactionsCount,
|
||||
receivedReactionsCount,
|
||||
noteFavoritesCount,
|
||||
pageLikesCount,
|
||||
pageLikedCount,
|
||||
driveFilesCount,
|
||||
driveUsage,
|
||||
reversiCount,
|
||||
] = await Promise.all([
|
||||
Notes.createQueryBuilder('note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
Notes.createQueryBuilder('note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.andWhere('note.replyId IS NOT NULL')
|
||||
.getCount(),
|
||||
Notes.createQueryBuilder('note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.andWhere('note.renoteId IS NOT NULL')
|
||||
.getCount(),
|
||||
Notes.createQueryBuilder('note')
|
||||
.where('note.replyUserId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
Notes.createQueryBuilder('note')
|
||||
.where('note.renoteUserId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
PollVotes.createQueryBuilder('vote')
|
||||
.where('vote.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
PollVotes.createQueryBuilder('vote')
|
||||
.innerJoin('vote.note', 'note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
Followings.createQueryBuilder('following')
|
||||
.where('following.followerId = :userId', { userId: user.id })
|
||||
.andWhere('following.followeeHost IS NULL')
|
||||
.getCount(),
|
||||
Followings.createQueryBuilder('following')
|
||||
.where('following.followerId = :userId', { userId: user.id })
|
||||
.andWhere('following.followeeHost IS NOT NULL')
|
||||
.getCount(),
|
||||
Followings.createQueryBuilder('following')
|
||||
.where('following.followeeId = :userId', { userId: user.id })
|
||||
.andWhere('following.followerHost IS NULL')
|
||||
.getCount(),
|
||||
Followings.createQueryBuilder('following')
|
||||
.where('following.followeeId = :userId', { userId: user.id })
|
||||
.andWhere('following.followerHost IS NOT NULL')
|
||||
.getCount(),
|
||||
NoteReactions.createQueryBuilder('reaction')
|
||||
.where('reaction.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
NoteReactions.createQueryBuilder('reaction')
|
||||
.innerJoin('reaction.note', 'note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
NoteFavorites.createQueryBuilder('favorite')
|
||||
.where('favorite.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
PageLikes.createQueryBuilder('like')
|
||||
.where('like.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
PageLikes.createQueryBuilder('like')
|
||||
.innerJoin('like.page', 'page')
|
||||
.where('page.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
DriveFiles.createQueryBuilder('file')
|
||||
.where('file.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
DriveFiles.calcDriveUsageOf(user),
|
||||
ReversiGames.createQueryBuilder('game')
|
||||
.where('game.user1Id = :userId', { userId: user.id })
|
||||
.orWhere('game.user2Id = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
]);
|
||||
|
||||
return {
|
||||
notesCount,
|
||||
repliesCount,
|
||||
renotesCount,
|
||||
repliedCount,
|
||||
renotedCount,
|
||||
pollVotesCount,
|
||||
pollVotedCount,
|
||||
localFollowingCount,
|
||||
remoteFollowingCount,
|
||||
localFollowersCount,
|
||||
remoteFollowersCount,
|
||||
followingCount: localFollowingCount + remoteFollowingCount,
|
||||
followersCount: localFollowersCount + remoteFollowersCount,
|
||||
sentReactionsCount,
|
||||
receivedReactionsCount,
|
||||
noteFavoritesCount,
|
||||
pageLikesCount,
|
||||
pageLikedCount,
|
||||
driveFilesCount,
|
||||
driveUsage,
|
||||
reversiCount,
|
||||
};
|
||||
});
|
Reference in New Issue
Block a user