1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-12-12 05:38:55 +09:00

Revert "drop group (#9942)"

This reverts commit 8caf288ac1.
This commit is contained in:
NoriDev 2023-05-11 21:38:13 +09:00
parent 520b072c09
commit 8373e6642d
52 changed files with 1585 additions and 66 deletions

View File

@ -430,15 +430,24 @@ markAsReadAllTalkMessages: "すべてのチャットを既読にする"
help: "ヘルプ" help: "ヘルプ"
inputMessageHere: "ここにメッセージを入力" inputMessageHere: "ここにメッセージを入力"
close: "閉じる" close: "閉じる"
group: "グループ"
groups: "グループ"
createGroup: "グループを作成"
ownedGroups: "所有グループ"
joinedGroups: "参加しているグループ"
invites: "招待" invites: "招待"
groupName: "グループ名"
members: "メンバー" members: "メンバー"
transfer: "譲渡" transfer: "譲渡"
messagingWithUser: "ユーザーとチャット"
messagingWithGroup: "グループでチャット"
title: "タイトル" title: "タイトル"
text: "テキスト" text: "テキスト"
enable: "有効にする" enable: "有効にする"
next: "次" next: "次"
retype: "再入力" retype: "再入力"
noteOf: "{user}のノート" noteOf: "{user}のノート"
inviteToGroup: "グループに招待"
quoteAttached: "引用付き" quoteAttached: "引用付き"
quoteQuestion: "引用として添付しますか?" quoteQuestion: "引用として添付しますか?"
noMessagesYet: "まだチャットはありません" noMessagesYet: "まだチャットはありません"
@ -464,10 +473,13 @@ tapSecurityKey: "セキュリティキーにタッチ"
or: "もしくは" or: "もしくは"
language: "言語" language: "言語"
uiLanguage: "UIの表示言語" uiLanguage: "UIの表示言語"
groupInvited: "グループに招待されました"
aboutX: "{x}について" aboutX: "{x}について"
emojiStyle: "絵文字のスタイル" emojiStyle: "絵文字のスタイル"
native: "ネイティブ" native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示しない" disableDrawer: "メニューをドロワーで表示しない"
youHaveNoGroups: "グループがありません"
joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。"
noHistory: "履歴はありません" noHistory: "履歴はありません"
signinHistory: "ログイン履歴" signinHistory: "ログイン履歴"
enableAdvancedMfm: "高度なMFMを有効にする" enableAdvancedMfm: "高度なMFMを有効にする"
@ -842,6 +854,8 @@ deleteAccountConfirm: "アカウントが削除されます。よろしいです
incorrectPassword: "パスワードが間違っています。" incorrectPassword: "パスワードが間違っています。"
voteConfirm: "「{choice}」に投票しますか?" voteConfirm: "「{choice}」に投票しますか?"
hide: "隠す" hide: "隠す"
leaveGroup: "グループから抜ける"
leaveGroupConfirm: "「{name}」から抜けますか?"
useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示" useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示"
welcomeBackWithName: "おかえりなさい、{name}さん" welcomeBackWithName: "おかえりなさい、{name}さん"
clickToFinishEmailVerification: "[{ok}]を押して、メールアドレスの確認を完了してください。" clickToFinishEmailVerification: "[{ok}]を押して、メールアドレスの確認を完了してください。"
@ -1673,6 +1687,7 @@ _antennaSources:
homeTimeline: "フォローしているユーザーのノート" homeTimeline: "フォローしているユーザーのノート"
users: "指定した一人または複数のユーザーのノート" users: "指定した一人または複数のユーザーのノート"
userList: "指定したリストのユーザーのノート" userList: "指定したリストのユーザーのノート"
userGroup: "指定したグループのユーザーのノート"
_weekday: _weekday:
sunday: "日曜日" sunday: "日曜日"
@ -1902,9 +1917,12 @@ _notification:
youGotReply: "{name}からのリプライ" youGotReply: "{name}からのリプライ"
youGotQuote: "{name}による引用" youGotQuote: "{name}による引用"
youRenoted: "{name}がRenoteしました" youRenoted: "{name}がRenoteしました"
youGotMessagingMessageFromUser: "{name}からのチャットがあります"
youGotMessagingMessageFromGroup: "{name}のチャットがあります"
youWereFollowed: "フォローされました" youWereFollowed: "フォローされました"
youReceivedFollowRequest: "フォローリクエストが来ました" youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました" yourFollowRequestAccepted: "フォローリクエストが承認されました"
youWereInvitedToGroup: "{userName}があなたをグループに招待しました"
pollEnded: "アンケートの結果が出ました" pollEnded: "アンケートの結果が出ました"
unreadAntennaNote: "アンテナ {name}" unreadAntennaNote: "アンテナ {name}"
emptyPushNotificationMessage: "プッシュ通知の更新をしました" emptyPushNotificationMessage: "プッシュ通知の更新をしました"
@ -1921,6 +1939,7 @@ _notification:
pollEnded: "アンケートが終了" pollEnded: "アンケートが終了"
receiveFollowRequest: "フォロー申請を受け取った" receiveFollowRequest: "フォロー申請を受け取った"
followRequestAccepted: "フォローが受理された" followRequestAccepted: "フォローが受理された"
groupInvited: "グループに招待された"
app: "連携アプリからの通知" app: "連携アプリからの通知"
_actions: _actions:

View File

@ -1,47 +0,0 @@
export class dropGroup1676434944993 {
name = 'dropGroup1676434944993'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "antenna" DROP CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb"`);
await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_8fe87814e978053a53b1beb7e98"`);
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "userGroupJoiningId"`);
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "userGroupInvitationId"`);
await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum" RENAME TO "antenna_src_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."antenna_src_enum" AS ENUM('home', 'all', 'users', 'list')`);
await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "public"."antenna_src_enum" USING "src"::"text"::"public"."antenna_src_enum"`);
await queryRunner.query(`DROP TYPE "public"."antenna_src_enum_old"`);
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app')`);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "emailNotificationTypes" SET DEFAULT '["follow","receiveFollowRequest"]'`);
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app')`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "emailNotificationTypes" SET DEFAULT '["follow", "receiveFollowRequest", "groupInvited"]'`);
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app')`);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
await queryRunner.query(`CREATE TYPE "public"."antenna_src_enum_old" AS ENUM('home', 'all', 'users', 'list', 'group')`);
await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "public"."antenna_src_enum_old" USING "src"::"text"::"public"."antenna_src_enum_old"`);
await queryRunner.query(`DROP TYPE "public"."antenna_src_enum"`);
await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum_old" RENAME TO "antenna_src_enum"`);
await queryRunner.query(`ALTER TABLE "notification" ADD "userGroupInvitationId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "antenna" ADD "userGroupJoiningId" character varying(32)`);
await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_8fe87814e978053a53b1beb7e98" FOREIGN KEY ("userGroupInvitationId") REFERENCES "user_group_invitation"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "antenna" ADD CONSTRAINT "FK_ccbf5a8c0be4511133dcc50ddeb" FOREIGN KEY ("userGroupJoiningId") REFERENCES "user_group_joining"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
}

View File

@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/schema.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js'; import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
@ -39,6 +39,9 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.antennasRepository) @Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository, private antennasRepository: AntennasRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
@Inject(DI.userListJoiningsRepository) @Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository, private userListJoiningsRepository: UserListJoiningsRepository,
@ -157,6 +160,14 @@ export class AntennaService implements OnApplicationShutdown {
})).map(x => x.userId); })).map(x => x.userId);
if (!listUsers.includes(note.userId)) return false; if (!listUsers.includes(note.userId)) return false;
} else if (antenna.src === 'group') {
const joining = await this.userGroupJoiningsRepository.findOneByOrFail({ id: antenna.userGroupJoiningId! });
const groupUsers = (await this.userGroupJoiningsRepository.findBy({
userGroupId: joining.userGroupId,
})).map(x => x.userId);
if (!groupUsers.includes(note.userId)) return false;
} else if (antenna.src === 'users') { } else if (antenna.src === 'users') {
const accts = antenna.users.map(x => { const accts = antenna.users.map(x => {
const { username, host } = Acct.parse(x); const { username, host } = Acct.parse(x);

View File

@ -91,6 +91,8 @@ import { PageEntityService } from './entities/PageEntityService.js';
import { PageLikeEntityService } from './entities/PageLikeEntityService.js'; import { PageLikeEntityService } from './entities/PageLikeEntityService.js';
import { SigninEntityService } from './entities/SigninEntityService.js'; import { SigninEntityService } from './entities/SigninEntityService.js';
import { UserEntityService } from './entities/UserEntityService.js'; import { UserEntityService } from './entities/UserEntityService.js';
import { UserGroupEntityService } from './entities/UserGroupEntityService.js';
import { UserGroupInvitationEntityService } from './entities/UserGroupInvitationEntityService.js';
import { UserListEntityService } from './entities/UserListEntityService.js'; import { UserListEntityService } from './entities/UserListEntityService.js';
import { FlashEntityService } from './entities/FlashEntityService.js'; import { FlashEntityService } from './entities/FlashEntityService.js';
import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js'; import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
@ -212,6 +214,8 @@ const $PageEntityService: Provider = { provide: 'PageEntityService', useExisting
const $PageLikeEntityService: Provider = { provide: 'PageLikeEntityService', useExisting: PageLikeEntityService }; const $PageLikeEntityService: Provider = { provide: 'PageLikeEntityService', useExisting: PageLikeEntityService };
const $SigninEntityService: Provider = { provide: 'SigninEntityService', useExisting: SigninEntityService }; const $SigninEntityService: Provider = { provide: 'SigninEntityService', useExisting: SigninEntityService };
const $UserEntityService: Provider = { provide: 'UserEntityService', useExisting: UserEntityService }; const $UserEntityService: Provider = { provide: 'UserEntityService', useExisting: UserEntityService };
const $UserGroupEntityService: Provider = { provide: 'UserGroupEntityService', useExisting: UserGroupEntityService };
const $UserGroupInvitationEntityService: Provider = { provide: 'UserGroupInvitationEntityService', useExisting: UserGroupInvitationEntityService };
const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService }; const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService };
const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService }; const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService };
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
@ -334,6 +338,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PageLikeEntityService, PageLikeEntityService,
SigninEntityService, SigninEntityService,
UserEntityService, UserEntityService,
UserGroupEntityService,
UserGroupInvitationEntityService,
UserListEntityService, UserListEntityService,
FlashEntityService, FlashEntityService,
FlashLikeEntityService, FlashLikeEntityService,
@ -451,6 +457,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PageLikeEntityService, $PageLikeEntityService,
$SigninEntityService, $SigninEntityService,
$UserEntityService, $UserEntityService,
$UserGroupEntityService,
$UserGroupInvitationEntityService,
$UserListEntityService, $UserListEntityService,
$FlashEntityService, $FlashEntityService,
$FlashLikeEntityService, $FlashLikeEntityService,
@ -568,6 +576,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PageLikeEntityService, PageLikeEntityService,
SigninEntityService, SigninEntityService,
UserEntityService, UserEntityService,
UserGroupEntityService,
UserGroupInvitationEntityService,
UserListEntityService, UserListEntityService,
FlashEntityService, FlashEntityService,
FlashLikeEntityService, FlashLikeEntityService,
@ -684,6 +694,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PageLikeEntityService, $PageLikeEntityService,
$SigninEntityService, $SigninEntityService,
$UserEntityService, $UserEntityService,
$UserGroupEntityService,
$UserGroupInvitationEntityService,
$UserListEntityService, $UserListEntityService,
$FlashEntityService, $FlashEntityService,
$FlashLikeEntityService, $FlashLikeEntityService,

View File

@ -3,6 +3,7 @@ import Redis from 'ioredis';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import type { UserList } from '@/models/entities/UserList.js'; import type { UserList } from '@/models/entities/UserList.js';
import type { UserGroup } from '@/models/entities/UserGroup.js';
import type { Antenna } from '@/models/entities/Antenna.js'; import type { Antenna } from '@/models/entities/Antenna.js';
import type { Channel } from '@/models/entities/Channel.js'; import type { Channel } from '@/models/entities/Channel.js';
import type { import type {

View File

@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { AntennaNotesRepository, AntennasRepository } from '@/models/index.js'; import type { AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/schema.js';
import type { Antenna } from '@/models/entities/Antenna.js'; import type { Antenna } from '@/models/entities/Antenna.js';
@ -14,6 +14,9 @@ export class AntennaEntityService {
@Inject(DI.antennaNotesRepository) @Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository, private antennaNotesRepository: AntennaNotesRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
) { ) {
} }
@ -24,6 +27,7 @@ export class AntennaEntityService {
const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src }); const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src });
const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null; const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null;
const userGroupJoining = antenna.userGroupJoiningId ? await this.userGroupJoiningsRepository.findOneBy({ id: antenna.userGroupJoiningId }) : null;
return { return {
id: antenna.id, id: antenna.id,
@ -33,6 +37,7 @@ export class AntennaEntityService {
excludeKeywords: antenna.excludeKeywords, excludeKeywords: antenna.excludeKeywords,
src: antenna.src, src: antenna.src,
userListId: antenna.userListId, userListId: antenna.userListId,
userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null,
users: antenna.users, users: antenna.users,
caseSensitive: antenna.caseSensitive, caseSensitive: antenna.caseSensitive,
notify: antenna.notify, notify: antenna.notify,

View File

@ -13,11 +13,13 @@ import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
import type { UserGroupInvitationEntityService } from './UserGroupInvitationEntityService.js';
@Injectable() @Injectable()
export class NotificationEntityService implements OnModuleInit { export class NotificationEntityService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService; private noteEntityService: NoteEntityService;
private userGroupInvitationEntityService: UserGroupInvitationEntityService;
private customEmojiService: CustomEmojiService; private customEmojiService: CustomEmojiService;
constructor( constructor(
@ -34,6 +36,7 @@ export class NotificationEntityService implements OnModuleInit {
//private userEntityService: UserEntityService, //private userEntityService: UserEntityService,
//private noteEntityService: NoteEntityService, //private noteEntityService: NoteEntityService,
//private userGroupInvitationEntityService: UserGroupInvitationEntityService,
//private customEmojiService: CustomEmojiService, //private customEmojiService: CustomEmojiService,
) { ) {
} }
@ -41,6 +44,7 @@ export class NotificationEntityService implements OnModuleInit {
onModuleInit() { onModuleInit() {
this.userEntityService = this.moduleRef.get('UserEntityService'); this.userEntityService = this.moduleRef.get('UserEntityService');
this.noteEntityService = this.moduleRef.get('NoteEntityService'); this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.userGroupInvitationEntityService = this.moduleRef.get('UserGroupInvitationEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService');
} }
@ -107,6 +111,9 @@ export class NotificationEntityService implements OnModuleInit {
_hint_: options._hintForEachNotes_, _hint_: options._hintForEachNotes_,
}), }),
} : {}), } : {}),
...(notification.type === 'groupInvited' ? {
invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId!),
} : {}),
...(notification.type === 'achievementEarned' ? { ...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement, achievement: notification.achievement,
} : {}), } : {}),

View File

@ -12,7 +12,7 @@ import { Cache } from '@/misc/cache.js';
import type { Instance } from '@/models/entities/Instance.js'; import type { Instance } from '@/models/entities/Instance.js';
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js'; import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js'; import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js'; import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -102,6 +102,9 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.announcementReadsRepository) @Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository, private announcementReadsRepository: AnnouncementReadsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
@Inject(DI.announcementsRepository) @Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository, private announcementsRepository: AnnouncementsRepository,

View File

@ -0,0 +1,44 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { UserGroupJoiningsRepository, UserGroupsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { UserGroup } from '@/models/entities/UserGroup.js';
import { UserEntityService } from './UserEntityService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class UserGroupEntityService {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private userEntityService: UserEntityService,
) {
}
@bindThis
public async pack(
src: UserGroup['id'] | UserGroup,
): Promise<Packed<'UserGroup'>> {
const userGroup = typeof src === 'object' ? src : await this.userGroupsRepository.findOneByOrFail({ id: src });
const users = await this.userGroupJoiningsRepository.findBy({
userGroupId: userGroup.id,
});
return {
id: userGroup.id,
createdAt: userGroup.createdAt.toISOString(),
name: userGroup.name,
ownerId: userGroup.userId,
userIds: users.map(x => x.userId),
};
}
}

View File

@ -0,0 +1,42 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { UserGroupInvitationsRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/schema.js';
import type { } from '@/models/entities/Blocking.js';
import type { User } from '@/models/entities/User.js';
import type { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js';
import { UserEntityService } from './UserEntityService.js';
import { UserGroupEntityService } from './UserGroupEntityService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class UserGroupInvitationEntityService {
constructor(
@Inject(DI.userGroupInvitationsRepository)
private userGroupInvitationsRepository: UserGroupInvitationsRepository,
private userGroupEntityService: UserGroupEntityService,
) {
}
@bindThis
public async pack(
src: UserGroupInvitation['id'] | UserGroupInvitation,
) {
const invitation = typeof src === 'object' ? src : await this.userGroupInvitationsRepository.findOneByOrFail({ id: src });
return {
id: invitation.id,
group: await this.userGroupEntityService.pack(invitation.userGroup ?? invitation.userGroupId),
};
}
@bindThis
public packMany(
invitations: any[],
) {
return Promise.all(invitations.map(x => this.pack(x)));
}
}

View File

@ -24,6 +24,9 @@ export const DI = {
userPublickeysRepository: Symbol('userPublickeysRepository'), userPublickeysRepository: Symbol('userPublickeysRepository'),
userListsRepository: Symbol('userListsRepository'), userListsRepository: Symbol('userListsRepository'),
userListJoiningsRepository: Symbol('userListJoiningsRepository'), userListJoiningsRepository: Symbol('userListJoiningsRepository'),
userGroupsRepository: Symbol('userGroupsRepository'),
userGroupJoiningsRepository: Symbol('userGroupJoiningsRepository'),
userGroupInvitationsRepository: Symbol('userGroupInvitationsRepository'),
userNotePiningsRepository: Symbol('userNotePiningsRepository'), userNotePiningsRepository: Symbol('userNotePiningsRepository'),
userIpsRepository: Symbol('userIpsRepository'), userIpsRepository: Symbol('userIpsRepository'),
usedUsernamesRepository: Symbol('usedUsernamesRepository'), usedUsernamesRepository: Symbol('usedUsernamesRepository'),

View File

@ -19,6 +19,7 @@ import { packedBlockingSchema } from '@/models/schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js'; import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/schema/hashtag.js'; import { packedHashtagSchema } from '@/models/schema/hashtag.js';
import { packedPageSchema } from '@/models/schema/page.js'; import { packedPageSchema } from '@/models/schema/page.js';
import { packedUserGroupSchema } from '@/models/schema/user-group.js';
import { packedNoteFavoriteSchema } from '@/models/schema/note-favorite.js'; import { packedNoteFavoriteSchema } from '@/models/schema/note-favorite.js';
import { packedChannelSchema } from '@/models/schema/channel.js'; import { packedChannelSchema } from '@/models/schema/channel.js';
import { packedAntennaSchema } from '@/models/schema/antenna.js'; import { packedAntennaSchema } from '@/models/schema/antenna.js';
@ -38,6 +39,7 @@ export const refs = {
User: packedUserSchema, User: packedUserSchema,
UserList: packedUserListSchema, UserList: packedUserListSchema,
UserGroup: packedUserGroupSchema,
App: packedAppSchema, App: packedAppSchema,
Note: packedNoteSchema, Note: packedNoteSchema,
NoteReaction: packedNoteReactionSchema, NoteReaction: packedNoteReactionSchema,

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js'; import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js';
import type { DataSource } from 'typeorm'; import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
@ -118,6 +118,24 @@ const $userListJoiningsRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
const $userGroupsRepository: Provider = {
provide: DI.userGroupsRepository,
useFactory: (db: DataSource) => db.getRepository(UserGroup),
inject: [DI.db],
};
const $userGroupJoiningsRepository: Provider = {
provide: DI.userGroupJoiningsRepository,
useFactory: (db: DataSource) => db.getRepository(UserGroupJoining),
inject: [DI.db],
};
const $userGroupInvitationsRepository: Provider = {
provide: DI.userGroupInvitationsRepository,
useFactory: (db: DataSource) => db.getRepository(UserGroupInvitation),
inject: [DI.db],
};
const $userNotePiningsRepository: Provider = { const $userNotePiningsRepository: Provider = {
provide: DI.userNotePiningsRepository, provide: DI.userNotePiningsRepository,
useFactory: (db: DataSource) => db.getRepository(UserNotePining), useFactory: (db: DataSource) => db.getRepository(UserNotePining),
@ -411,6 +429,9 @@ const $roleAssignmentsRepository: Provider = {
$userPublickeysRepository, $userPublickeysRepository,
$userListsRepository, $userListsRepository,
$userListJoiningsRepository, $userListJoiningsRepository,
$userGroupsRepository,
$userGroupJoiningsRepository,
$userGroupInvitationsRepository,
$userNotePiningsRepository, $userNotePiningsRepository,
$userIpsRepository, $userIpsRepository,
$usedUsernamesRepository, $usedUsernamesRepository,
@ -477,6 +498,9 @@ const $roleAssignmentsRepository: Provider = {
$userPublickeysRepository, $userPublickeysRepository,
$userListsRepository, $userListsRepository,
$userListJoiningsRepository, $userListJoiningsRepository,
$userGroupsRepository,
$userGroupJoiningsRepository,
$userGroupInvitationsRepository,
$userNotePiningsRepository, $userNotePiningsRepository,
$userIpsRepository, $userIpsRepository,
$usedUsernamesRepository, $usedUsernamesRepository,

View File

@ -2,6 +2,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
import { id } from '../id.js'; import { id } from '../id.js';
import { User } from './User.js'; import { User } from './User.js';
import { UserList } from './UserList.js'; import { UserList } from './UserList.js';
import { UserGroupJoining } from './UserGroupJoining.js';
@Entity() @Entity()
export class Antenna { export class Antenna {
@ -32,8 +33,8 @@ export class Antenna {
}) })
public name: string; public name: string;
@Column('enum', { enum: ['home', 'all', 'users', 'list'] }) @Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] })
public src: 'home' | 'all' | 'users' | 'list'; public src: 'home' | 'all' | 'users' | 'list' | 'group';
@Column({ @Column({
...id(), ...id(),
@ -47,6 +48,18 @@ export class Antenna {
@JoinColumn() @JoinColumn()
public userList: UserList | null; public userList: UserList | null;
@Column({
...id(),
nullable: true,
})
public userGroupJoiningId: UserGroupJoining['id'] | null;
@ManyToOne(type => UserGroupJoining, {
onDelete: 'CASCADE',
})
@JoinColumn()
public userGroupJoining: UserGroupJoining | null;
@Column('varchar', { @Column('varchar', {
length: 1024, array: true, length: 1024, array: true,
default: '{}', default: '{}',

View File

@ -4,6 +4,7 @@ import { id } from '../id.js';
import { User } from './User.js'; import { User } from './User.js';
import { Note } from './Note.js'; import { Note } from './Note.js';
import { FollowRequest } from './FollowRequest.js'; import { FollowRequest } from './FollowRequest.js';
import { UserGroupInvitation } from './UserGroupInvitation.js';
import { AccessToken } from './AccessToken.js'; import { AccessToken } from './AccessToken.js';
@Entity() @Entity()
@ -62,6 +63,7 @@ export class Notification {
* pollEnded - * pollEnded -
* receiveFollowRequest - * receiveFollowRequest -
* followRequestAccepted - * followRequestAccepted -
* groupInvited -
* achievementEarned - * achievementEarned -
* app - * app -
*/ */
@ -106,6 +108,18 @@ export class Notification {
@JoinColumn() @JoinColumn()
public followRequest: FollowRequest | null; public followRequest: FollowRequest | null;
@Column({
...id(),
nullable: true,
})
public userGroupInvitationId: UserGroupInvitation['id'] | null;
@ManyToOne(type => UserGroupInvitation, {
onDelete: 'CASCADE',
})
@JoinColumn()
public userGroupInvitation: UserGroupInvitation | null;
@Column('varchar', { @Column('varchar', {
length: 128, nullable: true, length: 128, nullable: true,
}) })

View File

@ -0,0 +1,46 @@
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { id } from '../id.js';
import { User } from './User.js';
@Entity()
export class UserGroup {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the UserGroup.',
})
public createdAt: Date;
@Column('varchar', {
length: 256,
})
public name: string;
@Index()
@Column({
...id(),
comment: 'The ID of owner.',
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: User | null;
@Column('boolean', {
default: false,
})
public isPrivate: boolean;
constructor(data: Partial<UserGroup>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,42 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from '../id.js';
import { User } from './User.js';
import { UserGroup } from './UserGroup.js';
@Entity()
@Index(['userId', 'userGroupId'], { unique: true })
export class UserGroupInvitation {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the UserGroupInvitation.',
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The user ID.',
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: User | null;
@Index()
@Column({
...id(),
comment: 'The group ID.',
})
public userGroupId: UserGroup['id'];
@ManyToOne(type => UserGroup, {
onDelete: 'CASCADE',
})
@JoinColumn()
public userGroup: UserGroup | null;
}

View File

@ -0,0 +1,42 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from '../id.js';
import { User } from './User.js';
import { UserGroup } from './UserGroup.js';
@Entity()
@Index(['userId', 'userGroupId'], { unique: true })
export class UserGroupJoining {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the UserGroupJoining.',
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The user ID.',
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: User | null;
@Index()
@Column({
...id(),
comment: 'The group ID.',
})
public userGroupId: UserGroup['id'];
@ManyToOne(type => UserGroup, {
onDelete: 'CASCADE',
})
@JoinColumn()
public userGroup: UserGroup | null;
}

View File

@ -71,7 +71,7 @@ export class UserProfile {
public emailVerified: boolean; public emailVerified: boolean;
@Column('jsonb', { @Column('jsonb', {
default: ['follow', 'receiveFollowRequest'], default: ['follow', 'receiveFollowRequest', 'groupInvited'],
}) })
public emailNotificationTypes: string[]; public emailNotificationTypes: string[];

View File

@ -46,6 +46,9 @@ import { Signin } from '@/models/entities/Signin.js';
import { SwSubscription } from '@/models/entities/SwSubscription.js'; import { SwSubscription } from '@/models/entities/SwSubscription.js';
import { UsedUsername } from '@/models/entities/UsedUsername.js'; import { UsedUsername } from '@/models/entities/UsedUsername.js';
import { User } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js';
import { UserGroup } from '@/models/entities/UserGroup.js';
import { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js';
import { UserGroupJoining } from '@/models/entities/UserGroupJoining.js';
import { UserIp } from '@/models/entities/UserIp.js'; import { UserIp } from '@/models/entities/UserIp.js';
import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UserKeypair } from '@/models/entities/UserKeypair.js';
import { UserList } from '@/models/entities/UserList.js'; import { UserList } from '@/models/entities/UserList.js';
@ -113,6 +116,9 @@ export {
SwSubscription, SwSubscription,
UsedUsername, UsedUsername,
User, User,
UserGroup,
UserGroupInvitation,
UserGroupJoining,
UserIp, UserIp,
UserKeypair, UserKeypair,
UserList, UserList,
@ -179,6 +185,9 @@ export type SigninsRepository = Repository<Signin>;
export type SwSubscriptionsRepository = Repository<SwSubscription>; export type SwSubscriptionsRepository = Repository<SwSubscription>;
export type UsedUsernamesRepository = Repository<UsedUsername>; export type UsedUsernamesRepository = Repository<UsedUsername>;
export type UsersRepository = Repository<User>; export type UsersRepository = Repository<User>;
export type UserGroupsRepository = Repository<UserGroup>;
export type UserGroupInvitationsRepository = Repository<UserGroupInvitation>;
export type UserGroupJoiningsRepository = Repository<UserGroupJoining>;
export type UserIpsRepository = Repository<UserIp>; export type UserIpsRepository = Repository<UserIp>;
export type UserKeypairsRepository = Repository<UserKeypair>; export type UserKeypairsRepository = Repository<UserKeypair>;
export type UserListsRepository = Repository<UserList>; export type UserListsRepository = Repository<UserList>;

View File

@ -42,13 +42,18 @@ export const packedAntennaSchema = {
src: { src: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
enum: ['home', 'all', 'users', 'list'], enum: ['home', 'all', 'users', 'list', 'group'],
}, },
userListId: { userListId: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
format: 'id', format: 'id',
}, },
userGroupId: {
type: 'string',
optional: false, nullable: true,
format: 'id',
},
users: { users: {
type: 'array', type: 'array',
optional: false, nullable: false, optional: false, nullable: false,

View File

@ -0,0 +1,34 @@
export const packedUserGroupSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
name: {
type: 'string',
optional: false, nullable: false,
},
ownerId: {
type: 'string',
nullable: false, optional: false,
format: 'id',
},
userIds: {
type: 'array',
nullable: false, optional: true,
items: {
type: 'string',
nullable: false, optional: false,
format: 'id',
},
},
},
} as const;

View File

@ -54,6 +54,9 @@ import { Signin } from '@/models/entities/Signin.js';
import { SwSubscription } from '@/models/entities/SwSubscription.js'; import { SwSubscription } from '@/models/entities/SwSubscription.js';
import { UsedUsername } from '@/models/entities/UsedUsername.js'; import { UsedUsername } from '@/models/entities/UsedUsername.js';
import { User } from '@/models/entities/User.js'; import { User } from '@/models/entities/User.js';
import { UserGroup } from '@/models/entities/UserGroup.js';
import { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js';
import { UserGroupJoining } from '@/models/entities/UserGroupJoining.js';
import { UserIp } from '@/models/entities/UserIp.js'; import { UserIp } from '@/models/entities/UserIp.js';
import { UserKeypair } from '@/models/entities/UserKeypair.js'; import { UserKeypair } from '@/models/entities/UserKeypair.js';
import { UserList } from '@/models/entities/UserList.js'; import { UserList } from '@/models/entities/UserList.js';
@ -133,6 +136,9 @@ export const entities = [
UserPublickey, UserPublickey,
UserList, UserList,
UserListJoining, UserListJoining,
UserGroup,
UserGroupJoining,
UserGroupInvitation,
UserNotePining, UserNotePining,
UserSecurityKey, UserSecurityKey,
UsedUsername, UsedUsername,

View File

@ -211,6 +211,7 @@ import * as ep___i_signinHistory from './endpoints/i/signin-history.js';
import * as ep___i_unpin from './endpoints/i/unpin.js'; import * as ep___i_unpin from './endpoints/i/unpin.js';
import * as ep___i_updateEmail from './endpoints/i/update-email.js'; import * as ep___i_updateEmail from './endpoints/i/update-email.js';
import * as ep___i_update from './endpoints/i/update.js'; import * as ep___i_update from './endpoints/i/update.js';
import * as ep___i_userGroupInvites from './endpoints/i/user-group-invites.js';
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
@ -293,6 +294,18 @@ import * as ep___users_followers from './endpoints/users/followers.js';
import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_following from './endpoints/users/following.js';
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
import * as ep___users_groups_create from './endpoints/users/groups/create.js';
import * as ep___users_groups_delete from './endpoints/users/groups/delete.js';
import * as ep___users_groups_invitations_accept from './endpoints/users/groups/invitations/accept.js';
import * as ep___users_groups_invitations_reject from './endpoints/users/groups/invitations/reject.js';
import * as ep___users_groups_invite from './endpoints/users/groups/invite.js';
import * as ep___users_groups_joined from './endpoints/users/groups/joined.js';
import * as ep___users_groups_leave from './endpoints/users/groups/leave.js';
import * as ep___users_groups_owned from './endpoints/users/groups/owned.js';
import * as ep___users_groups_pull from './endpoints/users/groups/pull.js';
import * as ep___users_groups_show from './endpoints/users/groups/show.js';
import * as ep___users_groups_transfer from './endpoints/users/groups/transfer.js';
import * as ep___users_groups_update from './endpoints/users/groups/update.js';
import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js';
import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; import * as ep___users_lists_delete from './endpoints/users/lists/delete.js';
import * as ep___users_lists_list from './endpoints/users/lists/list.js'; import * as ep___users_lists_list from './endpoints/users/lists/list.js';
@ -527,6 +540,7 @@ const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: e
const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.default }; const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.default };
const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default }; const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default };
const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default }; const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default };
const $i_userGroupInvites: Provider = { provide: 'ep:i/user-group-invites', useClass: ep___i_userGroupInvites.default };
const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default }; const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default };
const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default }; const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default };
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default }; const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
@ -609,6 +623,18 @@ const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep
const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default }; const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default };
const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default }; const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default };
const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default }; const $users_getFrequentlyRepliedUsers: Provider = { provide: 'ep:users/get-frequently-replied-users', useClass: ep___users_getFrequentlyRepliedUsers.default };
const $users_groups_create: Provider = { provide: 'ep:users/groups/create', useClass: ep___users_groups_create.default };
const $users_groups_delete: Provider = { provide: 'ep:users/groups/delete', useClass: ep___users_groups_delete.default };
const $users_groups_invitations_accept: Provider = { provide: 'ep:users/groups/invitations/accept', useClass: ep___users_groups_invitations_accept.default };
const $users_groups_invitations_reject: Provider = { provide: 'ep:users/groups/invitations/reject', useClass: ep___users_groups_invitations_reject.default };
const $users_groups_invite: Provider = { provide: 'ep:users/groups/invite', useClass: ep___users_groups_invite.default };
const $users_groups_joined: Provider = { provide: 'ep:users/groups/joined', useClass: ep___users_groups_joined.default };
const $users_groups_leave: Provider = { provide: 'ep:users/groups/leave', useClass: ep___users_groups_leave.default };
const $users_groups_owned: Provider = { provide: 'ep:users/groups/owned', useClass: ep___users_groups_owned.default };
const $users_groups_pull: Provider = { provide: 'ep:users/groups/pull', useClass: ep___users_groups_pull.default };
const $users_groups_show: Provider = { provide: 'ep:users/groups/show', useClass: ep___users_groups_show.default };
const $users_groups_transfer: Provider = { provide: 'ep:users/groups/transfer', useClass: ep___users_groups_transfer.default };
const $users_groups_update: Provider = { provide: 'ep:users/groups/update', useClass: ep___users_groups_update.default };
const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default }; const $users_lists_create: Provider = { provide: 'ep:users/lists/create', useClass: ep___users_lists_create.default };
const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default }; const $users_lists_delete: Provider = { provide: 'ep:users/lists/delete', useClass: ep___users_lists_delete.default };
const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default }; const $users_lists_list: Provider = { provide: 'ep:users/lists/list', useClass: ep___users_lists_list.default };
@ -847,6 +873,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_unpin, $i_unpin,
$i_updateEmail, $i_updateEmail,
$i_update, $i_update,
$i_userGroupInvites,
$i_webhooks_create, $i_webhooks_create,
$i_webhooks_list, $i_webhooks_list,
$i_webhooks_show, $i_webhooks_show,
@ -929,6 +956,18 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_following, $users_following,
$users_gallery_posts, $users_gallery_posts,
$users_getFrequentlyRepliedUsers, $users_getFrequentlyRepliedUsers,
$users_groups_create,
$users_groups_delete,
$users_groups_invitations_accept,
$users_groups_invitations_reject,
$users_groups_invite,
$users_groups_joined,
$users_groups_leave,
$users_groups_owned,
$users_groups_pull,
$users_groups_show,
$users_groups_transfer,
$users_groups_update,
$users_lists_create, $users_lists_create,
$users_lists_delete, $users_lists_delete,
$users_lists_list, $users_lists_list,
@ -1161,6 +1200,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_unpin, $i_unpin,
$i_updateEmail, $i_updateEmail,
$i_update, $i_update,
$i_userGroupInvites,
$i_webhooks_create, $i_webhooks_create,
$i_webhooks_list, $i_webhooks_list,
$i_webhooks_show, $i_webhooks_show,
@ -1241,6 +1281,18 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_following, $users_following,
$users_gallery_posts, $users_gallery_posts,
$users_getFrequentlyRepliedUsers, $users_getFrequentlyRepliedUsers,
$users_groups_create,
$users_groups_delete,
$users_groups_invitations_accept,
$users_groups_invitations_reject,
$users_groups_invite,
$users_groups_joined,
$users_groups_leave,
$users_groups_owned,
$users_groups_pull,
$users_groups_show,
$users_groups_transfer,
$users_groups_update,
$users_lists_create, $users_lists_create,
$users_lists_delete, $users_lists_delete,
$users_lists_list, $users_lists_list,

View File

@ -210,6 +210,7 @@ import * as ep___i_signinHistory from './endpoints/i/signin-history.js';
import * as ep___i_unpin from './endpoints/i/unpin.js'; import * as ep___i_unpin from './endpoints/i/unpin.js';
import * as ep___i_updateEmail from './endpoints/i/update-email.js'; import * as ep___i_updateEmail from './endpoints/i/update-email.js';
import * as ep___i_update from './endpoints/i/update.js'; import * as ep___i_update from './endpoints/i/update.js';
import * as ep___i_userGroupInvites from './endpoints/i/user-group-invites.js';
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js'; import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
@ -292,6 +293,18 @@ import * as ep___users_followers from './endpoints/users/followers.js';
import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_following from './endpoints/users/following.js';
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js'; import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
import * as ep___users_groups_create from './endpoints/users/groups/create.js';
import * as ep___users_groups_delete from './endpoints/users/groups/delete.js';
import * as ep___users_groups_invitations_accept from './endpoints/users/groups/invitations/accept.js';
import * as ep___users_groups_invitations_reject from './endpoints/users/groups/invitations/reject.js';
import * as ep___users_groups_invite from './endpoints/users/groups/invite.js';
import * as ep___users_groups_joined from './endpoints/users/groups/joined.js';
import * as ep___users_groups_leave from './endpoints/users/groups/leave.js';
import * as ep___users_groups_owned from './endpoints/users/groups/owned.js';
import * as ep___users_groups_pull from './endpoints/users/groups/pull.js';
import * as ep___users_groups_show from './endpoints/users/groups/show.js';
import * as ep___users_groups_transfer from './endpoints/users/groups/transfer.js';
import * as ep___users_groups_update from './endpoints/users/groups/update.js';
import * as ep___users_lists_create from './endpoints/users/lists/create.js'; import * as ep___users_lists_create from './endpoints/users/lists/create.js';
import * as ep___users_lists_delete from './endpoints/users/lists/delete.js'; import * as ep___users_lists_delete from './endpoints/users/lists/delete.js';
import * as ep___users_lists_list from './endpoints/users/lists/list.js'; import * as ep___users_lists_list from './endpoints/users/lists/list.js';
@ -524,6 +537,7 @@ const eps = [
['i/unpin', ep___i_unpin], ['i/unpin', ep___i_unpin],
['i/update-email', ep___i_updateEmail], ['i/update-email', ep___i_updateEmail],
['i/update', ep___i_update], ['i/update', ep___i_update],
['i/user-group-invites', ep___i_userGroupInvites],
['i/webhooks/create', ep___i_webhooks_create], ['i/webhooks/create', ep___i_webhooks_create],
['i/webhooks/list', ep___i_webhooks_list], ['i/webhooks/list', ep___i_webhooks_list],
['i/webhooks/show', ep___i_webhooks_show], ['i/webhooks/show', ep___i_webhooks_show],
@ -606,6 +620,18 @@ const eps = [
['users/following', ep___users_following], ['users/following', ep___users_following],
['users/gallery/posts', ep___users_gallery_posts], ['users/gallery/posts', ep___users_gallery_posts],
['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers], ['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers],
['users/groups/create', ep___users_groups_create],
['users/groups/delete', ep___users_groups_delete],
['users/groups/invitations/accept', ep___users_groups_invitations_accept],
['users/groups/invitations/reject', ep___users_groups_invitations_reject],
['users/groups/invite', ep___users_groups_invite],
['users/groups/joined', ep___users_groups_joined],
['users/groups/leave', ep___users_groups_leave],
['users/groups/owned', ep___users_groups_owned],
['users/groups/pull', ep___users_groups_pull],
['users/groups/show', ep___users_groups_show],
['users/groups/transfer', ep___users_groups_transfer],
['users/groups/update', ep___users_groups_update],
['users/lists/create', ep___users_lists_create], ['users/lists/create', ep___users_lists_create],
['users/lists/delete', ep___users_lists_delete], ['users/lists/delete', ep___users_lists_delete],
['users/lists/list', ep___users_lists_list], ['users/lists/list', ep___users_lists_list],

View File

@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { UserListsRepository, AntennasRepository } from '@/models/index.js'; import type { UserListsRepository, UserGroupJoiningsRepository, AntennasRepository } from '@/models/index.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -22,6 +22,12 @@ export const meta = {
id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f', id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f',
}, },
noSuchUserGroup: {
message: 'No such user group.',
code: 'NO_SUCH_USER_GROUP',
id: 'aa3c0b9a-8cae-47c0-92ac-202ce5906682',
},
tooManyAntennas: { tooManyAntennas: {
message: 'You cannot create antenna any more.', message: 'You cannot create antenna any more.',
code: 'TOO_MANY_ANTENNAS', code: 'TOO_MANY_ANTENNAS',
@ -40,8 +46,9 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
name: { type: 'string', minLength: 1, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'group'] },
userListId: { type: 'string', format: 'misskey:id', nullable: true }, userListId: { type: 'string', format: 'misskey:id', nullable: true },
userGroupId: { type: 'string', format: 'misskey:id', nullable: true },
keywords: { type: 'array', items: { keywords: { type: 'array', items: {
type: 'array', items: { type: 'array', items: {
type: 'string', type: 'string',
@ -73,6 +80,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.userListsRepository) @Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private antennaEntityService: AntennaEntityService, private antennaEntityService: AntennaEntityService,
private roleService: RoleService, private roleService: RoleService,
private idService: IdService, private idService: IdService,
@ -87,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
let userList; let userList;
let userGroupJoining;
if (ps.src === 'list' && ps.userListId) { if (ps.src === 'list' && ps.userListId) {
userList = await this.userListsRepository.findOneBy({ userList = await this.userListsRepository.findOneBy({
@ -97,6 +108,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (userList == null) { if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList); throw new ApiError(meta.errors.noSuchUserList);
} }
} else if (ps.src === 'group' && ps.userGroupId) {
userGroupJoining = await this.userGroupJoiningsRepository.findOneBy({
userGroupId: ps.userGroupId,
userId: me.id,
});
if (userGroupJoining == null) {
throw new ApiError(meta.errors.noSuchUserGroup);
}
} }
const antenna = await this.antennasRepository.insert({ const antenna = await this.antennasRepository.insert({
@ -106,6 +126,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: ps.name, name: ps.name,
src: ps.src, src: ps.src,
userListId: userList ? userList.id : null, userListId: userList ? userList.id : null,
userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
keywords: ps.keywords, keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords, excludeKeywords: ps.excludeKeywords,
users: ps.users, users: ps.users,

View File

@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AntennasRepository, UserListsRepository } from '@/models/index.js'; import type { AntennasRepository, UserListsRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -25,6 +25,12 @@ export const meta = {
code: 'NO_SUCH_USER_LIST', code: 'NO_SUCH_USER_LIST',
id: '1c6b35c9-943e-48c2-81e4-2844989407f7', id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
}, },
noSuchUserGroup: {
message: 'No such user group.',
code: 'NO_SUCH_USER_GROUP',
id: '109ed789-b6eb-456e-b8a9-6059d567d385',
},
}, },
res: { res: {
@ -39,8 +45,9 @@ export const paramDef = {
properties: { properties: {
antennaId: { type: 'string', format: 'misskey:id' }, antennaId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'group'] },
userListId: { type: 'string', format: 'misskey:id', nullable: true }, userListId: { type: 'string', format: 'misskey:id', nullable: true },
userGroupId: { type: 'string', format: 'misskey:id', nullable: true },
keywords: { type: 'array', items: { keywords: { type: 'array', items: {
type: 'array', items: { type: 'array', items: {
type: 'string', type: 'string',
@ -71,6 +78,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.userListsRepository) @Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository, private userListsRepository: UserListsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private antennaEntityService: AntennaEntityService, private antennaEntityService: AntennaEntityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
@ -87,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
let userList; let userList;
let userGroupJoining;
if (ps.src === 'list' && ps.userListId) { if (ps.src === 'list' && ps.userListId) {
userList = await this.userListsRepository.findOneBy({ userList = await this.userListsRepository.findOneBy({
@ -97,12 +108,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (userList == null) { if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList); throw new ApiError(meta.errors.noSuchUserList);
} }
} else if (ps.src === 'group' && ps.userGroupId) {
userGroupJoining = await this.userGroupJoiningsRepository.findOneBy({
userGroupId: ps.userGroupId,
userId: me.id,
});
if (userGroupJoining == null) {
throw new ApiError(meta.errors.noSuchUserGroup);
}
} }
await this.antennasRepository.update(antenna.id, { await this.antennasRepository.update(antenna.id, {
name: ps.name, name: ps.name,
src: ps.src, src: ps.src,
userListId: userList ? userList.id : null, userListId: userList ? userList.id : null,
userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
keywords: ps.keywords, keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords, excludeKeywords: ps.excludeKeywords,
users: ps.users, users: ps.users,

View File

@ -0,0 +1,69 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserGroupInvitationsRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
import { UserGroupInvitationEntityService } from '@/core/entities/UserGroupInvitationEntityService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['account', 'groups'],
requireCredential: true,
kind: 'read:user-groups',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
group: {
type: 'object',
optional: false, nullable: false,
ref: 'UserGroup',
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupInvitationsRepository)
private userGroupInvitationsRepository: UserGroupInvitationsRepository,
private userGroupInvitationEntityService: UserGroupInvitationEntityService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.userGroupInvitationsRepository.createQueryBuilder('invitation'), ps.sinceId, ps.untilId)
.andWhere('invitation.userId = :meId', { meId: me.id })
.leftJoinAndSelect('invitation.userGroup', 'user_group');
const invitations = await query
.take(ps.limit)
.getMany();
return await this.userGroupInvitationEntityService.packMany(invitations);
});
}
}

View File

@ -0,0 +1,72 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import type { UserGroup } from '@/models/entities/UserGroup.js';
import type { UserGroupJoining } from '@/models/entities/UserGroupJoining.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['groups'],
requireCredential: true,
kind: 'write:user-groups',
description: 'Create a new group.',
limit: {
duration: ms('1hour'),
max: 10,
},
res: {
type: 'object',
optional: false, nullable: false,
ref: 'UserGroup',
},
} as const;
export const paramDef = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
},
required: ['name'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private userGroupEntityService: UserGroupEntityService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const userGroup = await this.userGroupsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
userId: me.id,
name: ps.name,
} as UserGroup).then(x => this.userGroupsRepository.findOneByOrFail(x.identifiers[0]));
// Push the owner
await this.userGroupJoiningsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
userId: me.id,
userGroupId: userGroup.id,
} as UserGroupJoining);
return await this.userGroupEntityService.pack(userGroup);
});
}
}

View File

@ -0,0 +1,53 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['groups'],
requireCredential: true,
kind: 'write:user-groups',
description: 'Delete an existing group.',
errors: {
noSuchGroup: {
message: 'No such group.',
code: 'NO_SUCH_GROUP',
id: '63dbd64c-cd77-413f-8e08-61781e210b38',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
groupId: { type: 'string', format: 'misskey:id' },
},
required: ['groupId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
) {
super(meta, paramDef, async (ps, me) => {
const userGroup = await this.userGroupsRepository.findOneBy({
id: ps.groupId,
userId: me.id,
});
if (userGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
await this.userGroupsRepository.delete(userGroup.id);
});
}
}

View File

@ -0,0 +1,72 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupInvitationsRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import type { UserGroupJoining } from '@/models/entities/UserGroupJoining.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../../error.js';
export const meta = {
tags: ['groups', 'users'],
requireCredential: true,
kind: 'write:user-groups',
description: 'Join a group the authenticated user has been invited to.',
errors: {
noSuchInvitation: {
message: 'No such invitation.',
code: 'NO_SUCH_INVITATION',
id: '98c11eca-c890-4f42-9806-c8c8303ebb5e',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
invitationId: { type: 'string', format: 'misskey:id' },
},
required: ['invitationId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupInvitationsRepository)
private userGroupInvitationsRepository: UserGroupInvitationsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch the invitation
const invitation = await this.userGroupInvitationsRepository.findOneBy({
id: ps.invitationId,
});
if (invitation == null) {
throw new ApiError(meta.errors.noSuchInvitation);
}
if (invitation.userId !== me.id) {
throw new ApiError(meta.errors.noSuchInvitation);
}
// Push the user
await this.userGroupJoiningsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
userId: me.id,
userGroupId: invitation.userGroupId,
} as UserGroupJoining);
this.userGroupInvitationsRepository.delete(invitation.id);
});
}
}

View File

@ -0,0 +1,57 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupInvitationsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../../error.js';
export const meta = {
tags: ['groups', 'users'],
requireCredential: true,
kind: 'write:user-groups',
description: 'Delete an existing group invitation for the authenticated user without joining the group.',
errors: {
noSuchInvitation: {
message: 'No such invitation.',
code: 'NO_SUCH_INVITATION',
id: 'ad7471d4-2cd9-44b4-ac68-e7136b4ce656',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
invitationId: { type: 'string', format: 'misskey:id' },
},
required: ['invitationId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupInvitationsRepository)
private userGroupInvitationsRepository: UserGroupInvitationsRepository,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch the invitation
const invitation = await this.userGroupInvitationsRepository.findOneBy({
id: ps.invitationId,
});
if (invitation == null) {
throw new ApiError(meta.errors.noSuchInvitation);
}
if (invitation.userId !== me.id) {
throw new ApiError(meta.errors.noSuchInvitation);
}
await this.userGroupInvitationsRepository.delete(invitation.id);
});
}
}

View File

@ -0,0 +1,122 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupsRepository, UserGroupJoiningsRepository, UserGroupInvitationsRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import type { UserGroupInvitation } from '@/models/entities/UserGroupInvitation.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['groups', 'users'],
requireCredential: true,
kind: 'write:user-groups',
description: 'Invite a user to an existing group.',
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',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
groupId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id' },
},
required: ['groupId', 'userId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
@Inject(DI.userGroupInvitationsRepository)
private userGroupInvitationsRepository: UserGroupInvitationsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private idService: IdService,
private getterService: GetterService,
private createNotificationService: CreateNotificationService,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch the group
const userGroup = await this.userGroupsRepository.findOneBy({
id: ps.groupId,
userId: me.id,
});
if (userGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
// Fetch the user
const user = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
});
const joining = await this.userGroupJoiningsRepository.findOneBy({
userGroupId: userGroup.id,
userId: user.id,
});
if (joining) {
throw new ApiError(meta.errors.alreadyAdded);
}
const existInvitation = await this.userGroupInvitationsRepository.findOneBy({
userGroupId: userGroup.id,
userId: user.id,
});
if (existInvitation) {
throw new ApiError(meta.errors.alreadyInvited);
}
const invitation = await this.userGroupInvitationsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
userId: user.id,
userGroupId: userGroup.id,
} as UserGroupInvitation).then(x => this.userGroupInvitationsRepository.findOneByOrFail(x.identifiers[0]));
// 通知を作成
this.createNotificationService.createNotification(user.id, 'groupInvited', {
notifierId: me.id,
userGroupInvitationId: invitation.id,
});
});
}
}

View File

@ -0,0 +1,61 @@
import { Not, In } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['groups', 'account'],
requireCredential: true,
kind: 'read:user-groups',
description: 'List the groups that the authenticated user is a member of.',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'UserGroup',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private userGroupEntityService: UserGroupEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const ownedGroups = await this.userGroupsRepository.findBy({
userId: me.id,
});
const joinings = await this.userGroupJoiningsRepository.findBy({
userId: me.id,
...(ownedGroups.length > 0 ? {
userGroupId: Not(In(ownedGroups.map(x => x.id))),
} : {}),
});
return await Promise.all(joinings.map(x => this.userGroupEntityService.pack(x.userGroupId)));
});
}
}

View File

@ -0,0 +1,66 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['groups', 'users'],
requireCredential: true,
kind: 'write:user-groups',
description: 'Leave a group. The owner of a group can not leave. They must transfer ownership or delete the group instead.',
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',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
groupId: { type: 'string', format: 'misskey:id' },
},
required: ['groupId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch the group
const userGroup = await this.userGroupsRepository.findOneBy({
id: ps.groupId,
});
if (userGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
if (me.id === userGroup.userId) {
throw new ApiError(meta.errors.youAreOwner);
}
await this.userGroupJoiningsRepository.delete({ userGroupId: userGroup.id, userId: me.id });
});
}
}

View File

@ -0,0 +1,50 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['groups', 'account'],
requireCredential: true,
kind: 'read:user-groups',
description: 'List the groups that the authenticated user is the owner of.',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'UserGroup',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
private userGroupEntityService: UserGroupEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const userGroups = await this.userGroupsRepository.findBy({
userId: me.id,
});
return await Promise.all(userGroups.map(x => this.userGroupEntityService.pack(x)));
});
}
}

View File

@ -0,0 +1,84 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['groups', 'users'],
requireCredential: true,
kind: 'write:user-groups',
description: 'Removes a specified user from a group. The owner can not be removed.',
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',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
groupId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id' },
},
required: ['groupId', 'userId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch the group
const userGroup = await this.userGroupsRepository.findOneBy({
id: ps.groupId,
userId: me.id,
});
if (userGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
// Fetch the user
const user = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
});
if (user.id === userGroup.userId) {
throw new ApiError(meta.errors.isOwner);
}
// Pull the user
await this.userGroupJoiningsRepository.delete({ userGroupId: userGroup.id, userId: user.id });
});
}
}

View File

@ -0,0 +1,74 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['groups', 'account'],
requireCredential: true,
kind: 'read:user-groups',
description: 'Show the properties of a group.',
res: {
type: 'object',
optional: false, nullable: false,
ref: 'UserGroup',
},
errors: {
noSuchGroup: {
message: 'No such group.',
code: 'NO_SUCH_GROUP',
id: 'ea04751e-9b7e-487b-a509-330fb6bd6b9b',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
groupId: { type: 'string', format: 'misskey:id' },
},
required: ['groupId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private userGroupEntityService: UserGroupEntityService,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch the group
const userGroup = await this.userGroupsRepository.findOneBy({
id: ps.groupId,
});
if (userGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
const joining = await this.userGroupJoiningsRepository.findOneBy({
userId: me.id,
userGroupId: userGroup.id,
});
if (joining == null && userGroup.userId !== me.id) {
throw new ApiError(meta.errors.noSuchGroup);
}
return await this.userGroupEntityService.pack(userGroup);
});
}
}

View File

@ -0,0 +1,100 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupsRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['groups', 'users'],
requireCredential: true,
kind: 'write:user-groups',
description: 'Transfer ownership of a group from the authenticated user to another user.',
res: {
type: 'object',
optional: false, nullable: false,
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',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
groupId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id' },
},
required: ['groupId', 'userId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private userGroupEntityService: UserGroupEntityService,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch the group
const userGroup = await this.userGroupsRepository.findOneBy({
id: ps.groupId,
userId: me.id,
});
if (userGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
// Fetch the user
const user = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
});
const joining = await this.userGroupJoiningsRepository.findOneBy({
userGroupId: userGroup.id,
userId: user.id,
});
if (joining == null) {
throw new ApiError(meta.errors.noSuchGroupMember);
}
await this.userGroupsRepository.update(userGroup.id, {
userId: ps.userId,
});
return await this.userGroupEntityService.pack(userGroup.id);
});
}
}

View File

@ -0,0 +1,68 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserGroupEntityService } from '@/core/entities/UserGroupEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['groups'],
requireCredential: true,
kind: 'write:user-groups',
description: 'Update the properties of a group.',
res: {
type: 'object',
optional: false, nullable: false,
ref: 'UserGroup',
},
errors: {
noSuchGroup: {
message: 'No such group.',
code: 'NO_SUCH_GROUP',
id: '9081cda3-7a9e-4fac-a6ce-908d70f282f6',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
groupId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 },
},
required: ['groupId', 'name'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userGroupsRepository)
private userGroupsRepository: UserGroupsRepository,
private userGroupEntityService: UserGroupEntityService,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch the group
const userGroup = await this.userGroupsRepository.findOneBy({
id: ps.groupId,
userId: me.id,
});
if (userGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
await this.userGroupsRepository.update(userGroup.id, {
name: ps.name,
});
return await this.userGroupEntityService.pack(userGroup.id);
});
}
}

View File

@ -3,6 +3,7 @@ import type { Channel as ChannelModel } from '@/models/entities/Channel.js';
import type { FollowingsRepository, MutingsRepository, UserProfilesRepository, ChannelFollowingsRepository, BlockingsRepository } from '@/models/index.js'; import type { FollowingsRepository, MutingsRepository, UserProfilesRepository, ChannelFollowingsRepository, BlockingsRepository } from '@/models/index.js';
import type { AccessToken } from '@/models/entities/AccessToken.js'; import type { AccessToken } from '@/models/entities/AccessToken.js';
import type { UserProfile } from '@/models/entities/UserProfile.js'; import type { UserProfile } from '@/models/entities/UserProfile.js';
import type { UserGroup } from '@/models/entities/UserGroup.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/schema.js';
import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js';
import type { NoteReadService } from '@/core/NoteReadService.js'; import type { NoteReadService } from '@/core/NoteReadService.js';

View File

@ -6,6 +6,7 @@ import type { Antenna } from '@/models/entities/Antenna.js';
import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { DriveFolder } from '@/models/entities/DriveFolder.js'; import type { DriveFolder } from '@/models/entities/DriveFolder.js';
import type { UserList } from '@/models/entities/UserList.js'; import type { UserList } from '@/models/entities/UserList.js';
import type { UserGroup } from '@/models/entities/UserGroup.js';
import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js'; import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
import type { Signin } from '@/models/entities/Signin.js'; import type { Signin } from '@/models/entities/Signin.js';
import type { Page } from '@/models/entities/Page.js'; import type { Page } from '@/models/entities/Page.js';

View File

@ -1,4 +1,4 @@
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] as const; export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'achievementEarned', 'app'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;

View File

@ -9,6 +9,7 @@
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i> <i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i> <i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i> <i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i>
<i v-else-if="notification.type === 'groupInvited'" class="ti ti-certificate-2"></i>
<i v-else-if="notification.type === 'renote'" class="ti ti-repeat"></i> <i v-else-if="notification.type === 'renote'" class="ti ti-repeat"></i>
<i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i> <i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i>
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i> <i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
@ -73,6 +74,12 @@
<button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button> <button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button>
</div> </div>
</template> </template>
<template v-else-if="notification.type === 'groupInvited'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b></span>
<div v-if="full && !groupInviteDone">
<button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button>
</div>
</template>
<span v-else-if="notification.type === 'app'" :class="$style.text"> <span v-else-if="notification.type === 'app'" :class="$style.text">
<Mfm :text="notification.body" :nowrap="false"/> <Mfm :text="notification.body" :nowrap="false"/>
</span> </span>
@ -138,6 +145,7 @@ onUnmounted(() => {
}); });
const followRequestDone = ref(false); const followRequestDone = ref(false);
const groupInviteDone = ref(false);
const acceptFollowRequest = () => { const acceptFollowRequest = () => {
followRequestDone.value = true; followRequestDone.value = true;
@ -149,6 +157,16 @@ const rejectFollowRequest = () => {
os.api('following/requests/reject', { userId: props.notification.user.id }); os.api('following/requests/reject', { userId: props.notification.user.id });
}; };
const acceptGroupInvitation = () => {
groupInviteDone.value = true;
os.apiWithDialog('users/groups/invitations/accept', { invitationId: props.notification.invitation.id });
};
const rejectGroupInvitation = () => {
groupInviteDone.value = true;
os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id });
};
useTooltip(reactionRef, (showing) => { useTooltip(reactionRef, (showing) => {
os.popup(XReactionTooltip, { os.popup(XReactionTooltip, {
showing, showing,
@ -206,7 +224,7 @@ useTooltip(reactionRef, (showing) => {
} }
} }
.t_follow, .t_followRequestAccepted, .t_receiveFollowRequest { .t_follow, .t_followRequestAccepted, .t_receiveFollowRequest, .t_groupInvited {
padding: 3px; padding: 3px;
background: #36aed2; background: #36aed2;
pointer-events: none; pointer-events: none;

View File

@ -50,6 +50,14 @@ export const navbarItemDef = reactive({
show: computed(() => $i != null), show: computed(() => $i != null),
to: '/my/lists', to: '/my/lists',
}, },
/*
groups: {
title: i18n.ts.groups,
icon: 'ti ti-users',
show: computed(() => $i != null),
to: '/my/groups',
},
*/
antennas: { antennas: {
title: i18n.ts.antennas, title: i18n.ts.antennas,
icon: 'ti ti-antenna', icon: 'ti ti-antenna',

View File

@ -17,6 +17,7 @@ let draft = $ref({
name: '', name: '',
src: 'all', src: 'all',
userListId: null, userListId: null,
userGroupId: null,
users: [], users: [],
keywords: [], keywords: [],
excludeKeywords: [], excludeKeywords: [],

View File

@ -11,11 +11,16 @@
<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>--> <!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
<option value="users">{{ i18n.ts._antennaSources.users }}</option> <option value="users">{{ i18n.ts._antennaSources.users }}</option>
<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>--> <!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
<!--<option value="group">{{ i18n.ts._antennaSources.userGroup }}</option>-->
</MkSelect> </MkSelect>
<MkSelect v-if="src === 'list'" v-model="userListId"> <MkSelect v-if="src === 'list'" v-model="userListId">
<template #label>{{ i18n.ts.userList }}</template> <template #label>{{ i18n.ts.userList }}</template>
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option> <option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
</MkSelect> </MkSelect>
<MkSelect v-else-if="src === 'group'" v-model="userGroupId">
<template #label>{{ i18n.ts.userGroup }}</template>
<option v-for="group in userGroups" :key="group.id" :value="group.id">{{ group.name }}</option>
</MkSelect>
<MkTextarea v-else-if="src === 'users'" v-model="users"> <MkTextarea v-else-if="src === 'users'" v-model="users">
<template #label>{{ i18n.ts.users }}</template> <template #label>{{ i18n.ts.users }}</template>
<template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template> <template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template>
@ -65,6 +70,7 @@ const emit = defineEmits<{
let name: string = $ref(props.antenna.name); let name: string = $ref(props.antenna.name);
let src: string = $ref(props.antenna.src); let src: string = $ref(props.antenna.src);
let userListId: any = $ref(props.antenna.userListId); let userListId: any = $ref(props.antenna.userListId);
let userGroupId: any = $ref(props.antenna.userGroupId);
let users: string = $ref(props.antenna.users.join('\n')); let users: string = $ref(props.antenna.users.join('\n'));
let keywords: string = $ref(props.antenna.keywords.map(x => x.join(' ')).join('\n')); let keywords: string = $ref(props.antenna.keywords.map(x => x.join(' ')).join('\n'));
let excludeKeywords: string = $ref(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n')); let excludeKeywords: string = $ref(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
@ -73,11 +79,19 @@ let withReplies: boolean = $ref(props.antenna.withReplies);
let withFile: boolean = $ref(props.antenna.withFile); let withFile: boolean = $ref(props.antenna.withFile);
let notify: boolean = $ref(props.antenna.notify); let notify: boolean = $ref(props.antenna.notify);
let userLists: any = $ref(null); let userLists: any = $ref(null);
let userGroups: any = $ref(null);
watch(() => src, async () => { watch(() => src, async () => {
if (src === 'list' && userLists === null) { if (src === 'list' && userLists === null) {
userLists = await os.api('users/lists/list'); userLists = await os.api('users/lists/list');
} }
if (src === 'group' && userGroups === null) {
const groups1 = await os.api('users/groups/owned');
const groups2 = await os.api('users/groups/joined');
userGroups = [...groups1, ...groups2];
}
}); });
async function saveAntenna() { async function saveAntenna() {
@ -85,6 +99,7 @@ async function saveAntenna() {
name, name,
src, src,
userListId, userListId,
userGroupId,
withReplies, withReplies,
withFile, withFile,
notify, notify,

View File

@ -34,6 +34,9 @@
<MkSwitch v-model="emailNotification_receiveFollowRequest"> <MkSwitch v-model="emailNotification_receiveFollowRequest">
{{ i18n.ts._notification._types.receiveFollowRequest }} {{ i18n.ts._notification._types.receiveFollowRequest }}
</MkSwitch> </MkSwitch>
<MkSwitch v-model="emailNotification_groupInvited">
{{ i18n.ts._notification._types.groupInvited }}
</MkSwitch>
</div> </div>
</FormSection> </FormSection>
</div> </div>
@ -75,6 +78,7 @@ const emailNotification_reply = ref($i!.emailNotificationTypes.includes('reply')
const emailNotification_quote = ref($i!.emailNotificationTypes.includes('quote')); const emailNotification_quote = ref($i!.emailNotificationTypes.includes('quote'));
const emailNotification_follow = ref($i!.emailNotificationTypes.includes('follow')); const emailNotification_follow = ref($i!.emailNotificationTypes.includes('follow'));
const emailNotification_receiveFollowRequest = ref($i!.emailNotificationTypes.includes('receiveFollowRequest')); const emailNotification_receiveFollowRequest = ref($i!.emailNotificationTypes.includes('receiveFollowRequest'));
const emailNotification_groupInvited = ref($i!.emailNotificationTypes.includes('groupInvited'));
const saveNotificationSettings = () => { const saveNotificationSettings = () => {
os.api('i/update', { os.api('i/update', {
@ -84,11 +88,12 @@ const saveNotificationSettings = () => {
...[emailNotification_quote.value ? 'quote' : null], ...[emailNotification_quote.value ? 'quote' : null],
...[emailNotification_follow.value ? 'follow' : null], ...[emailNotification_follow.value ? 'follow' : null],
...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null], ...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null],
...[emailNotification_groupInvited.value ? 'groupInvited' : null],
].filter(x => x != null), ].filter(x => x != null),
}); });
}; };
watch([emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest], () => { watch([emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest, emailNotification_groupInvited], () => {
saveNotificationSettings(); saveNotificationSettings();
}); });

View File

@ -35,6 +35,28 @@ export function getUserMenu(user, router: Router = mainRouter) {
}); });
} }
async function inviteGroup() {
const groups = await os.api('users/groups/owned');
if (groups.length === 0) {
os.alert({
type: 'error',
text: i18n.ts.youHaveNoGroups,
});
return;
}
const { canceled, result: groupId } = await os.select({
title: i18n.ts.group,
items: groups.map(group => ({
value: group.id, text: group.name,
})),
});
if (canceled) return;
os.apiWithDialog('users/groups/invite', {
groupId: groupId,
userId: user.id,
});
}
async function toggleMute() { async function toggleMute() {
if (user.isMuted) { if (user.isMuted) {
os.apiWithDialog('mute/delete', { os.apiWithDialog('mute/delete', {
@ -134,11 +156,20 @@ export function getUserMenu(user, router: Router = mainRouter) {
action: () => { action: () => {
os.post({ specified: user }); os.post({ specified: user });
}, },
}, null, { }, meId !== user.id ? {
type: 'link',
icon: 'ti ti-messages',
text: i18n.ts.startMessaging,
to: '/my/messaging/' + Acct.toString(user),
} : undefined, null, {
icon: 'ti ti-list', icon: 'ti ti-list',
text: i18n.ts.addToList, text: i18n.ts.addToList,
action: pushList, action: pushList,
}] as any; }, meId !== user.id ? {
icon: 'ti ti-users',
text: i18n.ts.inviteToGroup,
action: inviteGroup,
} : undefined] as any;
if ($i && meId !== user.id) { if ($i && meId !== user.id) {
menu = menu.concat([null, { menu = menu.concat([null, {

View File

@ -209,6 +209,23 @@ async function composeNotification(data: pushNotificationDataMap[keyof pushNotif
data, data,
}]; }];
case 'groupInvited':
return [t('_notification.youWereInvitedToGroup', { userName: getUserName(data.body.user) }), {
body: data.body.invitation.group.name,
badge: iconUrl('users'),
data,
actions: [
{
action: 'accept',
title: t('accept'),
},
{
action: 'reject',
title: t('reject'),
},
],
}];
case 'app': case 'app':
return [data.body.header ?? data.body.body, { return [data.body.header ?? data.body.body, {
body: data.body.header ? data.body.body : '', body: data.body.header ? data.body.body : '',

View File

@ -32,10 +32,18 @@ export function openAntenna(antennaId: string, loginId: string) {
return openClient('push', `/timeline/antenna/${antennaId}`, loginId, { antennaId }); return openClient('push', `/timeline/antenna/${antennaId}`, loginId, { antennaId });
} }
export async function openChat(body: any, loginId: string) {
if (body.groupId === null) {
return openClient('push', `/my/messaging/${getAcct(body.user)}`, loginId, { body });
} else {
return openClient('push', `/my/messaging/group/${body.groupId}`, loginId, { body });
}
}
// post-formのオプションから投稿フォームを開く // post-formのオプションから投稿フォームを開く
export async function openPost(options: any, loginId: string) { export async function openPost(options: any, loginId: string) {
// クエリを作成しておく // クエリを作成しておく
let url = '/share?'; let url = `/share?`;
if (options.initialText) url += `text=${options.initialText}&`; if (options.initialText) url += `text=${options.initialText}&`;
if (options.reply) url += `replyId=${options.reply.id}&`; if (options.reply) url += `replyId=${options.reply.id}&`;
if (options.renote) url += `renoteId=${options.renote.id}&`; if (options.renote) url += `renoteId=${options.renote.id}&`;
@ -56,7 +64,7 @@ export async function openClient(order: swMessageOrderType, url: string, loginId
export async function findClient() { export async function findClient() {
const clients = await self.clients.matchAll({ const clients = await self.clients.matchAll({
type: 'window', type: 'window'
}); });
for (const c of clients) { for (const c of clients) {
if (c.url.indexOf('?zen') < 0) return c; if (c.url.indexOf('?zen') < 0) return c;

View File

@ -118,6 +118,9 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
case 'receiveFollowRequest': case 'receiveFollowRequest':
await swos.api('following/requests/accept', loginId, { userId: data.body.userId }); await swos.api('following/requests/accept', loginId, { userId: data.body.userId });
break; break;
case 'groupInvited':
await swos.api('users/groups/invitations/accept', loginId, { invitationId: data.body.invitation.id });
break;
} }
break; break;
case 'reject': case 'reject':
@ -125,6 +128,9 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
case 'receiveFollowRequest': case 'receiveFollowRequest':
await swos.api('following/requests/reject', loginId, { userId: data.body.userId }); await swos.api('following/requests/reject', loginId, { userId: data.body.userId });
break; break;
case 'groupInvited':
await swos.api('users/groups/invitations/reject', loginId, { invitationId: data.body.invitation.id });
break;
} }
break; break;
case 'showFollowRequests': case 'showFollowRequests':
@ -135,6 +141,9 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
case 'receiveFollowRequest': case 'receiveFollowRequest':
client = await swos.openClient('push', '/my/follow-requests', loginId); client = await swos.openClient('push', '/my/follow-requests', loginId);
break; break;
case 'groupInvited':
client = await swos.openClient('push', '/my/groups', loginId);
break;
case 'reaction': case 'reaction':
client = await swos.openNote(data.body.note.id, loginId); client = await swos.openNote(data.body.note.id, loginId);
break; break;