Merge remote-tracking branch 'misskey-dev/develop' into prismisskey

# Conflicts:
#	package.json
This commit is contained in:
mattyatea 2023-09-21 19:08:33 +09:00
commit 6e0b64bebe
35 changed files with 864 additions and 363 deletions

View file

@ -0,0 +1,11 @@
export class VerifiedLinks1695260774117 {
name = 'VerifiedLinks1695260774117'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "verifiedLinks" character varying array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "verifiedLinks"`);
}
}

View file

@ -0,0 +1,13 @@
export class FollowingNotify1695288787870 {
name = 'FollowingNotify1695288787870'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "following" ADD "notify" character varying(32)`);
await queryRunner.query(`CREATE INDEX "IDX_5108098457488634a4768e1d12" ON "following" ("notify") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_5108098457488634a4768e1d12"`);
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "notify"`);
}
}

View file

@ -64,7 +64,7 @@
"@bull-board/ui": "5.8.4",
"@discordapp/twemoji": "14.1.2",
"@fastify/accepts": "4.2.0",
"@fastify/cookie": "9.0.4",
"@fastify/cookie": "9.1.0",
"@fastify/cors": "8.4.0",
"@fastify/express": "2.3.0",
"@fastify/http-proxy": "9.2.1",
@ -86,7 +86,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "4.11.1",
"bullmq": "4.11.2",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.1",
"chalk": "5.3.0",
@ -158,7 +158,7 @@
"systeminformation": "5.21.8",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.7",
"tsc-alias": "1.8.8",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.17",

View file

@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js';
import type { ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js';
@ -185,6 +185,9 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private idService: IdService,
@ -505,6 +508,20 @@ export class NoteCreateService implements OnApplicationShutdown {
this.saveReply(data.reply, note);
}
if (data.reply == null) {
this.followingsRepository.findBy({
followeeId: user.id,
notify: 'normal',
}).then(followings => {
for (const following of followings) {
this.notificationService.createNotification(following.followerId, 'note', {
notifierId: user.id,
noteId: note.id,
});
}
});
}
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
if (!user.isBot) this.incRenoteCount(data.renote);

View file

@ -20,7 +20,7 @@ import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
@Injectable()
export class NotificationEntityService implements OnModuleInit {

View file

@ -146,15 +146,14 @@ export class UserEntityService implements OnModuleInit {
@bindThis
public async getRelation(me: MiUser['id'], target: MiUser['id']) {
const following = await this.followingsRepository.findOneBy({
followerId: me,
followeeId: target,
});
return awaitAll({
id: target,
isFollowing: this.followingsRepository.count({
where: {
followerId: me,
followeeId: target,
},
take: 1,
}).then(n => n > 0),
following,
isFollowing: following != null,
isFollowed: this.followingsRepository.count({
where: {
followerId: target,
@ -384,6 +383,7 @@ export class UserEntityService implements OnModuleInit {
birthday: profile!.birthday,
lang: profile!.lang,
fields: profile!.fields,
verifiedLinks: profile!.verifiedLinks,
followersCount: followersCount ?? 0,
followingCount: followingCount ?? 0,
notesCount: user.notesCount,
@ -485,6 +485,7 @@ export class UserEntityService implements OnModuleInit {
isBlocked: relation.isBlocked,
isMuted: relation.isMuted,
isRenoteMuted: relation.isRenoteMuted,
notify: relation.following?.notify ?? 'none',
} : {}),
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;

View file

@ -45,6 +45,13 @@ export class MiFollowing {
@JoinColumn()
public follower: MiUser | null;
@Index()
@Column('varchar', {
length: 32,
nullable: true,
})
public notify: 'normal' | null;
//#region Denormalized fields
@Index()
@Column('varchar', {

View file

@ -22,18 +22,6 @@ export type MiNotification = {
/**
*
* follow -
* mention - 稿
* reply - 稿
* renote - 稿Renoteされた
* quote - 稿Renoteされた
* reaction - 稿
* pollEnded -
* receiveFollowRequest -
* followRequestAccepted -
* achievementEarned -
* app -
* test -
*/
type: typeof notificationTypes[number];

View file

@ -48,6 +48,12 @@ export class MiUserProfile {
value: string;
}[];
@Column('varchar', {
array: true,
default: '{}',
})
public verifiedLinks: string[];
@Column('varchar', {
length: 32, nullable: true,
})

View file

@ -169,6 +169,15 @@ export const packedUserDetailedNotMeOnlySchema = {
},
},
},
verifiedLinks: {
type: 'array',
nullable: false, optional: false,
items: {
type: 'string',
nullable: false, optional: false,
format: 'url',
},
},
followersCount: {
type: 'number',
nullable: false, optional: false,
@ -264,6 +273,10 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'string',
nullable: false, optional: true,
},
notify: {
type: 'string',
nullable: false, optional: true,
},
//#endregion
},
} as const;

View file

@ -160,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js';
import * as ep___federation_stats from './endpoints/federation/stats.js';
import * as ep___following_create from './endpoints/following/create.js';
import * as ep___following_delete from './endpoints/following/delete.js';
import * as ep___following_update from './endpoints/following/update.js';
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
@ -508,6 +509,7 @@ const $federation_users: Provider = { provide: 'ep:federation/users', useClass:
const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default };
const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default };
const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default };
const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default };
const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default };
const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
@ -860,6 +862,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$federation_stats,
$following_create,
$following_delete,
$following_update,
$following_invalidate,
$following_requests_accept,
$following_requests_cancel,
@ -1206,6 +1209,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$federation_stats,
$following_create,
$following_delete,
$following_update,
$following_invalidate,
$following_requests_accept,
$following_requests_cancel,

View file

@ -160,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js';
import * as ep___federation_stats from './endpoints/federation/stats.js';
import * as ep___following_create from './endpoints/following/create.js';
import * as ep___following_delete from './endpoints/following/delete.js';
import * as ep___following_update from './endpoints/following/update.js';
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
@ -506,6 +507,7 @@ const eps = [
['federation/stats', ep___federation_stats],
['following/create', ep___following_create],
['following/delete', ep___following_delete],
['following/update', ep___following_update],
['following/invalidate', ep___following_invalidate],
['following/requests/accept', ep___following_requests_accept],
['following/requests/cancel', ep___following_requests_cancel],

View file

@ -19,7 +19,7 @@ export const meta = {
limit: {
duration: ms('1hour'),
max: 50,
max: 100,
},
requireCredential: true,

View file

@ -29,7 +29,7 @@ export const meta = {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8',
id: 'b77e6ae6-a3e5-40da-9cc8-c240115479cc',
},
followerIsYourself: {
@ -41,7 +41,7 @@ export const meta = {
notFollowing: {
message: 'The other use is not following you.',
code: 'NOT_FOLLOWING',
id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09',
id: '918faac3-074f-41ae-9c43-ed5d2946770d',
},
},

View file

@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { FollowingsRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['following', 'users'],
limit: {
duration: ms('1hour'),
max: 100,
},
requireCredential: true,
kind: 'write:following',
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '14318698-f67e-492a-99da-5353a5ac52be',
},
followeeIsYourself: {
message: 'Followee is yourself.',
code: 'FOLLOWEE_IS_YOURSELF',
id: '4c4cbaf9-962a-463b-8418-a5e365dbf2eb',
},
notFollowing: {
message: 'You are not following that user.',
code: 'NOT_FOLLOWING',
id: 'b8dc75cf-1cb5-46c9-b14b-5f1ffbd782c9',
},
},
res: {
type: 'object',
optional: false, nullable: false,
ref: 'UserLite',
},
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
notify: { type: 'string', enum: ['normal', 'none'] },
},
required: ['userId', 'notify'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private userEntityService: UserEntityService,
private getterService: GetterService,
private userFollowingService: UserFollowingService,
) {
super(meta, paramDef, async (ps, me) => {
const follower = me;
// Check if the follower is yourself
if (me.id === ps.userId) {
throw new ApiError(meta.errors.followeeIsYourself);
}
// Get followee
const followee = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
});
// Check not following
const exist = await this.followingsRepository.findOneBy({
followerId: follower.id,
followeeId: followee.id,
});
if (exist == null) {
throw new ApiError(meta.errors.notFollowing);
}
await this.followingsRepository.update({
id: exist.id,
}, {
notify: ps.notify === 'none' ? null : ps.notify,
});
return await this.userEntityService.pack(follower.id, me);
});
}
}

View file

@ -6,11 +6,13 @@
import RE2 from 're2';
import * as mfm from 'mfm-js';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { JSDOM } from 'jsdom';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
import { notificationTypes } from '@/types.js';
@ -27,6 +29,9 @@ import { RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type { Config } from '@/config.js';
import { safeForSql } from '@/misc/safe-for-sql.js';
import { ApiLoggerService } from '../../ApiLoggerService.js';
import { ApiError } from '../../error.js';
@ -37,6 +42,11 @@ export const meta = {
kind: 'write:account',
limit: {
duration: ms('1hour'),
max: 10,
},
errors: {
noSuchAvatar: {
message: 'No such avatar file.',
@ -173,6 +183,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@ -195,9 +208,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private hashtagService: HashtagService,
private roleService: RoleService,
private cacheService: CacheService,
private httpRequestService: HttpRequestService,
) {
super(meta, paramDef, async (ps, _user, token) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id });
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
const isSecure = token == null;
const updates = {} as Partial<MiUser>;
@ -296,9 +310,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.fields) {
profileUpdates.fields = ps.fields
.filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '')
.filter(x => typeof x.name === 'string' && x.name.trim() !== '' && typeof x.value === 'string' && x.value.trim() !== '')
.map(x => {
return { name: x.name, value: x.value };
return { name: x.name.trim(), value: x.value.trim() };
});
}
@ -364,7 +378,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Object.keys(updates).includes('alsoKnownAs')) {
this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates });
}
if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates);
await this.userProfilesRepository.update(user.id, {
...profileUpdates,
verifiedLinks: [],
});
const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
detail: true,
@ -386,7 +404,34 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// フォロワーにUpdateを配信
this.accountUpdateService.publishToFollowers(user.id);
const urls = updatedProfile.fields.filter(x => x.value.startsWith('https://'));
for (const url of urls) {
this.verifyLink(url.value, user);
}
return iObj;
});
}
private async verifyLink(url: string, user: MiLocalUser) {
if (!safeForSql(url)) return;
const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html);
const doc = window.document;
const myLink = `${this.config.url}/@${user.username}`;
const includesMyLink = Array.from(doc.getElementsByTagName('a')).some(a => a.href === myLink);
if (includesMyLink) {
await this.userProfilesRepository.createQueryBuilder('profile').update()
.where('userId = :userId', { userId: user.id })
.set({
verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
})
.execute();
}
}
}

View file

@ -3,7 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const;
/**
* note - 稿
* follow -
* mention - 稿
* reply - 稿
* renote - 稿Renoteされた
* quote - 稿Renoteされた
* reaction - 稿
* pollEnded -
* receiveFollowRequest -
* followRequestAccepted -
* achievementEarned -
* app -
* test -
*/
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const;
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;

View file

@ -102,6 +102,7 @@ describe('ユーザー', () => {
birthday: user.birthday,
lang: user.lang,
fields: user.fields,
verifiedLinks: user.verifiedLinks,
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
@ -369,6 +370,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.birthday, null);
assert.strictEqual(response.lang, null);
assert.deepStrictEqual(response.fields, []);
assert.deepStrictEqual(response.verifiedLinks, []);
assert.strictEqual(response.followersCount, 0);
assert.strictEqual(response.followingCount, 0);
assert.strictEqual(response.notesCount, 0);
@ -492,7 +494,7 @@ describe('ユーザー', () => {
{ parameters: (): object => ({ mutedWords: [] }) },
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
{ parameters: (): object => ({ mutedInstances: [] }) },
{ parameters: (): object => ({ mutingNotificationTypes: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
{ parameters: (): object => ({ mutingNotificationTypes: ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
{ parameters: (): object => ({ mutingNotificationTypes: [] }) },
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: (): object => ({ emailNotificationTypes: [] }) },