1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-11-28 06:48:36 +09:00

Merge remote-branch 'upstream/develop'

This commit is contained in:
NoriDev 2023-05-11 21:34:09 +09:00
commit 520b072c09
90 changed files with 118 additions and 4876 deletions

View File

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

View File

@ -0,0 +1,47 @@
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

@ -0,0 +1,9 @@
export class ad1676438468213 {
name = 'ad1676438468213';
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "ad" ADD "startAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "startAt"`);
}
}

View File

@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
import * as Acct from '@/misc/acct.js';
import type { Packed } from '@/misc/schema.js';
import { DI } from '@/di-symbols.js';
import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
@ -39,9 +39,6 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
@Inject(DI.userListJoiningsRepository)
private userListJoiningsRepository: UserListJoiningsRepository,
@ -160,14 +157,6 @@ export class AntennaService implements OnApplicationShutdown {
})).map(x => x.userId);
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') {
const accts = antenna.users.map(x => {
const { username, host } = Acct.parse(x);

View File

@ -22,7 +22,6 @@ import { IdService } from './IdService.js';
import { ImageProcessingService } from './ImageProcessingService.js';
import { InstanceActorService } from './InstanceActorService.js';
import { InternalStorageService } from './InternalStorageService.js';
import { MessagingService } from './MessagingService.js';
import { MetaService } from './MetaService.js';
import { MfmService } from './MfmService.js';
import { ModerationLogService } from './ModerationLogService.js';
@ -82,7 +81,6 @@ import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js
import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js';
import { HashtagEntityService } from './entities/HashtagEntityService.js';
import { InstanceEntityService } from './entities/InstanceEntityService.js';
import { MessagingMessageEntityService } from './entities/MessagingMessageEntityService.js';
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
import { MutingEntityService } from './entities/MutingEntityService.js';
import { NoteEntityService } from './entities/NoteEntityService.js';
@ -93,8 +91,6 @@ import { PageEntityService } from './entities/PageEntityService.js';
import { PageLikeEntityService } from './entities/PageLikeEntityService.js';
import { SigninEntityService } from './entities/SigninEntityService.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 { FlashEntityService } from './entities/FlashEntityService.js';
import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
@ -146,7 +142,6 @@ const $IdService: Provider = { provide: 'IdService', useExisting: IdService };
const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService };
const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService };
const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService };
const $MessagingService: Provider = { provide: 'MessagingService', useExisting: MessagingService };
const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService };
const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService };
const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService };
@ -207,7 +202,6 @@ const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService
const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService };
const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService };
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
const $MessagingMessageEntityService: Provider = { provide: 'MessagingMessageEntityService', useExisting: MessagingMessageEntityService };
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
const $NoteEntityService: Provider = { provide: 'NoteEntityService', useExisting: NoteEntityService };
@ -218,8 +212,6 @@ const $PageEntityService: Provider = { provide: 'PageEntityService', useExisting
const $PageLikeEntityService: Provider = { provide: 'PageLikeEntityService', useExisting: PageLikeEntityService };
const $SigninEntityService: Provider = { provide: 'SigninEntityService', useExisting: SigninEntityService };
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 $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService };
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
@ -273,7 +265,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ImageProcessingService,
InstanceActorService,
InternalStorageService,
MessagingService,
MetaService,
MfmService,
ModerationLogService,
@ -333,7 +324,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
GalleryPostEntityService,
HashtagEntityService,
InstanceEntityService,
MessagingMessageEntityService,
ModerationLogEntityService,
MutingEntityService,
NoteEntityService,
@ -344,8 +334,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PageLikeEntityService,
SigninEntityService,
UserEntityService,
UserGroupEntityService,
UserGroupInvitationEntityService,
UserListEntityService,
FlashEntityService,
FlashLikeEntityService,
@ -394,7 +382,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ImageProcessingService,
$InstanceActorService,
$InternalStorageService,
$MessagingService,
$MetaService,
$MfmService,
$ModerationLogService,
@ -454,7 +441,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$GalleryPostEntityService,
$HashtagEntityService,
$InstanceEntityService,
$MessagingMessageEntityService,
$ModerationLogEntityService,
$MutingEntityService,
$NoteEntityService,
@ -465,8 +451,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PageLikeEntityService,
$SigninEntityService,
$UserEntityService,
$UserGroupEntityService,
$UserGroupInvitationEntityService,
$UserListEntityService,
$FlashEntityService,
$FlashLikeEntityService,
@ -516,7 +500,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ImageProcessingService,
InstanceActorService,
InternalStorageService,
MessagingService,
MetaService,
MfmService,
ModerationLogService,
@ -575,7 +558,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
GalleryPostEntityService,
HashtagEntityService,
InstanceEntityService,
MessagingMessageEntityService,
ModerationLogEntityService,
MutingEntityService,
NoteEntityService,
@ -586,8 +568,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PageLikeEntityService,
SigninEntityService,
UserEntityService,
UserGroupEntityService,
UserGroupInvitationEntityService,
UserListEntityService,
FlashEntityService,
FlashLikeEntityService,
@ -636,7 +616,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$ImageProcessingService,
$InstanceActorService,
$InternalStorageService,
$MessagingService,
$MetaService,
$MfmService,
$ModerationLogService,
@ -695,7 +674,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$GalleryPostEntityService,
$HashtagEntityService,
$InstanceEntityService,
$MessagingMessageEntityService,
$ModerationLogEntityService,
$MutingEntityService,
$NoteEntityService,
@ -706,8 +684,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PageLikeEntityService,
$SigninEntityService,
$UserEntityService,
$UserGroupEntityService,
$UserGroupInvitationEntityService,
$UserListEntityService,
$FlashEntityService,
$FlashLikeEntityService,

View File

@ -3,7 +3,6 @@ import Redis from 'ioredis';
import type { User } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.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 { Channel } from '@/models/entities/Channel.js';
import type {
@ -11,13 +10,9 @@ import type {
AdminStreamTypes,
AntennaStreamTypes,
BroadcastTypes,
ChannelStreamTypes,
DriveStreamTypes,
GroupMessagingStreamTypes,
InternalStreamTypes,
MainStreamTypes,
MessagingIndexStreamTypes,
MessagingStreamTypes,
NoteStreamTypes,
UserListStreamTypes,
UserStreamTypes,
@ -83,11 +78,6 @@ export class GlobalEventService {
});
}
@bindThis
public publishChannelStream<K extends keyof ChannelStreamTypes>(channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void {
this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishUserListStream<K extends keyof UserListStreamTypes>(listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void {
this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value);
@ -98,21 +88,6 @@ export class GlobalEventService {
this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishMessagingStream<K extends keyof MessagingStreamTypes>(userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void {
this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishGroupMessagingStream<K extends keyof GroupMessagingStreamTypes>(groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void {
this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishMessagingIndexStream<K extends keyof MessagingIndexStreamTypes>(userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void {
this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
@bindThis
public publishNotesStream(note: Packed<'Note'>): void {
this.publish('notesStream', null, note);

View File

@ -1,307 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { In, Not } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
import type { Note } from '@/models/entities/Note.js';
import type { User, RemoteUser } from '@/models/entities/User.js';
import type { UserGroup } from '@/models/entities/UserGroup.js';
import { QueueService } from '@/core/QueueService.js';
import { toArray } from '@/misc/prelude/array.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { MessagingMessagesRepository, MutingsRepository, UserGroupJoiningsRepository, UsersRepository } from '@/models/index.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class MessagingService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
private userEntityService: UserEntityService,
private messagingMessageEntityService: MessagingMessageEntityService,
private idService: IdService,
private globalEventService: GlobalEventService,
private apRendererService: ApRendererService,
private queueService: QueueService,
private pushNotificationService: PushNotificationService,
) {
}
@bindThis
public async createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) {
const message = {
id: this.idService.genId(),
createdAt: new Date(),
fileId: file ? file.id : null,
recipientId: recipientUser ? recipientUser.id : null,
groupId: recipientGroup ? recipientGroup.id : null,
text: text ? text.trim() : null,
userId: user.id,
isRead: false,
reads: [] as any[],
uri,
} as MessagingMessage;
await this.messagingMessagesRepository.insert(message);
const messageObj = await this.messagingMessageEntityService.pack(message);
if (recipientUser) {
if (this.userEntityService.isLocalUser(user)) {
// 自分のストリーム
this.globalEventService.publishMessagingStream(message.userId, recipientUser.id, 'message', messageObj);
this.globalEventService.publishMessagingIndexStream(message.userId, 'message', messageObj);
this.globalEventService.publishMainStream(message.userId, 'messagingMessage', messageObj);
}
if (this.userEntityService.isLocalUser(recipientUser)) {
// 相手のストリーム
this.globalEventService.publishMessagingStream(recipientUser.id, message.userId, 'message', messageObj);
this.globalEventService.publishMessagingIndexStream(recipientUser.id, 'message', messageObj);
this.globalEventService.publishMainStream(recipientUser.id, 'messagingMessage', messageObj);
}
} else if (recipientGroup) {
// グループのストリーム
this.globalEventService.publishGroupMessagingStream(recipientGroup.id, 'message', messageObj);
// メンバーのストリーム
const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: recipientGroup.id });
for (const joining of joinings) {
this.globalEventService.publishMessagingIndexStream(joining.userId, 'message', messageObj);
this.globalEventService.publishMainStream(joining.userId, 'messagingMessage', messageObj);
}
}
// 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
setTimeout(async () => {
const freshMessage = await this.messagingMessagesRepository.findOneBy({ id: message.id });
if (freshMessage == null) return; // メッセージが削除されている場合もある
if (recipientUser && this.userEntityService.isLocalUser(recipientUser)) {
if (freshMessage.isRead) return; // 既読
//#region ただしミュートされているなら発行しない
const mute = await this.mutingsRepository.findBy({
muterId: recipientUser.id,
});
if (mute.map(m => m.muteeId).includes(user.id)) return;
//#endregion
this.globalEventService.publishMainStream(recipientUser.id, 'unreadMessagingMessage', messageObj);
this.pushNotificationService.pushNotification(recipientUser.id, 'unreadMessagingMessage', messageObj);
} else if (recipientGroup) {
const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: recipientGroup.id, userId: Not(user.id) });
for (const joining of joinings) {
if (freshMessage.reads.includes(joining.userId)) return; // 既読
this.globalEventService.publishMainStream(joining.userId, 'unreadMessagingMessage', messageObj);
this.pushNotificationService.pushNotification(joining.userId, 'unreadMessagingMessage', messageObj);
}
}
}, 2000);
if (recipientUser && this.userEntityService.isLocalUser(user) && this.userEntityService.isRemoteUser(recipientUser)) {
const note = {
id: message.id,
createdAt: message.createdAt,
fileIds: message.fileId ? [message.fileId] : [],
text: message.text,
userId: message.userId,
visibility: 'specified',
mentions: [recipientUser].map(u => u.id),
mentionedRemoteUsers: JSON.stringify([recipientUser].map(u => ({
uri: u.uri,
username: u.username,
host: u.host,
}))),
} as Note;
const activity = this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false, true), note));
this.queueService.deliver(user, activity, recipientUser.inbox);
}
return messageObj;
}
@bindThis
public async deleteMessage(message: MessagingMessage) {
await this.messagingMessagesRepository.delete(message.id);
this.postDeleteMessage(message);
}
@bindThis
private async postDeleteMessage(message: MessagingMessage) {
if (message.recipientId) {
const user = await this.usersRepository.findOneByOrFail({ id: message.userId });
const recipient = await this.usersRepository.findOneByOrFail({ id: message.recipientId });
if (this.userEntityService.isLocalUser(user)) this.globalEventService.publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id);
if (this.userEntityService.isLocalUser(recipient)) this.globalEventService.publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id);
if (this.userEntityService.isLocalUser(user) && this.userEntityService.isRemoteUser(recipient)) {
const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), user));
this.queueService.deliver(user, activity, recipient.inbox);
}
} else if (message.groupId) {
this.globalEventService.publishGroupMessagingStream(message.groupId, 'deleted', message.id);
}
}
/**
* Mark messages as read
*/
@bindThis
public async readUserMessagingMessage(
userId: User['id'],
otherpartyId: User['id'],
messageIds: MessagingMessage['id'][],
) {
if (messageIds.length === 0) return;
const messages = await this.messagingMessagesRepository.findBy({
id: In(messageIds),
});
for (const message of messages) {
if (message.recipientId !== userId) {
throw new IdentifiableError('e140a4bf-49ce-4fb6-b67c-b78dadf6b52f', 'Access denied (user).');
}
}
// Update documents
await this.messagingMessagesRepository.update({
id: In(messageIds),
userId: otherpartyId,
recipientId: userId,
isRead: false,
}, {
isRead: true,
});
// Publish event
this.globalEventService.publishMessagingStream(otherpartyId, userId, 'read', messageIds);
this.globalEventService.publishMessagingIndexStream(userId, 'read', messageIds);
if (!await this.userEntityService.getHasUnreadMessagingMessage(userId)) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllMessagingMessages');
this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessages', undefined);
} else {
// そのユーザーとのメッセージで未読がなければイベント発行
const count = await this.messagingMessagesRepository.count({
where: {
userId: otherpartyId,
recipientId: userId,
isRead: false,
},
take: 1,
});
if (!count) {
this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId });
}
}
}
/**
* Mark messages as read
*/
@bindThis
public async readGroupMessagingMessage(
userId: User['id'],
groupId: UserGroup['id'],
messageIds: MessagingMessage['id'][],
) {
if (messageIds.length === 0) return;
// check joined
const joining = await this.userGroupJoiningsRepository.findOneBy({
userId: userId,
userGroupId: groupId,
});
if (joining == null) {
throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).');
}
const messages = await this.messagingMessagesRepository.findBy({
id: In(messageIds),
});
const reads: MessagingMessage['id'][] = [];
for (const message of messages) {
if (message.userId === userId) continue;
if (message.reads.includes(userId)) continue;
// Update document
await this.messagingMessagesRepository.createQueryBuilder().update()
.set({
reads: (() => `array_append("reads", '${joining.userId}')`) as any,
})
.where('id = :id', { id: message.id })
.execute();
reads.push(message.id);
}
// Publish event
this.globalEventService.publishGroupMessagingStream(groupId, 'read', {
ids: reads,
userId: userId,
});
this.globalEventService.publishMessagingIndexStream(userId, 'read', reads);
if (!await this.userEntityService.getHasUnreadMessagingMessage(userId)) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllMessagingMessages');
this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessages', undefined);
} else {
// そのグループにおいて未読がなければイベント発行
const unreadExist = await this.messagingMessagesRepository.createQueryBuilder('message')
.where('message.groupId = :groupId', { groupId: groupId })
.andWhere('message.userId != :userId', { userId: userId })
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
.andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない
.getOne().then(x => x != null);
if (!unreadExist) {
this.pushNotificationService.pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId });
}
}
}
@bindThis
public async deliverReadActivity(user: { id: User['id']; host: null; }, recipient: RemoteUser, messages: MessagingMessage | MessagingMessage[]) {
messages = toArray(messages).filter(x => x.uri);
const contents = messages.map(x => this.apRendererService.renderRead(user, x));
if (contents.length > 1) {
const collection = this.apRendererService.renderOrderedCollection(null, contents.length, undefined, undefined, contents);
this.queueService.deliver(user, this.apRendererService.addContext(collection), recipient.inbox);
} else {
for (const content of contents) {
this.queueService.deliver(user, this.apRendererService.addContext(content), recipient.inbox);
}
}
}
}

View File

@ -11,15 +11,12 @@ import { bindThis } from '@/decorators.js';
// Defined also packages/sw/types.ts#L13
type pushNotificationsTypes = {
'notification': Packed<'Notification'>;
'unreadMessagingMessage': Packed<'MessagingMessage'>;
'unreadAntennaNote': {
antenna: { id: string, name: string };
note: Packed<'Note'>;
};
'readNotifications': { notificationIds: string[] };
'readAllNotifications': undefined;
'readAllMessagingMessages': undefined;
'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string };
'readAntenna': { antennaId: string };
'readAllAntennas': undefined;
};
@ -40,11 +37,10 @@ function truncateBody<T extends keyof pushNotificationsTypes>(type: T, body: pus
reply: undefined,
renote: undefined,
user: type === 'notification' ? undefined as any : body.note.user,
}
},
} : {}),
};
return body;
}
@Injectable()
@ -81,8 +77,6 @@ export class PushNotificationService {
if ([
'readNotifications',
'readAllNotifications',
'readAllMessagingMessages',
'readAllMessagingMessagesOfARoom',
'readAntenna',
'readAllAntennas',
].includes(type) && !subscription.sendReadMessage) continue;

View File

@ -1,13 +1,12 @@
import { Inject, Injectable } from '@nestjs/common';
import escapeRegexp from 'escape-regexp';
import { DI } from '@/di-symbols.js';
import type { MessagingMessagesRepository, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import { Cache } from '@/misc/cache.js';
import type { UserPublickey } from '@/models/entities/UserPublickey.js';
import { UserCacheService } from '@/core/UserCacheService.js';
import type { Note } from '@/models/entities/Note.js';
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
import { bindThis } from '@/decorators.js';
import { RemoteUser, User } from '@/models/entities/User.js';
import { getApId } from './type.js';
@ -42,9 +41,6 @@ export class ApDbResolverService {
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@ -101,23 +97,6 @@ export class ApDbResolverService {
}
}
@bindThis
public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> {
const parsed = this.parseUri(value);
if (parsed.local) {
if (parsed.type !== 'notes') return null;
return await this.messagingMessagesRepository.findOneBy({
id: parsed.id,
});
} else {
return await this.messagingMessagesRepository.findOneBy({
uri: parsed.uri,
});
}
}
/**
* AP Person => Misskey User in DB
*/

View File

@ -19,8 +19,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { QueueService } from '@/core/QueueService.js';
import { MessagingService } from '@/core/MessagingService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, MessagingMessagesRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import type { RemoteUser } from '@/models/entities/User.js';
import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isRead, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
@ -51,9 +50,6 @@ export class ApInboxService {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
@ -81,7 +77,6 @@ export class ApInboxService {
private apPersonService: ApPersonService,
private apQuestionService: ApQuestionService,
private queueService: QueueService,
private messagingService: MessagingService,
) {
this.logger = this.apLoggerService.logger;
}
@ -124,8 +119,6 @@ export class ApInboxService {
await this.delete(actor, activity);
} else if (isUpdate(activity)) {
await this.update(actor, activity);
} else if (isRead(activity)) {
await this.read(actor, activity);
} else if (isFollow(activity)) {
await this.follow(actor, activity);
} else if (isAccept(activity)) {
@ -185,29 +178,6 @@ export class ApInboxService {
}).then(() => 'ok');
}
@bindThis
private async read(actor: RemoteUser, activity: IRead): Promise<string> {
const id = await getApId(activity.object);
if (!this.utilityService.isSelfHost(this.utilityService.extractDbHost(id))) {
return `skip: Read to foreign host (${id})`;
}
const messageId = id.split('/').pop();
const message = await this.messagingMessagesRepository.findOneBy({ id: messageId });
if (message == null) {
return 'skip: message not found';
}
if (actor.id !== message.recipientId) {
return 'skip: actor is not a message recipient';
}
await this.messagingService.readUserMessagingMessage(message.recipientId!, message.userId, [message.id]);
return `ok: mark as read (${message.userId} => ${message.recipientId} ${message.id})`;
}
@bindThis
private async accept(actor: RemoteUser, activity: IAccept): Promise<string> {
const uri = activity.id ?? activity;
@ -504,16 +474,7 @@ export class ApInboxService {
const note = await this.apDbResolverService.getNoteFromApId(uri);
if (note == null) {
const message = await this.apDbResolverService.getMessageFromApId(uri);
if (message == null) return 'message not found';
if (message.userId !== actor.id) {
return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
}
await this.messagingService.deleteMessage(message);
return 'ok: message deleted';
return 'message not found';
}
if (note.userId !== actor.id) {

View File

@ -13,7 +13,6 @@ import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { NoteReaction } from '@/models/entities/NoteReaction.js';
import type { Emoji } from '@/models/entities/Emoji.js';
import type { Poll } from '@/models/entities/Poll.js';
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
import type { PollVote } from '@/models/entities/PollVote.js';
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
import { MfmService } from '@/core/MfmService.js';
@ -293,7 +292,7 @@ export class ApRendererService {
}
@bindThis
public async renderNote(note: Note, dive = true, isTalk = false): Promise<IPost> {
public async renderNote(note: Note, dive = true): Promise<IPost> {
const getPromisedFiles = async (ids: string[]) => {
if (!ids || ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
@ -407,11 +406,7 @@ export class ApRendererService {
},
})),
} as const : {};
const asTalk = isTalk ? {
_misskey_talk: true,
} as const : {};
return {
id: `${this.config.url}/notes/${note.id}`,
type: 'Note',
@ -433,7 +428,6 @@ export class ApRendererService {
sensitive: note.cw != null || files.some(file => file.isSensitive),
tag,
...asPoll,
...asTalk,
};
}
@ -532,15 +526,6 @@ export class ApRendererService {
};
}
@bindThis
public renderRead(user: { id: User['id'] }, message: MessagingMessage): IRead {
return {
type: 'Read',
actor: `${this.config.url}/users/${user.id}`,
object: message.uri!,
};
}
@bindThis
public renderReject(object: any, user: { id: User['id'] }): IReject {
return {
@ -643,7 +628,6 @@ export class ApRendererService {
'_misskey_quote': 'misskey:_misskey_quote',
'_misskey_reaction': 'misskey:_misskey_reaction',
'_misskey_votes': 'misskey:_misskey_votes',
'_misskey_talk': 'misskey:_misskey_talk',
'isCat': 'misskey:isCat',
// vcard
vcard: 'http://www.w3.org/2006/vcard/ns#',

View File

@ -1,7 +1,7 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import promiseLimit from 'promise-limit';
import { DI } from '@/di-symbols.js';
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
import type { PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type { RemoteUser } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js';
@ -16,7 +16,6 @@ import { IdService } from '@/core/IdService.js';
import { PollService } from '@/core/PollService.js';
import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MessagingService } from '@/core/MessagingService.js';
import { bindThis } from '@/decorators.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
@ -47,9 +46,6 @@ export class ApNoteService {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
private idService: IdService,
private apMfmService: ApMfmService,
private apResolverService: ApResolverService,
@ -64,7 +60,6 @@ export class ApNoteService {
private apImageService: ApImageService,
private apQuestionService: ApQuestionService,
private metaService: MetaService,
private messagingService: MessagingService,
private appLockService: AppLockService,
private pollService: PollService,
private noteCreateService: NoteCreateService,
@ -165,8 +160,6 @@ export class ApNoteService {
}
}
let isMessaging = note._misskey_talk && visibility === 'specified';
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
const apHashtags = await extractApHashtags(note.tag);
@ -193,17 +186,6 @@ export class ApNoteService {
return x;
}
}).catch(async err => {
// トークだったらinReplyToのエラーは無視
const uri = getApId(note.inReplyTo);
if (uri.startsWith(this.config.url + '/')) {
const id = uri.split('/').pop();
const talk = await this.messagingMessagesRepository.findOneBy({ id });
if (talk) {
isMessaging = true;
return null;
}
}
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
throw err;
})
@ -292,14 +274,7 @@ export class ApNoteService {
const apEmojis = emojis.map(emoji => emoji.name);
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
if (isMessaging) {
for (const recipient of visibleUsers) {
await this.messagingService.createMessage(actor, recipient, undefined, text ?? undefined, (files && files.length > 0) ? files[0] : null, object.id);
return null;
}
}
return await this.noteCreateService.create(actor, {
createdAt: note.published ? new Date(note.published) : null,
files,

View File

@ -113,7 +113,6 @@ export interface IPost extends IObject {
_misskey_quote?: string;
_misskey_content?: string;
quoteUrl?: string;
_misskey_talk?: boolean;
}
export interface IQuestion extends IObject {

View File

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

View File

@ -1,59 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { MessagingMessagesRepository } 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 { MessagingMessage } from '@/models/entities/MessagingMessage.js';
import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js';
import { UserGroupEntityService } from './UserGroupEntityService.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class MessagingMessageEntityService {
constructor(
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
private userEntityService: UserEntityService,
private userGroupEntityService: UserGroupEntityService,
private driveFileEntityService: DriveFileEntityService,
) {
}
@bindThis
public async pack(
src: MessagingMessage['id'] | MessagingMessage,
me?: { id: User['id'] } | null | undefined,
options?: {
populateRecipient?: boolean,
populateGroup?: boolean,
},
): Promise<Packed<'MessagingMessage'>> {
const opts = options ?? {
populateRecipient: true,
populateGroup: true,
};
const message = typeof src === 'object' ? src : await this.messagingMessagesRepository.findOneByOrFail({ id: src });
return {
id: message.id,
createdAt: message.createdAt.toISOString(),
text: message.text,
userId: message.userId,
user: await this.userEntityService.pack(message.user ?? message.userId, me),
recipientId: message.recipientId,
recipient: message.recipientId && opts.populateRecipient ? await this.userEntityService.pack(message.recipient ?? message.recipientId, me) : undefined,
groupId: message.groupId,
group: message.groupId && opts.populateGroup ? await this.userGroupEntityService.pack(message.group ?? message.groupId) : undefined,
fileId: message.fileId,
file: message.fileId ? await this.driveFileEntityService.pack(message.fileId) : null,
isRead: message.isRead,
reads: message.reads,
};
}
}

View File

@ -13,13 +13,11 @@ import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
import type { UserGroupInvitationEntityService } from './UserGroupInvitationEntityService.js';
@Injectable()
export class NotificationEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService;
private userGroupInvitationEntityService: UserGroupInvitationEntityService;
private customEmojiService: CustomEmojiService;
constructor(
@ -36,7 +34,6 @@ export class NotificationEntityService implements OnModuleInit {
//private userEntityService: UserEntityService,
//private noteEntityService: NoteEntityService,
//private userGroupInvitationEntityService: UserGroupInvitationEntityService,
//private customEmojiService: CustomEmojiService,
) {
}
@ -44,7 +41,6 @@ export class NotificationEntityService implements OnModuleInit {
onModuleInit() {
this.userEntityService = this.moduleRef.get('UserEntityService');
this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.userGroupInvitationEntityService = this.moduleRef.get('UserGroupInvitationEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
}
@ -111,9 +107,6 @@ export class NotificationEntityService implements OnModuleInit {
_hint_: options._hintForEachNotes_,
}),
} : {}),
...(notification.type === 'groupInvited' ? {
invitation: this.userGroupInvitationEntityService.pack(notification.userGroupInvitationId!),
} : {}),
...(notification.type === 'achievementEarned' ? {
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 { LocalUser, RemoteUser, User } 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, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile } from '@/models/index.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 { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { OnModuleInit } from '@nestjs/common';
@ -102,12 +102,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.announcementReadsRepository)
private announcementReadsRepository: AnnouncementReadsRepository,
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
@ -204,36 +198,6 @@ export class UserEntityService implements OnModuleInit {
});
}
@bindThis
public async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
const mute = await this.mutingsRepository.findBy({
muterId: userId,
});
const joinings = await this.userGroupJoiningsRepository.findBy({ userId: userId });
const groupQs = Promise.all(joinings.map(j => this.messagingMessagesRepository.createQueryBuilder('message')
.where('message.groupId = :groupId', { groupId: j.userGroupId })
.andWhere('message.userId != :userId', { userId: userId })
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
.getOne().then(x => x != null)));
const [withUser, withGroups] = await Promise.all([
this.messagingMessagesRepository.count({
where: {
recipientId: userId,
isRead: false,
...(mute.length > 0 ? { userId: Not(In(mute.map(x => x.muteeId))) } : {}),
},
take: 1,
}).then(count => count > 0),
groupQs,
]);
return withUser || withGroups.some(x => x);
}
@bindThis
public async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
const reads = await this.announcementReadsRepository.findBy({
@ -492,7 +456,6 @@ export class UserEntityService implements OnModuleInit {
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
hasUnreadChannel: this.getHasUnreadChannel(user.id),
hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
mutedWords: profile!.mutedWords,

View File

@ -1,44 +0,0 @@
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

@ -1,42 +0,0 @@
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,9 +24,6 @@ export const DI = {
userPublickeysRepository: Symbol('userPublickeysRepository'),
userListsRepository: Symbol('userListsRepository'),
userListJoiningsRepository: Symbol('userListJoiningsRepository'),
userGroupsRepository: Symbol('userGroupsRepository'),
userGroupJoiningsRepository: Symbol('userGroupJoiningsRepository'),
userGroupInvitationsRepository: Symbol('userGroupInvitationsRepository'),
userNotePiningsRepository: Symbol('userNotePiningsRepository'),
userIpsRepository: Symbol('userIpsRepository'),
usedUsernamesRepository: Symbol('usedUsernamesRepository'),
@ -47,7 +44,6 @@ export const DI = {
authSessionsRepository: Symbol('authSessionsRepository'),
accessTokensRepository: Symbol('accessTokensRepository'),
signinsRepository: Symbol('signinsRepository'),
messagingMessagesRepository: Symbol('messagingMessagesRepository'),
pagesRepository: Symbol('pagesRepository'),
pageLikesRepository: Symbol('pageLikesRepository'),
galleryPostsRepository: Symbol('galleryPostsRepository'),

View File

@ -10,7 +10,6 @@ import {
import { packedNoteSchema } from '@/models/schema/note.js';
import { packedUserListSchema } from '@/models/schema/user-list.js';
import { packedAppSchema } from '@/models/schema/app.js';
import { packedMessagingMessageSchema } from '@/models/schema/messaging-message.js';
import { packedNotificationSchema } from '@/models/schema/notification.js';
import { packedDriveFileSchema } from '@/models/schema/drive-file.js';
import { packedDriveFolderSchema } from '@/models/schema/drive-folder.js';
@ -20,7 +19,6 @@ import { packedBlockingSchema } from '@/models/schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/schema/hashtag.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 { packedChannelSchema } from '@/models/schema/channel.js';
import { packedAntennaSchema } from '@/models/schema/antenna.js';
@ -40,9 +38,7 @@ export const refs = {
User: packedUserSchema,
UserList: packedUserListSchema,
UserGroup: packedUserGroupSchema,
App: packedAppSchema,
MessagingMessage: packedMessagingMessageSchema,
Note: packedNoteSchema,
NoteReaction: packedNoteReactionSchema,
NoteFavorite: packedNoteFavoriteSchema,

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
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, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, 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, 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 { Provider } from '@nestjs/common';
@ -118,24 +118,6 @@ const $userListJoiningsRepository: Provider = {
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 = {
provide: DI.userNotePiningsRepository,
useFactory: (db: DataSource) => db.getRepository(UserNotePining),
@ -256,12 +238,6 @@ const $signinsRepository: Provider = {
inject: [DI.db],
};
const $messagingMessagesRepository: Provider = {
provide: DI.messagingMessagesRepository,
useFactory: (db: DataSource) => db.getRepository(MessagingMessage),
inject: [DI.db],
};
const $pagesRepository: Provider = {
provide: DI.pagesRepository,
useFactory: (db: DataSource) => db.getRepository(Page),
@ -435,9 +411,6 @@ const $roleAssignmentsRepository: Provider = {
$userPublickeysRepository,
$userListsRepository,
$userListJoiningsRepository,
$userGroupsRepository,
$userGroupJoiningsRepository,
$userGroupInvitationsRepository,
$userNotePiningsRepository,
$userIpsRepository,
$usedUsernamesRepository,
@ -458,7 +431,6 @@ const $roleAssignmentsRepository: Provider = {
$authSessionsRepository,
$accessTokensRepository,
$signinsRepository,
$messagingMessagesRepository,
$pagesRepository,
$pageLikesRepository,
$galleryPostsRepository,
@ -505,9 +477,6 @@ const $roleAssignmentsRepository: Provider = {
$userPublickeysRepository,
$userListsRepository,
$userListJoiningsRepository,
$userGroupsRepository,
$userGroupJoiningsRepository,
$userGroupInvitationsRepository,
$userNotePiningsRepository,
$userIpsRepository,
$usedUsernamesRepository,
@ -528,7 +497,6 @@ const $roleAssignmentsRepository: Provider = {
$authSessionsRepository,
$accessTokensRepository,
$signinsRepository,
$messagingMessagesRepository,
$pagesRepository,
$pageLikesRepository,
$galleryPostsRepository,

View File

@ -18,6 +18,12 @@ export class Ad {
})
public expiresAt: Date;
@Index()
@Column('timestamp with time zone', {
comment: 'The expired date of the Ad.',
})
public startAt: Date;
@Column('varchar', {
length: 32, nullable: false,
})

View File

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

View File

@ -1,89 +0,0 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from '../id.js';
import { User } from './User.js';
import { DriveFile } from './DriveFile.js';
import { UserGroup } from './UserGroup.js';
@Entity()
export class MessagingMessage {
@PrimaryColumn(id())
public id: string;
@Index()
@Column('timestamp with time zone', {
comment: 'The created date of the MessagingMessage.',
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The sender user ID.',
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: User | null;
@Index()
@Column({
...id(), nullable: true,
comment: 'The recipient user ID.',
})
public recipientId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public recipient: User | null;
@Index()
@Column({
...id(), nullable: true,
comment: 'The recipient group ID.',
})
public groupId: UserGroup['id'] | null;
@ManyToOne(type => UserGroup, {
onDelete: 'CASCADE',
})
@JoinColumn()
public group: UserGroup | null;
@Column('varchar', {
length: 4096, nullable: true,
})
public text: string | null;
@Column('boolean', {
default: false,
})
public isRead: boolean;
@Column('varchar', {
length: 512, nullable: true,
})
public uri: string | null;
@Column({
...id(),
array: true, default: '{}',
})
public reads: User['id'][];
@Column({
...id(),
nullable: true,
})
public fileId: DriveFile['id'] | null;
@ManyToOne(type => DriveFile, {
onDelete: 'CASCADE',
})
@JoinColumn()
public file: DriveFile | null;
}

View File

@ -4,7 +4,6 @@ import { id } from '../id.js';
import { User } from './User.js';
import { Note } from './Note.js';
import { FollowRequest } from './FollowRequest.js';
import { UserGroupInvitation } from './UserGroupInvitation.js';
import { AccessToken } from './AccessToken.js';
@Entity()
@ -63,7 +62,6 @@ export class Notification {
* pollEnded -
* receiveFollowRequest -
* followRequestAccepted -
* groupInvited -
* achievementEarned -
* app -
*/
@ -108,18 +106,6 @@ export class Notification {
@JoinColumn()
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', {
length: 128, nullable: true,
})

View File

@ -1,46 +0,0 @@
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

@ -1,42 +0,0 @@
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

@ -1,42 +0,0 @@
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;
@Column('jsonb', {
default: ['follow', 'receiveFollowRequest', 'groupInvited'],
default: ['follow', 'receiveFollowRequest'],
})
public emailNotificationTypes: string[];

View File

@ -22,7 +22,6 @@ import { GalleryLike } from '@/models/entities/GalleryLike.js';
import { GalleryPost } from '@/models/entities/GalleryPost.js';
import { Hashtag } from '@/models/entities/Hashtag.js';
import { Instance } from '@/models/entities/Instance.js';
import { MessagingMessage } from '@/models/entities/MessagingMessage.js';
import { Meta } from '@/models/entities/Meta.js';
import { ModerationLog } from '@/models/entities/ModerationLog.js';
import { MutedNote } from '@/models/entities/MutedNote.js';
@ -47,9 +46,6 @@ import { Signin } from '@/models/entities/Signin.js';
import { SwSubscription } from '@/models/entities/SwSubscription.js';
import { UsedUsername } from '@/models/entities/UsedUsername.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 { UserKeypair } from '@/models/entities/UserKeypair.js';
import { UserList } from '@/models/entities/UserList.js';
@ -93,7 +89,6 @@ export {
GalleryPost,
Hashtag,
Instance,
MessagingMessage,
Meta,
ModerationLog,
MutedNote,
@ -118,9 +113,6 @@ export {
SwSubscription,
UsedUsername,
User,
UserGroup,
UserGroupInvitation,
UserGroupJoining,
UserIp,
UserKeypair,
UserList,
@ -163,7 +155,6 @@ export type GalleryLikesRepository = Repository<GalleryLike>;
export type GalleryPostsRepository = Repository<GalleryPost>;
export type HashtagsRepository = Repository<Hashtag>;
export type InstancesRepository = Repository<Instance>;
export type MessagingMessagesRepository = Repository<MessagingMessage>;
export type MetasRepository = Repository<Meta>;
export type ModerationLogsRepository = Repository<ModerationLog>;
export type MutedNotesRepository = Repository<MutedNote>;
@ -188,9 +179,6 @@ export type SigninsRepository = Repository<Signin>;
export type SwSubscriptionsRepository = Repository<SwSubscription>;
export type UsedUsernamesRepository = Repository<UsedUsername>;
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 UserKeypairsRepository = Repository<UserKeypair>;
export type UserListsRepository = Repository<UserList>;

View File

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

View File

@ -1,73 +0,0 @@
export const packedMessagingMessageSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
userId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
user: {
type: 'object',
ref: 'UserLite',
optional: true, nullable: false,
},
text: {
type: 'string',
optional: false, nullable: true,
},
fileId: {
type: 'string',
optional: true, nullable: true,
format: 'id',
},
file: {
type: 'object',
optional: true, nullable: true,
ref: 'DriveFile',
},
recipientId: {
type: 'string',
optional: false, nullable: true,
format: 'id',
},
recipient: {
type: 'object',
optional: true, nullable: true,
ref: 'UserLite',
},
groupId: {
type: 'string',
optional: false, nullable: true,
format: 'id',
},
group: {
type: 'object',
optional: true, nullable: true,
ref: 'UserGroup',
},
isRead: {
type: 'boolean',
optional: true, nullable: false,
},
reads: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
},
} as const;

View File

@ -1,34 +0,0 @@
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

@ -311,10 +311,6 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
hasUnreadMessagingMessage: {
type: 'boolean',
nullable: false, optional: false,
},
hasUnreadNotification: {
type: 'boolean',
nullable: false, optional: false,

View File

@ -30,7 +30,6 @@ import { GalleryLike } from '@/models/entities/GalleryLike.js';
import { GalleryPost } from '@/models/entities/GalleryPost.js';
import { Hashtag } from '@/models/entities/Hashtag.js';
import { Instance } from '@/models/entities/Instance.js';
import { MessagingMessage } from '@/models/entities/MessagingMessage.js';
import { Meta } from '@/models/entities/Meta.js';
import { ModerationLog } from '@/models/entities/ModerationLog.js';
import { MutedNote } from '@/models/entities/MutedNote.js';
@ -55,9 +54,6 @@ import { Signin } from '@/models/entities/Signin.js';
import { SwSubscription } from '@/models/entities/SwSubscription.js';
import { UsedUsername } from '@/models/entities/UsedUsername.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 { UserKeypair } from '@/models/entities/UserKeypair.js';
import { UserList } from '@/models/entities/UserList.js';
@ -137,9 +133,6 @@ export const entities = [
UserPublickey,
UserList,
UserListJoining,
UserGroup,
UserGroupJoining,
UserGroupInvitation,
UserNotePining,
UserSecurityKey,
UsedUsername,
@ -167,7 +160,6 @@ export const entities = [
SwSubscription,
AbuseUserReport,
RegistrationTicket,
MessagingMessage,
Signin,
ModerationLog,
Clip,

View File

@ -30,8 +30,6 @@ import { HashtagChannelService } from './api/stream/channels/hashtag.js';
import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js';
import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js';
import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js';
import { MessagingIndexChannelService } from './api/stream/channels/messaging-index.js';
import { MessagingChannelService } from './api/stream/channels/messaging.js';
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
import { UserListChannelService } from './api/stream/channels/user-list.js';
@ -71,8 +69,6 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
HomeTimelineChannelService,
HybridTimelineChannelService,
LocalTimelineChannelService,
MessagingIndexChannelService,
MessagingChannelService,
QueueStatsChannelService,
ServerStatsChannelService,
UserListChannelService,

View File

@ -195,7 +195,6 @@ import * as ep___i_notifications from './endpoints/i/notifications.js';
import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
import * as ep___i_pages from './endpoints/i/pages.js';
import * as ep___i_pin from './endpoints/i/pin.js';
import * as ep___i_readAllMessagingMessages from './endpoints/i/read-all-messaging-messages.js';
import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js';
import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js';
import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js';
@ -212,17 +211,11 @@ 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_updateEmail from './endpoints/i/update-email.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_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
import * as ep___messaging_history from './endpoints/messaging/history.js';
import * as ep___messaging_messages from './endpoints/messaging/messages.js';
import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js';
import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js';
import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js';
import * as ep___meta from './endpoints/meta.js';
import * as ep___emojis from './endpoints/emojis.js';
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
@ -300,18 +293,6 @@ import * as ep___users_followers from './endpoints/users/followers.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_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_delete from './endpoints/users/lists/delete.js';
import * as ep___users_lists_list from './endpoints/users/lists/list.js';
@ -530,7 +511,6 @@ const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep
const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default };
const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default };
const $i_pin: Provider = { provide: 'ep:i/pin', useClass: ep___i_pin.default };
const $i_readAllMessagingMessages: Provider = { provide: 'ep:i/read-all-messaging-messages', useClass: ep___i_readAllMessagingMessages.default };
const $i_readAllUnreadNotes: Provider = { provide: 'ep:i/read-all-unread-notes', useClass: ep___i_readAllUnreadNotes.default };
const $i_readAnnouncement: Provider = { provide: 'ep:i/read-announcement', useClass: ep___i_readAnnouncement.default };
const $i_regenerateToken: Provider = { provide: 'ep:i/regenerate-token', useClass: ep___i_regenerateToken.default };
@ -547,17 +527,11 @@ 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_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_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_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_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
const $messaging_history: Provider = { provide: 'ep:messaging/history', useClass: ep___messaging_history.default };
const $messaging_messages: Provider = { provide: 'ep:messaging/messages', useClass: ep___messaging_messages.default };
const $messaging_messages_create: Provider = { provide: 'ep:messaging/messages/create', useClass: ep___messaging_messages_create.default };
const $messaging_messages_delete: Provider = { provide: 'ep:messaging/messages/delete', useClass: ep___messaging_messages_delete.default };
const $messaging_messages_read: Provider = { provide: 'ep:messaging/messages/read', useClass: ep___messaging_messages_read.default };
const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default };
@ -635,18 +609,6 @@ 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_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_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_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 };
@ -869,7 +831,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_pageLikes,
$i_pages,
$i_pin,
$i_readAllMessagingMessages,
$i_readAllUnreadNotes,
$i_readAnnouncement,
$i_regenerateToken,
@ -886,17 +847,11 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_unpin,
$i_updateEmail,
$i_update,
$i_userGroupInvites,
$i_webhooks_create,
$i_webhooks_list,
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
$messaging_history,
$messaging_messages,
$messaging_messages_create,
$messaging_messages_delete,
$messaging_messages_read,
$meta,
$emojis,
$miauth_genToken,
@ -974,18 +929,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_following,
$users_gallery_posts,
$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_delete,
$users_lists_list,
@ -1202,7 +1145,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_pageLikes,
$i_pages,
$i_pin,
$i_readAllMessagingMessages,
$i_readAllUnreadNotes,
$i_readAnnouncement,
$i_regenerateToken,
@ -1219,17 +1161,11 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_unpin,
$i_updateEmail,
$i_update,
$i_userGroupInvites,
$i_webhooks_create,
$i_webhooks_list,
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
$messaging_history,
$messaging_messages,
$messaging_messages_create,
$messaging_messages_delete,
$messaging_messages_read,
$meta,
$emojis,
$miauth_genToken,
@ -1305,18 +1241,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_following,
$users_gallery_posts,
$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_delete,
$users_lists_list,

View File

@ -194,7 +194,6 @@ import * as ep___i_notifications from './endpoints/i/notifications.js';
import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
import * as ep___i_pages from './endpoints/i/pages.js';
import * as ep___i_pin from './endpoints/i/pin.js';
import * as ep___i_readAllMessagingMessages from './endpoints/i/read-all-messaging-messages.js';
import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js';
import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js';
import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js';
@ -211,17 +210,11 @@ 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_updateEmail from './endpoints/i/update-email.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_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
import * as ep___messaging_history from './endpoints/messaging/history.js';
import * as ep___messaging_messages from './endpoints/messaging/messages.js';
import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js';
import * as ep___messaging_messages_delete from './endpoints/messaging/messages/delete.js';
import * as ep___messaging_messages_read from './endpoints/messaging/messages/read.js';
import * as ep___meta from './endpoints/meta.js';
import * as ep___emojis from './endpoints/emojis.js';
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
@ -299,18 +292,6 @@ import * as ep___users_followers from './endpoints/users/followers.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_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_delete from './endpoints/users/lists/delete.js';
import * as ep___users_lists_list from './endpoints/users/lists/list.js';
@ -527,7 +508,6 @@ const eps = [
['i/page-likes', ep___i_pageLikes],
['i/pages', ep___i_pages],
['i/pin', ep___i_pin],
['i/read-all-messaging-messages', ep___i_readAllMessagingMessages],
['i/read-all-unread-notes', ep___i_readAllUnreadNotes],
['i/read-announcement', ep___i_readAnnouncement],
['i/regenerate-token', ep___i_regenerateToken],
@ -544,17 +524,11 @@ const eps = [
['i/unpin', ep___i_unpin],
['i/update-email', ep___i_updateEmail],
['i/update', ep___i_update],
['i/user-group-invites', ep___i_userGroupInvites],
['i/webhooks/create', ep___i_webhooks_create],
['i/webhooks/list', ep___i_webhooks_list],
['i/webhooks/show', ep___i_webhooks_show],
['i/webhooks/update', ep___i_webhooks_update],
['i/webhooks/delete', ep___i_webhooks_delete],
['messaging/history', ep___messaging_history],
['messaging/messages', ep___messaging_messages],
['messaging/messages/create', ep___messaging_messages_create],
['messaging/messages/delete', ep___messaging_messages_delete],
['messaging/messages/read', ep___messaging_messages_read],
['meta', ep___meta],
['emojis', ep___emojis],
['miauth/gen-token', ep___miauth_genToken],
@ -632,18 +606,6 @@ const eps = [
['users/following', ep___users_following],
['users/gallery/posts', ep___users_gallery_posts],
['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/delete', ep___users_lists_delete],
['users/lists/list', ep___users_lists_list],

View File

@ -20,9 +20,10 @@ export const paramDef = {
priority: { type: 'string' },
ratio: { type: 'integer' },
expiresAt: { type: 'integer' },
startAt: { type: 'integer' },
imageUrl: { type: 'string', minLength: 1 },
},
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'imageUrl'],
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startAt', 'imageUrl'],
} as const;
// eslint-disable-next-line import/no-default-export
@ -39,6 +40,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
id: this.idService.genId(),
createdAt: new Date(),
expiresAt: new Date(ps.expiresAt),
startAt: new Date(ps.startAt),
url: ps.url,
imageUrl: ps.imageUrl,
priority: ps.priority,

View File

@ -32,8 +32,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId)
.andWhere('ad.expiresAt > :now', { now: new Date() });
const ads = await query.take(ps.limit).getMany();
return ads;

View File

@ -30,8 +30,9 @@ export const paramDef = {
priority: { type: 'string' },
ratio: { type: 'integer' },
expiresAt: { type: 'integer' },
startAt: { type: 'integer' },
},
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt'],
required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startAt'],
} as const;
// eslint-disable-next-line import/no-default-export
@ -54,6 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
memo: ps.memo,
imageUrl: ps.imageUrl,
expiresAt: new Date(ps.expiresAt),
startAt: new Date(ps.startAt),
});
});
}

View File

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

View File

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

View File

@ -1,56 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['account', 'messaging'],
requireCredential: true,
kind: 'write:account',
} 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.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
// Update documents
await this.messagingMessagesRepository.update({
recipientId: me.id,
isRead: false,
}, {
isRead: true,
});
const joinings = await this.userGroupJoiningsRepository.findBy({ userId: me.id });
await Promise.all(joinings.map(j => this.messagingMessagesRepository.createQueryBuilder().update()
.set({
reads: (() => `array_append("reads", '${me.id}')`) as any,
})
.where('groupId = :groupId', { groupId: j.userGroupId })
.andWhere('userId != :userId', { userId: me.id })
.andWhere('NOT (:userId = ANY(reads))', { userId: me.id })
.execute()));
this.globalEventService.publishMainStream(me.id, 'readAllMessagingMessages');
});
}
}

View File

@ -1,69 +0,0 @@
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

@ -1,110 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
import type { MutingsRepository, UserGroupJoiningsRepository, MessagingMessagesRepository } from '@/models/index.js';
import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['messaging'],
requireCredential: true,
kind: 'read:messaging',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'MessagingMessage',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
group: { type: 'boolean', default: false },
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private messagingMessageEntityService: MessagingMessageEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const mute = await this.mutingsRepository.findBy({
muterId: me.id,
});
const groups = ps.group ? await this.userGroupJoiningsRepository.findBy({
userId: me.id,
}).then(xs => xs.map(x => x.userGroupId)) : [];
if (ps.group && groups.length === 0) {
return [];
}
const history: MessagingMessage[] = [];
for (let i = 0; i < ps.limit; i++) {
const found = ps.group
? history.map(m => m.groupId!)
: history.map(m => (m.userId === me.id) ? m.recipientId! : m.userId!);
const query = this.messagingMessagesRepository.createQueryBuilder('message')
.orderBy('message.createdAt', 'DESC');
if (ps.group) {
query.where('message.groupId IN (:...groups)', { groups: groups });
if (found.length > 0) {
query.andWhere('message.groupId NOT IN (:...found)', { found: found });
}
} else {
query.where(new Brackets(qb => { qb
.where('message.userId = :userId', { userId: me.id })
.orWhere('message.recipientId = :userId', { userId: me.id });
}));
query.andWhere('message.groupId IS NULL');
if (found.length > 0) {
query.andWhere('message.userId NOT IN (:...found)', { found: found });
query.andWhere('message.recipientId NOT IN (:...found)', { found: found });
}
if (mute.length > 0) {
query.andWhere('message.userId NOT IN (:...mute)', { mute: mute.map(m => m.muteeId) });
query.andWhere('message.recipientId NOT IN (:...mute)', { mute: mute.map(m => m.muteeId) });
}
}
const message = await query.getOne();
if (message) {
history.push(message);
} else {
break;
}
}
return await Promise.all(history.map(h => this.messagingMessageEntityService.pack(h.id, me)));
});
}
}

View File

@ -1,165 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserGroupsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository } from '@/models/index.js';
import { QueryService } from '@/core/QueryService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { MessagingMessageEntityService } from '@/core/entities/MessagingMessageEntityService.js';
import { MessagingService } from '@/core/MessagingService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js';
export const meta = {
tags: ['messaging'],
requireCredential: true,
kind: 'read:messaging',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'MessagingMessage',
},
},
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '11795c64-40ea-4198-b06e-3c873ed9039d',
},
noSuchGroup: {
message: 'No such group.',
code: 'NO_SUCH_GROUP',
id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f',
},
groupAccessDenied: {
message: 'You can not read messages of groups that you have not joined.',
code: 'GROUP_ACCESS_DENIED',
id: 'a053a8dd-a491-4718-8f87-50775aad9284',
},
},
} 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' },
markAsRead: { type: 'boolean', default: true },
},
anyOf: [
{
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
},
{
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.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
@Inject(DI.userGroupsRepository)
private userGroupRepository: UserGroupsRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private messagingMessageEntityService: MessagingMessageEntityService,
private messagingService: MessagingService,
private userEntityService: UserEntityService,
private queryService: QueryService,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
if (ps.userId != null) {
// Fetch recipient (user)
const recipient = 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 query = this.queryService.makePaginationQuery(this.messagingMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => { qb
.where(new Brackets(qb => { qb
.where('message.userId = :meId')
.andWhere('message.recipientId = :recipientId');
}))
.orWhere(new Brackets(qb => { qb
.where('message.userId = :recipientId')
.andWhere('message.recipientId = :meId');
}));
}))
.setParameter('meId', me.id)
.setParameter('recipientId', recipient.id);
const messages = await query.take(ps.limit).getMany();
// Mark all as read
if (ps.markAsRead) {
this.messagingService.readUserMessagingMessage(me.id, recipient.id, messages.filter(m => m.recipientId === me.id).map(x => x.id));
// リモートユーザーとのメッセージだったら既読配信
if (this.userEntityService.isLocalUser(me) && this.userEntityService.isRemoteUser(recipient)) {
this.messagingService.deliverReadActivity(me, recipient, messages);
}
}
return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, {
populateRecipient: false,
})));
} else if (ps.groupId != null) {
// Fetch recipient (group)
const recipientGroup = await this.userGroupRepository.findOneBy({ id: ps.groupId });
if (recipientGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
// check joined
const joining = await this.userGroupJoiningsRepository.findOneBy({
userId: me.id,
userGroupId: recipientGroup.id,
});
if (joining == null) {
throw new ApiError(meta.errors.groupAccessDenied);
}
const query = this.queryService.makePaginationQuery(this.messagingMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId)
.andWhere('message.groupId = :groupId', { groupId: recipientGroup.id });
const messages = await query.take(ps.limit).getMany();
// Mark all as read
if (ps.markAsRead) {
this.messagingService.readGroupMessagingMessage(me.id, recipientGroup.id, messages.map(x => x.id));
}
return await Promise.all(messages.map(message => this.messagingMessageEntityService.pack(message, me, {
populateGroup: false,
})));
}
});
}
}

View File

@ -1,179 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { BlockingsRepository, UserGroupJoiningsRepository, DriveFilesRepository, UserGroupsRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import type { UserGroup } from '@/models/entities/UserGroup.js';
import { GetterService } from '@/server/api/GetterService.js';
import { MessagingService } from '@/core/MessagingService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['messaging'],
requireCredential: true,
kind: 'write:messaging',
limit: {
duration: ms('1hour'),
max: 120,
},
res: {
type: 'object',
optional: false, nullable: false,
ref: 'MessagingMessage',
},
errors: {
recipientIsYourself: {
message: 'You can not send a message to yourself.',
code: 'RECIPIENT_IS_YOURSELF',
id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e',
},
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '11795c64-40ea-4198-b06e-3c873ed9039d',
},
noSuchGroup: {
message: 'No such group.',
code: 'NO_SUCH_GROUP',
id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537',
},
groupAccessDenied: {
message: 'You can not send messages to groups that you have not joined.',
code: 'GROUP_ACCESS_DENIED',
id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd',
},
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: '4372b8e2-185d-4146-8749-2f68864a3e5f',
},
contentRequired: {
message: 'Content required. You need to set text or fileId.',
code: 'CONTENT_REQUIRED',
id: '25587321-b0e6-449c-9239-f8925092942c',
},
youHaveBeenBlocked: {
message: 'You cannot send a message because you have been blocked by this user.',
code: 'YOU_HAVE_BEEN_BLOCKED',
id: 'c15a5199-7422-4968-941a-2a462c478f7d',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
text: { type: 'string', nullable: true, maxLength: 3000 },
fileId: { type: 'string', format: 'misskey:id' },
},
anyOf: [
{
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
},
{
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,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private getterService: GetterService,
private messagingService: MessagingService,
) {
super(meta, paramDef, async (ps, me) => {
let recipientUser: User | null;
let recipientGroup: UserGroup | null;
if (ps.userId != null) {
// Myself
if (ps.userId === me.id) {
throw new ApiError(meta.errors.recipientIsYourself);
}
// Fetch recipient (user)
recipientUser = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;
});
// Check blocking
const block = await this.blockingsRepository.findOneBy({
blockerId: recipientUser.id,
blockeeId: me.id,
});
if (block) {
throw new ApiError(meta.errors.youHaveBeenBlocked);
}
} else if (ps.groupId != null) {
// Fetch recipient (group)
recipientGroup = await this.userGroupsRepository.findOneBy({ id: ps.groupId! });
if (recipientGroup == null) {
throw new ApiError(meta.errors.noSuchGroup);
}
// check joined
const joining = await this.userGroupJoiningsRepository.findOneBy({
userId: me.id,
userGroupId: recipientGroup.id,
});
if (joining == null) {
throw new ApiError(meta.errors.groupAccessDenied);
}
}
let file = null;
if (ps.fileId != null) {
file = await this.driveFilesRepository.findOneBy({
id: ps.fileId,
userId: me.id,
});
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
}
// テキストが無いかつ添付ファイルも無かったらエラー
if (ps.text == null && file == null) {
throw new ApiError(meta.errors.contentRequired);
}
return await this.messagingService.createMessage(me, recipientUser, recipientGroup, ps.text, file);
});
}
}

View File

@ -1,61 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MessagingMessagesRepository } from '@/models/index.js';
import { MessagingService } from '@/core/MessagingService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['messaging'],
requireCredential: true,
kind: 'write:messaging',
limit: {
duration: ms('1hour'),
max: 300,
minInterval: ms('1sec'),
},
errors: {
noSuchMessage: {
message: 'No such message.',
code: 'NO_SUCH_MESSAGE',
id: '54b5b326-7925-42cf-8019-130fda8b56af',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
messageId: { type: 'string', format: 'misskey:id' },
},
required: ['messageId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
private messagingService: MessagingService,
) {
super(meta, paramDef, async (ps, me) => {
const message = await this.messagingMessagesRepository.findOneBy({
id: ps.messageId,
userId: me.id,
});
if (message == null) {
throw new ApiError(meta.errors.noSuchMessage);
}
await this.messagingService.deleteMessage(message);
});
}
}

View File

@ -1,61 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MessagingMessagesRepository } from '@/models/index.js';
import { MessagingService } from '@/core/MessagingService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['messaging'],
requireCredential: true,
kind: 'write:messaging',
errors: {
noSuchMessage: {
message: 'No such message.',
code: 'NO_SUCH_MESSAGE',
id: '86d56a2f-a9c3-4afb-b13c-3e9bfef9aa14',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
messageId: { type: 'string', format: 'misskey:id' },
},
required: ['messageId'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
private messagingService: MessagingService,
) {
super(meta, paramDef, async (ps, me) => {
const message = await this.messagingMessagesRepository.findOneBy({ id: ps.messageId });
if (message == null) {
throw new ApiError(meta.errors.noSuchMessage);
}
if (message.recipientId) {
await this.messagingService.readUserMessagingMessage(me.id, message.userId, [message.id]).catch(err => {
if (err.id === 'e140a4bf-49ce-4fb6-b67c-b78dadf6b52f') throw new ApiError(meta.errors.noSuchMessage);
throw err;
});
} else if (message.groupId) {
await this.messagingService.readGroupMessagingMessage(me.id, message.groupId, [message.id]).catch(err => {
if (err.id === '930a270c-714a-46b2-b776-ad27276dc569') throw new ApiError(meta.errors.noSuchMessage);
throw err;
});
}
});
}
}

View File

@ -1,4 +1,4 @@
import { IsNull, MoreThan } from 'typeorm';
import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { AdsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
import { MAX_NOTE_TEXT_LENGTH, DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
@ -262,6 +262,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const ads = await this.adsRepository.find({
where: {
expiresAt: MoreThan(new Date()),
startAt: LessThanOrEqual(new Date()),
},
});

View File

@ -1,72 +0,0 @@
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

@ -1,53 +0,0 @@
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

@ -1,72 +0,0 @@
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

@ -1,57 +0,0 @@
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

@ -1,122 +0,0 @@
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

@ -1,61 +0,0 @@
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

@ -1,66 +0,0 @@
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

@ -1,50 +0,0 @@
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

@ -1,84 +0,0 @@
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

@ -1,74 +0,0 @@
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

@ -1,100 +0,0 @@
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

@ -1,68 +0,0 @@
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

@ -1,5 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { HybridTimelineChannelService } from './channels/hybrid-timeline.js';
import { LocalTimelineChannelService } from './channels/local-timeline.js';
import { HomeTimelineChannelService } from './channels/home-timeline.js';
@ -11,11 +12,8 @@ import { ServerStatsChannelService } from './channels/server-stats.js';
import { QueueStatsChannelService } from './channels/queue-stats.js';
import { UserListChannelService } from './channels/user-list.js';
import { AntennaChannelService } from './channels/antenna.js';
import { MessagingChannelService } from './channels/messaging.js';
import { MessagingIndexChannelService } from './channels/messaging-index.js';
import { DriveChannelService } from './channels/drive.js';
import { HashtagChannelService } from './channels/hashtag.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ChannelsService {
@ -29,8 +27,6 @@ export class ChannelsService {
private hashtagChannelService: HashtagChannelService,
private antennaChannelService: AntennaChannelService,
private channelChannelService: ChannelChannelService,
private messagingChannelService: MessagingChannelService,
private messagingIndexChannelService: MessagingIndexChannelService,
private driveChannelService: DriveChannelService,
private serverStatsChannelService: ServerStatsChannelService,
private queueStatsChannelService: QueueStatsChannelService,
@ -50,8 +46,6 @@ export class ChannelsService {
case 'hashtag': return this.hashtagChannelService;
case 'antenna': return this.antennaChannelService;
case 'channel': return this.channelChannelService;
case 'messaging': return this.messagingChannelService;
case 'messagingIndex': return this.messagingIndexChannelService;
case 'drive': return this.driveChannelService;
case 'serverStats': return this.serverStatsChannelService;
case 'queueStats': return this.queueStatsChannelService;

View File

@ -1,32 +1,25 @@
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository, UsersRepository } from '@/models/index.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import type { User } from '@/models/entities/User.js';
import type { Packed } from '@/misc/schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import Channel from '../channel.js';
import type { StreamMessages } from '../types.js';
class ChannelChannel extends Channel {
public readonly chName = 'channel';
public static shouldShare = false;
public static requireCredential = false;
private channelId: string;
private typers: Record<User['id'], Date> = {};
private emitTypersIntervalId: ReturnType<typeof setInterval>;
constructor(
private noteEntityService: NoteEntityService,
private userEntityService: UserEntityService,
id: string,
connection: Channel['connection'],
) {
super(id, connection);
//this.onNote = this.onNote.bind(this);
//this.emitTypers = this.emitTypers.bind(this);
}
@bindThis
@ -35,8 +28,6 @@ class ChannelChannel extends Channel {
// Subscribe stream
this.subscriber.on('notesStream', this.onNote);
this.subscriber.on(`channelStream:${this.channelId}`, this.onEvent);
this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
}
@bindThis
@ -66,42 +57,10 @@ class ChannelChannel extends Channel {
this.send('note', note);
}
@bindThis
private onEvent(data: StreamMessages['channel']['payload']) {
if (data.type === 'typing') {
const id = data.body;
const begin = this.typers[id] == null;
this.typers[id] = new Date();
if (begin) {
this.emitTypers();
}
}
}
@bindThis
private async emitTypers() {
const now = new Date();
// Remove not typing users
for (const [userId, date] of Object.entries(this.typers)) {
if (now.getTime() - date.getTime() > 5000) delete this.typers[userId];
}
const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { detail: false });
this.send({
type: 'typers',
body: users,
});
}
@bindThis
public dispose() {
// Unsubscribe events
this.subscriber.off('notesStream', this.onNote);
this.subscriber.off(`channelStream:${this.channelId}`, this.onEvent);
clearInterval(this.emitTypersIntervalId);
}
}
@ -112,7 +71,6 @@ export class ChannelChannelService {
constructor(
private noteEntityService: NoteEntityService,
private userEntityService: UserEntityService,
) {
}
@ -120,7 +78,6 @@ export class ChannelChannelService {
public create(id: string, connection: Channel['connection']): ChannelChannel {
return new ChannelChannel(
this.noteEntityService,
this.userEntityService,
id,
connection,
);

View File

@ -1,35 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import Channel from '../channel.js';
class MessagingIndexChannel extends Channel {
public readonly chName = 'messagingIndex';
public static shouldShare = true;
public static requireCredential = true;
@bindThis
public async init(params: any) {
// Subscribe messaging index stream
this.subscriber.on(`messagingIndexStream:${this.user!.id}`, data => {
this.send(data);
});
}
}
@Injectable()
export class MessagingIndexChannelService {
public readonly shouldShare = MessagingIndexChannel.shouldShare;
public readonly requireCredential = MessagingIndexChannel.requireCredential;
constructor(
) {
}
@bindThis
public create(id: string, connection: Channel['connection']): MessagingIndexChannel {
return new MessagingIndexChannel(
id,
connection,
);
}
}

View File

@ -1,159 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import type { UserGroupJoiningsRepository, UsersRepository, MessagingMessagesRepository } from '@/models/index.js';
import type { User, LocalUser, RemoteUser } from '@/models/entities/User.js';
import type { UserGroup } from '@/models/entities/UserGroup.js';
import { MessagingService } from '@/core/MessagingService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import Channel from '../channel.js';
import type { StreamMessages } from '../types.js';
class MessagingChannel extends Channel {
public readonly chName = 'messaging';
public static shouldShare = false;
public static requireCredential = true;
private otherpartyId: string | null;
private otherparty: User | null;
private groupId: string | null;
private subCh: `messagingStream:${User['id']}-${User['id']}` | `messagingStream:${UserGroup['id']}`;
private typers: Record<User['id'], Date> = {};
private emitTypersIntervalId: ReturnType<typeof setInterval>;
constructor(
private usersRepository: UsersRepository,
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private messagingMessagesRepository: MessagingMessagesRepository,
private userEntityService: UserEntityService,
private messagingService: MessagingService,
id: string,
connection: Channel['connection'],
) {
super(id, connection);
//this.onEvent = this.onEvent.bind(this);
//this.onMessage = this.onMessage.bind(this);
//this.emitTypers = this.emitTypers.bind(this);
}
@bindThis
public async init(params: any) {
this.otherpartyId = params.otherparty;
this.otherparty = this.otherpartyId ? await this.usersRepository.findOneByOrFail({ id: this.otherpartyId }) : null;
this.groupId = params.group;
// Check joining
if (this.groupId) {
const joining = await this.userGroupJoiningsRepository.findOneBy({
userId: this.user!.id,
userGroupId: this.groupId,
});
if (joining == null) {
return;
}
}
this.emitTypersIntervalId = setInterval(this.emitTypers, 5000);
this.subCh = this.otherpartyId
? `messagingStream:${this.user!.id}-${this.otherpartyId}`
: `messagingStream:${this.groupId}`;
// Subscribe messaging stream
this.subscriber.on(this.subCh, this.onEvent);
}
@bindThis
private onEvent(data: StreamMessages['messaging']['payload'] | StreamMessages['groupMessaging']['payload']) {
if (data.type === 'typing') {
const id = data.body;
const begin = this.typers[id] == null;
this.typers[id] = new Date();
if (begin) {
this.emitTypers();
}
} else {
this.send(data);
}
}
@bindThis
public onMessage(type: string, body: any) {
switch (type) {
case 'read':
if (this.otherpartyId) {
this.messagingService.readUserMessagingMessage(this.user!.id, this.otherpartyId, [body.id]);
// リモートユーザーからのメッセージだったら既読配信
if (this.userEntityService.isLocalUser(this.user!) && this.userEntityService.isRemoteUser(this.otherparty!)) {
this.messagingMessagesRepository.findOneBy({ id: body.id }).then(message => {
if (message) this.messagingService.deliverReadActivity(this.user as LocalUser, this.otherparty as RemoteUser, message);
});
}
} else if (this.groupId) {
this.messagingService.readGroupMessagingMessage(this.user!.id, this.groupId, [body.id]);
}
break;
}
}
@bindThis
private async emitTypers() {
const now = new Date();
// Remove not typing users
for (const [userId, date] of Object.entries(this.typers)) {
if (now.getTime() - date.getTime() > 5000) delete this.typers[userId];
}
const users = await this.userEntityService.packMany(Object.keys(this.typers), null, { detail: false });
this.send({
type: 'typers',
body: users,
});
}
@bindThis
public dispose() {
this.subscriber.off(this.subCh, this.onEvent);
clearInterval(this.emitTypersIntervalId);
}
}
@Injectable()
export class MessagingChannelService {
public readonly shouldShare = MessagingChannel.shouldShare;
public readonly requireCredential = MessagingChannel.requireCredential;
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userGroupJoiningsRepository)
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
@Inject(DI.messagingMessagesRepository)
private messagingMessagesRepository: MessagingMessagesRepository,
private userEntityService: UserEntityService,
private messagingService: MessagingService,
) {
}
@bindThis
public create(id: string, connection: Channel['connection']): MessagingChannel {
return new MessagingChannel(
this.usersRepository,
this.userGroupJoiningsRepository,
this.messagingMessagesRepository,
this.userEntityService,
this.messagingService,
id,
connection,
);
}
}

View File

@ -3,7 +3,6 @@ import type { Channel as ChannelModel } from '@/models/entities/Channel.js';
import type { FollowingsRepository, MutingsRepository, UserProfilesRepository, ChannelFollowingsRepository, BlockingsRepository } from '@/models/index.js';
import type { AccessToken } from '@/models/entities/AccessToken.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 { GlobalEventService } from '@/core/GlobalEventService.js';
import type { NoteReadService } from '@/core/NoteReadService.js';
@ -147,12 +146,6 @@ export default class Connection {
case 'disconnect': this.onChannelDisconnectRequested(body); break;
case 'channel': this.onChannelMessageRequested(body); break;
case 'ch': this.onChannelMessageRequested(body); break; // alias
// 個々のチャンネルではなくルートレベルでこれらのメッセージを受け取る理由は、
// クライアントの事情を考慮したとき、入力フォームはノートチャンネルやメッセージのメインコンポーネントとは別
// なこともあるため、それらのコンポーネントがそれぞれ各チャンネルに接続するようにするのは面倒なため。
case 'typingOnChannel': this.typingOnChannel(body.channel); break;
case 'typingOnMessaging': this.typingOnMessaging(body); break;
}
}
@ -325,24 +318,6 @@ export default class Connection {
}
}
@bindThis
private typingOnChannel(channel: ChannelModel['id']) {
if (this.user) {
this.globalEventService.publishChannelStream(channel, 'typing', this.user.id);
}
}
@bindThis
private typingOnMessaging(param: { partner?: User['id']; group?: UserGroup['id']; }) {
if (this.user) {
if (param.partner) {
this.globalEventService.publishMessagingStream(param.partner, this.user.id, 'typing', this.user.id);
} else if (param.group) {
this.globalEventService.publishGroupMessagingStream(param.group, 'typing', this.user.id);
}
}
}
@bindThis
private async updateFollowing() {
const followings = await this.followingsRepository.find({

View File

@ -6,8 +6,6 @@ import type { Antenna } from '@/models/entities/Antenna.js';
import type { DriveFile } from '@/models/entities/DriveFile.js';
import type { DriveFolder } from '@/models/entities/DriveFolder.js';
import type { UserList } from '@/models/entities/UserList.js';
import type { MessagingMessage } from '@/models/entities/MessagingMessage.js';
import type { UserGroup } from '@/models/entities/UserGroup.js';
import type { AbuseUserReport } from '@/models/entities/AbuseUserReport.js';
import type { Signin } from '@/models/entities/Signin.js';
import type { Page } from '@/models/entities/Page.js';
@ -96,9 +94,6 @@ export interface MainStreamTypes {
readAllUnreadMentions: undefined;
unreadSpecifiedNote: Note['id'];
readAllUnreadSpecifiedNotes: undefined;
readAllMessagingMessages: undefined;
messagingMessage: Packed<'MessagingMessage'>;
unreadMessagingMessage: Packed<'MessagingMessage'>;
readAllAntennas: undefined;
unreadAntenna: Antenna;
readAllAnnouncements: undefined;
@ -153,10 +148,6 @@ type NoteStreamEventTypes = {
};
};
export interface ChannelStreamTypes {
typing: User['id'];
}
export interface UserListStreamTypes {
userAdded: Packed<'User'>;
userRemoved: Packed<'User'>;
@ -166,28 +157,6 @@ export interface AntennaStreamTypes {
note: Note;
}
export interface MessagingStreamTypes {
read: MessagingMessage['id'][];
typing: User['id'];
message: Packed<'MessagingMessage'>;
deleted: MessagingMessage['id'];
}
export interface GroupMessagingStreamTypes {
read: {
ids: MessagingMessage['id'][];
userId: User['id'];
};
typing: User['id'];
message: Packed<'MessagingMessage'>;
deleted: MessagingMessage['id'];
}
export interface MessagingIndexStreamTypes {
read: MessagingMessage['id'][];
message: Packed<'MessagingMessage'>;
}
export interface AdminStreamTypes {
newAbuseUserReport: {
id: AbuseUserReport['id'];
@ -242,10 +211,6 @@ export type StreamMessages = {
name: `noteStream:${Note['id']}`;
payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>;
};
channel: {
name: `channelStream:${Channel['id']}`;
payload: EventUnionFromDictionary<SerializedAll<ChannelStreamTypes>>;
};
userList: {
name: `userListStream:${UserList['id']}`;
payload: EventUnionFromDictionary<SerializedAll<UserListStreamTypes>>;
@ -254,18 +219,6 @@ export type StreamMessages = {
name: `antennaStream:${Antenna['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AntennaStreamTypes>>;
};
messaging: {
name: `messagingStream:${User['id']}-${User['id']}`;
payload: EventUnionFromDictionary<SerializedAll<MessagingStreamTypes>>;
};
groupMessaging: {
name: `messagingStream:${UserGroup['id']}`;
payload: EventUnionFromDictionary<SerializedAll<GroupMessagingStreamTypes>>;
};
messagingIndex: {
name: `messagingIndexStream:${User['id']}`;
payload: EventUnionFromDictionary<SerializedAll<MessagingIndexStreamTypes>>;
};
admin: {
name: `adminStream:${User['id']}`;
payload: EventUnionFromDictionary<SerializedAll<AdminStreamTypes>>;

View File

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

View File

@ -778,63 +778,6 @@ describe('API: Endpoints', () => {
}));
});
describe('messaging/messages/create', () => {
test('メッセージを送信できる', async () => {
const res = await api('/messaging/messages/create', {
userId: bob.id,
text: 'test'
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.text, 'test');
}));
test('自分自身にはメッセージを送信できない', async () => {
const res = await api('/messaging/messages/create', {
userId: alice.id,
text: 'Yo'
}, alice);
assert.strictEqual(res.status, 400);
}));
test('存在しないユーザーにはメッセージを送信できない', async () => {
const res = await api('/messaging/messages/create', {
userId: '000000000000000000000000',
text: 'test'
}, alice);
assert.strictEqual(res.status, 400);
}));
test('不正なユーザーIDで怒られる', async () => {
const res = await api('/messaging/messages/create', {
userId: 'foo',
text: 'test'
}, alice);
assert.strictEqual(res.status, 400);
}));
test('テキストが無くて怒られる', async () => {
const res = await api('/messaging/messages/create', {
userId: bob.id
}, alice);
assert.strictEqual(res.status, 400);
}));
test('文字数オーバーで怒られる', async () => {
const res = await api('/messaging/messages/create', {
userId: bob.id,
text: '!'.repeat(1001)
}, alice);
assert.strictEqual(res.status, 400);
}));
});
describe('notes/replies', () => {
test('自分に閲覧権限のない投稿は含まれない', async () => {
const alicePost = await post(alice, {

View File

@ -9,7 +9,6 @@
<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 === '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 === 'reply'" class="ti ti-arrow-back-up"></i>
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
@ -74,12 +73,6 @@
<button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button>
</div>
</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">
<Mfm :text="notification.body" :nowrap="false"/>
</span>
@ -145,7 +138,6 @@ onUnmounted(() => {
});
const followRequestDone = ref(false);
const groupInviteDone = ref(false);
const acceptFollowRequest = () => {
followRequestDone.value = true;
@ -157,16 +149,6 @@ const rejectFollowRequest = () => {
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) => {
os.popup(XReactionTooltip, {
showing,
@ -224,7 +206,7 @@ useTooltip(reactionRef, (showing) => {
}
}
.t_follow, .t_followRequestAccepted, .t_receiveFollowRequest, .t_groupInvited {
.t_follow, .t_followRequestAccepted, .t_receiveFollowRequest {
padding: 3px;
background: #36aed2;
pointer-events: none;

View File

@ -162,12 +162,6 @@ let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') || '[]')
let imeText = $ref('');
let disableRightClick = $ref(false);
const typing = throttle(3000, () => {
if (props.channel) {
stream.send('typingOnChannel', { channel: props.channel.id });
}
});
const draftKey = $computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : '';
@ -463,12 +457,10 @@ function onKeydown(ev: KeyboardEvent) {
}
if (ev.which === 27) emit('esc');
typing();
}
function onCompositionUpdate(ev: CompositionEvent) {
imeText = ev.data;
typing();
}
function onCompositionEnd(ev: CompositionEvent) {

View File

@ -508,15 +508,6 @@ if ($i) {
updateAccount({ hasUnreadSpecifiedNotes: false });
});
main.on('readAllMessagingMessages', () => {
updateAccount({ hasUnreadMessagingMessage: false });
});
main.on('unreadMessagingMessage', () => {
updateAccount({ hasUnreadMessagingMessage: true });
sound.play('chatBg');
});
main.on('readAllAntennas', () => {
updateAccount({ hasUnreadAntenna: false });
});

View File

@ -15,13 +15,6 @@ export const navbarItemDef = reactive({
indicated: computed(() => $i != null && $i.hasUnreadNotification),
to: '/my/notifications',
},
messaging: {
title: i18n.ts.messaging,
icon: 'ti ti-messages',
show: computed(() => $i != null),
indicated: computed(() => $i != null && $i.hasUnreadMessagingMessage),
to: '/my/messaging',
},
drive: {
title: i18n.ts.drive,
icon: 'ti ti-cloud',
@ -57,14 +50,6 @@ export const navbarItemDef = reactive({
show: computed(() => $i != null),
to: '/my/lists',
},
/*
groups: {
title: i18n.ts.groups,
icon: 'ti ti-users',
show: computed(() => $i != null),
to: '/my/groups',
},
*/
antennas: {
title: i18n.ts.antennas,
icon: 'ti ti-antenna',

View File

@ -29,6 +29,9 @@
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ i18n.ts.ratio }}</template>
</MkInput>
<MkInput v-model="ad.startAt" type="datetime-local">
<template #label>{{ i18n.ts.startingperiod }}</template>
</MkInput>
<MkInput v-model="ad.expiresAt" type="datetime-local">
<template #label>{{ i18n.ts.expiration }}</template>
</MkInput>
@ -66,11 +69,14 @@ const localTimeDiff = localTime.getTimezoneOffset() * 60 * 1000;
os.api('admin/ad/list').then(adsResponse => {
ads = adsResponse.map(r => {
const date = new Date(r.expiresAt);
date.setMilliseconds(date.getMilliseconds() - localTimeDiff);
const exdate = new Date(r.expiresAt);
const stdate = new Date(r.startAt);
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
return {
...r,
expiresAt: date.toISOString().slice(0, 16),
expiresAt: exdate.toISOString().slice(0, 16),
startAt: stdate.toISOString().slice(0, 16),
};
});
});
@ -85,6 +91,7 @@ function add() {
url: '',
imageUrl: null,
expiresAt: null,
startAt: null,
});
}
@ -106,11 +113,13 @@ function save(ad) {
os.apiWithDialog('admin/ad/create', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime(),
startAt: new Date(ad.startAt).getTime(),
});
} else {
os.apiWithDialog('admin/ad/update', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime(),
startAt: new Date(ad.startAt).getTime(),
});
}
}

View File

@ -1,305 +0,0 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
<div class="yweeujhr">
<MkButton primary class="start" @click="start"><i class="ti ti-plus"></i> {{ $ts.startMessaging }}</MkButton>
<div v-if="messages.length > 0" class="history">
<MkA
v-for="(message, i) in messages"
:key="message.id"
v-anim="i"
class="message _panel"
:class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
:to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
:data-index="i"
>
<div>
<MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" indicator link preview/>
<header v-if="message.groupId">
<span class="name">{{ message.group.name }}</span>
<MkTime :time="message.createdAt" class="time"/>
</header>
<header v-else>
<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
<MkTime :time="message.createdAt" class="time"/>
</header>
<div class="body">
<p class="text"><span v-if="isMe(message)" class="me">{{ $ts.you }}:</span>{{ message.text }}</p>
</div>
</div>
</MkA>
</div>
<div v-if="!fetching && messages.length == 0" class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $ts.noHistory }}</div>
</div>
<MkLoading v-if="fetching"/>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, defineComponent, inject, markRaw, onMounted, onUnmounted } from 'vue';
import * as Acct from 'misskey-js/built/acct';
import MkButton from '@/components/MkButton.vue';
import { acct } from '@/filters/user';
import * as os from '@/os';
import { stream } from '@/stream';
import { useRouter } from '@/router';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import { $i } from '@/account';
const router = useRouter();
let fetching = $ref(true);
let moreFetching = $ref(false);
let messages = $ref([]);
let connection = $ref(null);
const getAcct = Acct.toString;
function isMe(message) {
return message.userId === $i.id;
}
function onMessage(message) {
if (message.recipientId) {
messages = messages.filter(m => !(
(m.recipientId === message.recipientId && m.userId === message.userId) ||
(m.recipientId === message.userId && m.userId === message.recipientId)));
messages.unshift(message);
} else if (message.groupId) {
messages = messages.filter(m => m.groupId !== message.groupId);
messages.unshift(message);
}
}
function onRead(ids) {
for (const id of ids) {
const found = messages.find(m => m.id === id);
if (found) {
if (found.recipientId) {
found.isRead = true;
} else if (found.groupId) {
found.reads.push($i.id);
}
}
}
}
function start(ev) {
os.popupMenu([{
text: i18n.ts.messagingWithUser,
icon: 'ti ti-user',
action: () => { startUser(); },
}, {
text: i18n.ts.messagingWithGroup,
icon: 'ti ti-users',
action: () => { startGroup(); },
}], ev.currentTarget ?? ev.target);
}
async function startUser() {
os.selectUser().then(user => {
router.push(`/my/messaging/${Acct.toString(user)}`);
});
}
async function startGroup() {
const groups1 = await os.api('users/groups/owned');
const groups2 = await os.api('users/groups/joined');
if (groups1.length === 0 && groups2.length === 0) {
os.alert({
type: 'warning',
title: i18n.ts.youHaveNoGroups,
text: i18n.ts.joinOrCreateGroup,
});
return;
}
const { canceled, result: group } = await os.select({
title: i18n.ts.group,
items: groups1.concat(groups2).map(group => ({
value: group, text: group.name,
})),
});
if (canceled) return;
router.push(`/my/messaging/group/${group.id}`);
}
onMounted(() => {
connection = markRaw(stream.useChannel('messagingIndex'));
connection.on('message', onMessage);
connection.on('read', onRead);
os.api('messaging/history', { group: false }).then(userMessages => {
os.api('messaging/history', { group: true }).then(groupMessages => {
const _messages = userMessages.concat(groupMessages);
_messages.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
messages = _messages;
fetching = false;
});
});
});
onUnmounted(() => {
if (connection) connection.dispose();
});
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.messaging,
icon: 'ti ti-messages',
});
</script>
<style lang="scss" scoped>
.yweeujhr {
> .start {
margin: 0 auto var(--margin) auto;
}
> .history {
> .message {
display: block;
text-decoration: none;
margin-bottom: var(--margin);
* {
pointer-events: none;
user-select: none;
}
&:hover {
.avatar {
filter: saturate(200%);
}
}
&:active {
}
&.isRead,
&.isMe {
opacity: 0.8;
}
&:not(.isMe):not(.isRead) {
> div {
background-image: url("/client-assets/unread.svg");
background-repeat: no-repeat;
background-position: 0 center;
}
}
&:after {
content: "";
display: block;
clear: both;
}
> div {
padding: 20px 30px;
&:after {
content: "";
display: block;
clear: both;
}
> header {
display: flex;
align-items: center;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
> .name {
margin: 0;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1em;
font-weight: bold;
transition: all 0.1s ease;
}
> .username {
margin: 0 8px;
}
> .time {
margin: 0 0 0 auto;
}
}
> .avatar {
float: left;
width: 54px;
height: 54px;
margin: 0 16px 0 0;
border-radius: 8px;
transition: all 0.1s ease;
}
> .body {
> .text {
display: block;
margin: 0 0 0 0;
padding: 0;
overflow: hidden;
overflow-wrap: break-word;
font-size: 1.1em;
color: var(--faceText);
.me {
opacity: 0.7;
}
}
> .image {
display: block;
max-width: 100%;
max-height: 512px;
}
}
}
}
}
}
@container (max-width: 400px) {
.yweeujhr {
> .history {
> .message {
&:not(.isMe):not(.isRead) {
> div {
background-image: none;
border-left: solid 4px #3aa2dc;
}
}
> div {
padding: 16px;
font-size: 0.9em;
> .avatar {
margin: 0 12px 0 0;
}
}
}
}
}
}
</style>

View File

@ -1,372 +0,0 @@
<template>
<div
:class="$style['root']"
@dragover.stop="onDragover"
@drop.stop="onDrop"
>
<textarea
:class="$style['textarea']"
class="_acrylic"
ref="textEl"
v-model="text"
:placeholder="i18n.ts.inputMessageHere"
@keydown="onKeydown"
@compositionupdate="onCompositionUpdate"
@paste="onPaste"
></textarea>
<footer :class="$style['footer']">
<div v-if="file" :class="$style['file']" @click="file = null">{{ file.name }}</div>
<div :class="$style['buttons']">
<button class="_button" :class="$style['button']" @click="chooseFile"><i class="ti ti-photo-plus"></i></button>
<button class="_button" :class="$style['button']" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button class="_button" :class="[$style['button'], $style['send']]" :disabled="!canSend || sending" :title="i18n.ts.send" @click="send">
<template v-if="!sending"><i class="ti ti-send"></i></template><template v-if="sending"><MkLoading :em="true"/></template>
</button>
</div>
</footer>
<input :class="$style['file-input']" ref="fileEl" type="file" @change="onChangeFile"/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, watch } from 'vue';
import * as Misskey from 'misskey-js';
import autosize from 'autosize';
//import insertTextAtCursor from 'insert-text-at-cursor';
import { throttle } from 'throttle-debounce';
import { formatTimeString } from '@/scripts/format-time-string';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
import { stream } from '@/stream';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
//import { Autocomplete } from '@/scripts/autocomplete';
import { uploadFile } from '@/scripts/upload';
import { miLocalStorage } from '@/local-storage';
const props = defineProps<{
user?: Misskey.entities.UserDetailed | null;
group?: Misskey.entities.UserGroup | null;
}>();
let textEl = $shallowRef<HTMLTextAreaElement>();
let fileEl = $shallowRef<HTMLInputElement>();
let text = $ref<string>('');
let file = $ref<Misskey.entities.DriveFile | null>(null);
let sending = $ref(false);
const typing = throttle(3000, () => {
stream.send('typingOnMessaging', props.user ? { partner: props.user.id } : { group: props.group?.id });
});
let draftKey = $computed(() => props.user ? 'user:' + props.user.id : 'group:' + props.group?.id);
let canSend = $computed(() => (text != null && text !== '') || file != null);
watch([$$(text), $$(file)], saveDraft);
async function onPaste(ev: ClipboardEvent) {
if (!ev.clipboardData) return;
const clipboardData = ev.clipboardData;
const items = clipboardData.items;
if (items.length === 1) {
if (items[0].kind === 'file') {
const pastedFile = items[0].getAsFile();
if (!pastedFile) return;
const lio = pastedFile.name.lastIndexOf('.');
const ext = lio >= 0 ? pastedFile.name.slice(lio) : '';
const formatted = formatTimeString(new Date(pastedFile.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, '1') + ext;
if (formatted) upload(pastedFile, formatted);
}
} else {
if (items[0].kind === 'file') {
os.alert({
type: 'error',
text: i18n.ts.onlyOneFileCanBeAttached,
});
}
}
}
function onDragover(ev: DragEvent) {
if (!ev.dataTransfer) return;
const isFile = ev.dataTransfer.items[0].kind === 'file';
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) {
ev.preventDefault();
switch (ev.dataTransfer.effectAllowed) {
case 'all':
case 'uninitialized':
case 'copy':
case 'copyLink':
case 'copyMove':
ev.dataTransfer.dropEffect = 'copy';
break;
case 'linkMove':
case 'move':
ev.dataTransfer.dropEffect = 'move';
break;
default:
ev.dataTransfer.dropEffect = 'none';
break;
}
}
}
function onDrop(ev: DragEvent): void {
if (!ev.dataTransfer) return;
//
if (ev.dataTransfer.files.length === 1) {
ev.preventDefault();
upload(ev.dataTransfer.files[0]);
return;
} else if (ev.dataTransfer.files.length > 1) {
ev.preventDefault();
os.alert({
type: 'error',
text: i18n.ts.onlyOneFileCanBeAttached,
});
return;
}
//#region
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') {
file = JSON.parse(driveFile);
ev.preventDefault();
}
//#endregion
}
function onKeydown(ev: KeyboardEvent) {
typing();
if (defaultStore.state.useEnterToSend && !ev.shiftKey) {
if ((ev.key === 'Enter') && canSend) {
send();
}
} else {
if ((ev.key === 'Enter') && (ev.ctrlKey || ev.metaKey) && canSend) {
send();
}
}
}
function onCompositionUpdate() {
typing();
}
function chooseFile(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then(selectedFile => {
file = selectedFile;
});
}
function onChangeFile() {
if (fileEl.files![0]) upload(fileEl.files[0]);
}
function upload(fileToUpload: File, name?: string) {
uploadFile(fileToUpload, defaultStore.state.uploadFolder, name).then(res => {
file = res;
});
}
function send() {
sending = true;
os.api('messaging/messages/create', {
userId: props.user ? props.user.id : undefined,
groupId: props.group ? props.group.id : undefined,
text: text ? text : undefined,
fileId: file ? file.id : undefined,
}).then(message => {
clear();
}).catch(err => {
console.error(err);
}).then(() => {
sending = false;
});
}
function clear() {
text = '';
file = null;
deleteDraft();
}
function saveDraft() {
const drafts = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}');
drafts[draftKey] = {
updatedAt: new Date(),
// eslint-disable-next-line id-denylist
data: {
text: text,
file: file,
},
};
miLocalStorage.setItem('message_drafts', JSON.stringify(drafts));
}
function deleteDraft() {
const drafts = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}');
delete drafts[draftKey];
miLocalStorage.setItem('message_drafts', JSON.stringify(drafts));
}
async function insertEmoji(ev: MouseEvent) {
os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textEl);
}
onMounted(() => {
autosize(textEl);
// TODO: detach when unmount
// TODO
//new Autocomplete(textEl, this, { model: 'text' });
// 稿
const draft = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}')[draftKey];
if (draft) {
text = draft.data.text;
file = draft.data.file;
}
});
defineExpose({
file,
upload,
});
</script>
<style lang="scss" module>
.root {
position: relative;
}
.textarea {
cursor: auto;
display: block;
width: 100%;
min-width: 100%;
max-width: 100%;
min-height: 80px;
margin: 0;
padding: 16px 16px 0 16px;
resize: none;
font-size: 1em;
font-family: inherit;
outline: none;
border: none;
border-radius: 0;
box-shadow: none;
box-sizing: border-box;
color: var(--fg);
}
.footer {
position: sticky;
bottom: 0;
background: var(--panel);
}
.file {
padding: 8px;
color: var(--fg);
background: transparent;
cursor: pointer;
}
/*
.files {
display: block;
margin: 0;
padding: 0 8px;
list-style: none;
&:after {
content: '';
display: block;
clear: both;
}
> li {
display: block;
float: left;
margin: 4px;
padding: 0;
width: 64px;
height: 64px;
background-color: #eee;
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
cursor: move;
&:hover {
> .remove {
display: block;
}
}
}
}
.file-remove {
display: none;
position: absolute;
right: -6px;
top: -6px;
margin: 0;
padding: 0;
background: transparent;
outline: none;
border: none;
border-radius: 0;
box-shadow: none;
cursor: pointer;
}
*/
.buttons {
display: flex;
}
.button {
margin: 0;
padding: 16px;
font-size: 1em;
font-weight: normal;
text-decoration: none;
transition: color 0.1s ease;
&:hover {
color: var(--accent);
}
&:active {
color: var(--accentDarken);
transition: color 0s ease;
}
}
.send {
margin-left: auto;
color: var(--accent);
&:hover {
color: var(--accentLighten);
}
&:active {
color: var(--accentDarken);
transition: color 0s ease;
}
}
.file-input {
display: none;
}
</style>

View File

@ -1,338 +0,0 @@
<template>
<div class="thvuemwp" :class="{ isMe }">
<MkAvatar class="avatar" :user="message.user" indicator link preview/>
<div class="content">
<div class="balloon" :class="{ noText: message.text == null }">
<button v-if="isMe" class="delete-button" :title="$ts.delete" @click="del">
<img src="/client-assets/remove.png" alt="Delete"/>
</button>
<div v-if="!message.isDeleted" class="content">
<Mfm v-if="message.text" ref="text" class="text" :text="message.text" :i="$i"/>
<div v-if="message.file" class="file">
<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
<p v-else>{{ message.file.name }}</p>
</a>
</div>
</div>
<div v-else class="content">
<p class="is-deleted">{{ $ts.deleted }}</p>
</div>
</div>
<div></div>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/>
<footer>
<template v-if="isGroup">
<span v-if="message.reads.length > 0" class="read">{{ $ts.messageRead }} {{ message.reads.length }}</span>
</template>
<template v-else>
<span v-if="isMe && message.isRead" class="read">{{ $ts.messageRead }}</span>
</template>
<MkTime :time="message.createdAt"/>
<template v-if="message.is_edited"><i class="ti ti-pencil"></i></template>
</footer>
</div>
</div>
</template>
<script lang="ts" setup>
import { } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import MkUrlPreview from '@/components/MkUrlPreview.vue';
import * as os from '@/os';
import { $i } from '@/account';
const props = defineProps<{
message: Misskey.entities.MessagingMessage;
isGroup?: boolean;
}>();
const isMe = $computed(() => props.message.userId === $i?.id);
const urls = $computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
function del(): void {
os.api('messaging/messages/delete', {
messageId: props.message.id,
});
}
</script>
<style lang="scss" scoped>
.thvuemwp {
$me-balloon-color: var(--accent);
position: relative;
background-color: transparent;
display: flex;
> .avatar {
position: sticky;
top: calc(var(--stickyTop, 0px) + 16px);
display: block;
width: 54px;
height: 54px;
transition: all 0.1s ease;
}
> .content {
min-width: 0;
> .balloon {
position: relative;
display: inline-flex;
align-items: center;
padding: 0;
min-height: 38px;
border-radius: 16px;
max-width: 100%;
&:before {
content: "";
pointer-events: none;
display: block;
position: absolute;
top: 12px;
}
& + * {
clear: both;
}
&:hover {
> .delete-button {
display: block;
}
}
> .delete-button {
display: none;
position: absolute;
z-index: 1;
top: -4px;
right: -4px;
margin: 0;
padding: 0;
cursor: pointer;
outline: none;
border: none;
border-radius: 0;
box-shadow: none;
background: transparent;
> img {
vertical-align: bottom;
width: 16px;
height: 16px;
cursor: pointer;
}
}
> .content {
max-width: 100%;
> .is-deleted {
display: block;
margin: 0;
padding: 0;
overflow: hidden;
overflow-wrap: break-word;
font-size: 1em;
color: rgba(#000, 0.5);
}
> .text {
display: block;
margin: 0;
padding: 12px 18px;
overflow: hidden;
overflow-wrap: break-word;
word-break: break-word;
font-size: 1em;
color: rgba(#000, 0.8);
& + .file {
> a {
border-radius: 0 0 16px 16px;
}
}
}
> .file {
> a {
display: block;
max-width: 100%;
border-radius: 16px;
overflow: hidden;
text-decoration: none;
&:hover {
text-decoration: none;
> p {
background: #ccc;
}
}
> * {
display: block;
margin: 0;
width: 100%;
max-height: 512px;
object-fit: contain;
box-sizing: border-box;
}
> p {
padding: 30px;
text-align: center;
color: #555;
background: #ddd;
}
}
}
}
}
> footer {
display: block;
margin: 2px 0 0 0;
font-size: 0.65em;
> .read {
margin: 0 8px;
}
> i {
margin-left: 4px;
}
}
}
&:not(.isMe) {
padding-left: var(--margin);
> .content {
padding-left: 16px;
padding-right: 32px;
> .balloon {
$color: var(--messageBg);
background: $color;
&.noText {
background: transparent;
}
&:not(.noText):before {
left: -14px;
border-top: solid 8px transparent;
border-right: solid 8px $color;
border-bottom: solid 8px transparent;
border-left: solid 8px transparent;
}
> .content {
> .text {
color: var(--fg);
}
}
}
> footer {
text-align: left;
}
}
}
&.isMe {
flex-direction: row-reverse;
padding-right: var(--margin);
right: var(--margin); // position: absolute使
> .content {
padding-right: 16px;
padding-left: 32px;
text-align: right;
> .balloon {
background: $me-balloon-color;
text-align: left;
::selection {
color: var(--accent);
background-color: #fff;
}
&.noText {
background: transparent;
}
&:not(.noText):before {
right: -14px;
left: auto;
border-top: solid 8px transparent;
border-right: solid 8px transparent;
border-bottom: solid 8px transparent;
border-left: solid 8px $me-balloon-color;
}
> .content {
> p.is-deleted {
color: rgba(#fff, 0.5);
}
> .text {
&, ::v-deep(*) {
color: var(--fgOnAccent) !important;
}
}
}
}
> footer {
text-align: right;
> .read {
user-select: none;
}
}
}
}
}
@container (max-width: 400px) {
.thvuemwp {
> .avatar {
width: 48px;
height: 48px;
}
> .content {
> .balloon {
> .content {
> .text {
font-size: 0.9em;
}
}
}
}
}
}
@container (max-width: 500px) {
.thvuemwp {
> .content {
> .balloon {
> .content {
> .text {
padding: 8px 16px;
}
}
}
}
}
}
</style>

View File

@ -1,415 +0,0 @@
<template>
<MkStickyContainer>
<template #header>
<MkPageHeader />
</template>
<div
ref="rootEl"
:class="$style['root']"
@dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop"
>
<div :class="$style['body']">
<MkPagination v-if="pagination" ref="pagingComponent" :key="userAcct || groupId" :pagination="pagination">
<template #empty>
<div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ i18n.ts.noMessagesYet }}</div>
</div>
</template>
<template #default="{ items: messages, fetching: pFetching }">
<MkDateSeparatedList
v-if="messages.length > 0"
v-slot="{ item: message }"
:class="{ [$style['messages']]: true, 'deny-move-transition': pFetching }"
:items="messages"
direction="up"
reversed
>
<XMessage :key="message.id" :message="message" :is-group="group != null"/>
</MkDateSeparatedList>
</template>
</MkPagination>
</div>
<footer :class="$style['footer']">
<div v-if="typers.length > 0" :class="$style['typers']">
<I18n :src="i18n.ts.typingUsers" text-tag="span">
<template #users>
<b v-for="typer in typers" :key="typer.id" :class="$style['user']">{{ typer.username }}</b>
</template>
</I18n>
<MkEllipsis/>
</div>
<Transition :name="animation ? 'fade' : ''">
<div v-show="showIndicator" :class="$style['new-message']">
<button class="_buttonPrimary" @click="onIndicatorClick" :class="$style['new-message-button']">
<i class="fas ti-fw fa-arrow-circle-down" :class="$style['new-message-icon']"></i>{{ i18n.ts.newMessageExists }}
</button>
</div>
</Transition>
<XForm v-if="!fetching" ref="formEl" :user="user" :group="group" :class="$style['form']"/>
</footer>
</div>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
import * as Misskey from 'misskey-js';
import * as Acct from 'misskey-js/built/acct';
import XMessage from './messaging-room.message.vue';
import XForm from './messaging-room.form.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { isBottomVisible, onScrollBottom, scrollToBottom } from '@/scripts/scroll';
import * as os from '@/os';
import { stream } from '@/stream';
import * as sound from '@/scripts/sound';
import { i18n } from '@/i18n';
import { $i } from '@/account';
import { defaultStore } from '@/store';
import { definePageMetadata } from '@/scripts/page-metadata';
const props = defineProps<{
userAcct?: string;
groupId?: string;
}>();
let rootEl = $shallowRef<HTMLDivElement>();
let formEl = $shallowRef<InstanceType<typeof XForm>>();
let pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>();
let fetching = $ref(true);
let user: Misskey.entities.UserDetailed | null = $ref(null);
let group: Misskey.entities.UserGroup | null = $ref(null);
let typers: Misskey.entities.User[] = $ref([]);
let connection: Misskey.ChannelConnection<Misskey.Channels['messaging']> | null = $ref(null);
let showIndicator = $ref(false);
const {
animation,
} = defaultStore.reactiveState;
let pagination: Paging | null = $ref(null);
watch([() => props.userAcct, () => props.groupId], () => {
if (connection) connection.dispose();
fetch();
});
async function fetch() {
fetching = true;
if (props.userAcct) {
const acct = Acct.parse(props.userAcct);
user = await os.api('users/show', { username: acct.username, host: acct.host || undefined });
group = null;
pagination = {
endpoint: 'messaging/messages',
limit: 20,
params: {
userId: user.id,
},
reversed: true,
pageEl: $$(rootEl).value,
};
connection = stream.useChannel('messaging', {
otherparty: user.id,
});
} else {
user = null;
group = await os.api('users/groups/show', { groupId: props.groupId });
pagination = {
endpoint: 'messaging/messages',
limit: 20,
params: {
groupId: group?.id,
},
reversed: true,
pageEl: $$(rootEl).value,
};
connection = stream.useChannel('messaging', {
group: group?.id,
});
}
connection.on('message', onMessage);
connection.on('read', onRead);
connection.on('deleted', onDeleted);
connection.on('typers', _typers => {
typers = _typers.filter(u => u.id !== $i?.id);
});
document.addEventListener('visibilitychange', onVisibilitychange);
nextTick(() => {
pagingComponent.inited.then(() => {
thisScrollToBottom();
});
window.setTimeout(() => {
fetching = false;
}, 300);
});
}
function onDragover(ev: DragEvent) {
if (!ev.dataTransfer) return;
const isFile = ev.dataTransfer.items[0].kind === 'file';
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) {
switch (ev.dataTransfer.effectAllowed) {
case 'all':
case 'uninitialized':
case 'copy':
case 'copyLink':
case 'copyMove':
ev.dataTransfer.dropEffect = 'copy';
break;
case 'linkMove':
case 'move':
ev.dataTransfer.dropEffect = 'move';
break;
default:
ev.dataTransfer.dropEffect = 'none';
break;
}
} else {
ev.dataTransfer.dropEffect = 'none';
}
}
function onDrop(ev: DragEvent): void {
if (!ev.dataTransfer) return;
//
if (ev.dataTransfer.files.length === 1) {
formEl.upload(ev.dataTransfer.files[0]);
return;
} else if (ev.dataTransfer.files.length > 1) {
os.alert({
type: 'error',
text: i18n.ts.onlyOneFileCanBeAttached,
});
return;
}
//#region
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile);
formEl.file = file;
}
//#endregion
}
function onMessage(message) {
sound.play('chat');
const _isBottom = isBottomVisible(rootEl, 64);
pagingComponent.prepend(message);
if (message.userId !== $i?.id && !document.hidden) {
connection?.send('read', {
id: message.id,
});
}
if (_isBottom) {
// Scroll to bottom
nextTick(() => {
thisScrollToBottom();
});
} else if (message.userId !== $i?.id) {
// Notify
notifyNewMessage();
}
}
function onRead(x) {
if (user) {
if (!Array.isArray(x)) x = [x];
for (const id of x) {
if (pagingComponent.items.some(y => y.id === id)) {
const exist = pagingComponent.items.map(y => y.id).indexOf(id);
pagingComponent.items[exist] = {
...pagingComponent.items[exist],
isRead: true,
};
}
}
} else if (group) {
for (const id of x.ids) {
if (pagingComponent.items.some(y => y.id === id)) {
const exist = pagingComponent.items.map(y => y.id).indexOf(id);
pagingComponent.items[exist] = {
...pagingComponent.items[exist],
reads: [...pagingComponent.items[exist].reads, x.userId],
};
}
}
}
}
function onDeleted(id) {
const msg = pagingComponent.items.find(m => m.id === id);
if (msg) {
pagingComponent.items = pagingComponent.items.filter(m => m.id !== msg.id);
}
}
function thisScrollToBottom() {
scrollToBottom($$(rootEl).value, { behavior: 'smooth' });
}
function onIndicatorClick() {
showIndicator = false;
thisScrollToBottom();
}
let scrollRemove: (() => void) | null = $ref(null);
function notifyNewMessage() {
showIndicator = true;
scrollRemove = onScrollBottom(rootEl, () => {
showIndicator = false;
scrollRemove = null;
});
}
function onVisibilitychange() {
if (document.hidden) return;
for (const message of pagingComponent.items) {
if (message.userId !== $i?.id && !message.isRead) {
connection?.send('read', {
id: message.id,
});
}
}
}
onMounted(() => {
fetch();
});
onBeforeUnmount(() => {
connection?.dispose();
document.removeEventListener('visibilitychange', onVisibilitychange);
if (scrollRemove) scrollRemove();
});
definePageMetadata(computed(() => !fetching ? user ? {
userName: user,
avatar: user,
} : {
title: group?.name,
icon: 'ti ti-users',
} : null));
</script>
<style lang="scss" module>
.root {
display: content;
}
.body {
min-height: 80%;
}
.more {
display: block;
margin: 16px auto;
padding: 0 12px;
line-height: 24px;
color: #fff;
background: rgba(#000, 0.3);
border-radius: 12px;
&:hover {
background: rgba(#000, 0.4);
}
&:active {
background: rgba(#000, 0.5);
}
> i {
margin-right: 4px;
}
}
.fetching {
cursor: wait;
}
.messages {
padding: 16px 0 0;
> * {
margin-bottom: 16px;
}
}
.footer {
width: 100%;
position: sticky;
z-index: 2;
padding-top: 8px;
bottom: var(--minBottomSpacing);
}
.new-message {
width: 100%;
padding-bottom: 8px;
text-align: center;
}
.new-message-button {
display: inline-block;
margin: 0;
padding: 0 12px;
line-height: 32px;
font-size: 12px;
border-radius: 16px;
}
.new-message-icon {
display: inline-block;
margin-right: 8px;
}
.typers {
position: absolute;
bottom: 100%;
padding: 0 8px 0 8px;
font-size: 0.9em;
color: var(--fgTransparentWeak);
}
.user + .user:before {
content: ", ";
font-weight: normal;
}
.user:last-of-type:after {
content: " ";
}
.form {
max-height: 12em;
overflow-y: scroll;
border-top: solid 0.5px var(--divider);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.1s;
}
.fade-enter-from, .fade-leave-to {
transition: opacity 0.5s;
opacity: 0;
}
</style>

View File

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

View File

@ -11,16 +11,11 @@
<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
<option value="users">{{ i18n.ts._antennaSources.users }}</option>
<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
<!--<option value="group">{{ i18n.ts._antennaSources.userGroup }}</option>-->
</MkSelect>
<MkSelect v-if="src === 'list'" v-model="userListId">
<template #label>{{ i18n.ts.userList }}</template>
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
</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">
<template #label>{{ i18n.ts.users }}</template>
<template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template>
@ -70,7 +65,6 @@ const emit = defineEmits<{
let name: string = $ref(props.antenna.name);
let src: string = $ref(props.antenna.src);
let userListId: any = $ref(props.antenna.userListId);
let userGroupId: any = $ref(props.antenna.userGroupId);
let users: string = $ref(props.antenna.users.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'));
@ -79,19 +73,11 @@ let withReplies: boolean = $ref(props.antenna.withReplies);
let withFile: boolean = $ref(props.antenna.withFile);
let notify: boolean = $ref(props.antenna.notify);
let userLists: any = $ref(null);
let userGroups: any = $ref(null);
watch(() => src, async () => {
if (src === 'list' && userLists === null) {
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() {
@ -99,7 +85,6 @@ async function saveAntenna() {
name,
src,
userListId,
userGroupId,
withReplies,
withFile,
notify,

View File

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

View File

@ -5,7 +5,6 @@
<div class="_gaps_m">
<FormLink @click="readAllNotifications">{{ i18n.ts.markAsReadAllNotifications }}</FormLink>
<FormLink @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink>
<FormLink @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
</div>
</FormSection>
<FormSection>
@ -47,10 +46,6 @@ async function readAllUnreadNotes() {
await os.api('i/read-all-unread-notes');
}
async function readAllMessagingMessages() {
await os.api('i/read-all-messaging-messages');
}
async function readAllNotifications() {
await os.api('notifications/mark-all-as-read');
}

View File

@ -35,28 +35,6 @@ 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() {
if (user.isMuted) {
os.apiWithDialog('mute/delete', {
@ -156,20 +134,11 @@ export function getUserMenu(user, router: Router = mainRouter) {
action: () => {
os.post({ specified: user });
},
}, meId !== user.id ? {
type: 'link',
icon: 'ti ti-messages',
text: i18n.ts.startMessaging,
to: '/my/messaging/' + Acct.toString(user),
} : undefined, null, {
}, null, {
icon: 'ti ti-list',
text: i18n.ts.addToList,
action: pushList,
}, meId !== user.id ? {
icon: 'ti ti-users',
text: i18n.ts.inviteToGroup,
action: inviteGroup,
} : undefined] as any;
}] as any;
if ($i && meId !== user.id) {
menu = menu.concat([null, {

View File

@ -209,23 +209,6 @@ async function composeNotification(data: pushNotificationDataMap[keyof pushNotif
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':
return [data.body.header ?? data.body.body, {
body: data.body.header ? data.body.body : '',
@ -236,23 +219,6 @@ async function composeNotification(data: pushNotificationDataMap[keyof pushNotif
default:
return null;
}
case 'unreadMessagingMessage':
if (data.body.groupId === null) {
return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), {
icon: data.body.user.avatarUrl,
badge: iconUrl('messages'),
tag: `messaging:user:${data.body.userId}`,
data,
renotify: true,
}];
}
return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group?.name ?? '' }), {
icon: data.body.user.avatarUrl,
badge: iconUrl('messages'),
tag: `messaging:group:${data.body.groupId}`,
data,
renotify: true,
}];
case 'unreadAntennaNote':
return [t('_notification.unreadAntennaNote', { name: data.body.antenna.name }), {
body: `${getUserName(data.body.note.user)}: ${data.body.note.text ?? ''}`,

View File

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

View File

@ -15,9 +15,9 @@ globalThis.addEventListener('activate', ev => {
.then(cacheNames => Promise.all(
cacheNames
.filter((v) => v !== swLang.cacheName)
.map(name => caches.delete(name))
.map(name => caches.delete(name)),
))
.then(() => self.clients.claim())
.then(() => self.clients.claim()),
);
});
@ -34,7 +34,7 @@ globalThis.addEventListener('fetch', ev => {
if (!isHTMLRequest) return;
ev.respondWith(
fetch(ev.request)
.catch(() => new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 }))
.catch(() => new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 })),
);
});
@ -42,14 +42,13 @@ globalThis.addEventListener('push', ev => {
// クライアント取得
ev.waitUntil(self.clients.matchAll({
includeUncontrolled: true,
type: 'window'
type: 'window',
}).then(async (clients: readonly WindowClient[]) => {
const data: pushNotificationDataMap[keyof pushNotificationDataMap] = ev.data?.json();
switch (data.type) {
// case 'driveFileCreated':
case 'notification':
case 'unreadMessagingMessage':
case 'unreadAntennaNote':
// 1日以上経過している場合は無視
if ((new Date()).getTime() - data.dateTime > 1000 * 60 * 60 * 24) break;
@ -63,11 +62,6 @@ globalThis.addEventListener('push', ev => {
if (n?.data?.type === 'notification') n.close();
}
break;
case 'readAllMessagingMessages':
for (const n of await self.registration.getNotifications()) {
if (n?.data?.type === 'unreadMessagingMessage') n.close();
}
break;
case 'readAllAntennas':
for (const n of await self.registration.getNotifications()) {
if (n?.data?.type === 'unreadAntennaNote') n.close();
@ -75,25 +69,14 @@ globalThis.addEventListener('push', ev => {
break;
case 'readNotifications':
for (const n of await self.registration.getNotifications()) {
if (data.body?.notificationIds?.includes(n.data.body.id)) {
if (data.body.notificationIds.includes(n.data.body.id)) {
n.close();
}
}
break;
case 'readAllMessagingMessagesOfARoom':
for (const n of await self.registration.getNotifications()) {
if (n.data.type === 'unreadMessagingMessage'
&& ('userId' in data.body
? data.body.userId === n.data.body.userId
: data.body.groupId === n.data.body.groupId)
) {
n.close();
}
}
break;
case 'readAntenna':
for (const n of await self.registration.getNotifications()) {
if (n?.data?.type === 'unreadAntennaNote' && data.body?.antennaId === n.data.body.antenna.id) {
if (n?.data?.type === 'unreadAntennaNote' && data.body.antennaId === n.data.body.antenna.id) {
n.close();
}
}
@ -135,9 +118,6 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
case 'receiveFollowRequest':
await swos.api('following/requests/accept', loginId, { userId: data.body.userId });
break;
case 'groupInvited':
await swos.api('users/groups/invitations/accept', loginId, { invitationId: data.body.invitation.id });
break;
}
break;
case 'reject':
@ -145,9 +125,6 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
case 'receiveFollowRequest':
await swos.api('following/requests/reject', loginId, { userId: data.body.userId });
break;
case 'groupInvited':
await swos.api('users/groups/invitations/reject', loginId, { invitationId: data.body.invitation.id });
break;
}
break;
case 'showFollowRequests':
@ -158,9 +135,6 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
case 'receiveFollowRequest':
client = await swos.openClient('push', '/my/follow-requests', loginId);
break;
case 'groupInvited':
client = await swos.openClient('push', '/my/groups', loginId);
break;
case 'reaction':
client = await swos.openNote(data.body.note.id, loginId);
break;
@ -174,9 +148,6 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
}
}
break;
case 'unreadMessagingMessage':
client = await swos.openChat(data.body, loginId);
break;
case 'unreadAntennaNote':
client = await swos.openAntenna(data.body.antenna.id, loginId);
}
@ -207,7 +178,7 @@ globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['me
// Cache Storage全削除
await caches.keys()
.then(cacheNames => Promise.all(
cacheNames.map(name => caches.delete(name))
cacheNames.map(name => caches.delete(name)),
));
return; // TODO
}

View File

@ -13,15 +13,12 @@ export type SwMessage = {
// Defined also @/core/PushNotificationService.ts#L12
type pushNotificationDataSourceMap = {
notification: Misskey.entities.Notification;
unreadMessagingMessage: Misskey.entities.MessagingMessage;
unreadAntennaNote: {
antenna: { id: string, name: string };
note: Misskey.entities.Note;
};
readNotifications: { notificationIds: string[] };
readAllNotifications: undefined;
readAllMessagingMessages: undefined;
readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string };
readAntenna: { antennaId: string };
readAllAntennas: undefined;
};