mirror of
https://github.com/MisskeyIO/misskey
synced 2024-11-27 14:28:49 +09:00
Merge pull request MisskeyIO#740 from update-host
This commit is contained in:
commit
1ebda086fc
@ -72,7 +72,7 @@ dbReplications: false
|
||||
#───┘ Redis configuration └─────────────────────────────────────
|
||||
|
||||
redis:
|
||||
host: keydb
|
||||
host: dragonfly
|
||||
port: 6379
|
||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
#pass: example-pass
|
||||
@ -80,7 +80,7 @@ redis:
|
||||
#db: 1
|
||||
|
||||
#redisForPubsub:
|
||||
# host: keydb
|
||||
# host: dragonfly
|
||||
# port: 6379
|
||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
# #pass: example-pass
|
||||
@ -88,7 +88,7 @@ redis:
|
||||
# #db: 1
|
||||
|
||||
#redisForJobQueue:
|
||||
# host: keydb
|
||||
# host: dragonfly
|
||||
# port: 6379
|
||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
# #pass: example-pass
|
||||
@ -96,7 +96,7 @@ redis:
|
||||
# #db: 1
|
||||
|
||||
#redisForTimelines:
|
||||
# host: keydb
|
||||
# host: dragonfly
|
||||
# port: 6379
|
||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
# #pass: example-pass
|
||||
|
@ -72,7 +72,7 @@ dbReplications: false
|
||||
#───┘ Redis configuration └─────────────────────────────────────
|
||||
|
||||
redis:
|
||||
host: keydb
|
||||
host: dragonfly
|
||||
port: 6379
|
||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
#pass: example-pass
|
||||
@ -80,7 +80,7 @@ redis:
|
||||
#db: 1
|
||||
|
||||
#redisForPubsub:
|
||||
# host: keydb
|
||||
# host: dragonfly
|
||||
# port: 6379
|
||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
# #pass: example-pass
|
||||
@ -88,7 +88,7 @@ redis:
|
||||
# #db: 1
|
||||
|
||||
#redisForJobQueue:
|
||||
# host: keydb
|
||||
# host: dragonfly
|
||||
# port: 6379
|
||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
# #pass: example-pass
|
||||
@ -96,7 +96,7 @@ redis:
|
||||
# #db: 1
|
||||
|
||||
#redisForTimelines:
|
||||
# host: keydb
|
||||
# host: dragonfly
|
||||
# port: 6379
|
||||
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
# #pass: example-pass
|
||||
|
@ -15,17 +15,25 @@ services:
|
||||
- internal_network
|
||||
- external_network
|
||||
|
||||
keydb:
|
||||
dragonfly:
|
||||
restart: unless-stopped
|
||||
image: eqalpha/keydb:latest
|
||||
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||
ulimits:
|
||||
memlock: -1
|
||||
environment:
|
||||
DFLY_snapshot_cron: '* * * * *'
|
||||
DFLY_version_check: false
|
||||
DFLY_tcp_backlog: 2048
|
||||
DFLY_default_lua_flags: allow-undeclared-keys
|
||||
DFLY_pipeline_squash: 0
|
||||
DFLY_multi_exec_squash: false
|
||||
DFLY_conn_io_threads: 4
|
||||
DFLY_epoll_file_threads: 4
|
||||
DFLY_proactor_threads: 4
|
||||
networks:
|
||||
- internal_network
|
||||
volumes:
|
||||
- keydb-data:/data
|
||||
healthcheck:
|
||||
test: "keydb-cli ping"
|
||||
interval: 5s
|
||||
retries: 20
|
||||
- dragonfly-data:/data
|
||||
|
||||
db:
|
||||
restart: unless-stopped
|
||||
@ -45,7 +53,7 @@ services:
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
keydb-data:
|
||||
dragonfly-data:
|
||||
|
||||
networks:
|
||||
internal_network:
|
||||
|
@ -11,7 +11,6 @@ docker-compose.yml
|
||||
node_modules/
|
||||
packages/*/node_modules
|
||||
redis/
|
||||
keydb/
|
||||
files/
|
||||
fluent-emojis/
|
||||
.pnp.*
|
||||
|
28
.github/workflows/test-backend.yml
vendored
28
.github/workflows/test-backend.yml
vendored
@ -32,8 +32,18 @@ jobs:
|
||||
env:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
keydb:
|
||||
image: eqalpha/keydb:latest
|
||||
dragonfly:
|
||||
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||
options: --ulimit "memlock=-1"
|
||||
env:
|
||||
DFLY_version_check: false
|
||||
DFLY_tcp_backlog: 2048
|
||||
DFLY_default_lua_flags: allow-undeclared-keys
|
||||
DFLY_pipeline_squash: 0
|
||||
DFLY_multi_exec_squash: false
|
||||
DFLY_conn_io_threads: 4
|
||||
DFLY_epoll_file_threads: 4
|
||||
DFLY_proactor_threads: 4
|
||||
ports:
|
||||
- 56312:6379
|
||||
|
||||
@ -84,8 +94,18 @@ jobs:
|
||||
env:
|
||||
POSTGRES_DB: test-misskey
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
keydb:
|
||||
image: eqalpha/keydb:latest
|
||||
dragonfly:
|
||||
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||
options: --ulimit "memlock=-1"
|
||||
env:
|
||||
DFLY_version_check: false
|
||||
DFLY_tcp_backlog: 2048
|
||||
DFLY_default_lua_flags: allow-undeclared-keys
|
||||
DFLY_pipeline_squash: 0
|
||||
DFLY_multi_exec_squash: false
|
||||
DFLY_conn_io_threads: 4
|
||||
DFLY_epoll_file_threads: 4
|
||||
DFLY_proactor_threads: 4
|
||||
ports:
|
||||
- 56312:6379
|
||||
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -53,7 +53,6 @@ run.bat
|
||||
api-docs.json
|
||||
*.log
|
||||
/redis
|
||||
/keydb
|
||||
*.code-workspace
|
||||
.DS_Store
|
||||
/files
|
||||
|
@ -37,8 +37,25 @@ spec:
|
||||
value: "misskey"
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
- name: keydb
|
||||
image: eqalpha/keydb:latest
|
||||
- name: dragonfly
|
||||
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||
env:
|
||||
- name: DFLY_version_check
|
||||
value: false
|
||||
- name: DFLY_tcp_backlog
|
||||
value: 2048
|
||||
- name: DFLY_default_lua_flags
|
||||
value: allow-undeclared-keys
|
||||
- name: DFLY_pipeline_squash
|
||||
value: 0
|
||||
- name: DFLY_multi_exec_squash
|
||||
value: false
|
||||
- name: DFLY_conn_io_threads
|
||||
value: 4
|
||||
- name: DFLY_epoll_file_threads
|
||||
value: 4
|
||||
- name: DFLY_proactor_threads
|
||||
value: 4
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
volumes:
|
||||
|
@ -3,17 +3,25 @@ version: "3"
|
||||
# このconfigは、 dockerでMisskey本体を起動せず、 redisとpostgresql などだけを起動します
|
||||
|
||||
services:
|
||||
keydb:
|
||||
dragonfly:
|
||||
restart: always
|
||||
image: eqalpha/keydb:latest
|
||||
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||
ulimits:
|
||||
memlock: -1
|
||||
environment:
|
||||
DFLY_snapshot_cron: '* * * * *'
|
||||
DFLY_version_check: false
|
||||
DFLY_tcp_backlog: 2048
|
||||
DFLY_default_lua_flags: allow-undeclared-keys
|
||||
DFLY_pipeline_squash: 0
|
||||
DFLY_multi_exec_squash: false
|
||||
DFLY_conn_io_threads: 4
|
||||
DFLY_epoll_file_threads: 4
|
||||
DFLY_proactor_threads: 4
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./keydb:/data
|
||||
healthcheck:
|
||||
test: "keydb-cli ping"
|
||||
interval: 5s
|
||||
retries: 20
|
||||
- ./redis:/data
|
||||
|
||||
db:
|
||||
restart: always
|
||||
|
@ -6,13 +6,13 @@ services:
|
||||
restart: always
|
||||
links:
|
||||
- db
|
||||
- keydb
|
||||
- dragonfly
|
||||
# - mcaptcha
|
||||
# - meilisearch
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
keydb:
|
||||
dragonfly:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3000:3000"
|
||||
@ -23,17 +23,25 @@ services:
|
||||
- ./files:/misskey/files
|
||||
- ./.config:/misskey/.config:ro
|
||||
|
||||
keydb:
|
||||
dragonfly:
|
||||
restart: always
|
||||
image: eqalpha/keydb:latest
|
||||
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||
ulimits:
|
||||
memlock: -1
|
||||
environment:
|
||||
DFLY_snapshot_cron: '* * * * *'
|
||||
DFLY_version_check: false
|
||||
DFLY_tcp_backlog: 2048
|
||||
DFLY_default_lua_flags: allow-undeclared-keys
|
||||
DFLY_pipeline_squash: 0
|
||||
DFLY_multi_exec_squash: false
|
||||
DFLY_conn_io_threads: 4
|
||||
DFLY_epoll_file_threads: 4
|
||||
DFLY_proactor_threads: 4
|
||||
networks:
|
||||
- internal_network
|
||||
volumes:
|
||||
- ./keydb:/data
|
||||
healthcheck:
|
||||
test: "keydb-cli ping"
|
||||
interval: 5s
|
||||
retries: 20
|
||||
- ./redis:/data
|
||||
|
||||
db:
|
||||
restart: always
|
||||
|
@ -442,6 +442,10 @@ moderation: "Moderation"
|
||||
moderationNote: "Moderation note"
|
||||
addModerationNote: "Add moderation note"
|
||||
moderationLogs: "Moderation logs"
|
||||
userAccountMoveLogs: "Account migration logs"
|
||||
userAccountMoveLogsTitle: "{from} migrated the account to {to}"
|
||||
movedToId: "ID of the account migrated to"
|
||||
moveFromId: "ID of the account migrated from"
|
||||
nUsersMentioned: "Mentioned by {n} users"
|
||||
securityKeyAndPasskey: "Security- and passkeys"
|
||||
securityKey: "Security key"
|
||||
@ -1087,6 +1091,7 @@ audioFiles: "Audio"
|
||||
dataSaver: "Data Saver"
|
||||
accountMigration: "Account Migration"
|
||||
accountMoved: "This user has moved to a new account:"
|
||||
accountMovedFrom: "This user has been migrated from the following account:"
|
||||
accountMovedShort: "This account has been migrated."
|
||||
operationForbidden: "Operation forbidden"
|
||||
forceShowAds: "Always show ads"
|
||||
|
@ -441,6 +441,10 @@ moderation: "조정"
|
||||
moderationNote: "조정 기록"
|
||||
addModerationNote: "조정 기록 추가하기"
|
||||
moderationLogs: "모더레이션 로그"
|
||||
userAccountMoveLogs: "계정 이사 사용 로그"
|
||||
userAccountMoveLogsTitle: "{from} 가 {to} 로 계정을 이사했습니다"
|
||||
movedToId: "이사 후 계정의 ID"
|
||||
moveFromId: "이사 전 계정의 ID"
|
||||
nUsersMentioned: "{n}명이 언급함"
|
||||
securityKeyAndPasskey: "보안 키 또는 패스 키"
|
||||
securityKey: "보안 키"
|
||||
@ -907,7 +911,8 @@ unmuteThread: "글타래 뮤트 해제"
|
||||
followingVisibility: "팔로우의 공개 범위"
|
||||
followersVisibility: "팔로워의 공개 범위"
|
||||
continueThread: "글타래 더 보기"
|
||||
deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? "
|
||||
deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까?"
|
||||
deleteAccountConfirmAndWarn: "계정이 삭제됩니다.\n삭제 요청 후 다시 로그인하면 계정 삭제가 중단되어 버립니다.\n계속하시겠습니까?"
|
||||
incorrectPassword: "비밀번호가 올바르지 않습니다."
|
||||
voteConfirm: "\"{choice}\"에 투표하시겠습니까?"
|
||||
hide: "숨기기"
|
||||
@ -1085,6 +1090,7 @@ audioFiles: "소리"
|
||||
dataSaver: "데이터 절약 모드"
|
||||
accountMigration: "계정 이동"
|
||||
accountMoved: "이 사용자는 다음 계정으로 이사했습니다:"
|
||||
accountMovedFrom: "이 사용자는 다음 계정에서 이사했습니다:"
|
||||
accountMovedShort: "이사한 계정입니다"
|
||||
operationForbidden: "사용할 수 없습니다"
|
||||
forceShowAds: "광고를 항상 표시"
|
||||
@ -1798,6 +1804,7 @@ _accountDelete:
|
||||
requestAccountDelete: "계정 삭제 요청"
|
||||
started: "삭제 작업이 시작되었습니다."
|
||||
inProgress: "삭제 진행 중"
|
||||
dontLogin: "삭제가 중단되어 버릴 수 있으므로, 계정에 로그인하지 않는 것을 권장합니다."
|
||||
_ad:
|
||||
back: "뒤로"
|
||||
reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2024.5.0-host.2e",
|
||||
"version": "2024.5.0-host.2f",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -138,7 +138,7 @@ export class AnnouncementService {
|
||||
limit: number,
|
||||
offset: number,
|
||||
moderator: MiUser,
|
||||
): Promise<(MiAnnouncement & { userInfo: Packed<'UserLite'> | null, reads: number })[]> {
|
||||
): Promise<(MiAnnouncement & { userInfo: Packed<'UserLite'> | null, reads: number, lastReadAt: Date | null })[]> {
|
||||
const query = this.announcementsRepository.createQueryBuilder('announcement');
|
||||
|
||||
if (userId) {
|
||||
@ -157,13 +157,14 @@ export class AnnouncementService {
|
||||
.offset(offset)
|
||||
.getMany();
|
||||
|
||||
const reads = new Map<MiAnnouncement, number>();
|
||||
|
||||
for (const announcement of announcements) {
|
||||
reads.set(announcement, await this.announcementReadsRepository.countBy({
|
||||
announcementId: announcement.id,
|
||||
}));
|
||||
}
|
||||
const reads = announcements.length > 0
|
||||
? await this.announcementReadsRepository.createQueryBuilder()
|
||||
.select('"announcementId", count(*) as "reads", max("id") as "lastReadId"')
|
||||
.where('"announcementId" IN (:...announcementIds)', { announcementIds: announcements.map(a => a.id) })
|
||||
.groupBy('"announcementId"')
|
||||
.getRawMany<{ announcementId: string, reads: number, lastReadId: string | null }>()
|
||||
.then(rs => new Map(rs.map(r => [r.announcementId, { reads: r.reads, lastReadAt: r.lastReadId ? this.idService.parse(r.lastReadId).date : null }])))
|
||||
: new Map();
|
||||
|
||||
const users = await this.usersRepository.findBy({
|
||||
id: In(announcements.map(a => a.userId).filter(id => id != null)),
|
||||
@ -174,8 +175,8 @@ export class AnnouncementService {
|
||||
|
||||
return announcements.map(announcement => ({
|
||||
...announcement,
|
||||
...reads.get(announcement.id) ?? { reads: 0, lastReadAt: null },
|
||||
userInfo: packedUsers.find(u => u.id === announcement.userId) ?? null,
|
||||
reads: reads.get(announcement) ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -293,18 +294,20 @@ export class AnnouncementService {
|
||||
'read.id IS NOT NULL as "isRead"',
|
||||
]);
|
||||
query
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.orWhere('announcement."userId" = :userId', { userId: me.id });
|
||||
qb.orWhere('announcement."userId" IS NULL');
|
||||
}),
|
||||
)
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.orWhere('announcement."forExistingUsers" = false');
|
||||
qb.orWhere('announcement.id > :userId', { userId: me.id });
|
||||
}),
|
||||
);
|
||||
.andWhere(new Brackets((qb) => {
|
||||
qb.orWhere(new Brackets((nqb) => {
|
||||
nqb.andWhere('announcement."userId" = :userId', { userId: me.id });
|
||||
nqb.andWhere(isActive ? 'read.id IS NULL' : 'read.id IS NOT NULL');
|
||||
}));
|
||||
qb.orWhere(new Brackets((nqb) => {
|
||||
nqb.andWhere('announcement."userId" IS NULL');
|
||||
nqb.andWhere('announcement."isActive" = :isActive', { isActive });
|
||||
}));
|
||||
}))
|
||||
.andWhere(new Brackets((qb) => {
|
||||
qb.orWhere('announcement."forExistingUsers" = false');
|
||||
qb.orWhere('announcement.id > :userId', { userId: me.id });
|
||||
}));
|
||||
} else {
|
||||
query.select([
|
||||
'announcement.*',
|
||||
@ -312,12 +315,9 @@ export class AnnouncementService {
|
||||
]);
|
||||
query.andWhere('announcement."userId" IS NULL');
|
||||
query.andWhere('announcement."forExistingUsers" = false');
|
||||
query.andWhere('announcement."isActive" = :isActive', { isActive });
|
||||
}
|
||||
|
||||
query.andWhere('announcement."isActive" = :isActive', {
|
||||
isActive: isActive,
|
||||
});
|
||||
|
||||
if (isActive) {
|
||||
query.orderBy({
|
||||
'"isRead"': 'ASC',
|
||||
|
@ -219,7 +219,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('note:create');
|
||||
this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -364,6 +364,16 @@ export class QueueService {
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createUserSuspendJob(user: ThinUser) {
|
||||
return this.dbQueue.add('userSuspend', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createReportAbuseJob(report: MiAbuseUserReport) {
|
||||
return this.dbQueue.add('reportAbuse', report);
|
||||
|
@ -9,16 +9,7 @@ import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type {
|
||||
AntennasRepository,
|
||||
ClipNotesRepository,
|
||||
ClipsRepository,
|
||||
FollowingsRepository,
|
||||
FollowRequestsRepository,
|
||||
UserListMembershipsRepository,
|
||||
UserListsRepository,
|
||||
WebhooksRepository,
|
||||
} from '@/models/_.js';
|
||||
import type { FollowingsRepository } from '@/models/_.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
@ -36,27 +27,6 @@ export class UserSuspendService {
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
@Inject(DI.antennasRepository)
|
||||
private antennasRepository: AntennasRepository,
|
||||
|
||||
@Inject(DI.webhooksRepository)
|
||||
private webhooksRepository: WebhooksRepository,
|
||||
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.clipsRepository)
|
||||
private clipsRepository: ClipsRepository,
|
||||
|
||||
@Inject(DI.clipNotesRepository)
|
||||
private clipNotesRepository: ClipNotesRepository,
|
||||
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private apRendererService: ApRendererService,
|
||||
@ -72,41 +42,7 @@ export class UserSuspendService {
|
||||
|
||||
this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
|
||||
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
let cursor = '';
|
||||
while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
|
||||
const clipNotes = await this.clipNotesRepository.createQueryBuilder('c')
|
||||
.select('c.id')
|
||||
.innerJoin('c.note', 'n')
|
||||
.where('n.userId = :userId', { userId: user.id })
|
||||
.andWhere('c.id > :cursor', { cursor })
|
||||
.orderBy('c.id', 'ASC')
|
||||
.limit(500)
|
||||
.getRawMany<{ id: string }>();
|
||||
|
||||
if (clipNotes.length === 0) break;
|
||||
|
||||
cursor = clipNotes.at(-1)?.id ?? '';
|
||||
|
||||
promises.push(this.clipNotesRepository.createQueryBuilder()
|
||||
.delete()
|
||||
.where('id IN (:...ids)', { ids: clipNotes.map((clipNote) => clipNote.id) })
|
||||
.execute());
|
||||
}
|
||||
|
||||
await Promise.allSettled([
|
||||
this.followRequestsRepository.delete({ followeeId: user.id }),
|
||||
this.followRequestsRepository.delete({ followerId: user.id }),
|
||||
|
||||
this.antennasRepository.delete({ userId: user.id }),
|
||||
this.webhooksRepository.delete({ userId: user.id }),
|
||||
this.userListsRepository.delete({ userId: user.id }),
|
||||
this.clipsRepository.delete({ userId: user.id }),
|
||||
|
||||
...promises,
|
||||
this.userListMembershipsRepository.delete({ userId: user.id }),
|
||||
]);
|
||||
await this.queueService.createUserSuspendJob(user);
|
||||
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
// 知り得る全SharedInboxにDelete配信
|
||||
|
@ -7,6 +7,7 @@ import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { addTime, dateUTC, subtractTime } from '@/misc/prelude/time.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import Chart from '../core.js';
|
||||
@ -54,10 +55,30 @@ export default class PerUserPvChart extends Chart<typeof schema> { // eslint-dis
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getChartUsers(span: 'hour' | 'day', order: 'ASC' | 'DESC', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{
|
||||
userId: string;
|
||||
count: number;
|
||||
}[]> {
|
||||
return await this.getChartPv(span, amount, cursor, limit, offset, order);
|
||||
public async getUsersRanking(span: 'hour' | 'day', order: 'ASC' | 'DESC', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{ userId: string; count: number; }[]> {
|
||||
const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate();
|
||||
const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never;
|
||||
|
||||
const lt = dateUTC([y, m, d, h, _m, _s, _ms]);
|
||||
|
||||
const gt =
|
||||
span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
|
||||
span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
|
||||
new Error('not happen') as never;
|
||||
|
||||
const repository =
|
||||
span === 'hour' ? this.repositoryForHour :
|
||||
span === 'day' ? this.repositoryForDay :
|
||||
new Error('not happen') as never;
|
||||
|
||||
// ログ取得
|
||||
return await repository.createQueryBuilder()
|
||||
.select('"group" as "userId", sum("___upv_user" + "___upv_visitor") as "count"')
|
||||
.where('date BETWEEN :gt AND :lt', { gt: Chart.dateToTimestamp(gt), lt: Chart.dateToTimestamp(lt) })
|
||||
.groupBy('"userId"')
|
||||
.orderBy('"count"', order)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.getRawMany<{ userId: string, count: number }>();
|
||||
}
|
||||
}
|
||||
|
@ -147,10 +147,8 @@ export default abstract class Chart<T extends Schema> {
|
||||
// ↓にしたいけどfindOneとかで型エラーになる
|
||||
//private repositoryForHour: Repository<RawRecord<T>>;
|
||||
//private repositoryForDay: Repository<RawRecord<T>>;
|
||||
private repositoryForHour: Repository<{ id: number; group?: string | null; date: number;}>;
|
||||
private repositoryForDay: Repository<{ id: number; group?: string | null; date: number;}>;
|
||||
private repositoryUserPvForHour: Repository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>;
|
||||
private repositoryUserPvForDay: Repository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>;
|
||||
protected repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>;
|
||||
protected repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>;
|
||||
/**
|
||||
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
|
||||
*/
|
||||
@ -186,11 +184,11 @@ export default abstract class Chart<T extends Schema> {
|
||||
return columns;
|
||||
}
|
||||
|
||||
private static dateToTimestamp(x: Date): number {
|
||||
protected static dateToTimestamp(x: Date): number {
|
||||
return Math.floor(x.getTime() / 1000);
|
||||
}
|
||||
|
||||
private static parseDate(date: Date): [number, number, number, number, number, number, number] {
|
||||
protected static parseDate(date: Date): [number, number, number, number, number, number, number] {
|
||||
const y = date.getUTCFullYear();
|
||||
const m = date.getUTCMonth();
|
||||
const d = date.getUTCDate();
|
||||
@ -202,7 +200,7 @@ export default abstract class Chart<T extends Schema> {
|
||||
return [y, m, d, h, _m, _s, _ms];
|
||||
}
|
||||
|
||||
private static getCurrentDate() {
|
||||
protected static getCurrentDate() {
|
||||
return Chart.parseDate(new Date());
|
||||
}
|
||||
|
||||
@ -274,8 +272,6 @@ export default abstract class Chart<T extends Schema> {
|
||||
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
|
||||
this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour);
|
||||
this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day);
|
||||
this.repositoryUserPvForHour = db.getRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>(hour);
|
||||
this.repositoryUserPvForDay = db.getRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>(day);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@ -725,37 +721,4 @@ export default abstract class Chart<T extends Schema> {
|
||||
}
|
||||
return object as Unflatten<ChartResult<T>>;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getChartPv(span: 'hour' | 'day', amount: number, cursor: Date | null, limit: number, offset: number, order: 'ASC' | 'DESC'): Promise<
|
||||
{
|
||||
userId: string,
|
||||
count: number,
|
||||
}[]
|
||||
> {
|
||||
const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate();
|
||||
const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never;
|
||||
|
||||
const lt = dateUTC([y, m, d, h, _m, _s, _ms]);
|
||||
|
||||
const gt =
|
||||
span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
|
||||
span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
|
||||
new Error('not happen') as never;
|
||||
|
||||
const repository =
|
||||
span === 'hour' ? this.repositoryUserPvForHour :
|
||||
span === 'day' ? this.repositoryUserPvForDay :
|
||||
new Error('not happen') as never;
|
||||
|
||||
// ログ取得
|
||||
return await repository.createQueryBuilder()
|
||||
.select('"group" as "userId", sum("___upv_user" + "___upv_visitor") as "count"')
|
||||
.where('date BETWEEN :gt AND :lt', { gt: Chart.dateToTimestamp(gt), lt: Chart.dateToTimestamp(lt) })
|
||||
.groupBy('"userId"')
|
||||
.orderBy('"count"', order)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.getRawMany<{ userId: string, count: number }>();
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu
|
||||
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
|
||||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
||||
import { UserSuspendProcessorService } from './processors/UserSuspendProcessorService.js';
|
||||
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
|
||||
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
||||
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
|
||||
@ -68,6 +69,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||
ImportUserListsProcessorService,
|
||||
ImportCustomEmojisProcessorService,
|
||||
ImportAntennasProcessorService,
|
||||
UserSuspendProcessorService,
|
||||
DeleteAccountProcessorService,
|
||||
DeleteFileProcessorService,
|
||||
CleanRemoteFilesProcessorService,
|
||||
|
@ -28,6 +28,7 @@ import { ImportBlockingProcessorService } from './processors/ImportBlockingProce
|
||||
import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js';
|
||||
import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js';
|
||||
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
|
||||
import { UserSuspendProcessorService } from './processors/UserSuspendProcessorService.js';
|
||||
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
|
||||
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
||||
@ -106,6 +107,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
private importUserListsProcessorService: ImportUserListsProcessorService,
|
||||
private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService,
|
||||
private importAntennasProcessorService: ImportAntennasProcessorService,
|
||||
private userSuspendProcessorService: UserSuspendProcessorService,
|
||||
private deleteAccountProcessorService: DeleteAccountProcessorService,
|
||||
private deleteFileProcessorService: DeleteFileProcessorService,
|
||||
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
|
||||
@ -184,6 +186,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
case 'importCustomEmojis': return this.importCustomEmojisProcessorService.process(job);
|
||||
case 'importAntennas': return this.importAntennasProcessorService.process(job);
|
||||
case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
|
||||
case 'userSuspend': return this.userSuspendProcessorService.process(job);
|
||||
case 'reportAbuse': return this.reportAbuseProcessorService.process(job);
|
||||
default: throw new Error(`unrecognized job type ${job.name} for db`);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ export class DeleteAccountProcessorService {
|
||||
private roleService: RoleService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('delete-account');
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('account:delete');
|
||||
}
|
||||
|
||||
private async deleteNotes(user: MiUser) {
|
||||
|
@ -51,7 +51,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
|
||||
this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
|
||||
this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -0,0 +1,101 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import type {
|
||||
AntennasRepository,
|
||||
ClipNotesRepository,
|
||||
ClipsRepository,
|
||||
FollowRequestsRepository,
|
||||
UserListMembershipsRepository,
|
||||
UserListsRepository, UsersRepository,
|
||||
WebhooksRepository,
|
||||
} from '@/models/_.js';
|
||||
import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
|
||||
import type * as Bull from "bullmq";
|
||||
import type { DbUserSuspendJobData } from "@/queue/types.js";
|
||||
|
||||
@Injectable()
|
||||
export class UserSuspendProcessorService {
|
||||
public logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
@Inject(DI.antennasRepository)
|
||||
private antennasRepository: AntennasRepository,
|
||||
|
||||
@Inject(DI.webhooksRepository)
|
||||
private webhooksRepository: WebhooksRepository,
|
||||
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.clipsRepository)
|
||||
private clipsRepository: ClipsRepository,
|
||||
|
||||
@Inject(DI.clipNotesRepository)
|
||||
private clipNotesRepository: ClipNotesRepository,
|
||||
|
||||
@Inject(DI.userListMembershipsRepository)
|
||||
private userListMembershipsRepository: UserListMembershipsRepository,
|
||||
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('account:suspend');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserSuspendJobData>): Promise<string | void> {
|
||||
this.logger.warn(`Cleaning up suspended account of ${job.data.user.id} ...`, { userSuspendJobData: job.data });
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
return 'User not found';
|
||||
}
|
||||
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
let cursor = '';
|
||||
while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
|
||||
const clipNotes = await this.clipNotesRepository.createQueryBuilder('c')
|
||||
.select('c.id')
|
||||
.innerJoin('c.note', 'n')
|
||||
.where('n.userId = :userId', { userId: user.id })
|
||||
.andWhere('c.id > :cursor', { cursor })
|
||||
.orderBy('c.id', 'ASC')
|
||||
.limit(100)
|
||||
.getRawMany<{ id: string }>();
|
||||
|
||||
if (clipNotes.length === 0) break;
|
||||
|
||||
cursor = clipNotes.at(-1)?.id ?? '';
|
||||
|
||||
promises.push(this.clipNotesRepository.createQueryBuilder()
|
||||
.delete()
|
||||
.where('id IN (:...ids)', { ids: clipNotes.map((clipNote) => clipNote.id) })
|
||||
.execute());
|
||||
}
|
||||
|
||||
await Promise.allSettled([
|
||||
this.followRequestsRepository.delete({ followeeId: user.id }),
|
||||
this.followRequestsRepository.delete({ followerId: user.id }),
|
||||
|
||||
this.antennasRepository.delete({ userId: user.id }),
|
||||
this.webhooksRepository.delete({ userId: user.id }),
|
||||
this.userListsRepository.delete({ userId: user.id }),
|
||||
this.clipsRepository.delete({ userId: user.id }),
|
||||
|
||||
...promises,
|
||||
this.userListMembershipsRepository.delete({ userId: user.id }),
|
||||
]);
|
||||
|
||||
this.logger.info(`Completed cleaning up suspended account of ${job.data.user.id}`);
|
||||
|
||||
return 'done';
|
||||
}
|
||||
}
|
@ -83,6 +83,10 @@ export type DbUserDeleteJobData = {
|
||||
onlyFiles?: boolean;
|
||||
};
|
||||
|
||||
export type DbUserSuspendJobData = {
|
||||
user: ThinUser
|
||||
};
|
||||
|
||||
export type DbUserImportJobData = {
|
||||
user: ThinUser;
|
||||
fileId: MiDriveFile['id'];
|
||||
|
@ -97,6 +97,11 @@ export const meta = {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
lastReadAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -140,6 +145,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
userId: announcement.userId,
|
||||
user: announcement.userInfo,
|
||||
reads: announcement.reads,
|
||||
lastReadAt: announcement.lastReadAt?.toISOString() ?? null,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
@ -72,12 +72,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (ps.hostname) {
|
||||
query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() });
|
||||
}
|
||||
const chartUsers: { userId: string; count: number; }[] = [];
|
||||
|
||||
let pvRankedUsers: { userId: string; count: number; }[] | undefined = undefined;
|
||||
if (ps.sort?.endsWith('pv')) {
|
||||
await this.perUserPvChart.getChartUsers('hour', ps.sort === '+pv' ? 'DESC' : 'ASC', 0, null, ps.limit, ps.offset).then(users => {
|
||||
chartUsers.push(...users);
|
||||
});
|
||||
// 直近12時間のPVランキングを取得
|
||||
pvRankedUsers = await this.perUserPvChart.getUsersRanking(
|
||||
'hour', ps.sort.startsWith('+') ? 'DESC' : 'ASC',
|
||||
12, null, ps.limit, ps.offset,
|
||||
);
|
||||
}
|
||||
|
||||
switch (ps.sort) {
|
||||
case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
|
||||
case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
|
||||
@ -85,16 +89,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
case '-createdAt': query.orderBy('user.id', 'ASC'); break;
|
||||
case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break;
|
||||
case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break;
|
||||
case '+pv':
|
||||
if (chartUsers.length > 0) {
|
||||
query.andWhere('user.id IN (:...userIds)', { userIds: chartUsers.map(user => user.userId) });
|
||||
}
|
||||
break;
|
||||
case '-pv':
|
||||
if (chartUsers.length > 0) {
|
||||
query.andWhere('user.id IN (:...userIds)', { userIds: chartUsers.map(user => user.userId) });
|
||||
}
|
||||
break;
|
||||
case '+pv': query.andWhere((pvRankedUsers?.length ?? 0) > 0 ? 'user.id IN (:...userIds)' : '1 = 0', { userIds: pvRankedUsers?.map(user => user.userId) ?? [] }); break;
|
||||
case '-pv': query.andWhere((pvRankedUsers?.length ?? 0) > 0 ? 'user.id IN (:...userIds)' : '1 = 0', { userIds: pvRankedUsers?.map(user => user.userId) ?? [] }); break;
|
||||
default: query.orderBy('user.id', 'ASC'); break;
|
||||
}
|
||||
|
||||
@ -107,14 +103,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
const users = await query.getMany();
|
||||
if (ps.sort === '+pv') {
|
||||
users.sort((a, b) => {
|
||||
const aPv = chartUsers.find(user => user.userId === a.id)?.count ?? 0;
|
||||
const bPv = chartUsers.find(user => user.userId === b.id)?.count ?? 0;
|
||||
const aPv = pvRankedUsers?.find(u => u.userId === a.id)?.count ?? 0;
|
||||
const bPv = pvRankedUsers?.find(u => u.userId === b.id)?.count ?? 0;
|
||||
return bPv - aPv;
|
||||
});
|
||||
} else if (ps.sort === '-pv') {
|
||||
users.sort((a, b) => {
|
||||
const aPv = chartUsers.find(user => user.userId === a.id)?.count ?? 0;
|
||||
const bPv = chartUsers.find(user => user.userId === b.id)?.count ?? 0;
|
||||
const aPv = pvRankedUsers?.find(u => u.userId === a.id)?.count ?? 0;
|
||||
const bPv = pvRankedUsers?.find(u => u.userId === b.id)?.count ?? 0;
|
||||
return aPv - bPv;
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,19 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
keydbtest:
|
||||
image: eqalpha/keydb:latest
|
||||
dragonflytest:
|
||||
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||
ulimits:
|
||||
memlock: -1
|
||||
environment:
|
||||
DFLY_version_check: false
|
||||
DFLY_tcp_backlog: 2048
|
||||
DFLY_default_lua_flags: allow-undeclared-keys
|
||||
DFLY_pipeline_squash: 0
|
||||
DFLY_multi_exec_squash: false
|
||||
DFLY_conn_io_threads: 4
|
||||
DFLY_epoll_file_threads: 4
|
||||
DFLY_proactor_threads: 4
|
||||
ports:
|
||||
- "127.0.0.1:56312:6379"
|
||||
|
||||
|
@ -189,7 +189,7 @@ async function onSubmit(): Promise<void> {
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
await misskeyApi('signup', {
|
||||
await os.apiWithDialog('signup', {
|
||||
username: username.value,
|
||||
password: password.value.password,
|
||||
emailAddress: email.value,
|
||||
@ -198,35 +198,27 @@ async function onSubmit(): Promise<void> {
|
||||
'm-captcha-response': mCaptchaResponse.value,
|
||||
'g-recaptcha-response': reCaptchaResponse.value,
|
||||
'turnstile-response': turnstileResponse.value,
|
||||
});
|
||||
if (instance.emailRequiredForSignup) {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts._signup.almostThere,
|
||||
text: i18n.tsx._signup.emailSent({ email: email.value }),
|
||||
});
|
||||
emit('signupEmailPending');
|
||||
} else {
|
||||
const res = await misskeyApi('signin', {
|
||||
username: username.value,
|
||||
password: password.value.password,
|
||||
});
|
||||
emit('signup', res);
|
||||
}, undefined, (res) => {
|
||||
if (instance.emailRequiredForSignup) {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts._signup.almostThere,
|
||||
text: i18n.tsx._signup.emailSent({ email: email.value }),
|
||||
});
|
||||
emit('signupEmailPending');
|
||||
} else {
|
||||
emit('signup', { id: res.id, i: res.token });
|
||||
|
||||
if (props.autoSet) {
|
||||
return login(res.i);
|
||||
if (props.autoSet) {
|
||||
login(res.token);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
submitting.value = false;
|
||||
hcaptcha.value?.reset?.();
|
||||
recaptcha.value?.reset?.();
|
||||
turnstile.value?.reset?.();
|
||||
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.somethingHappened,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
{{ i18n.ts._announcement.silence }}
|
||||
<template #caption>{{ i18n.ts._announcement.silenceDescription }}</template>
|
||||
</MkSwitch>
|
||||
<p v-if="reads">{{ i18n.tsx.nUsersRead({ n: reads }) }}</p>
|
||||
<p v-if="reads">{{ i18n.tsx.nUsersRead({ n: reads }) }} <span v-if="lastReadAt">(<MkTime :time="lastReadAt" mode="absolute"/>)</span></p>
|
||||
<MkUserCardMini v-if="props.user.id" :user="props.user"></MkUserCardMini>
|
||||
<MkButton v-if="announcement" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
@ -94,6 +94,7 @@ const closeDuration = ref<number>(props.announcement ? props.announcement.closeD
|
||||
const displayOrder = ref<number>(props.announcement ? props.announcement.displayOrder : 0);
|
||||
const silence = ref<boolean>(props.announcement ? props.announcement.silence : false);
|
||||
const reads = ref<number>(props.announcement ? props.announcement.reads : 0);
|
||||
const lastReadAt = ref<string | null>(props.announcement ? props.announcement.lastReadAt : null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
|
@ -34,60 +34,77 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
|
||||
endpoint: E,
|
||||
data: P = {} as P,
|
||||
token?: string | null | undefined,
|
||||
) => {
|
||||
onSuccess?: ((res: Misskey.api.SwitchCaseResponseType<E, P>) => void) | null | undefined,
|
||||
onFailure?: ((err: Misskey.api.APIError) => void) | null,
|
||||
): Promise<Misskey.api.SwitchCaseResponseType<E, P>> => {
|
||||
const promise = misskeyApi(endpoint, data, token);
|
||||
promiseDialog(promise, null, async (err) => {
|
||||
let title: string | undefined;
|
||||
let text = err.message + '\n' + err.id;
|
||||
if (err.code === 'INTERNAL_ERROR') {
|
||||
title = i18n.ts.internalServerError;
|
||||
text = i18n.ts.internalServerErrorDescription;
|
||||
const date = new Date().toISOString();
|
||||
const { result } = await actions({
|
||||
type: 'error',
|
||||
title,
|
||||
text,
|
||||
details: err.info,
|
||||
actions: [{
|
||||
value: 'ok',
|
||||
text: i18n.ts.gotIt,
|
||||
primary: true,
|
||||
}, {
|
||||
value: 'copy',
|
||||
text: i18n.ts.copyErrorInfo,
|
||||
}],
|
||||
});
|
||||
if (result === 'copy') {
|
||||
copyToClipboard(`Endpoint: ${endpoint}\nInfo: ${JSON.stringify(err.info)}\nDate: ${date}`);
|
||||
success();
|
||||
}
|
||||
return;
|
||||
} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
|
||||
title = i18n.ts.cannotPerformTemporary;
|
||||
text = i18n.ts.cannotPerformTemporaryDescription;
|
||||
} else if (err.code === 'INVALID_PARAM') {
|
||||
title = i18n.ts.invalidParamError;
|
||||
text = i18n.ts.invalidParamErrorDescription;
|
||||
} else if (err.code === 'ROLE_PERMISSION_DENIED') {
|
||||
title = i18n.ts.permissionDeniedError;
|
||||
text = i18n.ts.permissionDeniedErrorDescription;
|
||||
} else if (err.code.startsWith('TOO_MANY')) {
|
||||
title = i18n.ts.youCannotCreateAnymore;
|
||||
text = `${i18n.ts.error}: ${err.id}`;
|
||||
} else if (err.message.startsWith('Unexpected token')) {
|
||||
title = i18n.ts.gotInvalidResponseError;
|
||||
text = i18n.ts.gotInvalidResponseErrorDescription;
|
||||
}
|
||||
alert({
|
||||
promiseDialog(promise, onSuccess, onFailure ?? (err => apiErrorHandler(err, endpoint)));
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
export async function apiErrorHandler(err: Misskey.api.APIError, endpoint?: string): Promise<void> {
|
||||
let title: string | undefined;
|
||||
let text = err.message + '\n' + err.id;
|
||||
|
||||
if (err.code === 'INTERNAL_ERROR') {
|
||||
title = i18n.ts.internalServerError;
|
||||
text = i18n.ts.internalServerErrorDescription;
|
||||
const date = new Date().toISOString();
|
||||
const { result } = await actions({
|
||||
type: 'error',
|
||||
title,
|
||||
text,
|
||||
details: err.info,
|
||||
actions: [{
|
||||
value: 'ok',
|
||||
text: i18n.ts.gotIt,
|
||||
primary: true,
|
||||
}, {
|
||||
value: 'copy',
|
||||
text: i18n.ts.copyErrorInfo,
|
||||
}],
|
||||
});
|
||||
});
|
||||
if (result === 'copy') {
|
||||
copyToClipboard(`Endpoint: ${endpoint}\nInfo: ${JSON.stringify(err.info)}\nDate: ${date}`);
|
||||
success();
|
||||
}
|
||||
return;
|
||||
} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
|
||||
title = i18n.ts.cannotPerformTemporary;
|
||||
text = i18n.ts.cannotPerformTemporaryDescription;
|
||||
} else if (err.code === 'INVALID_PARAM') {
|
||||
title = i18n.ts.invalidParamError;
|
||||
text = i18n.ts.invalidParamErrorDescription;
|
||||
} else if (err.code === 'ROLE_PERMISSION_DENIED') {
|
||||
title = i18n.ts.permissionDeniedError;
|
||||
text = i18n.ts.permissionDeniedErrorDescription;
|
||||
} else if (err.code?.startsWith('TOO_MANY')) {
|
||||
title = i18n.ts.youCannotCreateAnymore;
|
||||
text = `${i18n.ts.error}: ${err.id}`;
|
||||
}
|
||||
|
||||
return promise;
|
||||
}) as typeof misskeyApi;
|
||||
// @ts-expect-error Misskey内部で定義されていない不明なエラー
|
||||
if (!err.id && (err.statusCode ?? 0) > 499) {
|
||||
title = i18n.ts.gotInvalidResponseError;
|
||||
text = i18n.ts.gotInvalidResponseErrorDescription;
|
||||
}
|
||||
|
||||
if (err.id && !title) {
|
||||
title = i18n.ts.somethingHappened;
|
||||
} else if (!title) {
|
||||
title = i18n.ts.somethingHappened;
|
||||
text = err.message;
|
||||
}
|
||||
|
||||
alert({
|
||||
type: 'error',
|
||||
title,
|
||||
text,
|
||||
// @ts-expect-error Misskeyのエラーならinfoを、そうでなければそのまま表示
|
||||
details: err.id ? err.info : err as unknown,
|
||||
});
|
||||
}
|
||||
|
||||
export function promiseDialog<T>(
|
||||
promise: Promise<T>,
|
||||
|
@ -149,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<span>{{ announcement.title }}</span>
|
||||
<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span>
|
||||
<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }} <span v-if="announcement.lastReadAt">(<MkTime :time="announcement.lastReadAt" mode="absolute"/>)</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription">
|
||||
{{ i18n.ts._announcement.silence }}
|
||||
</MkSwitch>
|
||||
<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
|
||||
<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }} <span v-if="announcement.lastReadAt">(<MkTime :time="announcement.lastReadAt" mode="absolute"/>)</span></p>
|
||||
<MkUserCardMini v-if="announcement.userId" :user="announcement.user" @click="editUser(announcement)"></MkUserCardMini>
|
||||
<MkButton v-else class="button" inline primary @click="editUser(announcement)">{{ i18n.ts.specifyUser }}</MkButton>
|
||||
<div class="buttons _buttons">
|
||||
|
@ -114,7 +114,7 @@ async function deleteAccount() {
|
||||
{
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteAccountConfirmAndWarn,
|
||||
text: i18n.ts.deleteAccountConfirm,
|
||||
okWaitInitiate: 'dialog',
|
||||
okWaitDuration: 5,
|
||||
});
|
||||
|
@ -44,14 +44,15 @@ export function misskeyApi<
|
||||
},
|
||||
signal,
|
||||
}).then(async (res) => {
|
||||
const body = res.status === 204 ? null : await res.json();
|
||||
|
||||
if (res.status === 200) {
|
||||
if (res.ok && res.status !== 204) {
|
||||
const body = await res.json();
|
||||
resolve(body);
|
||||
} else if (res.status === 204) {
|
||||
resolve(undefined as _ResT); // void -> undefined
|
||||
} else {
|
||||
reject(body.error);
|
||||
// エラー応答で JSON.parse に失敗した場合は HTTP ステータスコードとメッセージを返す
|
||||
const body = await res.json().catch(() => ({ statusCode: res.status, message: res.statusText }));
|
||||
reject(typeof body.error === 'object' ? body.error : body);
|
||||
}
|
||||
}).catch(reject);
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2024.5.0-host.2e",
|
||||
"version": "2024.5.0-host.2f",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"types": "./built/dts/index.d.ts",
|
||||
"exports": {
|
||||
|
@ -6168,6 +6168,8 @@ export type operations = {
|
||||
userId: string | null;
|
||||
user: components['schemas']['UserLite'] | null;
|
||||
reads: number;
|
||||
/** Format: date-time */
|
||||
lastReadAt: string | null;
|
||||
})[];
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user