Compare commits

...

24 Commits

Author SHA1 Message Date
Johann150 80e2851378
fixup: remove trailing comma in JSON 2023-01-11 20:58:38 +01:00
Johann150 14b48fb07c
client: remove unused dependencies 2023-01-11 20:32:51 +01:00
Johann150 5e2a9224f3
add "fungus" as alias to emoji list 2023-01-11 19:50:29 +01:00
Johann150 624628d582
client: remove unused websocket libraries
The websocket functionality is provided by foundkey-js so there is no need to import
any websocket libraries.
2023-01-11 19:45:18 +01:00
Johann150 e68eeba7a6
fixup: remove admin/delete-account endpoint from foundkey-js
This is a fixup for commit c7ab8839dc.
2023-01-11 19:12:58 +01:00
Johann150 ee2fa2e0be
fixup: import 2023-01-10 20:35:03 +01:00
Johann150 57d1af1117
remove default export in streaming API 2023-01-10 20:30:47 +01:00
Johann150 8c2b7e20b2
translating comments, cleanup 2023-01-09 20:44:01 +01:00
Johann150 fdf30f60e6
server: remove SQL boolean comparisons 2023-01-09 20:43:12 +01:00
Johann150 4fe288f17c server: rewrite user status queries in SQL 2023-01-08 20:02:21 +00:00
Johann150 cd26e3a35c
fixup: missing parenthesis 2023-01-08 19:34:03 +01:00
Johann150 c7ab8839dc
BREAKING: remove admin/delete-account, change admin/accounts/delete
You should use the API endpoint admin/accounts/delete.
It has the same parameter and the same behaviour.

The admin/accounts/delete endpoint now requries administrator privileges
instead of just moderator privileges.

Changelog: Removed
2023-01-07 23:53:48 +01:00
Johann150 1eda1760d1
server: refactor to always use deleteAccount service
This should reduce code duplication around how deletion of an actor is
handled.
2023-01-07 19:46:05 +01:00
Johann150 8772181b6f
server: refactor remote host check to validateActor
Instead of checking that an actor is not from the local host separately,
it seems like a good idea to do it in the central place that is supposed
to validate an actor.
2023-01-07 19:46:05 +01:00
Norm 8f21275dd5 roadmap: Update links to reference labels instead of projects 2023-01-07 01:50:25 +00:00
Norm 5102d0bc2e
chore: remove unused user_group_invite table
Based on `1558257926829-UserGroupInvite.js` but switched `up` and `down`
migrations around.

Closes #314
2023-01-06 02:51:44 -05:00
Puniko cdba5447e6
server: remove joins to avatar and banners in children endpoint
Reviewed-on: FoundKeyGang/FoundKey#303
2023-01-05 21:05:22 +01:00
Johann150 4bb814adfc
client: add space between endpoint and code in error message 2023-01-05 20:55:03 +01:00
Johann150 35e9d7f958
client: fix null i18n interpolation values
Fixed the occurence that was reported in
<FoundKeyGang/FoundKey#317> along with a similar one.

Fixes <FoundKeyGang/FoundKey#317>

Also changed the i18n code so this should not happen any more in the general case.
2023-01-05 20:50:25 +01:00
Johann150 a0c2cf328e
server: fix redirected fetch
Don't throw a StatusError on an intended redirect.
2023-01-05 20:03:38 +01:00
Johann150 334368f6e2
fix: allow to pick higher visibility than chosen before
If you selected a lower visibility that one would then be used as
the parent visibility. Instead it is necessary to use two separate
variables, one for parent and one for the preselected visibility.
2023-01-04 21:39:33 +01:00
Johann150 3efa7046bd
meta: don't type check dependencies 2023-01-04 20:59:31 +01:00
Johann150 48f8fb97df
activitypub: use quoteUri instead of quoteUrl
It's not quite Mastodon, but still, I said they'd use a different approach...

Changelog: Changed
2023-01-04 20:56:06 +01:00
Johann150 0230f819e2
fixup: wrong negation
This is a fixup for commit 417d252e9d.
2023-01-04 19:09:03 +01:00
47 changed files with 214 additions and 280 deletions

View File

@ -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
Ive been thinking about a community misskey fork for a while now. To some of you, this is not a surprise. Lets talk about that.

View File

@ -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`);
}
}

View File

@ -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",

View File

@ -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);
}

View File

@ -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.',

View File

@ -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' {

View File

@ -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);
}

View File

@ -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}`;
}

View File

@ -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);

View File

@ -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;

View File

@ -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',

View File

@ -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,

View File

@ -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;

View File

@ -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();

View File

@ -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],

View File

@ -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) {

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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) {

View File

@ -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');
}
}

View File

@ -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 })

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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');
}
}

View File

@ -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;
}

View File

@ -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")');
}
}

View File

@ -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 })

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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, {

View File

@ -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),
});
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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"
}
}

View File

@ -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 => {

View File

@ -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));

View File

@ -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'));

View File

@ -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"] },

View File

@ -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());
}
}

View File

@ -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(),
});
});

View File

@ -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));

View File

@ -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 {

View File

@ -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}`,
});

View File

@ -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; };