Merge branch 'develop' into pizzax-indexeddb

This commit is contained in:
tamaina 2022-06-21 07:49:52 +00:00
commit f33654fb9a
459 changed files with 11027 additions and 9888 deletions

View file

@ -5,6 +5,6 @@
"loader=./test/loader.js"
],
"slow": 1000,
"timeout": 3000,
"timeout": 10000,
"exit": true
}

View file

@ -0,0 +1,5 @@
Font Awesome Icons
-------------------------
Ⓒ Font Awesome
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

View file

@ -14,7 +14,7 @@
"lodash": "^4.17.21"
},
"dependencies": {
"@bull-board/koa": "3.10.4",
"@bull-board/koa": "3.11.1",
"@discordapp/twemoji": "14.0.2",
"@elastic/elasticsearch": "7.11.0",
"@koa/cors": "3.1.0",
@ -28,10 +28,9 @@
"archiver": "5.3.1",
"autobind-decorator": "2.4.0",
"autwh": "0.1.0",
"aws-sdk": "2.1135.0",
"aws-sdk": "2.1152.0",
"bcryptjs": "2.4.3",
"blurhash": "1.1.5",
"broadcast-channel": "4.12.0",
"bull": "4.8.3",
"cacheable-lookup": "6.0.4",
"cbor": "8.1.0",
@ -44,18 +43,18 @@
"deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1",
"feed": "4.2.2",
"file-type": "17.1.1",
"file-type": "17.1.2",
"fluent-ffmpeg": "2.1.2",
"got": "12.0.4",
"got": "12.1.0",
"hpagent": "0.1.2",
"ip-cidr": "3.0.8",
"ip-cidr": "3.0.10",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
"jsdom": "19.0.0",
"json5": "2.2.1",
"json5-loader": "4.0.1",
"jsonld": "5.2.0",
"jsrsasign": "10.5.22",
"jsonld": "6.0.0",
"jsrsasign": "10.5.24",
"koa": "2.13.4",
"koa-bodyparser": "4.3.0",
"koa-favicon": "2.1.0",
@ -72,7 +71,7 @@
"ms": "3.0.0-canary.1",
"multer": "1.4.4",
"nested-property": "4.0.0",
"node-fetch": "3.2.4",
"node-fetch": "3.2.6",
"nodemailer": "6.7.5",
"os-utils": "0.0.14",
"parse5": "6.0.1",
@ -101,14 +100,14 @@
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"style-loader": "3.3.1",
"summaly": "2.5.0",
"summaly": "2.6.0",
"syslog-pro": "1.0.0",
"systeminformation": "5.11.15",
"systeminformation": "5.11.16",
"tinycolor2": "1.4.2",
"tmp": "0.2.1",
"ts-loader": "9.3.0",
"ts-node": "10.8.0",
"tsc-alias": "1.6.7",
"ts-node": "10.8.1",
"tsc-alias": "1.6.9",
"tsconfig-paths": "4.0.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.6",
@ -117,7 +116,7 @@
"uuid": "8.3.2",
"web-push": "3.5.0",
"websocket": "1.0.34",
"ws": "8.6.0",
"ws": "8.8.0",
"xev": "3.0.2"
},
"devDependencies": {
@ -145,7 +144,7 @@
"@types/koa__multer": "2.0.4",
"@types/koa__router": "8.0.11",
"@types/mocha": "9.1.1",
"@types/node": "17.0.35",
"@types/node": "17.0.41",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.4",
"@types/oauth": "0.9.1",
@ -167,12 +166,11 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.26.0",
"@typescript-eslint/parser": "5.26.0",
"typescript": "4.7.2",
"eslint": "8.16.0",
"@typescript-eslint/eslint-plugin": "5.27.1",
"@typescript-eslint/parser": "5.27.1",
"typescript": "4.7.3",
"eslint": "8.17.0",
"eslint-plugin-import": "2.26.0",
"cross-env": "7.0.3",
"execa": "6.1.0"
}

View file

@ -46,7 +46,7 @@ export default function load() {
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
mixin.clientEntry = clientManifest['src/init.ts'].file.replace(/^_client_dist_\//, '');
mixin.clientEntry = clientManifest['src/init.ts'];
if (!config.redis.prefix) config.redis.prefix = mixin.host;

View file

@ -73,6 +73,7 @@ import { entities as charts } from '@/services/chart/entities.js';
import { Webhook } from '@/models/entities/webhook.js';
import { envOption } from '../env.js';
import { dbLogger } from './logger.js';
import { redisClient } from './redis.js';
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
@ -207,7 +208,15 @@ export const db = new DataSource({
migrations: ['../../migration/*.js'],
});
export async function initDb() {
export async function initDb(force = false) {
if (force) {
if (db.isInitialized) {
await db.destroy();
}
await db.initialize();
return;
}
if (db.isInitialized) {
// nop
} else {
@ -217,6 +226,7 @@ export async function initDb() {
export async function resetDb() {
const reset = async () => {
await redisClient.FLUSHDB();
const tables = await db.query(`SELECT relname AS "table"
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema')

View file

@ -6,6 +6,9 @@ const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
export function fromHtml(html: string, hashtagNames?: string[]): string {
// some AP servers like Pixelfed use br tags as well as newlines
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
const dom = parse5.parseFragment(html);
let text = '';

View file

@ -11,9 +11,14 @@ export function createTemp(): Promise<[string, () => void]> {
export function createTempDir(): Promise<[string, () => void]> {
return new Promise<[string, () => void]>((res, rej) => {
tmp.dir((e, path, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
});
tmp.dir(
{
unsafeCleanup: true,
},
(e, path, cleanup) => {
if (e) return rej(e);
res([path, cleanup]);
}
);
});
}

View file

@ -0,0 +1,9 @@
import IPCIDR from 'ip-cidr';
export function getIpHash(ip: string) {
// because a single person may control many IPv6 addresses,
// only a /64 subnet prefix of any IP will be taken into account.
// (this means for IPv4 the entire address is used)
const prefix = IPCIDR.createAddress(ip).mask(64);
return 'ip-' + BigInt('0b' + prefix).toString(36);
}

View file

@ -0,0 +1,8 @@
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
const dictionary = {
'safe-file': FILE_TYPE_BROWSERSAFE,
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'],
};
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);

View file

@ -29,7 +29,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) {
const properties = structuredClone(file.properties);
// TODO
//const properties = structuredClone(file.properties);
const properties = JSON.parse(JSON.stringify(file.properties));
if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width];
}

View file

@ -26,6 +26,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
};
},

View file

@ -136,6 +136,7 @@ async function populateMyReaction(note: Note, meId: User['id'], _hint_?: {
export const NoteRepository = db.getRepository(Note).extend({
async isVisibleForMe(note: Note, meId: User['id'] | null): Promise<boolean> {
// This code must always be synchronized with the checks in generateVisibilityQuery.
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
@ -144,13 +145,7 @@ export const NoteRepository = db.getRepository(Note).extend({
return true;
} else {
// 指定されているかどうか
const specified = note.visibleUserIds.some((id: any) => meId === id);
if (specified) {
return true;
} else {
return false;
}
return note.visibleUserIds.some((id: any) => meId === id);
}
}
@ -169,9 +164,12 @@ export const NoteRepository = db.getRepository(Note).extend({
} else {
// フォロワーかどうか
const [following, user] = await Promise.all([
Followings.findOneBy({
followeeId: note.userId,
followerId: meId,
Followings.count({
where: {
followeeId: note.userId,
followerId: meId,
},
take: 1,
}),
Users.findOneByOrFail({ id: meId }),
]);
@ -183,7 +181,7 @@ export const NoteRepository = db.getRepository(Note).extend({
in which case we can never know the following. Instead we have
to assume that the users are following each other.
*/
return following != null || (note.userHost != null && user.host != null);
return following > 0 || (note.userHost != null && user.host != null);
}
}

View file

@ -61,47 +61,58 @@ export const UserRepository = db.getRepository(User).extend({
//#endregion
async getRelation(me: User['id'], target: User['id']) {
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
Followings.findOneBy({
followerId: me,
followeeId: target,
}),
Followings.findOneBy({
followerId: target,
followeeId: me,
}),
FollowRequests.findOneBy({
followerId: me,
followeeId: target,
}),
FollowRequests.findOneBy({
followerId: target,
followeeId: me,
}),
Blockings.findOneBy({
blockerId: me,
blockeeId: target,
}),
Blockings.findOneBy({
blockerId: target,
blockeeId: me,
}),
Mutings.findOneBy({
muterId: me,
muteeId: target,
}),
]);
return {
return awaitAll({
id: target,
isFollowing: following1 != null,
hasPendingFollowRequestFromYou: followReq1 != null,
hasPendingFollowRequestToYou: followReq2 != null,
isFollowed: following2 != null,
isBlocking: toBlocking != null,
isBlocked: fromBlocked != null,
isMuted: mute != null,
};
isFollowing: Followings.count({
where: {
followerId: me,
followeeId: target,
},
take: 1,
}).then(n => n > 0),
isFollowed: Followings.count({
where: {
followerId: target,
followeeId: me,
},
take: 1,
}).then(n => n > 0),
hasPendingFollowRequestFromYou: FollowRequests.count({
where: {
followerId: me,
followeeId: target,
},
take: 1,
}).then(n => n > 0),
hasPendingFollowRequestToYou: FollowRequests.count({
where: {
followerId: target,
followeeId: me,
},
take: 1,
}).then(n => n > 0),
isBlocking: Blockings.count({
where: {
blockerId: me,
blockeeId: target,
},
take: 1,
}).then(n => n > 0),
isBlocked: Blockings.count({
where: {
blockerId: target,
blockeeId: me,
},
take: 1,
}).then(n => n > 0),
isMuted: Mutings.count({
where: {
muterId: me,
muteeId: target,
},
take: 1,
}).then(n => n > 0),
});
},
async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {

View file

@ -88,6 +88,11 @@ export const packedFederationInstanceSchema = {
optional: false, nullable: true,
format: 'url',
},
faviconUrl: {
type: 'string',
optional: false, nullable: true,
format: 'url',
},
infoUpdatedAt: {
type: 'string',
optional: false, nullable: true,

View file

@ -305,11 +305,13 @@ export default function() {
systemQueue.add('resyncCharts', {
}, {
repeat: { cron: '0 0 * * *' },
removeOnComplete: true,
});
systemQueue.add('cleanCharts', {
}, {
repeat: { cron: '0 0 * * *' },
removeOnComplete: true,
});
systemQueue.add('checkExpiredMutings', {

View file

@ -9,7 +9,7 @@ import { addFile } from '@/services/drive/add-file.js';
import { format as dateFormat } from 'date-fns';
import { Users, Emojis } from '@/models/index.js';
import { } from '@/queue/types.js';
import { createTempDir } from '@/misc/create-temp.js';
import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { downloadUrl } from '@/misc/download-url.js';
import config from '@/config/index.js';
import { IsNull } from 'typeorm';

View file

@ -5,14 +5,52 @@ import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/
import { UserPublickey } from '@/models/entities/user-publickey.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js';
import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.js';
import { Cache } from '@/misc/cache.js';
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.js';
const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
export type UriParseResult = {
/** wether the URI was generated by us */
local: true;
/** id in DB */
id: string;
/** hint of type, e.g. "notes", "users" */
type: string;
/** any remaining text after type and id, not including the slash after id. undefined if empty */
rest?: string;
} | {
/** wether the URI was generated by us */
local: false;
/** uri in DB */
uri: string;
};
export function parseUri(value: string | IObject): UriParseResult {
const uri = getApId(value);
// the host part of a URL is case insensitive, so use the 'i' flag.
const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i');
const matchLocal = uri.match(localRegex);
if (matchLocal) {
return {
local: true,
type: matchLocal[1],
id: matchLocal[2],
rest: matchLocal[3],
};
} else {
return {
local: false,
uri,
};
}
}
export default class DbResolver {
constructor() {
}
@ -21,60 +59,54 @@ export default class DbResolver {
* AP Note => Misskey Note in DB
*/
public async getNoteFromApId(value: string | IObject): Promise<Note | null> {
const parsed = this.parseUri(value);
const parsed = parseUri(value);
if (parsed.local) {
if (parsed.type !== 'notes') return null;
if (parsed.id) {
return await Notes.findOneBy({
id: parsed.id,
});
}
if (parsed.uri) {
} else {
return await Notes.findOneBy({
uri: parsed.uri,
});
}
return null;
}
public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> {
const parsed = this.parseUri(value);
const parsed = parseUri(value);
if (parsed.local) {
if (parsed.type !== 'notes') return null;
if (parsed.id) {
return await MessagingMessages.findOneBy({
id: parsed.id,
});
}
if (parsed.uri) {
} else {
return await MessagingMessages.findOneBy({
uri: parsed.uri,
});
}
return null;
}
/**
* AP Person => Misskey User in DB
*/
public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> {
const parsed = this.parseUri(value);
const parsed = parseUri(value);
if (parsed.local) {
if (parsed.type !== 'users') return null;
if (parsed.id) {
return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
id: parsed.id,
}).then(x => x ?? undefined)) ?? null;
}
if (parsed.uri) {
} else {
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
uri: parsed.uri,
}));
}
return null;
}
/**
@ -120,31 +152,4 @@ export default class DbResolver {
key,
};
}
public parseUri(value: string | IObject): UriParseResult {
const uri = getApId(value);
const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)');
const matchLocal = uri.match(localRegex);
if (matchLocal) {
return {
type: matchLocal[1],
id: matchLocal[2],
};
} else {
return {
uri,
};
}
}
}
type UriParseResult = {
/** id in DB (local object only) */
id?: string;
/** uri in DB (remote object only) */
uri?: string;
/** hint of type (local object only, ex: notes, users) */
type?: string
};

View file

@ -3,8 +3,6 @@ import { Note } from '@/models/entities/note.js';
import { toHtml } from '../../../mfm/to-html.js';
export default function(note: Note) {
let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null;
if (html == null) html = '<p>.</p>';
return html;
if (!note.text) return '';
return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
}

View file

@ -3,9 +3,9 @@ import promiseLimit from 'promise-limit';
import config from '@/config/index.js';
import Resolver from '../resolver.js';
import post from '@/services/note/create.js';
import { resolvePerson, updatePerson } from './person.js';
import { resolvePerson } from './person.js';
import { resolveImage } from './image.js';
import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { htmlToMfm } from '../misc/html-to-mfm.js';
import { extractApHashtags } from './tag.js';
import { unique, toArray, toSingle } from '@/prelude/array.js';
@ -15,7 +15,7 @@ import { apLogger } from '../logger.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
import { extractDbHost, toPuny } from '@/misc/convert-host.js';
import { Emojis, Polls, MessagingMessages, Users } from '@/models/index.js';
import { Emojis, Polls, MessagingMessages } from '@/models/index.js';
import { Note } from '@/models/entities/note.js';
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js';
import { Emoji } from '@/models/entities/emoji.js';
@ -197,7 +197,14 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
const text = typeof note._misskey_content !== 'undefined' ? note._misskey_content : (note.content ? htmlToMfm(note.content, note.tag) : null);
let text: string | null = null;
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source?.content === 'string') {
text = note.source.content;
} else if (typeof note._misskey_content !== 'undefined') {
text = note._misskey_content;
} else if (typeof note.content === 'string') {
text = htmlToMfm(note.content, note.tag);
}
// vote
if (reply && reply.hasPoll) {

View file

@ -1,8 +1,20 @@
import config from '@/config/index.js';
import { ILocalUser, IRemoteUser } from '@/models/entities/user.js';
import { Blocking } from '@/models/entities/blocking.js';
export default (blocker: ILocalUser, blockee: IRemoteUser) => ({
type: 'Block',
actor: `${config.url}/users/${blocker.id}`,
object: blockee.uri,
});
/**
* Renders a block into its ActivityPub representation.
*
* @param block The block to be rendered. The blockee relation must be loaded.
*/
export function renderBlock(block: Blocking) {
if (block.blockee?.url == null) {
throw new Error('renderBlock: missing blockee uri');
}
return {
type: 'Block',
id: `${config.url}/blocks/${block.id}`,
actor: `${config.url}/users/${block.blockerId}`,
object: block.blockee.uri,
};
}

View file

@ -4,12 +4,11 @@ import { Users } from '@/models/index.js';
export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => {
const follow = {
id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`,
type: 'Follow',
actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri,
} as any;
if (requestId) follow.id = requestId;
return follow;
};

View file

@ -8,7 +8,7 @@ import { User } from '@/models/entities/user.js';
export const renderActivity = (x: any): IActivity | null => {
if (x == null) return null;
if (x !== null && typeof x === 'object' && x.id == null) {
if (typeof x === 'object' && x.id == null) {
x.id = `${config.url}/${uuid()}`;
}

View file

@ -82,7 +82,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
const files = await getPromisedFiles(note.fileIds);
const text = note.text;
const text = note.text ?? '';
let poll: Poll | null = null;
if (note.hasPoll) {
@ -90,7 +90,6 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
}
let apText = text;
if (apText == null) apText = '';
if (quote) {
apText += `\n\nRE: ${quote}`;
@ -138,6 +137,10 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
summary,
content,
_misskey_content: text,
source: {
content: text,
mediaType: "text/x.misskeymarkdown",
},
_misskey_quote: quote,
quoteUrl: quote,
published: note.createdAt.toISOString(),

View file

@ -3,9 +3,18 @@ import { getJson } from '@/misc/fetch.js';
import { ILocalUser } from '@/models/entities/user.js';
import { getInstanceActor } from '@/services/instance-actor.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { extractDbHost } from '@/misc/convert-host.js';
import { extractDbHost, isSelfHost } from '@/misc/convert-host.js';
import { signedGet } from './request.js';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
import { FollowRequests, Notes, NoteReactions, Polls, Users } from '@/models/index.js';
import { parseUri } from './db-resolver.js';
import renderNote from '@/remote/activitypub/renderer/note.js';
import { renderLike } from '@/remote/activitypub/renderer/like.js';
import { renderPerson } from '@/remote/activitypub/renderer/person.js';
import renderQuestion from '@/remote/activitypub/renderer/question.js';
import renderCreate from '@/remote/activitypub/renderer/create.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js';
export default class Resolver {
private history: Set<string>;
@ -40,14 +49,25 @@ export default class Resolver {
return value;
}
if (value.includes('#')) {
// URLs with fragment parts cannot be resolved correctly because
// the fragment part does not get transmitted over HTTP(S).
// Avoid strange behaviour by not trying to resolve these at all.
throw new Error(`cannot resolve URL with fragment: ${value}`);
}
if (this.history.has(value)) {
throw new Error('cannot resolve already resolved one');
}
this.history.add(value);
const meta = await fetchMeta();
const host = extractDbHost(value);
if (isSelfHost(host)) {
return await this.resolveLocal(value);
}
const meta = await fetchMeta();
if (meta.blockedHosts.includes(host)) {
throw new Error('Instance is blocked');
}
@ -70,4 +90,44 @@ export default class Resolver {
return object;
}
private resolveLocal(url: string): Promise<IObject> {
const parsed = parseUri(url);
if (!parsed.local) throw new Error('resolveLocal: not local');
switch (parsed.type) {
case 'notes':
return Notes.findOneByOrFail({ id: parsed.id })
.then(note => {
if (parsed.rest === 'activity') {
// this refers to the create activity and not the note itself
return renderActivity(renderCreate(renderNote(note)));
} else {
return renderNote(note);
}
});
case 'users':
return Users.findOneByOrFail({ id: parsed.id })
.then(user => renderPerson(user as ILocalUser));
case 'questions':
// Polls are indexed by the note they are attached to.
return Promise.all([
Notes.findOneByOrFail({ id: parsed.id }),
Polls.findOneByOrFail({ noteId: parsed.id }),
])
.then(([note, poll]) => renderQuestion({ id: note.userId }, note, poll));
case 'likes':
return NoteReactions.findOneByOrFail({ id: parsed.id }).then(reaction => renderActivity(renderLike(reaction, { uri: null })));
case 'follows':
// rest should be <followee id>
if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI');
return Promise.all(
[parsed.id, parsed.rest].map(id => Users.findOneByOrFail({ id }))
)
.then(([follower, followee]) => renderActivity(renderFollow(follower, followee, url)));
default:
throw new Error(`resolveLocal: type ${type} unhandled`);
}
}
}

View file

@ -106,7 +106,10 @@ export const isPost = (object: IObject): object is IPost =>
export interface IPost extends IObject {
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event';
_misskey_content?: string;
source?: {
content: string;
mediaType: string;
};
_misskey_quote?: string;
quoteUrl?: string;
_misskey_talk: boolean;
@ -114,7 +117,10 @@ export interface IPost extends IObject {
export interface IQuestion extends IObject {
type: 'Note' | 'Question';
_misskey_content?: string;
source?: {
content: string;
mediaType: string;
};
_misskey_quote?: string;
quoteUrl?: string;
oneOf?: IQuestionChoice[];

View file

@ -15,9 +15,10 @@ import { inbox as processInbox } from '@/queue/index.js';
import { isSelfHost } from '@/misc/convert-host.js';
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
import { ILocalUser, User } from '@/models/entities/user.js';
import { In, IsNull } from 'typeorm';
import { In, IsNull, Not } from 'typeorm';
import { renderLike } from '@/remote/activitypub/renderer/like.js';
import { getUserKeypair } from '@/misc/keypair-store.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js';
// Init router
const router = new Router();
@ -224,4 +225,30 @@ router.get('/likes/:like', async ctx => {
setResponseType(ctx);
});
// follow
router.get('/follows/:follower/:followee', async ctx => {
// This may be used before the follow is completed, so we do not
// check if the following exists.
const [follower, followee] = await Promise.all([
Users.findOneBy({
id: ctx.params.follower,
host: IsNull(),
}),
Users.findOneBy({
id: ctx.params.followee,
host: Not(IsNull()),
}),
]);
if (follower == null || followee == null) {
ctx.status = 404;
return;
}
ctx.body = renderActivity(renderFollow(follower, followee));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
});
export default router;

View file

@ -1,11 +1,12 @@
import Koa from 'koa';
import { performance } from 'perf_hooks';
import { limiter } from './limiter.js';
import Koa from 'koa';
import { CacheableLocalUser, User } from '@/models/entities/user.js';
import endpoints, { IEndpoint } from './endpoints.js';
import { AccessToken } from '@/models/entities/access-token.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import { limiter } from './limiter.js';
import endpoints, { IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js';
import { apiLogger } from './logger.js';
import { AccessToken } from '@/models/entities/access-token.js';
const accessDenied = {
message: 'Access denied.',
@ -15,6 +16,7 @@ const accessDenied = {
export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
const isSecure = user != null && token == null;
const isModerator = user != null && (user.isModerator || user.isAdmin);
const ep = endpoints.find(e => e.name === endpoint);
@ -31,6 +33,32 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
throw new ApiError(accessDenied);
}
if (ep.meta.limit) {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
let limitActor: string;
if (user) {
limitActor = user.id;
} else {
limitActor = getIpHash(ctx!.ip);
}
const limit = Object.assign({}, ep.meta.limit);
if (!limit.key) {
limit.key = ep.name;
}
// Rate limit
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(e => {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
});
});
}
if (ep.meta.requireCredential && user == null) {
throw new ApiError({
message: 'Credential required.',
@ -53,7 +81,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
}
if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) {
if (ep.meta.requireModerator && !isModerator) {
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
}
@ -65,18 +93,6 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
});
}
if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) {
// Rate limit
await limiter(ep as IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user!).catch(e => {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
});
});
}
// Cast non JSON input
if (ep.meta.requireFile && ep.params.properties) {
for (const k of Object.keys(ep.params.properties)) {
@ -104,20 +120,20 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
if (e instanceof ApiError) {
throw e;
} else {
apiLogger.error(`Internal error occurred in ${ep.name}: ${e?.message}`, {
apiLogger.error(`Internal error occurred in ${ep.name}: ${e.message}`, {
ep: ep.name,
ps: data,
e: {
message: e?.message,
code: e?.name,
stack: e?.stack,
message: e.message,
code: e.name,
stack: e.stack,
},
});
throw new ApiError(null, {
e: {
message: e?.message,
code: e?.name,
stack: e?.stack,
message: e.message,
code: e.name,
stack: e.stack,
},
});
}

View file

@ -3,6 +3,7 @@ import { Followings } from '@/models/index.js';
import { Brackets, SelectQueryBuilder } from 'typeorm';
export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: User['id'] } | null) {
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
if (me == null) {
q.andWhere(new Brackets(qb => { qb
.where(`note.visibility = 'public'`)
@ -11,7 +12,7 @@ export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: U
} else {
const followingQuery = Followings.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: me.id });
.where('following.followerId = :meId');
q.andWhere(new Brackets(qb => { qb
// 公開投稿である
@ -20,21 +21,22 @@ export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: U
.orWhere(`note.visibility = 'home'`);
}))
// または 自分自身
.orWhere('note.userId = :userId1', { userId1: me.id })
.orWhere('note.userId = :meId')
// または 自分宛て
.orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`)
.orWhere(':meId = ANY(note.visibleUserIds)')
.orWhere(':meId = ANY(note.mentions)')
.orWhere(new Brackets(qb => { qb
// または フォロワー宛ての投稿であり、
.where('note.visibility = \'followers\'')
.where(`note.visibility = 'followers'`)
.andWhere(new Brackets(qb => { qb
// 自分がフォロワーである
.where(`note.userId IN (${ followingQuery.getQuery() })`)
// または 自分の投稿へのリプライ
.orWhere('note.replyUserId = :userId3', { userId3: me.id });
.orWhere('note.replyUserId = :meId');
}));
}));
}));
q.setParameters(followingQuery.getParameters());
q.setParameters({ meId: me.id });
}
}

View file

@ -9,6 +9,8 @@ export async function readNotification(
userId: User['id'],
notificationIds: Notification['id'][]
) {
if (notificationIds.length === 0) return;
// Update documents
await Notifications.update({
id: In(notificationIds),

View file

@ -99,6 +99,7 @@ import * as ep___charts_user_notes from './endpoints/charts/user/notes.js';
import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js';
import * as ep___charts_users from './endpoints/charts/users.js';
import * as ep___clips_addNote from './endpoints/clips/add-note.js';
import * as ep___clips_removeNote from './endpoints/clips/remove-note.js';
import * as ep___clips_create from './endpoints/clips/create.js';
import * as ep___clips_delete from './endpoints/clips/delete.js';
import * as ep___clips_list from './endpoints/clips/list.js';
@ -409,6 +410,7 @@ const eps = [
['charts/user/reactions', ep___charts_user_reactions],
['charts/users', ep___charts_users],
['clips/add-note', ep___clips_addNote],
['clips/remove-note', ep___clips_removeNote],
['clips/create', ep___clips_create],
['clips/delete', ep___clips_delete],
['clips/list', ep___clips_list],
@ -654,7 +656,6 @@ export interface IEndpointMeta {
/**
*
*
* withCredential false
*/
readonly limit?: {

View file

@ -1,5 +1,5 @@
import { Signins, UserProfiles, Users } from '@/models/index.js';
import define from '../../define.js';
import { Users } from '@/models/index.js';
export const meta = {
tags: ['admin'],
@ -23,9 +23,12 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
const user = await Users.findOneBy({ id: ps.userId });
const [user, profile] = await Promise.all([
Users.findOneBy({ id: ps.userId }),
UserProfiles.findOneBy({ userId: ps.userId })
]);
if (user == null) {
if (user == null || profile == null) {
throw new Error('user not found');
}
@ -34,8 +37,37 @@ export default define(meta, paramDef, async (ps, me) => {
throw new Error('cannot show info of admin');
}
if (!_me.isAdmin) {
return {
isModerator: user.isModerator,
isSilenced: user.isSilenced,
isSuspended: user.isSuspended,
};
}
const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken'];
Object.keys(profile.integrations).forEach(integration => {
maskedKeys.forEach(key => profile.integrations[integration][key] = '<MASKED>');
});
const signins = await Signins.findBy({ userId: user.id });
return {
...user,
token: user.token != null ? '<MASKED>' : user.token,
email: profile.email,
emailVerified: profile.emailVerified,
autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle,
alwaysMarkNsfw: profile.alwaysMarkNsfw,
carefulBot: profile.carefulBot,
injectFeaturedNote: profile.injectFeaturedNote,
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
integrations: profile.integrations,
mutedWords: profile.mutedWords,
mutedInstances: profile.mutedInstances,
mutingNotificationTypes: profile.mutingNotificationTypes,
isModerator: user.isModerator,
isSilenced: user.isSilenced,
isSuspended: user.isSuspended,
signins,
};
});

View file

@ -1,5 +1,5 @@
import define from '../define.js';
import { Announcements, AnnouncementReads } from '@/models/index.js';
import define from '../define.js';
import { makePaginationQuery } from '../common/make-pagination-query.js';
export const meta = {

View file

@ -0,0 +1,57 @@
import define from '../../define.js';
import { ClipNotes, Clips } from '@/models/index.js';
import { ApiError } from '../../error.js';
import { getNote } from '../../common/getters.js';
export const meta = {
tags: ['account', 'notes', 'clips'],
requireCredential: true,
kind: 'write:account',
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52',
},
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'aff017de-190e-434b-893e-33a9ff5049d8',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
clipId: { type: 'string', format: 'misskey:id' },
noteId: { type: 'string', format: 'misskey:id' },
},
required: ['clipId', 'noteId'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
const clip = await Clips.findOneBy({
id: ps.clipId,
userId: user.id,
});
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
const note = await getNote(ps.noteId).catch(e => {
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw e;
});
await ClipNotes.delete({
noteId: note.id,
clipId: clip.id,
});
});

View file

@ -1,6 +1,6 @@
import define from '../define.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { DriveFiles } from '@/models/index.js';
import define from '../define.js';
export const meta = {
tags: ['drive', 'account'],

View file

@ -9,6 +9,8 @@ export const meta = {
kind: 'read:drive',
description: 'Find the notes to which the given file is attached.',
res: {
type: 'array',
optional: false, nullable: false,

View file

@ -8,6 +8,8 @@ export const meta = {
kind: 'read:drive',
description: 'Check if a given file exists.',
res: {
type: 'boolean',
optional: false, nullable: false,

View file

@ -20,6 +20,8 @@ export const meta = {
kind: 'write:drive',
description: 'Upload a new drive file.',
res: {
type: 'object',
optional: false, nullable: false,

View file

@ -11,6 +11,8 @@ export const meta = {
kind: 'write:drive',
description: 'Delete an existing drive file.',
errors: {
noSuchFile: {
message: 'No such file.',

View file

@ -8,6 +8,8 @@ export const meta = {
kind: 'read:drive',
description: 'Search for a drive file by a hash of the contents.',
res: {
type: 'array',
optional: false, nullable: false,

View file

@ -9,6 +9,8 @@ export const meta = {
kind: 'read:drive',
description: 'Search for a drive file by the given parameters.',
res: {
type: 'array',
optional: false, nullable: false,

View file

@ -10,6 +10,8 @@ export const meta = {
kind: 'read:drive',
description: 'Show the properties of a drive file.',
res: {
type: 'object',
optional: false, nullable: false,

View file

@ -11,6 +11,8 @@ export const meta = {
kind: 'write:drive',
description: 'Update the properties of a drive file.',
errors: {
invalidFileName: {
message: 'Invalid file name.',

View file

@ -13,6 +13,8 @@ export const meta = {
max: 60,
},
description: 'Request the server to download a new drive file from the specified URL.',
requireCredential: true,
kind: 'write:drive',

View file

@ -1,6 +1,6 @@
import define from '../define.js';
import { createExportCustomEmojisJob } from '@/queue/index.js';
import ms from 'ms';
import { createExportCustomEmojisJob } from '@/queue/index.js';
import define from '../define.js';
export const meta = {
secure: true,

View file

@ -1,6 +1,6 @@
import { MoreThan } from 'typeorm';
import { USER_ONLINE_THRESHOLD } from '@/const.js';
import { Users } from '@/models/index.js';
import { MoreThan } from 'typeorm';
import define from '../define.js';
export const meta = {

View file

@ -1,5 +1,5 @@
import define from '../define.js';
import { Users } from '@/models/index.js';
import define from '../define.js';
export const meta = {
tags: ['account'],

View file

@ -1,17 +1,22 @@
import { Brackets } from 'typeorm';
import { Notifications, Followings, Mutings, Users } from '@/models/index.js';
import { notificationTypes } from '@/types.js';
import read from '@/services/note/read.js';
import { readNotification } from '../../common/read-notification.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateMutedInstanceNotificationQuery } from '../../common/generate-muted-instance-query.js';
import { Notifications, Followings, Mutings, Users } from '@/models/index.js';
import { notificationTypes } from '@/types.js';
import read from '@/services/note/read.js';
import { Brackets } from 'typeorm';
export const meta = {
tags: ['account', 'notifications'],
requireCredential: true,
limit: {
duration: 60000,
max: 10,
},
kind: 'read:notifications',
res: {
@ -67,7 +72,7 @@ export default define(meta, paramDef, async (ps, user) => {
.where('users.isSuspended = TRUE');
const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
.andWhere(`notification.notifieeId = :meId`, { meId: user.id })
.andWhere('notification.notifieeId = :meId', { meId: user.id })
.leftJoinAndSelect('notification.notifier', 'notifier')
.leftJoinAndSelect('notification.note', 'note')
.leftJoinAndSelect('notifier.avatar', 'notifierAvatar')
@ -103,13 +108,13 @@ export default define(meta, paramDef, async (ps, user) => {
}
if (ps.includeTypes && ps.includeTypes.length > 0) {
query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes });
query.andWhere('notification.type IN (:...includeTypes)', { includeTypes: ps.includeTypes });
} else if (ps.excludeTypes && ps.excludeTypes.length > 0) {
query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes });
query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes: ps.excludeTypes });
}
if (ps.unreadOnly) {
query.andWhere(`notification.isRead = false`);
query.andWhere('notification.isRead = false');
}
const notifications = await query.take(ps.limit).getMany();

View file

@ -1,10 +1,10 @@
import { IsNull, MoreThan } from 'typeorm';
import config from '@/config/index.js';
import define from '../define.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Ads, Emojis, Users } from '@/models/index.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js';
import { IsNull, MoreThan } from 'typeorm';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import define from '../define.js';
export const meta = {
tags: ['meta'],

View file

@ -1,6 +1,6 @@
import { Notes } from '@/models/index.js';
import define from '../define.js';
import { makePaginationQuery } from '../common/make-pagination-query.js';
import { Notes } from '@/models/index.js';
export const meta = {
tags: ['notes'],
@ -34,8 +34,8 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
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('note.visibility = \'public\'')
.andWhere('note.localOnly = FALSE')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
@ -61,7 +61,7 @@ export default define(meta, paramDef, async (ps) => {
}
if (ps.withFiles !== undefined) {
query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`);
query.andWhere(ps.withFiles ? 'note.fileIds != \'{}\'' : 'note.fileIds = \'{}\'');
}
if (ps.poll !== undefined) {

View file

@ -1,9 +1,9 @@
import { Brackets } from 'typeorm';
import { Notes } from '@/models/index.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { Brackets } from 'typeorm';
import { Notes } from '@/models/index.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
@ -38,13 +38,13 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(new Brackets(qb => { qb
.where(`note.replyId = :noteId`, { noteId: ps.noteId })
.where('note.replyId = :noteId', { noteId: ps.noteId })
.orWhere(new Brackets(qb => { qb
.where(`note.renoteId = :noteId`, { noteId: ps.noteId })
.where('note.renoteId = :noteId', { noteId: ps.noteId })
.andWhere(new Brackets(qb => { qb
.where(`note.text IS NOT NULL`)
.orWhere(`note.fileIds != '{}'`)
.orWhere(`note.hasPoll = TRUE`);
.where('note.text IS NOT NULL')
.orWhere('note.fileIds != \'{}\'')
.orWhere('note.hasPoll = TRUE');
}));
}));
}))

View file

@ -1,8 +1,8 @@
import define from '../../define.js';
import { In } from 'typeorm';
import { ClipNotes, Clips } from '@/models/index.js';
import define from '../../define.js';
import { getNote } from '../../common/getters.js';
import { ApiError } from '../../error.js';
import { In } from 'typeorm';
export const meta = {
tags: ['clips', 'notes'],

View file

@ -1,8 +1,8 @@
import { Note } from '@/models/entities/note.js';
import { Notes } from '@/models/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { getNote } from '../../common/getters.js';
import { Note } from '@/models/entities/note.js';
import { Notes } from '@/models/index.js';
export const meta = {
tags: ['notes'],

View file

@ -134,7 +134,7 @@ export const paramDef = {
{
// (re)note with text, files and poll are optional
properties: {
text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false },
text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false },
},
required: ['text'],
},

View file

@ -1,9 +1,9 @@
import deleteNote from '@/services/note/delete.js';
import define from '../../define.js';
import ms from 'ms';
import deleteNote from '@/services/note/delete.js';
import { Users } from '@/models/index.js';
import define from '../../define.js';
import { getNote } from '../../common/getters.js';
import { ApiError } from '../../error.js';
import { Users } from '@/models/index.js';
export const meta = {
tags: ['notes'],

View file

@ -1,8 +1,8 @@
import { NoteFavorites } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { getNote } from '../../../common/getters.js';
import { NoteFavorites } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
export const meta = {
tags: ['notes', 'favorites'],

View file

@ -1,7 +1,7 @@
import { NoteFavorites } from '@/models/index.js';
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { getNote } from '../../../common/getters.js';
import { NoteFavorites } from '@/models/index.js';
export const meta = {
tags: ['notes', 'favorites'],

View file

@ -1,6 +1,6 @@
import { Notes } from '@/models/index.js';
import define from '../../define.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { Notes } from '@/models/index.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
export const meta = {
@ -36,9 +36,9 @@ export default define(meta, paramDef, async (ps, user) => {
const query = Notes.createQueryBuilder('note')
.addSelect('note.score')
.where('note.userHost IS NULL')
.andWhere(`note.score > 0`)
.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
.andWhere(`note.visibility = 'public'`)
.andWhere('note.score > 0')
.andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) })
.andWhere('note.visibility = \'public\'')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')

View file

@ -1,11 +1,11 @@
import define from '../../define.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Notes, Users } from '@/models/index.js';
import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { Notes, Users } from '@/models/index.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
import { activeUsersChart } from '@/services/chart/index.js';
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
@ -60,7 +60,7 @@ export default define(meta, paramDef, async (ps, user) => {
//#region Construct query
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.visibility = \'public\'')
.andWhere('note.channelId IS NULL')
.innerJoinAndSelect('note.user', 'user')

View file

@ -1,13 +1,13 @@
import define from '../../define.js';
import { Brackets } from 'typeorm';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Followings, Notes, Users } from '@/models/index.js';
import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { Followings, Notes, Users } from '@/models/index.js';
import { Brackets } from 'typeorm';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
import { activeUsersChart } from '@/services/chart/index.js';
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
import { generateChannelQuery } from '../../common/generate-channel-query.js';
@ -70,7 +70,7 @@ export default define(meta, paramDef, async (ps, user) => {
.where('following.followerId = :followerId', { followerId: user.id });
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere(new Brackets(qb => {
qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id })
.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');

View file

@ -1,12 +1,12 @@
import define from '../../define.js';
import { Brackets } from 'typeorm';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { ApiError } from '../../error.js';
import { Notes, Users } from '@/models/index.js';
import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { activeUsersChart } from '@/services/chart/index.js';
import { Brackets } from 'typeorm';
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
import { generateChannelQuery } from '../../common/generate-channel-query.js';
@ -66,7 +66,7 @@ export default define(meta, paramDef, async (ps, user) => {
//#region Construct query
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')

View file

@ -1,10 +1,10 @@
import define from '../../define.js';
import { Brackets } from 'typeorm';
import read from '@/services/note/read.js';
import { Notes, Followings } from '@/models/index.js';
import define from '../../define.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { Brackets } from 'typeorm';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query.js';

View file

@ -1,6 +1,6 @@
import define from '../../../define.js';
import { Polls, Mutings, Notes, PollVotes } from '@/models/index.js';
import { Brackets, In } from 'typeorm';
import { Polls, Mutings, Notes, PollVotes } from '@/models/index.js';
import define from '../../../define.js';
export const meta = {
tags: ['notes'],
@ -31,8 +31,8 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
const query = Polls.createQueryBuilder('poll')
.where('poll.userHost IS NULL')
.andWhere(`poll.userId != :meId`, { meId: user.id })
.andWhere(`poll.noteVisibility = 'public'`)
.andWhere('poll.userId != :meId', { meId: user.id })
.andWhere('poll.noteVisibility = \'public\'')
.andWhere(new Brackets(qb => { qb
.where('poll.expiresAt IS NULL')
.orWhere('poll.expiresAt > :now', { now: new Date() });

View file

@ -1,16 +1,16 @@
import { Not } from 'typeorm';
import { publishNoteStream } from '@/services/stream.js';
import { createNotification } from '@/services/create-notification.js';
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { getNote } from '../../../common/getters.js';
import { deliver } from '@/queue/index.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderVote from '@/remote/activitypub/renderer/vote.js';
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
import { PollVotes, NoteWatchings, Users, Polls, Blockings } from '@/models/index.js';
import { Not } from 'typeorm';
import { IRemoteUser } from '@/models/entities/user.js';
import { genId } from '@/misc/gen-id.js';
import { getNote } from '../../../common/getters.js';
import { ApiError } from '../../../error.js';
import define from '../../../define.js';
export const meta = {
tags: ['notes'],

View file

@ -1,6 +1,6 @@
import define from '../../../define.js';
import ms from 'ms';
import deleteReaction from '@/services/note/reaction/delete.js';
import define from '../../../define.js';
import { getNote } from '../../../common/getters.js';
import { ApiError } from '../../../error.js';

View file

@ -1,10 +1,10 @@
import { Notes } from '@/models/index.js';
import define from '../../define.js';
import { getNote } from '../../common/getters.js';
import { ApiError } from '../../error.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { Notes } from '@/models/index.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
export const meta = {
@ -50,7 +50,7 @@ export default define(meta, paramDef, async (ps, user) => {
});
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(`note.renoteId = :renoteId`, { renoteId: note.id })
.andWhere('note.renoteId = :renoteId', { renoteId: note.id })
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')

View file

@ -1,5 +1,5 @@
import define from '../../define.js';
import { Notes } from '@/models/index.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';

View file

@ -1,11 +1,11 @@
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { Notes } from '@/models/index.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { Brackets } from 'typeorm';
import { Notes } from '@/models/index.js';
import { safeForSql } from '@/misc/safe-for-sql.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
export const meta = {

View file

@ -1,8 +1,8 @@
import { In } from 'typeorm';
import { Notes } from '@/models/index.js';
import config from '@/config/index.js';
import es from '../../../../db/elasticsearch.js';
import define from '../../define.js';
import { Notes } from '@/models/index.js';
import { In } from 'typeorm';
import config from '@/config/index.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
@ -99,7 +99,7 @@ export default define(meta, paramDef, async (ps, me) => {
userHost: ps.host,
},
}] : []
: [];
: [];
const result = await es.search({
index: config.elasticsearch.index || 'misskey_note',

View file

@ -1,7 +1,7 @@
import { Notes } from '@/models/index.js';
import define from '../../define.js';
import { getNote } from '../../common/getters.js';
import { ApiError } from '../../error.js';
import { Notes } from '@/models/index.js';
export const meta = {
tags: ['notes'],

View file

@ -1,5 +1,5 @@
import define from '../../define.js';
import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index.js';
import define from '../../define.js';
export const meta = {
tags: ['notes'],

View file

@ -1,9 +1,9 @@
import define from '../../../define.js';
import { getNote } from '../../../common/getters.js';
import { ApiError } from '../../../error.js';
import { Notes, NoteThreadMutings } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import readNote from '@/services/note/read.js';
import define from '../../../define.js';
import { getNote } from '../../../common/getters.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['notes'],

View file

@ -1,7 +1,7 @@
import { NoteThreadMutings } from '@/models/index.js';
import define from '../../../define.js';
import { getNote } from '../../../common/getters.js';
import { ApiError } from '../../../error.js';
import { NoteThreadMutings } from '@/models/index.js';
export const meta = {
tags: ['notes'],

View file

@ -1,11 +1,11 @@
import { Brackets } from 'typeorm';
import { Notes, Followings } from '@/models/index.js';
import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { Notes, Followings } from '@/models/index.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
import { activeUsersChart } from '@/services/chart/index.js';
import { Brackets } from 'typeorm';
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
import { generateChannelQuery } from '../../common/generate-channel-query.js';
@ -62,10 +62,10 @@ export default define(meta, paramDef, async (ps, user) => {
.where('following.followerId = :followerId', { followerId: user.id });
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere(new Brackets(qb => { qb
.where('note.userId = :meId', { meId: user.id });
if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
}))
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')

View file

@ -1,9 +1,9 @@
import deleteNote from '@/services/note/delete.js';
import define from '../../define.js';
import ms from 'ms';
import deleteNote from '@/services/note/delete.js';
import { Notes, Users } from '@/models/index.js';
import define from '../../define.js';
import { getNote } from '../../common/getters.js';
import { ApiError } from '../../error.js';
import { Notes, Users } from '@/models/index.js';
export const meta = {
tags: ['notes'],

View file

@ -1,10 +1,10 @@
import { Brackets } from 'typeorm';
import { UserLists, UserListJoinings, Notes } from '@/models/index.js';
import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { UserLists, UserListJoinings, Notes } from '@/models/index.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { activeUsersChart } from '@/services/chart/index.js';
import { Brackets } from 'typeorm';
export const meta = {
tags: ['notes', 'lists'],

View file

@ -1,5 +1,5 @@
import define from '../../../define.js';
import watch from '@/services/note/watch.js';
import define from '../../../define.js';
import { getNote } from '../../../common/getters.js';
import { ApiError } from '../../../error.js';

View file

@ -1,5 +1,5 @@
import define from '../../../define.js';
import unwatch from '@/services/note/unwatch.js';
import define from '../../../define.js';
import { getNote } from '../../../common/getters.js';
import { ApiError } from '../../../error.js';

View file

@ -1,5 +1,5 @@
import define from '../../define.js';
import { createNotification } from '@/services/create-notification.js';
import define from '../../define.js';
export const meta = {
tags: ['notifications'],

View file

@ -1,7 +1,7 @@
import { publishMainStream } from '@/services/stream.js';
import { pushNotification } from '@/services/push-notification.js';
import define from '../../define.js';
import { Notifications } from '@/models/index.js';
import define from '../../define.js';
export const meta = {
tags: ['notifications', 'account'],

View file

@ -2,17 +2,14 @@ import define from '../../define.js';
import { readNotification } from '../../common/read-notification.js';
export const meta = {
desc: {
'ja-JP': '通知を既読にします。',
'en-US': 'Mark a notification as read.'
},
tags: ['notifications', 'account'],
requireCredential: true,
kind: 'write:notifications',
description: 'Mark a notification as read.',
errors: {
noSuchNotification: {
message: 'No such notification.',
@ -34,7 +31,11 @@ export const paramDef = {
{
type: 'object',
properties: {
notificationIds: { type: 'array', items: { type: 'string', format: 'misskey:id' } },
notificationIds: {
type: 'array',
items: { type: 'string', format: 'misskey:id' },
maxItems: 100,
},
},
required: ['notificationIds'],
},

View file

@ -1,6 +1,6 @@
import define from '../define.js';
import { publishMainStream } from '@/services/stream.js';
import { Users, Pages } from '@/models/index.js';
import define from '../define.js';
import { ApiError } from '../error.js';
export const meta = {

View file

@ -1,8 +1,8 @@
import ms from 'ms';
import define from '../../define.js';
import { Pages, DriveFiles } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import { Page } from '@/models/entities/page.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -51,7 +51,7 @@ export const paramDef = {
} },
script: { type: 'string' },
eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true },
font: { type: 'string', enum: ['serif', 'sans-serif'], default: "sans-serif" },
font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' },
alignCenter: { type: 'boolean', default: false },
hideTitleWhenPinned: { type: 'boolean', default: false },
},

View file

@ -1,6 +1,6 @@
import { Pages } from '@/models/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { Pages } from '@/models/index.js';
export const meta = {
tags: ['pages'],

View file

@ -1,5 +1,5 @@
import define from '../../define.js';
import { Pages } from '@/models/index.js';
import define from '../../define.js';
export const meta = {
tags: ['pages'],

View file

@ -1,7 +1,7 @@
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { Pages, PageLikes } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['pages'],

View file

@ -1,6 +1,6 @@
import { Pages, PageLikes } from '@/models/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { Pages, PageLikes } from '@/models/index.js';
export const meta = {
tags: ['pages'],

View file

@ -1,8 +1,8 @@
import ms from 'ms';
import { Not } from 'typeorm';
import { Pages, DriveFiles } from '@/models/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { Pages, DriveFiles } from '@/models/index.js';
import { Not } from 'typeorm';
export const meta = {
tags: ['pages'],

Some files were not shown because too many files have changed in this diff Show more