Merge branch 'develop' into pizzax-indexeddb
|
@ -5,6 +5,6 @@
|
|||
"loader=./test/loader.js"
|
||||
],
|
||||
"slow": 1000,
|
||||
"timeout": 3000,
|
||||
"timeout": 10000,
|
||||
"exit": true
|
||||
}
|
||||
|
|
5
packages/backend/assets/notification-badges/LICENSE
Normal file
|
@ -0,0 +1,5 @@
|
|||
Font Awesome Icons
|
||||
-------------------------
|
||||
|
||||
Ⓒ Font Awesome
|
||||
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
BIN
packages/backend/assets/notification-badges/at.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
packages/backend/assets/notification-badges/check.png
Normal file
After Width: | Height: | Size: 577 B |
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/backend/assets/notification-badges/clock.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/backend/assets/notification-badges/comments.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/backend/assets/notification-badges/id-card-alt.png
Normal file
After Width: | Height: | Size: 844 B |
BIN
packages/backend/assets/notification-badges/null.png
Normal file
After Width: | Height: | Size: 174 B |
BIN
packages/backend/assets/notification-badges/plus.png
Normal file
After Width: | Height: | Size: 507 B |
BIN
packages/backend/assets/notification-badges/poll-h.png
Normal file
After Width: | Height: | Size: 689 B |
BIN
packages/backend/assets/notification-badges/quote-right.png
Normal file
After Width: | Height: | Size: 772 B |
BIN
packages/backend/assets/notification-badges/reply.png
Normal file
After Width: | Height: | Size: 930 B |
BIN
packages/backend/assets/notification-badges/retweet.png
Normal file
After Width: | Height: | Size: 798 B |
BIN
packages/backend/assets/notification-badges/user-plus.png
Normal file
After Width: | Height: | Size: 991 B |
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
9
packages/backend/src/misc/get-ip-hash.ts
Normal 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);
|
||||
}
|
8
packages/backend/src/misc/is-mime-image.ts
Normal 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);
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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()}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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?: {
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
|
@ -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'],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -8,6 +8,8 @@ export const meta = {
|
|||
|
||||
kind: 'read:drive',
|
||||
|
||||
description: 'Check if a given file exists.',
|
||||
|
||||
res: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
@ -20,6 +20,8 @@ export const meta = {
|
|||
|
||||
kind: 'write:drive',
|
||||
|
||||
description: 'Upload a new drive file.',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
|||
|
||||
kind: 'write:drive',
|
||||
|
||||
description: 'Delete an existing drive file.',
|
||||
|
||||
errors: {
|
||||
noSuchFile: {
|
||||
message: 'No such 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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -11,6 +11,8 @@ export const meta = {
|
|||
|
||||
kind: 'write:drive',
|
||||
|
||||
description: 'Update the properties of a drive file.',
|
||||
|
||||
errors: {
|
||||
invalidFileName: {
|
||||
message: 'Invalid file name.',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
}));
|
||||
}));
|
||||
}))
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)');
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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() });
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'],
|
||||
|
|