Skip to content

Commit

Permalink
Merge pull request #46 from team-shahu/feat/schedule-note
Browse files Browse the repository at this point in the history
feat: 予約投稿の実装
  • Loading branch information
chan-mai authored Nov 15, 2024
2 parents 55392dc + 8147960 commit ed8a511
Show file tree
Hide file tree
Showing 51 changed files with 1,649 additions and 31 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
- 新着ノート通知があった時まとめるように https://github.com/team-shahu/misskey/pull/40
- いいねボタンの実装 https://github.com/team-shahu/misskey/pull/41 https://github.com/team-shahu/misskey/pull/44 https://github.com/team-shahu/misskey/pull/45
- 独自機能ページの追加 https://github.com/team-shahu/misskey/pull/42
- 予約投稿機能 https://github.com/team-shahu/misskey/pull/46

## Special Thanks
- [Misskey](https://github.com/misskey-dev/misskey)
Expand Down
2 changes: 2 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2103,6 +2103,8 @@ _permissions:
"read:mutes": "View your list of muted users"
"write:mutes": "Edit your list of muted users"
"write:notes": "Compose or delete notes"
"read:notes-schedule": "View your list of scheduled notes"
"write:notes-schedule": "Compose or delete scheduled notes"
"read:notifications": "View your notifications"
"write:notifications": "Manage your notifications"
"read:reactions": "View your reactions"
Expand Down
28 changes: 28 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7051,6 +7051,10 @@ export interface Locale extends ILocale {
* リストのインポートを許可
*/
"canImportUserLists": string;
/**
* 予約投稿の最大数
*/
"scheduleNoteMax": string;
};
"_condition": {
/**
Expand Down Expand Up @@ -8503,6 +8507,14 @@ export interface Locale extends ILocale {
* 違反を報告する
*/
"write:report-abuse": string;
/**
* 予約投稿を見る
*/
"read:notes-schedule": string;
/**
* 予約投稿を作成・削除する
*/
"write:notes-schedule": string;
};
"_auth": {
/**
Expand Down Expand Up @@ -9453,6 +9465,14 @@ export interface Locale extends ILocale {
* ロールが付与されました
*/
"roleAssigned": string;
/**
* 予約投稿に失敗しました
*/
"scheduledNoteFailed": string;
/**
* 予約投稿をノートしました
*/
"scheduledNotePosted": string;
/**
* プッシュ通知の更新をしました
*/
Expand Down Expand Up @@ -10694,6 +10714,14 @@ export interface Locale extends ILocale {
*/
"codeGeneratedDescription": string;
};
/**
* 予約投稿
*/
"schedulePost": string;
/**
* 予約投稿一覧
*/
"schedulePostList": string;
}
declare const locales: {
[lang: string]: Locale;
Expand Down
8 changes: 8 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1821,6 +1821,7 @@ _role:
canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可"
scheduleNoteMax: "予約投稿の最大数"
_condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー"
Expand Down Expand Up @@ -2228,6 +2229,8 @@ _permissions:
"read:clip-favorite": "クリップのいいねを見る"
"read:federation": "連合に関する情報を取得する"
"write:report-abuse": "違反を報告する"
"read:notes-schedule": "予約投稿を見る"
"write:notes-schedule": "予約投稿を作成・削除する"

_auth:
shareAccessTitle: "アプリへのアクセス許可"
Expand Down Expand Up @@ -2494,6 +2497,8 @@ _notification:
newNote: "新しい投稿"
unreadAntennaNote: "アンテナ {name}"
roleAssigned: "ロールが付与されました"
scheduledNoteFailed: "予約投稿に失敗しました"
scheduledNotePosted: "予約投稿をノートしました"
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得"
testNotification: "通知テスト"
Expand Down Expand Up @@ -2851,3 +2856,6 @@ _embedCodeGen:
generateCode: "埋め込みコードを作成"
codeGenerated: "コードが生成されました"
codeGeneratedDescription: "生成されたコードをウェブサイトに貼り付けてご利用ください。"
schedulePost: "予約投稿"
schedulePostList: "予約投稿一覧"

2 changes: 2 additions & 0 deletions locales/ko-KR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2084,6 +2084,8 @@ _permissions:
"read:mutes": "뮤트 여부를 확인합니다"
"write:mutes": "뮤트를 하거나 해제합니다"
"write:notes": "노트를 작성하거나 삭제합니다"
"read:notes-schedule": "게시를 예약한 노트를 봅니다"
"write:notes-schedule": "노트 게시를 예약하거나 삭제합니다"
"read:notifications": "알림을 확인합니다"
"write:notifications": "알림을 모두 읽음 처리합니다"
"read:reactions": "리액션을 확인합니다"
Expand Down
17 changes: 17 additions & 0 deletions packages/backend/migration/1699437894737-scheduleNote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class ScheduleNote1699437894737 {
name = 'ScheduleNote1699437894737'

async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "note_schedule" ("id" character varying(32) NOT NULL, "note" jsonb NOT NULL, "userId" character varying(260) NOT NULL, "scheduledAt" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_3a1ae2db41988f4994268218436" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_e798958c40009bf0cdef4f28b5" ON "note_schedule" ("userId") `);
}

async down(queryRunner) {
await queryRunner.query(`DROP TABLE "note_schedule"`);
}
}
12 changes: 12 additions & 0 deletions packages/backend/src/core/QueueModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
RelationshipJobData,
UserWebhookDeliverJobData,
SystemWebhookDeliverJobData,
ScheduleNotePostJobData,
} from '../queue/types.js';
import type { Provider } from '@nestjs/common';

Expand All @@ -29,6 +30,7 @@ export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
export type ObjectStorageQueue = Bull.Queue;
export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>;
export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>;
export type ScheduleNotePostQueue = Bull.Queue<ScheduleNotePostJobData>;

const $system: Provider = {
provide: 'queue:system',
Expand Down Expand Up @@ -90,6 +92,12 @@ const $systemWebhookDeliver: Provider = {
inject: [DI.config],
};

const $scheduleNotePost: Provider = {
provide: 'queue:scheduleNotePost',
useFactory: (config: Config) => new Bull.Queue(QUEUE.SCHEDULE_NOTE_POST, baseQueueOptions(config, QUEUE.SCHEDULE_NOTE_POST)),
inject: [DI.config],
};

@Module({
imports: [
],
Expand All @@ -104,6 +112,7 @@ const $systemWebhookDeliver: Provider = {
$objectStorage,
$userWebhookDeliver,
$systemWebhookDeliver,
$scheduleNotePost,
],
exports: [
$system,
Expand All @@ -116,6 +125,7 @@ const $systemWebhookDeliver: Provider = {
$objectStorage,
$userWebhookDeliver,
$systemWebhookDeliver,
$scheduleNotePost,
],
})
export class QueueModule implements OnApplicationShutdown {
Expand All @@ -130,6 +140,7 @@ export class QueueModule implements OnApplicationShutdown {
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
@Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue,
) {}

public async dispose(): Promise<void> {
Expand All @@ -147,6 +158,7 @@ export class QueueModule implements OnApplicationShutdown {
this.objectStorageQueue.close(),
this.userWebhookDeliverQueue.close(),
this.systemWebhookDeliverQueue.close(),
this.scheduleNotePostQueue.close(),
]);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/core/QueueService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
SystemQueue,
UserWebhookDeliverQueue,
SystemWebhookDeliverQueue,
ScheduleNotePostQueue,
} from './QueueModule.js';
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
Expand All @@ -52,6 +53,7 @@ export class QueueService {
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
@Inject('queue:scheduleNotePost') public ScheduleNotePostQueue: ScheduleNotePostQueue,
) {
this.systemQueue.add('tickCharts', {
}, {
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/core/RoleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;
canPublicNote: boolean;
scheduleNoteMax: number;
mentionLimit: number;
canInvite: boolean;
inviteLimit: number;
Expand Down Expand Up @@ -69,6 +70,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true,
ltlAvailable: true,
canPublicNote: true,
scheduleNoteMax: 15,
mentionLimit: 20,
canInvite: false,
inviteLimit: 0,
Expand Down Expand Up @@ -423,6 +425,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
scheduleNoteMax: calc('scheduleNoteMax', vs => Math.max(...vs)),
mentionLimit: calc('mentionLimit', vs => Math.max(...vs)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { OnModuleInit } from '@nestjs/common';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';

const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]);
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'scheduledNotePosted'] as (typeof groupedNotificationTypes[number])[]);

@Injectable()
export class NotificationEntityService implements OnModuleInit {
Expand Down Expand Up @@ -190,6 +190,9 @@ export class NotificationEntityService implements OnModuleInit {
exportedEntity: notification.exportedEntity,
fileId: notification.fileId,
} : {}),
...(notification.type === 'scheduledNoteFailed' ? {
reason: notification.reason,
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader,
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/di-symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,6 @@ export const DI = {
userMemosRepository: Symbol('userMemosRepository'),
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
reversiGamesRepository: Symbol('reversiGamesRepository'),
noteScheduleRepository: Symbol('noteScheduleRepository'),
//#endregion
};
58 changes: 58 additions & 0 deletions packages/backend/src/models/NoteSchedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Entity, Index, Column, PrimaryColumn } from 'typeorm';
import { MiNote } from '@/models/Note.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiChannel } from './Channel.js';
import type { MiDriveFile } from './DriveFile.js';

type MinimumUser = {
id: MiUser['id'];
host: MiUser['host'];
username: MiUser['username'];
uri: MiUser['uri'];
};

export type MiScheduleNoteType={
visibility: 'public' | 'home' | 'followers' | 'specified';
visibleUsers: MinimumUser[];
channel?: MiChannel['id'];
poll: {
multiple: boolean;
choices: string[];
/** Date.toISOString() */
expiresAt: string | null
} | undefined;
renote?: MiNote['id'];
localOnly: boolean;
cw?: string | null;
reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
files: MiDriveFile['id'][];
text?: string | null;
reply?: MiNote['id'];
apMentions?: MinimumUser[] | null;
apHashtags?: string[] | null;
apEmojis?: string[] | null;
}

@Entity('note_schedule')
export class MiNoteSchedule {
@PrimaryColumn(id())
public id: string;

@Column('jsonb')
public note: MiScheduleNoteType;

@Index()
@Column('varchar', {
length: 260,
})
public userId: MiUser['id'];

@Column('timestamp with time zone')
public scheduledAt: Date;
}
10 changes: 10 additions & 0 deletions packages/backend/src/models/Notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ export type MiNotification = {
type: 'test';
id: string;
createdAt: string;
} | {
type: 'scheduledNoteFailed';
id: string;
createdAt: string;
reason: string;
} | {
type: 'scheduledNotePosted';
id: string;
createdAt: string;
noteId: MiNote['id'];
};

export type MiGroupedNotification = MiNotification | {
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/models/RepositoryModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
MiNote,
MiNoteFavorite,
MiNoteReaction,
MiNoteSchedule,
MiNoteThreadMuting,
MiNoteUnread,
MiPage,
Expand Down Expand Up @@ -503,6 +504,12 @@ const $reversiGamesRepository: Provider = {
inject: [DI.db],
};

const $noteScheduleRepository: Provider = {
provide: DI.noteScheduleRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteSchedule).extend(miRepository as MiRepository<MiNoteSchedule>),
inject: [DI.db],
};

@Module({
imports: [],
providers: [
Expand Down Expand Up @@ -576,6 +583,7 @@ const $reversiGamesRepository: Provider = {
$userMemosRepository,
$bubbleGameRecordsRepository,
$reversiGamesRepository,
$noteScheduleRepository,
],
exports: [
$usersRepository,
Expand Down Expand Up @@ -648,6 +656,7 @@ const $reversiGamesRepository: Provider = {
$userMemosRepository,
$bubbleGameRecordsRepository,
$reversiGamesRepository,
$noteScheduleRepository,
],
})
export class RepositoryModule {
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/models/_.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js';
import { MiNoteSchedule } from '@/models/NoteSchedule.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';

export interface MiRepository<T extends ObjectLiteral> {
Expand Down Expand Up @@ -159,6 +160,7 @@ export {
MiNote,
MiNoteFavorite,
MiNoteReaction,
MiNoteSchedule,
MiNoteThreadMuting,
MiNoteUnread,
MiPage,
Expand Down Expand Up @@ -268,3 +270,4 @@ export type FlashLikesRepository = Repository<MiFlashLike> & MiRepository<MiFlas
export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMemo>;
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
export type NoteScheduleRepository = Repository<MiNoteSchedule>;
Loading

0 comments on commit ed8a511

Please sign in to comment.