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: 個人宛てお知らせ機能 #107

Merged
merged 28 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9a546aa
feat: 個人宛てお知らせ機能
CyberRex0 Jul 7, 2023
e1789af
Remove unused import
CyberRex0 Jul 7, 2023
5d3cd13
Update packages/backend/src/server/api/endpoints/admin/announcements/…
CyberRex0 Jul 7, 2023
4c6374b
Update packages/frontend/src/pages/announcements.vue
CyberRex0 Jul 7, 2023
ee6d506
Restore breakline
CyberRex0 Jul 7, 2023
1d63c68
一般向けAPIにはuserオブジェクトを提供しない
CyberRex0 Jul 7, 2023
3625e07
fix
CyberRex0 Jul 7, 2023
a2529c6
Merge branch 'pr-20230706-userannouncement' of github.com:CyberRex0/m…
CyberRex0 Jul 7, 2023
350da16
Fix
CyberRex0 Jul 7, 2023
0befa5b
Update packages/misskey-js/src/entities.ts
CyberRex0 Jul 7, 2023
1685318
Fix
CyberRex0 Jul 7, 2023
f3dc0d5
Update misskey-js.api.md
CyberRex0 Jul 8, 2023
ddff633
Fix lint
CyberRex0 Jul 8, 2023
1f96878
他のテーブルに合わせて character varying(32) にした
riku6460 Jul 7, 2023
21232e3
count クエリを1つにまとめた
riku6460 Jul 7, 2023
c69cb41
user を pack するようにした
riku6460 Jul 7, 2023
8ab6bf1
いろいろ修正
riku6460 Jul 7, 2023
d097002
個人宛てのお知らせの表示を改善
CyberRex0 Jul 11, 2023
478f556
Update misskey-js.api.md
CyberRex0 Jul 11, 2023
4253c6c
Merge migration scripts
CyberRex0 Jul 23, 2023
ae43682
Fix
CyberRex0 Jul 23, 2023
cc0ce44
Update packages/backend/migration/1688647797135-userannouncement.js
CyberRex0 Jul 23, 2023
baa00f2
Update packages/backend/src/models/entities/Announcement.ts
CyberRex0 Jul 23, 2023
a5c5615
Merge branch 'pr-20230711-userannouncement-v2' of github.com:CyberRex…
CyberRex0 Jul 23, 2023
5842618
Fix
CyberRex0 Jul 23, 2023
5fb1ce0
Update migration script
CyberRex0 Jul 23, 2023
08b49a6
Merge pull request #106 from CyberRex0/pr-20230711-userannouncement-v2
riku6460 Jul 23, 2023
da73d50
Merge branch 'io' into user-announcement
riku6460 Jul 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,9 @@ export interface Locale {
"additionalEmojiDictionary": string;
"installed": string;
"branding": string;
"newUserAnnouncementAvailable": string;
"viewAnnouncement": string;
"dialogCloseDuration": string;
"enableServerMachineStats": string;
"enableIdenticonGeneration": string;
"turnOffToImprovePerformance": string;
Expand Down
3 changes: 3 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,9 @@ goToMisskey: "Misskeyへ"
additionalEmojiDictionary: "絵文字の追加辞書"
installed: "インストール済み"
branding: "ブランディング"
newUserAnnouncementAvailable: "新着のあなた宛てのお知らせがあります"
viewAnnouncement: "お知らせを見る"
dialogCloseDuration: "ダイアログを閉じるまでの待機時間"
enableServerMachineStats: "サーバーのマシン情報を公開する"
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
Expand Down
15 changes: 15 additions & 0 deletions packages/backend/migration/1688647797135-userannouncement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export class Userannouncement1688647797135 {
name = 'Userannouncement1688647797135'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "announcement" ADD COLUMN "userId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "announcement" ADD COLUMN "closeDuration" integer NOT NULL DEFAULT 0`);
await queryRunner.query(`CREATE INDEX "IDX_fd25dfe3da37df1715f11ba6ec" ON "announcement" ("userId") `);
}

async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_fd25dfe3da37df1715f11ba6ec"`);
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "userId"`);
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "closeDuration"`);
}
}
10 changes: 6 additions & 4 deletions packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { In, Not } from 'typeorm';
import { In, IsNull, Not } from 'typeorm';
import * as Redis from 'ioredis';
import _Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
Expand Down Expand Up @@ -218,9 +218,11 @@ export class UserEntityService implements OnModuleInit {
userId: userId,
});

const count = await this.announcementsRepository.countBy(reads.length > 0 ? {
id: Not(In(reads.map(read => read.announcementId))),
} : {});
const id = reads.length > 0 ? Not(In(reads.map(read => read.announcementId))) : undefined;
const count = await this.announcementsRepository.countBy([
{ id, userId: IsNull() },
{ id, userId: userId },
]);

return count > 0;
}
Expand Down
13 changes: 13 additions & 0 deletions packages/backend/src/models/entities/Announcement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ export class Announcement {
})
public imageUrl: string | null;

@Index()
@Column('varchar', {
...id(),
nullable: true,
})
public userId: string | null;

@Column('integer', {
nullable: false,
default: 0,
})
public closeDuration: number;

constructor(data: Partial<Announcement>) {
if (data == null) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
userId: {
type: 'string',
optional: false, nullable: true,
},
closeDuration: {
type: 'number',
optional: false, nullable: false,
},
},
},
} as const;
Expand All @@ -52,6 +60,8 @@ export const paramDef = {
title: { type: 'string', minLength: 1 },
text: { type: 'string', minLength: 1 },
imageUrl: { type: 'string', nullable: true, minLength: 1 },
userId: { type: 'string', nullable: true, format: 'misskey:id' },
closeDuration: { type: 'number', nullable: false },
},
required: ['title', 'text', 'imageUrl'],
} as const;
Expand All @@ -73,6 +83,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
title: ps.title,
text: ps.text,
imageUrl: ps.imageUrl,
userId: ps.userId ?? null,
closeDuration: ps.closeDuration,
}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));

return Object.assign({}, announcement, { createdAt: announcement.createdAt.toISOString(), updatedAt: null });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Inject, Injectable } from '@nestjs/common';
import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/index.js';
import { In } from 'typeorm';
import type { AnnouncementsRepository, AnnouncementReadsRepository, UsersRepository } from '@/models/index.js';
import type { Announcement } from '@/models/entities/Announcement.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';

export const meta = {
Expand Down Expand Up @@ -46,10 +48,23 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
userId: {
type: 'string',
optional: false, nullable: true,
},
user: {
type: 'object',
optional: true, nullable: false,
ref: 'UserLite',
},
reads: {
type: 'number',
optional: false, nullable: false,
},
closeDuration: {
type: 'number',
optional: false, nullable: false,
},
},
},
},
Expand All @@ -61,6 +76,7 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id' },
},
required: [],
} as const;
Expand All @@ -75,10 +91,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,

@Inject(DI.usersRepository)
private usersRepository: UsersRepository,

private queryService: QueryService,
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
const builder = this.announcementsRepository.createQueryBuilder('announcement');
if (ps.userId) {
builder.where('"userId" = :userId', { userId: ps.userId });
} else {
builder.where('"userId" IS NULL');
}

const query = this.queryService.makePaginationQuery(builder, ps.sinceId, ps.untilId);

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

Expand All @@ -90,14 +117,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}));
}

const users = await this.usersRepository.findBy({
id: In(announcements.map(a => a.userId).filter(id => id != null)),
});
const packedUsers = await this.userEntityService.packMany(users, me, {
detail: false,
});

return announcements.map(announcement => ({
id: announcement.id,
createdAt: announcement.createdAt.toISOString(),
updatedAt: announcement.updatedAt?.toISOString() ?? null,
title: announcement.title,
text: announcement.text,
imageUrl: announcement.imageUrl,
userId: announcement.userId,
user: packedUsers.find(user => user.id === announcement.userId),
reads: reads.get(announcement)!,
closeDuration: announcement.closeDuration,
}));
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AnnouncementsRepository } from '@/models/index.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';

Expand All @@ -26,8 +26,10 @@ export const paramDef = {
title: { type: 'string', minLength: 1 },
text: { type: 'string', minLength: 1 },
imageUrl: { type: 'string', nullable: true, minLength: 0 },
userId: { type: 'string', nullable: true, format: 'misskey:id' },
closeDuration: { type: 'number', nullable: false },
},
required: ['id', 'title', 'text', 'imageUrl'],
required: ['id', 'title', 'text', 'imageUrl', 'closeDuration'],
} as const;

// eslint-disable-next-line import/no-default-export
Expand All @@ -36,18 +38,27 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,

@Inject(DI.announcementReadsRepository)
private announcementsReadsRepository: AnnouncementReadsRepository,
) {
super(meta, paramDef, async (ps, me) => {
const announcement = await this.announcementsRepository.findOneBy({ id: ps.id });

if (announcement == null) throw new ApiError(meta.errors.noSuchAnnouncement);

if (announcement.userId && announcement.userId !== ps.userId) {
await this.announcementsReadsRepository.delete({ id: announcement.id, userId: announcement.userId });
}

await this.announcementsRepository.update(announcement.id, {
updatedAt: new Date(),
title: ps.title,
text: ps.text,
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
imageUrl: ps.imageUrl || null,
userId: ps.userId ?? null,
closeDuration: ps.closeDuration,
});
});
}
Expand Down
23 changes: 22 additions & 1 deletion packages/backend/src/server/api/endpoints/announcements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ export const meta = {
type: 'boolean',
optional: true, nullable: false,
},
isPrivate: {
type: 'boolean',
optional: false, nullable: true,
},
closeDuration: {
type: 'number',
optional: false, nullable: false,
},
},
},
},
Expand All @@ -60,6 +68,7 @@ export const paramDef = {
withUnreads: { type: 'boolean', default: false },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
privateOnly: { type: 'boolean', default: false },
},
required: [],
} as const;
Expand All @@ -77,8 +86,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
const builder = this.announcementsRepository.createQueryBuilder('announcement');
if (me) {
if (ps.privateOnly) {
builder.where('"userId" = :userId', { userId: me.id });
} else {
builder.where('"userId" IS NULL');
builder.orWhere('"userId" = :userId', { userId: me.id });
}
} else {
builder.where('"userId" IS NULL');
}

const query = this.queryService.makePaginationQuery(builder, ps.sinceId, ps.untilId);
const announcements = await query.limit(ps.limit).getMany();

if (me) {
Expand All @@ -95,6 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
...a,
createdAt: a.createdAt.toISOString(),
updatedAt: a.updatedAt?.toISOString() ?? null,
isPrivate: !!a.userId,
}));
});
}
Expand Down
14 changes: 13 additions & 1 deletion packages/backend/src/server/api/endpoints/i/read-announcement.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js';
import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/index.js';
Expand Down Expand Up @@ -47,7 +48,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
// Check if announcement exists
const announcementExist = await this.announcementsRepository.exist({ where: { id: ps.announcementId } });
const announcementExist = await this.announcementsRepository.exist({
where: [
{
id: ps.announcementId,
userId: IsNull(),
},
{
id: ps.announcementId,
userId: me.id,
}
]
});

if (!announcementExist) {
throw new ApiError(meta.errors.noSuchAnnouncement);
Expand Down
7 changes: 6 additions & 1 deletion packages/frontend/src/boot/main-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { computed, createApp, watch, markRaw, version as vueVersion, defineAsync
import { common } from './common';
import { version, ui, lang, updateLocale } from '@/config';
import { i18n, updateI18n } from '@/i18n';
import { confirm, alert, post, popup, toast } from '@/os';
import { confirm, alert, post, popup, toast, api } from '@/os';
import { useStream } from '@/stream';
import * as sound from '@/scripts/sound';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
Expand Down Expand Up @@ -246,6 +246,11 @@ export async function mainBoot() {
main.on('myTokenRegenerated', () => {
signout();
});

const unreadUserAnnouncementsList = await api('announcements', { privateOnly: true, withUnreads: true });
if (unreadUserAnnouncementsList.length > 0) {
unreadUserAnnouncementsList.forEach((v) => popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementModal.vue')), { title: v.title, text: v.text, closeDuration: v.closeDuration, announcementId: v.id }, {}, 'closed'));
}
}

// shortcut
Expand Down
Loading