forked from FoundKeyGang/FoundKey
Merge branch 'main' into snug.moe
This commit is contained in:
commit
50bac9e2d3
47 changed files with 214 additions and 280 deletions
|
@ -3,9 +3,9 @@ Note: this document is historical.
|
|||
Everything starting with the next section is the original "idea" document that led to the foundation of FoundKey.
|
||||
|
||||
For the current status you should see the following:
|
||||
* The Behavioral Fixes [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/3)
|
||||
* The Technological Upkeep [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/4)
|
||||
* The Features [project](https://akkoma.dev/FoundKeyGang/FoundKey/projects/5)
|
||||
* Issues labeled with [behaviour-fix](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=44)
|
||||
* Issues labeled with [upkeep](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=43)
|
||||
* Issues labeled with [feature](https://akkoma.dev/FoundKeyGang/FoundKey/issues?labels=42)
|
||||
|
||||
## Misskey Goals
|
||||
I’ve been thinking about a community misskey fork for a while now. To some of you, this is not a surprise. Let’s talk about that.
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
export class removeUserGroupInvite1672991292018 {
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_e10924607d058004304611a436a"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_group_invite" DROP CONSTRAINT "FK_1039988afa3bf991185b277fe03"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_d9ecaed8c6dc43f3592c229282"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_78787741f9010886796f2320a4"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_e10924607d058004304611a436"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_1039988afa3bf991185b277fe0"`);
|
||||
await queryRunner.query(`DROP TABLE "user_group_invite"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "user_group_invite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "userGroupId" character varying(32) NOT NULL, CONSTRAINT "PK_3893884af0d3a5f4d01e7921a97" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_1039988afa3bf991185b277fe0" ON "user_group_invite" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_e10924607d058004304611a436" ON "user_group_invite" ("userGroupId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_78787741f9010886796f2320a4" ON "user_group_invite" ("userId", "userGroupId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9ecaed8c6dc43f3592c229282" ON "user_group_joining" ("userId", "userGroupId") `);
|
||||
await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_1039988afa3bf991185b277fe03" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "user_group_invite" ADD CONSTRAINT "FK_e10924607d058004304611a436a" FOREIGN KEY ("userGroupId") REFERENCES "user_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||
"watch": "node watch.mjs",
|
||||
"lint": "tsc --noEmit && eslint src --ext .ts",
|
||||
"lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts",
|
||||
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
|
||||
"migrate": "npx typeorm migration:run -d ormconfig.js",
|
||||
"start": "node --experimental-json-modules ./built/index.js",
|
||||
|
|
|
@ -54,7 +54,11 @@ export async function getResponse(args: { url: string, method: string, body?: st
|
|||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (
|
||||
!res.ok
|
||||
&&
|
||||
// intended redirect is not an error
|
||||
!(args.redirect != 'follow' && res.status >= 300 && res.status < 400)) {
|
||||
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
|
||||
}
|
||||
|
||||
|
|
|
@ -155,7 +155,14 @@ export class User {
|
|||
})
|
||||
public isExplorable: boolean;
|
||||
|
||||
// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
|
||||
// for local users:
|
||||
// Indicates a deletion in progress.
|
||||
// A hard delete of the record will follow after the deletion finishes.
|
||||
//
|
||||
// for remote users:
|
||||
// Indicates the user was deleted by an admin.
|
||||
// The users' data is not deleted from the database to keep them from reappearing.
|
||||
// A hard delete of the record may follow if we receive a matching Delete activity.
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether the User is deleted.',
|
||||
|
|
|
@ -5,7 +5,6 @@ import config from '@/config/index.js';
|
|||
import { Packed } from '@/misc/schema.js';
|
||||
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
|
||||
import { populateEmojis } from '@/misc/populate-emojis.js';
|
||||
import { getAntennas } from '@/misc/antenna-cache.js';
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD, HOUR } from '@/const.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
|
@ -126,92 +125,71 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
},
|
||||
|
||||
async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {
|
||||
const mute = await Mutings.findBy({
|
||||
muterId: userId,
|
||||
});
|
||||
return await db.query(
|
||||
`SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM "messaging_message"
|
||||
WHERE
|
||||
"recipientId" = $1
|
||||
AND
|
||||
NOT "isRead"
|
||||
AND
|
||||
"userId" NOT IN (
|
||||
SELECT "muteeId"
|
||||
FROM "muting"
|
||||
WHERE "muterId" = $1
|
||||
)
|
||||
|
||||
const joinings = await UserGroupJoinings.findBy({ userId });
|
||||
UNION
|
||||
|
||||
const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
|
||||
.where('message.groupId = :groupId', { groupId: j.userGroupId })
|
||||
.andWhere('message.userId != :userId', { userId })
|
||||
.andWhere('NOT (:userId = ANY(message.reads))', { userId })
|
||||
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
|
||||
.getOne().then(x => x != null)));
|
||||
|
||||
const [withUser, withGroups] = await Promise.all([
|
||||
MessagingMessages.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);
|
||||
SELECT 1
|
||||
FROM "messaging_message"
|
||||
JOIN "user_group_joining"
|
||||
ON "messaging_message"."groupId" = "user_group_joining"."userGroupId"
|
||||
WHERE
|
||||
"messaging_message"."userId" != $1
|
||||
AND
|
||||
NOT $1 = ANY("messaging_message"."reads")
|
||||
AND
|
||||
"messaging_message"."createdAt" > "user_group_joining"."createdAt"
|
||||
) AS exists`,
|
||||
[userId]
|
||||
).then(res => res[0].exists);
|
||||
},
|
||||
|
||||
async getHasUnreadAnnouncement(userId: User['id']): Promise<boolean> {
|
||||
const reads = await AnnouncementReads.findBy({
|
||||
userId,
|
||||
});
|
||||
|
||||
const count = await Announcements.countBy(reads.length > 0 ? {
|
||||
id: Not(In(reads.map(read => read.announcementId))),
|
||||
} : {});
|
||||
|
||||
return count > 0;
|
||||
return await db.query(
|
||||
`SELECT EXISTS (SELECT 1 FROM "announcement" WHERE "id" NOT IN (SELECT "announcementId" FROM "announcement_read" WHERE "userId" = $1)) AS exists`,
|
||||
[userId]
|
||||
).then(res => res[0].exists);
|
||||
},
|
||||
|
||||
async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
|
||||
const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
|
||||
|
||||
const unread = myAntennas.length > 0 ? await AntennaNotes.findOneBy({
|
||||
antennaId: In(myAntennas.map(x => x.id)),
|
||||
read: false,
|
||||
}) : null;
|
||||
|
||||
return unread != null;
|
||||
return await db.query(
|
||||
`SELECT EXISTS (SELECT 1 FROM "antenna_note" WHERE NOT "read" AND "antennaId" IN (SELECT "id" FROM "antenna" WHERE "userId" = $1)) AS exists`,
|
||||
[userId]
|
||||
).then(res => res[0].exists);
|
||||
},
|
||||
|
||||
async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
|
||||
const channels = await ChannelFollowings.findBy({ followerId: userId });
|
||||
|
||||
const unread = channels.length > 0 ? await NoteUnreads.findOneBy({
|
||||
userId,
|
||||
noteChannelId: In(channels.map(x => x.followeeId)),
|
||||
}) : null;
|
||||
|
||||
return unread != null;
|
||||
return await db.query(
|
||||
`SELECT EXISTS (SELECT 1 FROM "note_unread" WHERE "noteChannelId" IN (SELECT "followeeId" FROM "channel_following" WHERE "followerId" = $1)) AS exists`,
|
||||
[userId]
|
||||
).then(res => res[0].exists);
|
||||
},
|
||||
|
||||
async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||
const mute = await Mutings.findBy({
|
||||
muterId: userId,
|
||||
});
|
||||
const mutedUserIds = mute.map(m => m.muteeId);
|
||||
|
||||
const count = await Notifications.count({
|
||||
where: {
|
||||
notifieeId: userId,
|
||||
...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
|
||||
isRead: false,
|
||||
},
|
||||
take: 1,
|
||||
});
|
||||
|
||||
return count > 0;
|
||||
return await db.query(
|
||||
`SELECT EXISTS (SELECT 1 FROM "notification" WHERE NOT "isRead" AND "notifieeId" = $1 AND "notifierId" NOT IN (SELECT "muteeId" FROM "muting" WHERE "muterId" = $1)) AS exists`,
|
||||
[userId]
|
||||
).then(res => res[0].exists);
|
||||
},
|
||||
|
||||
async getHasPendingReceivedFollowRequest(userId: User['id']): Promise<boolean> {
|
||||
const count = await FollowRequests.countBy({
|
||||
followeeId: userId,
|
||||
});
|
||||
|
||||
return count > 0;
|
||||
return await db.query(
|
||||
`SELECT EXISTS (SELECT 1 FROM "follow_request" WHERE "followeeId" = $1) AS exists`,
|
||||
[userId]
|
||||
).then(res => res[0].exists);
|
||||
},
|
||||
|
||||
getOnlineStatus(user: User): 'unknown' | 'online' | 'active' | 'offline' {
|
||||
|
|
|
@ -83,10 +83,8 @@ export async function deleteAccount(job: Bull.Job<DbUserDeleteJobData>): Promise
|
|||
}
|
||||
}
|
||||
|
||||
// soft指定されている場合は物理削除しない
|
||||
if (job.data.soft) {
|
||||
// nop
|
||||
} else {
|
||||
// No physical deletion if soft is specified.
|
||||
if (!job.data.soft) {
|
||||
await Users.delete(job.data.user.id);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createDeleteAccountJob } from '@/queue/index.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import { apLogger } from '@/remote/activitypub/logger.js';
|
||||
import { deleteAccount } from '@/services/delete-account.js';
|
||||
|
||||
export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise<string> {
|
||||
apLogger.info(`Deleting the Actor: ${uri}`);
|
||||
|
@ -17,14 +17,9 @@ export async function deleteActor(actor: CacheableRemoteUser, uri: string): Prom
|
|||
return 'ok: gone';
|
||||
}
|
||||
if (user.isDeleted) {
|
||||
apLogger.info('skip: already deleted');
|
||||
// the actual deletion already happened by an admin, just delete the record
|
||||
await Users.delete(actor.id);
|
||||
} else {
|
||||
await deleteAccount(actor);
|
||||
}
|
||||
|
||||
const job = await createDeleteAccountJob(actor);
|
||||
|
||||
await Users.update(actor.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
return `ok: queued ${job.name} ${job.id}`;
|
||||
}
|
||||
|
|
|
@ -157,7 +157,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
|||
// 引用
|
||||
let quote: Note | undefined | null;
|
||||
|
||||
if (note._misskey_quote || note.quoteUrl) {
|
||||
if (note._misskey_quote || note.quoteUri) {
|
||||
const tryResolveNote = async (uri: string): Promise<{
|
||||
status: 'ok';
|
||||
res: Note | null;
|
||||
|
@ -184,7 +184,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
|||
}
|
||||
};
|
||||
|
||||
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string'));
|
||||
const uris = unique([note._misskey_quote, note.quoteUri].filter((x): x is string => typeof x === 'string'));
|
||||
const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
|
||||
|
||||
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import promiseLimit from 'promise-limit';
|
||||
import { Not, IsNull } from 'typeorm';
|
||||
|
||||
import config from '@/config/index.js';
|
||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||
|
@ -54,6 +55,12 @@ function validateActor(x: IObject): IActor {
|
|||
throw new Error('invalid Actor: wrong id');
|
||||
}
|
||||
|
||||
// This check is security critical.
|
||||
// Without this check, an entry could be inserted into UserPublickey for a local user.
|
||||
if (extractDbHost(uri) === extractDbHost(config.url)) {
|
||||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||
}
|
||||
|
||||
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
||||
throw new Error('invalid Actor: wrong inbox');
|
||||
}
|
||||
|
@ -83,9 +90,9 @@ function validateActor(x: IObject): IActor {
|
|||
throw new Error('invalid Actor: publicKey.id is not a string');
|
||||
}
|
||||
|
||||
const expectHost = extractDbHost(uri);
|
||||
const publicKeyIdHost = extractDbHost(x.publicKey.id);
|
||||
if (publicKeyIdHost !== expectHost) {
|
||||
// This is a security critical check to not insert or change an entry of
|
||||
// UserPublickey to point to a local key id.
|
||||
if (extractDbHost(uri) !== extractDbHost(x.publicKey.id)) {
|
||||
throw new Error('invalid Actor: publicKey.id has different host');
|
||||
}
|
||||
}
|
||||
|
@ -104,7 +111,7 @@ export async function fetchPerson(uri: string): Promise<CacheableUser | null> {
|
|||
const cached = uriPersonCache.get(uri);
|
||||
if (cached) return cached;
|
||||
|
||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||
// If the URI points to this server, fetch from database.
|
||||
if (uri.startsWith(config.url + '/')) {
|
||||
const id = uri.split('/').pop();
|
||||
const u = await Users.findOneBy({ id });
|
||||
|
@ -128,10 +135,6 @@ export async function fetchPerson(uri: string): Promise<CacheableUser | null> {
|
|||
* Personを作成します。
|
||||
*/
|
||||
export async function createPerson(value: string | IObject, resolver: Resolver): Promise<User> {
|
||||
if (getApId(value).startsWith(config.url)) {
|
||||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||
}
|
||||
|
||||
const object = await resolver.resolve(value) as any;
|
||||
|
||||
const person = validateActor(object);
|
||||
|
@ -275,13 +278,8 @@ export async function createPerson(value: string | IObject, resolver: Resolver):
|
|||
export async function updatePerson(value: IObject | string, resolver: Resolver): Promise<void> {
|
||||
const uri = getApId(value);
|
||||
|
||||
// skip local URIs
|
||||
if (uri.startsWith(config.url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// do we already know this user?
|
||||
const exist = await Users.findOneBy({ uri }) as IRemoteUser;
|
||||
const exist = await Users.findOneBy({ uri, host: Not(IsNull()) }) as IRemoteUser;
|
||||
|
||||
if (exist == null) {
|
||||
return;
|
||||
|
|
|
@ -21,12 +21,14 @@ export const renderActivity = (x: any): IActivity | null => {
|
|||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||
sensitive: 'as:sensitive',
|
||||
Hashtag: 'as:Hashtag',
|
||||
quoteUrl: 'as:quoteUrl',
|
||||
// Mastodon
|
||||
toot: 'http://joinmastodon.org/ns#',
|
||||
Emoji: 'toot:Emoji',
|
||||
featured: 'toot:featured',
|
||||
discoverable: 'toot:discoverable',
|
||||
// Fedibird
|
||||
fedibird: 'http://fedibird.com/ns#',
|
||||
quoteUri: 'fedibird:quoteUri',
|
||||
// schema
|
||||
schema: 'http://schema.org#',
|
||||
PropertyValue: 'schema:PropertyValue',
|
||||
|
|
|
@ -27,7 +27,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
|||
if (inReplyToNote != null) {
|
||||
const inReplyToUserExists = await Users.countBy({ id: inReplyToNote.userId });
|
||||
|
||||
if (!inReplyToUserExists) {
|
||||
if (inReplyToUserExists) {
|
||||
if (inReplyToNote.uri) {
|
||||
inReplyTo = inReplyToNote.uri;
|
||||
} else {
|
||||
|
@ -141,7 +141,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
|||
mediaType: 'text/x.misskeymarkdown',
|
||||
},
|
||||
_misskey_quote: quote,
|
||||
quoteUrl: quote,
|
||||
quoteUri: quote,
|
||||
published: note.createdAt.toISOString(),
|
||||
to,
|
||||
cc,
|
||||
|
|
|
@ -111,7 +111,7 @@ export interface IPost extends IObject {
|
|||
mediaType: string;
|
||||
};
|
||||
_misskey_quote?: string;
|
||||
quoteUrl?: string;
|
||||
quoteUri?: string;
|
||||
_misskey_talk: boolean;
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ export interface IQuestion extends IObject {
|
|||
mediaType: string;
|
||||
};
|
||||
_misskey_quote?: string;
|
||||
quoteUrl?: string;
|
||||
quoteUri?: string;
|
||||
oneOf?: IQuestionChoice[];
|
||||
anyOf?: IQuestionChoice[];
|
||||
endTime?: Date;
|
||||
|
|
|
@ -57,7 +57,7 @@ export default async (ctx: Router.RouterContext) => {
|
|||
.where('note.visibility = \'public\'')
|
||||
.orWhere('note.visibility = \'home\'');
|
||||
}))
|
||||
.andWhere('note.localOnly = FALSE');
|
||||
.andWhere('NOT note.localOnly');
|
||||
|
||||
const notes = await query.take(limit).getMany();
|
||||
|
||||
|
|
|
@ -54,7 +54,6 @@ import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
|
|||
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
||||
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
||||
import * as ep___admin_vacuum from './endpoints/admin/vacuum.js';
|
||||
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
|
||||
import * as ep___announcements from './endpoints/announcements.js';
|
||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
||||
|
@ -363,7 +362,6 @@ const eps = [
|
|||
['admin/unsuspend-user', ep___admin_unsuspendUser],
|
||||
['admin/update-meta', ep___admin_updateMeta],
|
||||
['admin/vacuum', ep___admin_vacuum],
|
||||
['admin/delete-account', ep___admin_deleteAccount],
|
||||
['announcements', ep___announcements],
|
||||
['antennas/create', ep___antennas_create],
|
||||
['antennas/delete', ep___antennas_delete],
|
||||
|
|
|
@ -93,8 +93,8 @@ export default define(meta, paramDef, async (ps) => {
|
|||
const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId);
|
||||
|
||||
switch (ps.state) {
|
||||
case 'resolved': query.andWhere('report.resolved = TRUE'); break;
|
||||
case 'unresolved': query.andWhere('report.resolved = FALSE'); break;
|
||||
case 'resolved': query.andWhere('report.resolved'); break;
|
||||
case 'unresolved': query.andWhere('NOT report.resolved'); break;
|
||||
}
|
||||
|
||||
switch (ps.reporterOrigin) {
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import { Users } from '@/models/index.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { doPostSuspend } from '@/services/suspend-user.js';
|
||||
import { publishUserEvent } from '@/services/stream.js';
|
||||
import { createDeleteAccountJob } from '@/queue/index.js';
|
||||
import { deleteAccount } from '@/services/delete-account.js';
|
||||
import define from '../../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
requireAdmin: true,
|
||||
|
||||
errors: ['NO_SUCH_USER', 'IS_ADMIN', 'IS_MODERATOR'],
|
||||
} as const;
|
||||
|
@ -24,7 +22,10 @@ export const paramDef = {
|
|||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps) => {
|
||||
const user = await Users.findOneBy({ id: ps.userId });
|
||||
const user = await Users.findOneBy({
|
||||
id: ps.userId,
|
||||
isDeleted: false,
|
||||
});
|
||||
|
||||
if (user == null) {
|
||||
throw new ApiError('NO_SUCH_USER');
|
||||
|
@ -34,25 +35,5 @@ export default define(meta, paramDef, async (ps) => {
|
|||
throw new ApiError('IS_MODERATOR');
|
||||
}
|
||||
|
||||
if (Users.isLocalUser(user)) {
|
||||
// 物理削除する前にDelete activityを送信する
|
||||
await doPostSuspend(user).catch(() => {});
|
||||
|
||||
createDeleteAccountJob(user, {
|
||||
soft: false,
|
||||
});
|
||||
} else {
|
||||
createDeleteAccountJob(user, {
|
||||
soft: true, // リモートユーザーの削除は、完全にDBから物理削除してしまうと再度連合してきてアカウントが復活する可能性があるため、soft指定する
|
||||
});
|
||||
}
|
||||
|
||||
await Users.update(user.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
if (Users.isLocalUser(user)) {
|
||||
// Terminate streaming
|
||||
publishUserEvent(user.id, 'terminate', {});
|
||||
}
|
||||
await deleteAccount(user);
|
||||
});
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import { Users } from '@/models/index.js';
|
||||
import { deleteAccount } from '@/services/delete-account.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
|
||||
res: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps) => {
|
||||
const user = await Users.findOneByOrFail({ id: ps.userId });
|
||||
if (user.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteAccount(user);
|
||||
});
|
|
@ -41,13 +41,13 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
const query = Users.createQueryBuilder('user');
|
||||
|
||||
switch (ps.state) {
|
||||
case 'available': query.where('user.isSuspended = FALSE'); break;
|
||||
case 'admin': query.where('user.isAdmin = TRUE'); break;
|
||||
case 'moderator': query.where('user.isModerator = TRUE'); break;
|
||||
case 'adminOrModerator': query.where('user.isAdmin = TRUE OR user.isModerator = TRUE'); break;
|
||||
case 'available': query.where('NOT user.isSuspended'); break;
|
||||
case 'admin': query.where('user.isAdmin'); break;
|
||||
case 'moderator': query.where('user.isModerator'); break;
|
||||
case 'adminOrModerator': query.where('user.isAdmin OR user.isModerator'); break;
|
||||
case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 5 * DAY) }); break;
|
||||
case 'silenced': query.where('user.isSilenced = TRUE'); break;
|
||||
case 'suspended': query.where('user.isSuspended = TRUE'); break;
|
||||
case 'silenced': query.where('user.isSilenced'); break;
|
||||
case 'suspended': query.where('user.isSuspended'); break;
|
||||
}
|
||||
|
||||
switch (ps.origin) {
|
||||
|
|
|
@ -69,17 +69,17 @@ export default define(meta, paramDef, async (ps) => {
|
|||
|
||||
if (typeof ps.notResponding === 'boolean') {
|
||||
if (ps.notResponding) {
|
||||
query.andWhere('instance.isNotResponding = TRUE');
|
||||
query.andWhere('instance.isNotResponding');
|
||||
} else {
|
||||
query.andWhere('instance.isNotResponding = FALSE');
|
||||
query.andWhere('NOT instance.isNotResponding');
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof ps.suspended === 'boolean') {
|
||||
if (ps.suspended) {
|
||||
query.andWhere('instance.isSuspended = TRUE');
|
||||
query.andWhere('instance.isSuspended');
|
||||
} else {
|
||||
query.andWhere('instance.isSuspended = FALSE');
|
||||
query.andWhere('NOT instance.isSuspended');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
const suspendedQuery = Users.createQueryBuilder('users')
|
||||
.select('users.id')
|
||||
.where('users.isSuspended = TRUE');
|
||||
.where('users.isSuspended');
|
||||
|
||||
const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
|
||||
.andWhere('notification.notifieeId = :meId', { meId: user.id })
|
||||
|
|
|
@ -35,7 +35,7 @@ export const paramDef = {
|
|||
export default define(meta, paramDef, async (ps) => {
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere('note.visibility = \'public\'')
|
||||
.andWhere('note.localOnly = FALSE')
|
||||
.andWhere('NOT note.localOnly')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
|
@ -65,7 +65,7 @@ export default define(meta, paramDef, async (ps) => {
|
|||
}
|
||||
|
||||
if (ps.poll !== undefined) {
|
||||
query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE');
|
||||
query.andWhere((ps.poll ? '' : 'NOT') + 'note.hasPoll');
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
|
|
@ -56,9 +56,7 @@ export const paramDef = {
|
|||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere('note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))', { noteId: ps.noteId, depth: ps.depth, limit: ps.limit })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner');
|
||||
.innerJoinAndSelect('note.user', 'user');
|
||||
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) {
|
||||
|
|
|
@ -98,7 +98,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
|
||||
if (ps.excludeNsfw) {
|
||||
query.andWhere('note.cw IS NULL');
|
||||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
|
||||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive")');
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
|
|
@ -127,9 +127,9 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
if (ps.poll != null) {
|
||||
if (ps.poll) {
|
||||
query.andWhere('note.hasPoll = TRUE');
|
||||
query.andWhere('note.hasPoll');
|
||||
} else {
|
||||
query.andWhere('note.hasPoll = FALSE');
|
||||
query.andWhere('NOT note.hasPoll');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,12 +41,12 @@ export const paramDef = {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const query = Users.createQueryBuilder('user');
|
||||
query.where('user.isExplorable = TRUE');
|
||||
query.where('user.isExplorable');
|
||||
|
||||
switch (ps.state) {
|
||||
case 'admin': query.andWhere('user.isAdmin = TRUE'); break;
|
||||
case 'moderator': query.andWhere('user.isModerator = TRUE'); break;
|
||||
case 'adminOrModerator': query.andWhere('user.isAdmin = TRUE OR user.isModerator = TRUE'); break;
|
||||
case 'admin': query.andWhere('user.isAdmin'); break;
|
||||
case 'moderator': query.andWhere('user.isModerator'); break;
|
||||
case 'adminOrModerator': query.andWhere('user.isAdmin OR user.isModerator'); break;
|
||||
case 'alive': query.andWhere('user.updatedAt > :date', { date: new Date(Date.now() - 5 * DAY) }); break;
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
if (ps.excludeNsfw) {
|
||||
query.andWhere('note.cw IS NULL');
|
||||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
|
||||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive")');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,8 +36,8 @@ export const paramDef = {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const query = Users.createQueryBuilder('user')
|
||||
.where('user.isLocked = FALSE')
|
||||
.andWhere('user.isExplorable = TRUE')
|
||||
.where('NOT user.isLocked')
|
||||
.andWhere('user.isExplorable')
|
||||
.andWhere('user.host IS NULL')
|
||||
.andWhere('user.updatedAt >= :date', { date: new Date(Date.now() - (7 * DAY)) })
|
||||
.andWhere('user.id != :meId', { meId: me.id })
|
||||
|
|
|
@ -44,7 +44,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
if (ps.host) {
|
||||
const q = Users.createQueryBuilder('user')
|
||||
.where('user.isSuspended = FALSE')
|
||||
.where('NOT user.isSuspended')
|
||||
.andWhere('user.host LIKE :host', { host: ps.host.toLowerCase() + '%' });
|
||||
|
||||
if (ps.username) {
|
||||
|
@ -68,7 +68,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
const query = Users.createQueryBuilder('user')
|
||||
.where(`user.id IN (${ followingQuery.getQuery() })`)
|
||||
.andWhere('user.id != :meId', { meId: me.id })
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.andWhere('NOT user.isSuspended')
|
||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where('user.updatedAt IS NULL')
|
||||
|
@ -86,7 +86,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
const otherQuery = await Users.createQueryBuilder('user')
|
||||
.where(`user.id NOT IN (${ followingQuery.getQuery() })`)
|
||||
.andWhere('user.id != :meId', { meId: me.id })
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.andWhere('user.isSuspended')
|
||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
||||
.andWhere('user.updatedAt IS NOT NULL');
|
||||
|
||||
|
@ -101,7 +101,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
}
|
||||
} else {
|
||||
users = await Users.createQueryBuilder('user')
|
||||
.where('user.isSuspended = FALSE')
|
||||
.where('user.isSuspended')
|
||||
.andWhere('user.usernameLower LIKE :username', { username: ps.username.toLowerCase() + '%' })
|
||||
.andWhere('user.updatedAt IS NOT NULL')
|
||||
.orderBy('user.updatedAt', 'DESC')
|
||||
|
|
|
@ -49,7 +49,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
}))
|
||||
.andWhere('user.isSuspended = FALSE');
|
||||
.andWhere('NOT user.isSuspended');
|
||||
|
||||
if (ps.origin === 'local') {
|
||||
usernameQuery.andWhere('user.host IS NULL');
|
||||
|
@ -76,7 +76,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
}))
|
||||
.andWhere('user.isSuspended = FALSE');
|
||||
.andWhere('NOT user.isSuspended');
|
||||
|
||||
if (ps.origin === 'local') {
|
||||
nameQuery.andWhere('user.host IS NULL');
|
||||
|
@ -109,7 +109,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
.where('user.updatedAt IS NULL')
|
||||
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold });
|
||||
}))
|
||||
.andWhere('user.isSuspended = FALSE')
|
||||
.andWhere('NOT user.isSuspended')
|
||||
.setParameters(profQuery.getParameters());
|
||||
|
||||
users = users.concat(await query
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Note } from '@/models/entities/note.js';
|
|||
import { Notes } from '@/models/index.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import Connection from './index.js';
|
||||
import { Connection } from './index.js';
|
||||
|
||||
/**
|
||||
* Stream channel
|
||||
|
|
|
@ -17,7 +17,7 @@ import { StreamEventEmitter, StreamMessages } from './types.js';
|
|||
/**
|
||||
* Main stream connection
|
||||
*/
|
||||
export default class Connection {
|
||||
export class Connection {
|
||||
public user?: User;
|
||||
public userProfile?: UserProfile | null;
|
||||
public following: Set<User['id']> = new Set();
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as websocket from 'websocket';
|
|||
|
||||
import { subscriber as redisClient } from '@/db/redis.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import MainStreamConnection from './stream/index.js';
|
||||
import { Connection } from './stream/index.js';
|
||||
import authenticate from './authenticate.js';
|
||||
|
||||
export const initializeStreamingServer = (server: http.Server): void => {
|
||||
|
@ -42,7 +42,7 @@ export const initializeStreamingServer = (server: http.Server): void => {
|
|||
|
||||
redisClient.on('message', onRedisMessage);
|
||||
|
||||
const main = new MainStreamConnection(connection, ev, user, app);
|
||||
const main = new Connection(connection, ev, user, app);
|
||||
|
||||
const intervalId = user ? setInterval(() => {
|
||||
Users.update(user.id, {
|
||||
|
|
|
@ -7,17 +7,21 @@ export async function deleteAccount(user: {
|
|||
id: string;
|
||||
host: string | null;
|
||||
}): Promise<void> {
|
||||
// Send Delete activity before physical deletion
|
||||
await doPostSuspend(user).catch(() => {});
|
||||
|
||||
createDeleteAccountJob(user, {
|
||||
soft: false,
|
||||
});
|
||||
|
||||
await Users.update(user.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
// Terminate streaming
|
||||
publishUserEvent(user.id, 'terminate', {});
|
||||
if (Users.isLocalUser(user)) {
|
||||
// Terminate streaming
|
||||
publishUserEvent(user.id, 'terminate', {});
|
||||
}
|
||||
|
||||
// Send Delete activity before physical deletion
|
||||
await doPostSuspend(user).catch(() => {});
|
||||
|
||||
createDeleteAccountJob(user, {
|
||||
// Deleting remote users is specified as SOFT, because if they are physically deleted
|
||||
// from the DB completely, they may be reassociated and their accounts may be reinstated.
|
||||
soft: Users.isLocalUser(user),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -293,7 +293,7 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, _type: string
|
|||
async function deleteOldFile(user: IRemoteUser): Promise<void> {
|
||||
const q = DriveFiles.createQueryBuilder('file')
|
||||
.where('file.userId = :userId', { userId: user.id })
|
||||
.andWhere('file.isLink = FALSE');
|
||||
.andWhere('NOT file.isLink');
|
||||
|
||||
if (user.avatarId) {
|
||||
q.andWhere('file.id != :avatarId', { avatarId: user.avatarId });
|
||||
|
@ -384,7 +384,7 @@ export async function addFile({
|
|||
if (Users.isLocalUser(user)) {
|
||||
throw new Error('no-free-space');
|
||||
} else {
|
||||
// (アバターまたはバナーを含まず)最も古いファイルを削除する
|
||||
// delete oldest file (excluding banner and avatar)
|
||||
deleteOldFile(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,27 +64,27 @@ export async function deleteFileSync(file: DriveFile, isExpired = false): Promis
|
|||
}
|
||||
|
||||
async function postProcess(file: DriveFile, isExpired = false): Promise<void> {
|
||||
// リモートファイル期限切れ削除後は直リンクにする
|
||||
if (isExpired && file.userHost !== null && file.uri != null) {
|
||||
// Turn into a direct link after expiring a remote file.
|
||||
if (isExpired && file.userHost != null && file.uri != null) {
|
||||
const id = uuid();
|
||||
DriveFiles.update(file.id, {
|
||||
isLink: true,
|
||||
url: file.uri,
|
||||
thumbnailUrl: null,
|
||||
webpublicUrl: null,
|
||||
storedInternal: false,
|
||||
// ローカルプロキシ用
|
||||
accessKey: uuid(),
|
||||
thumbnailAccessKey: 'thumbnail-' + uuid(),
|
||||
webpublicAccessKey: 'webpublic-' + uuid(),
|
||||
accessKey: id,
|
||||
thumbnailAccessKey: 'thumbnail-' + id,
|
||||
webpublicAccessKey: 'webpublic-' + id,
|
||||
});
|
||||
} else {
|
||||
DriveFiles.delete(file.id);
|
||||
}
|
||||
|
||||
// 統計を更新
|
||||
// update statistics
|
||||
driveChart.update(file, false);
|
||||
perUserDriveChart.update(file, false);
|
||||
if (file.userHost !== null) {
|
||||
if (file.userHost != null) {
|
||||
instanceChart.updateDrive(file, false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
"@rollup/pluginutils": "^4.2.1",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@vitejs/plugin-vue": "^3.1.0",
|
||||
"abort-controller": "3.0.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "5.0.1",
|
||||
"blurhash": "1.1.5",
|
||||
|
@ -26,12 +25,9 @@
|
|||
"chartjs-plugin-gradient": "0.5.0",
|
||||
"chartjs-plugin-zoom": "1.2.1",
|
||||
"compare-versions": "4.1.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"cropperjs": "2.0.0-beta.1",
|
||||
"date-fns": "2.28.0",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"feed": "4.2.2",
|
||||
"foundkey-js": "workspace:*",
|
||||
"idb-keyval": "6.2.0",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
|
@ -39,66 +35,38 @@
|
|||
"katex": "0.16.0",
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.22.1",
|
||||
"mocha": "10.0.0",
|
||||
"ms": "2.1.3",
|
||||
"nested-property": "4.0.0",
|
||||
"photoswipe": "5.2.8",
|
||||
"prismjs": "1.28.0",
|
||||
"private-ip": "2.3.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.1.1",
|
||||
"qrcode": "1.5.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rollup": "2.75.7",
|
||||
"sass": "1.53.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"talisman": "^1.1.4",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.142.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tsc-alias": "1.7.0",
|
||||
"tsconfig-paths": "4.1.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "^4.9.4",
|
||||
"uuid": "8.3.2",
|
||||
"v-debounce": "0.1.2",
|
||||
"vanilla-tilt": "1.7.2",
|
||||
"vite": "3.1.0",
|
||||
"vue": "3.2.45",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "4.0.1",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.8.0"
|
||||
"vuedraggable": "4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/glob": "7.2.0",
|
||||
"@types/gulp": "4.0.9",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/is-url": "1.2.30",
|
||||
"@types/katex": "0.14.0",
|
||||
"@types/matter-js": "0.17.7",
|
||||
"@types/mocha": "9.1.1",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.5.0",
|
||||
"@types/seedrandom": "3.0.2",
|
||||
"@types/throttle-debounce": "5.0.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/parser": "^5.46.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.3.0",
|
||||
"eslint": "^8.29.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-vue": "^9.8.0",
|
||||
"start-server-and-test": "1.14.0"
|
||||
"eslint-plugin-vue": "^9.8.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,11 @@ let poll = $ref<{
|
|||
let useCw = $ref(false);
|
||||
let showPreview = $ref(false);
|
||||
let cw = $ref<string | null>(null);
|
||||
|
||||
// these define the "maximum" these parameters can be set to and will be tightened further down
|
||||
let parentLocalOnly = false;
|
||||
let parentVisibility = 'public';
|
||||
|
||||
let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.defaultNoteLocalOnly);
|
||||
let visibility = $ref(props.initialVisibility ?? defaultStore.state.defaultNoteVisibility as foundkey.NoteVisibility);
|
||||
let visibleUsers = $ref([]);
|
||||
|
@ -251,12 +256,11 @@ if (props.reply && props.reply.text != null) {
|
|||
}
|
||||
|
||||
if (props.channel) {
|
||||
visibility = 'public';
|
||||
localOnly = true; // TODO: チャンネルが連合するようになった折には消す
|
||||
parentLocalOnly = true; // TODO: remove when channels are federated
|
||||
}
|
||||
|
||||
if (props.reply) {
|
||||
visibility = foundkey.minVisibility(props.reply.visibility, visibility);
|
||||
parentVisibility = foundkey.minVisibility(props.reply.visibility, parentVisibility);
|
||||
if (props.reply.visibility === 'specified') {
|
||||
os.api('users/show', {
|
||||
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId),
|
||||
|
@ -270,10 +274,11 @@ if (props.reply) {
|
|||
});
|
||||
}
|
||||
}
|
||||
parentLocalOnly ||= props.reply.localOnly;
|
||||
}
|
||||
|
||||
if (props.renote) {
|
||||
visibility = foundkey.minVisibility(props.renote.visibility, visibility);
|
||||
parentVisibility = foundkey.minVisibility(props.renote.visibility, parentVisibility);
|
||||
if (props.renote.visibility === 'specified') {
|
||||
os.api('users/show', {
|
||||
userIds: props.renote.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.renote.userId),
|
||||
|
@ -287,13 +292,18 @@ if (props.renote) {
|
|||
});
|
||||
}
|
||||
}
|
||||
parentLocalOnly ||= props.renote.localOnly;
|
||||
}
|
||||
|
||||
if (props.specified) {
|
||||
visibility = 'specified';
|
||||
parentVisibility = 'specified';
|
||||
pushVisibleUser(props.specified);
|
||||
}
|
||||
|
||||
// set visibility and local only defaults to minimum of preselected or allowed.
|
||||
visibility = foundkey.minVisibility(visibility, parentVisibility);
|
||||
localOnly ||= parentLocalOnly;
|
||||
|
||||
// keep cw when reply
|
||||
if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
|
||||
useCw = true;
|
||||
|
@ -393,8 +403,10 @@ function setVisibility() {
|
|||
}
|
||||
|
||||
os.popup(defineAsyncComponent(() => import('./visibility-picker.vue')), {
|
||||
parentVisibility: visibility,
|
||||
parentLocalOnly: localOnly,
|
||||
parentVisibility,
|
||||
parentLocalOnly,
|
||||
currentVisibility: visibility,
|
||||
currentLocalOnly: localOnly,
|
||||
src: visibilityButton,
|
||||
}, {
|
||||
changeVisibility: v => {
|
||||
|
|
|
@ -53,6 +53,7 @@ const modal = $ref<InstanceType<typeof MkModal>>();
|
|||
const props = withDefaults(defineProps<{
|
||||
parentVisibility: foundkey.NoteVisibility;
|
||||
parentLocalOnly: boolean;
|
||||
currentVisibility: foundkey.NoteVisibility;
|
||||
currentLocalOnly: boolean;
|
||||
src?: HTMLElement;
|
||||
}>(), {
|
||||
|
@ -64,8 +65,8 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
let v = $ref(props.parentVisibility);
|
||||
let localOnly = $ref(props.parentLocalOnly);
|
||||
let v = $ref(props.currentVisibility);
|
||||
let localOnly = $ref(props.currentLocalOnly);
|
||||
|
||||
const disabled = foundkey.noteVisibilities.reduce((acc, visibility) => {
|
||||
acc[visibility] = (visibility !== foundkey.minVisibility(visibility, props.parentVisibility));
|
||||
|
|
|
@ -5,7 +5,6 @@ export const host = address.host;
|
|||
export const hostname = address.hostname;
|
||||
export const url = address.origin;
|
||||
export const apiUrl = url + '/api';
|
||||
export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
|
||||
export const lang = localStorage.getItem('lang');
|
||||
export const langs = _LANGS_;
|
||||
export const locale = JSON.parse(localStorage.getItem('locale'));
|
||||
|
|
|
@ -619,7 +619,7 @@
|
|||
{ "category": "animals_and_nature", "char": "🌼", "name": "blossom", "keywords": ["nature", "yellow", "flowers"] },
|
||||
{ "category": "animals_and_nature", "char": "🌸", "name": "cherry_blossom", "keywords": ["plant", "flower", "nature", "spring"] },
|
||||
{ "category": "animals_and_nature", "char": "💐", "name": "bouquet", "keywords": ["nature", "spring", "flowers"] },
|
||||
{ "category": "animals_and_nature", "char": "🍄", "name": "mushroom", "keywords": ["plant", "vegetable"] },
|
||||
{ "category": "animals_and_nature", "char": "🍄", "name": "mushroom", "keywords": ["plant", "vegetable", "fungus"] },
|
||||
{ "category": "animals_and_nature", "char": "🪴", "name": "potted_plant", "keywords": ["plant"] },
|
||||
{ "category": "animals_and_nature", "char": "🌰", "name": "chestnut", "keywords": ["food", "squirrel"] },
|
||||
{ "category": "animals_and_nature", "char": "🎃", "name": "jack_o_lantern", "keywords": ["fall", "light", "creepy", "pumpkin", "halloween"] },
|
||||
|
|
|
@ -33,7 +33,7 @@ class I18n<T extends Record<string, any>> {
|
|||
// Perform string interpolation.
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
str = str.replace(`{${k}}`, v.toString());
|
||||
str = str.replace(`{${k}}`, v?.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ export const apiWithDialog = ((
|
|||
promiseDialog(promise, null, (err) => {
|
||||
alert({
|
||||
type: 'error',
|
||||
text: (err.message + '\n' + (err?.endpoint ?? '') + (err?.code ?? '')).trim(),
|
||||
text: (err.message + '\n' + (err?.endpoint ?? '') + ' ' + (err?.code ?? '')).trim(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ definePageMetadata(computed(() => note ? {
|
|||
avatar: note.user,
|
||||
path: `/notes/${note.id}`,
|
||||
share: {
|
||||
title: i18n.t('noteOf', { user: note.user.name }),
|
||||
title: i18n.t('noteOf', { user: note.user.name || note.user.username }),
|
||||
text: note.text,
|
||||
},
|
||||
} : null));
|
||||
|
|
|
@ -249,7 +249,7 @@ async function deleteAccount() {
|
|||
if (typed.canceled) return;
|
||||
|
||||
if (typed.result === user?.username) {
|
||||
await os.apiWithDialog('admin/delete-account', {
|
||||
await os.apiWithDialog('admin/accounts/delete', {
|
||||
userId: user.id,
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -183,7 +183,7 @@ export function getNoteMenu(props: {
|
|||
|
||||
function share(): void {
|
||||
navigator.share({
|
||||
title: i18n.t('noteOf', { user: appearNote.user.name }),
|
||||
title: i18n.t('noteOf', { user: appearNote.user.name || appearNote.user.username }),
|
||||
text: appearNote.text,
|
||||
url: `${url}/notes/${appearNote.id}`,
|
||||
});
|
||||
|
|
|
@ -65,7 +65,6 @@ export type Endpoints = {
|
|||
'admin/unsuspend-user': { req: TODO; res: TODO; };
|
||||
'admin/update-meta': { req: TODO; res: TODO; };
|
||||
'admin/vacuum': { req: TODO; res: TODO; };
|
||||
'admin/delete-account': { req: TODO; res: TODO; };
|
||||
'announcements': { req: { limit?: number; withUnreads?: boolean; sinceId?: Announcement['id']; untilId?: Announcement['id']; }; res: Announcement[]; };
|
||||
'antennas/create': { req: TODO; res: Antenna; };
|
||||
'antennas/delete': { req: { antennaId: Antenna['id']; }; res: null; };
|
||||
|
|
Loading…
Reference in a new issue