Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 個別のお知らせにリンクで飛べるように #13885

Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

### Client
- Feat: アップロードするファイルの名前をランダム文字列にできるように
- Feat: 個別のお知らせにリンクで飛べるように
(Cherry-picked from https://github.com/MisskeyIO/misskey)
- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
- Enhance: リアクション・いいねの総数を表示するように
Expand Down
49 changes: 22 additions & 27 deletions packages/backend/src/core/AnnouncementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
*/

import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Brackets, EntityNotFoundError } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MiUser } from '@/models/User.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository, MiAnnouncement, MiAnnouncementRead, UsersRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';

Expand All @@ -29,6 +30,7 @@ export class AnnouncementService {
private idService: IdService,
private globalEventService: GlobalEventService,
private moderationLogService: ModerationLogService,
private announcementEntityService: AnnouncementEntityService,
) {
}

Expand Down Expand Up @@ -79,7 +81,7 @@ export class AnnouncementService {
userId: values.userId,
}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));

const packed = (await this.packMany([announcement]))[0];
const packed = await this.announcementEntityService.pack(announcement);

if (values.userId) {
this.globalEventService.publishMainStream(values.userId, 'announcementCreated', {
Expand Down Expand Up @@ -177,6 +179,24 @@ export class AnnouncementService {
}
}

@bindThis
public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise<Packed<'Announcement'>> {
const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId });
if (me) {
if (announcement.userId && announcement.userId !== me.id) {
throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
}

const read = await this.announcementReadsRepository.findOneBy({
announcementId: announcement.id,
userId: me.id,
});
return this.announcementEntityService.pack({ ...announcement, isRead: read !== null }, me);
} else {
return this.announcementEntityService.pack(announcement, null);
}
}

@bindThis
public async read(user: MiUser, announcementId: MiAnnouncement['id']): Promise<void> {
try {
Expand All @@ -193,29 +213,4 @@ export class AnnouncementService {
this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
}
}

@bindThis
public async packMany(
announcements: MiAnnouncement[],
me?: { id: MiUser['id'] } | null | undefined,
options?: {
reads?: MiAnnouncementRead[];
},
): Promise<Packed<'Announcement'>[]> {
const reads = me ? (options?.reads ?? await this.getReads(me.id)) : [];
return announcements.map(announcement => ({
id: announcement.id,
createdAt: this.idService.parse(announcement.id).date.toISOString(),
updatedAt: announcement.updatedAt?.toISOString() ?? null,
text: announcement.text,
title: announcement.title,
imageUrl: announcement.imageUrl,
icon: announcement.icon,
display: announcement.display,
needConfirmationToRead: announcement.needConfirmationToRead,
silence: announcement.silence,
forYou: announcement.userId === me?.id,
isRead: reads.some(read => read.announcementId === announcement.id),
}));
}
}
6 changes: 6 additions & 0 deletions packages/backend/src/core/CoreModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import ApRequestChart from './chart/charts/ap-request.js';
import { ChartManagementService } from './chart/ChartManagementService.js';

import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js';
import { AnnouncementEntityService } from './entities/AnnouncementEntityService.js';
import { AntennaEntityService } from './entities/AntennaEntityService.js';
import { AppEntityService } from './entities/AppEntityService.js';
import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js';
Expand Down Expand Up @@ -223,6 +224,7 @@ const $ApRequestChart: Provider = { provide: 'ApRequestChart', useExisting: ApRe
const $ChartManagementService: Provider = { provide: 'ChartManagementService', useExisting: ChartManagementService };

const $AbuseUserReportEntityService: Provider = { provide: 'AbuseUserReportEntityService', useExisting: AbuseUserReportEntityService };
const $AnnouncementEntityService: Provider = { provide: 'AnnouncementEntityService', useExisting: AnnouncementEntityService };
const $AntennaEntityService: Provider = { provide: 'AntennaEntityService', useExisting: AntennaEntityService };
const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting: AppEntityService };
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
Expand Down Expand Up @@ -363,6 +365,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ChartManagementService,

AbuseUserReportEntityService,
AnnouncementEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
Expand Down Expand Up @@ -499,6 +502,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ChartManagementService,

$AbuseUserReportEntityService,
$AnnouncementEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
Expand Down Expand Up @@ -635,6 +639,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ChartManagementService,

AbuseUserReportEntityService,
AnnouncementEntityService,
AntennaEntityService,
AppEntityService,
AuthSessionEntityService,
Expand Down Expand Up @@ -770,6 +775,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ChartManagementService,

$AbuseUserReportEntityService,
$AnnouncementEntityService,
$AntennaEntityService,
$AppEntityService,
$AuthSessionEntityService,
Expand Down
71 changes: 71 additions & 0 deletions packages/backend/src/core/entities/AnnouncementEntityService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { AnnouncementsRepository, AnnouncementReadsRepository, MiAnnouncement, MiUser } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';

@Injectable()
export class AnnouncementEntityService {
constructor(
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,

@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,

private idService: IdService,
) {
}

@bindThis
public async pack(
src: MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null },
me?: { id: MiUser['id'] } | null | undefined,
): Promise<Packed<'Announcement'>> {
const announcement = typeof src === 'object'
? src
: await this.announcementsRepository.findOneByOrFail({
id: src,
}) as MiAnnouncement & { isRead?: boolean | null };

if (me && announcement.isRead === undefined) {
announcement.isRead = await this.announcementReadsRepository
.countBy({
announcementId: announcement.id,
userId: me.id,
})
.then((count: number) => count > 0);
}

return {
id: announcement.id,
createdAt: this.idService.parse(announcement.id).date.toISOString(),
updatedAt: announcement.updatedAt?.toISOString() ?? null,
title: announcement.title,
text: announcement.text,
imageUrl: announcement.imageUrl,
icon: announcement.icon,
display: announcement.display,
forYou: announcement.userId === me?.id,
needConfirmationToRead: announcement.needConfirmationToRead,
silence: announcement.silence,
isRead: announcement.isRead !== null ? announcement.isRead : undefined,
};
}

@bindThis
public async packMany(
announcements: (MiAnnouncement['id'] | MiAnnouncement & { isRead?: boolean | null } | MiAnnouncement)[],
me?: { id: MiUser['id'] } | null | undefined,
) : Promise<Packed<'Announcement'>[]> {
return (await Promise.allSettled(announcements.map(x => this.pack(x, me))))
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<Packed<'Announcement'>>).value);
}
}
4 changes: 4 additions & 0 deletions packages/backend/src/server/api/EndpointsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
import * as ep___antennas_list from './endpoints/antennas/list.js';
Expand Down Expand Up @@ -455,6 +456,7 @@ const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', us
const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
const $antennas_list: Provider = { provide: 'ep:antennas/list', useClass: ep___antennas_list.default };
Expand Down Expand Up @@ -831,6 +833,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
$announcements,
$announcements_show,
$antennas_create,
$antennas_delete,
$antennas_list,
Expand Down Expand Up @@ -1201,6 +1204,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
$announcements,
$announcements_show,
$antennas_create,
$antennas_delete,
$antennas_list,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/server/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
import * as ep___antennas_list from './endpoints/antennas/list.js';
Expand Down Expand Up @@ -453,6 +454,7 @@ const eps = [
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
['admin/roles/users', ep___admin_roles_users],
['announcements', ep___announcements],
['announcements/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete],
['antennas/list', ep___antennas_list],
Expand Down
11 changes: 4 additions & 7 deletions packages/backend/src/server/api/endpoints/announcements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { DI } from '@/di-symbols.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/_.js';
import type { AnnouncementsRepository } from '@/models/_.js';

export const meta = {
tags: ['meta'],
Expand Down Expand Up @@ -44,11 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,

@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,

private queryService: QueryService,
private announcementService: AnnouncementService,
private announcementEntityService: AnnouncementEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId)
Expand All @@ -60,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-

const announcements = await query.limit(ps.limit).getMany();

return this.announcementService.packMany(announcements, me);
return this.announcementEntityService.packMany(announcements, me);
});
}
}
54 changes: 54 additions & 0 deletions packages/backend/src/server/api/endpoints/announcements/show.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Injectable } from '@nestjs/common';
import { EntityNotFoundError } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { ApiError } from '../../error.js';

export const meta = {
tags: ['meta'],

requireCredential: false,

res: {
type: 'object',
optional: false, nullable: false,
ref: 'Announcement',
},

errors: {
noSuchAnnouncement: {
message: 'No such announcement.',
code: 'NO_SUCH_ANNOUNCEMENT',
id: 'b57b5e1d-4f49-404a-9edb-46b00268f121',
},
},
} as const;

export const paramDef = {
type: 'object',
properties: {
announcementId: { type: 'string', format: 'misskey:id' },
},
required: ['announcementId'],
} as const;

@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
try {
return await this.announcementService.getAnnouncement(ps.announcementId, me);
} catch (err) {
if (err instanceof EntityNotFoundError) throw new ApiError(meta.errors.noSuchAnnouncement);
throw err;
}
});
}
}
2 changes: 2 additions & 0 deletions packages/backend/test/unit/AnnouncementService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import { GlobalModule } from '@/GlobalModule.js';
import { AnnouncementService } from '@/core/AnnouncementService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import type {
AnnouncementReadsRepository,
AnnouncementsRepository,
Expand Down Expand Up @@ -67,6 +68,7 @@ describe('AnnouncementService', () => {
],
providers: [
AnnouncementService,
AnnouncementEntityService,
CacheService,
IdService,
],
Expand Down
Loading
Loading