Merge branch 'main' into snug.moe

This commit is contained in:
vib 2023-01-12 22:44:28 +02:00
commit 50bac9e2d3
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; };