Fixed 267U.pre2

This commit is contained in:
2026-07-26 18:25:37 +09:00
parent 50bfaeafdf
commit 317d00a284
1286 changed files with 80222 additions and 1 deletions
@@ -0,0 +1,73 @@
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 { generateBlockedUserQuery } from "../../common/generate-block-query.js";
export const meta = {
tags: [
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
description: "Get threaded/chained replies to a note",
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
depth: {
type: "integer",
minimum: 1,
maximum: 100,
default: 12
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const query = makePaginationQuery(Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId).andWhere("note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))", {
noteId: ps.noteId,
depth: ps.depth,
limit: ps.limit
}).innerJoinAndSelect("note.user", "user");
generateVisibilityQuery(query, user);
if (user) {
generateMutedUserQuery(query, user);
generateBlockedUserQuery(query, user);
}
const notes = await query.getMany();
return await Notes.packMany(notes, user, {
detail: false
});
});
@@ -0,0 +1,59 @@
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";
export const meta = {
tags: [
"clips",
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Clip"
}
},
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "47db1a1c-b0af-458d-8fb4-986e4efafe1e"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, me)=>{
const note = await getNote(ps.noteId, me, {
allowAdservice: true
}).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const clipNotes = await ClipNotes.findBy({
noteId: note.id
});
const clips = await Clips.findBy({
id: In(clipNotes.map((x)=>x.clipId)),
isPublic: true
});
return await Promise.all(clips.map((x)=>Clips.pack(x)));
});
@@ -0,0 +1,82 @@
import { Notes } from "../../../../models/index.js";
import define from "../../define.js";
import { ApiError } from "../../error.js";
import { getNote } from "../../common/getters.js";
export const meta = {
tags: [
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
description: "Get conversation of a note thread/chain by a reply",
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
},
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "e1035875-9551-45ec-afa8-1ded1fcb53c8"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id",
description: "Should be a reply"
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
offset: {
type: "integer",
default: 0
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const conversation = [];
let i = 0;
async function get(id) {
i++;
const p = await getNote(id, user).catch((e)=>{
if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") return null;
throw e;
});
if (p == null) return;
if (i > ps.offset) {
conversation.push(p);
}
if (conversation.length === ps.limit) {
return;
}
if (p.replyId) {
await get(p.replyId);
}
}
if (note.replyId) {
await get(note.replyId);
}
return await Notes.packMany(conversation, user);
});
@@ -0,0 +1,294 @@
import { Notes } from "../../../../models/index.js";
import { MAX_NOTE_TEXT_LENGTH } from "../../../../const.js";
import { noteVisibilities } from "../../../../types.js";
import { ApiError } from "../../error.js";
import define from "../../define.js";
import { HOUR } from "../../../../const.js";
import { createNoteFromApiData } from "../../../../services/note/create-from-api.js";
import { scheduleNote } from "../../../../services/note/scheduled.js";
import { enqueueScheduledNote } from "../../../../queue/queues/system/scheduled-note.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
limit: {
duration: HOUR,
max: 300
},
kind: "write:notes",
res: {
type: "object",
optional: false,
nullable: false,
properties: {
createdNote: {
type: "object",
optional: true,
nullable: false,
ref: "Note"
},
scheduledNote: {
type: "object",
optional: true,
nullable: false,
properties: {
id: {
type: "string",
optional: false,
nullable: false
},
scheduledAt: {
type: "string",
optional: false,
nullable: false
}
}
}
}
},
errors: {
noSuchRenoteTarget: {
message: "No such renote target.",
code: "NO_SUCH_RENOTE_TARGET",
id: "b5c90186-4ab0-49c8-9bba-a1f76c282ba4"
},
cannotReRenote: {
message: "You can not Renote a pure Renote.",
code: "CANNOT_RENOTE_TO_A_PURE_RENOTE",
id: "fd4cc33e-2a37-48dd-99cc-9b806eb2031a"
},
noSuchReplyTarget: {
message: "No such reply target.",
code: "NO_SUCH_REPLY_TARGET",
id: "749ee0f6-d3da-459a-bf02-282e2da4292c"
},
cannotReplyToPureRenote: {
message: "You can not reply to a pure Renote.",
code: "CANNOT_REPLY_TO_A_PURE_RENOTE",
id: "3ac74a84-8fd5-4bb0-870f-01804f82ce15"
},
cannotCreateAlreadyExpiredPoll: {
message: "Poll is already expired.",
code: "CANNOT_CREATE_ALREADY_EXPIRED_POLL",
id: "04da457d-b083-4055-9082-955525eda5a5"
},
noSuchChannel: {
message: "No such channel.",
code: "NO_SUCH_CHANNEL",
id: "b1653923-5453-4edc-b786-7c4f39bb0bbb"
},
youHaveBeenBlocked: {
message: "You have been blocked by this user.",
code: "YOU_HAVE_BEEN_BLOCKED",
id: "b390d7e1-8a5e-46ed-b625-06271cafd3d3"
},
accountLocked: {
message: "You migrated. Your account is now locked.",
code: "ACCOUNT_LOCKED",
id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3"
},
noSuchGroup: {
message: "No such group.",
code: "NO_SUCH_GROUP",
id: "7a42ce9c-bdbd-4dcc-8a5d-daf2f520bfaa"
},
cannotScheduleToPast: {
message: "Scheduled time must be in the future.",
code: "CANNOT_SCHEDULE_TO_PAST",
id: "181f385d-1f59-42c8-b3a2-b52253a92057"
}
}
};
export const paramDef = {
type: "object",
properties: {
visibility: {
type: "string",
enum: noteVisibilities,
default: "public"
},
visibleUserIds: {
type: "array",
uniqueItems: true,
items: {
type: "string",
format: "misskey:id"
}
},
text: {
type: "string",
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: true
},
cw: {
type: "string",
nullable: true,
maxLength: 100
},
localOnly: {
type: "boolean",
default: false
},
noExtractMentions: {
type: "boolean",
default: false
},
noExtractHashtags: {
type: "boolean",
default: false
},
noExtractEmojis: {
type: "boolean",
default: false
},
fileIds: {
type: "array",
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: {
type: "string",
format: "misskey:id"
}
},
mediaIds: {
deprecated: true,
description: "Use `fileIds` instead. If both are specified, this property is discarded.",
type: "array",
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: {
type: "string",
format: "misskey:id"
}
},
replyId: {
type: "string",
format: "misskey:id",
nullable: true
},
renoteId: {
type: "string",
format: "misskey:id",
nullable: true
},
channelId: {
type: "string",
format: "misskey:id",
nullable: true
},
groupId: {
type: "string",
format: "misskey:id",
nullable: true
},
scheduledAt: {
type: "integer",
nullable: true
},
poll: {
type: "object",
nullable: true,
properties: {
choices: {
type: "array",
uniqueItems: true,
minItems: 2,
maxItems: 10,
items: {
type: "string",
minLength: 1,
maxLength: 50
}
},
multiple: {
type: "boolean",
default: false
},
expiresAt: {
type: "integer",
nullable: true
},
expiredAfter: {
type: "integer",
nullable: true,
minimum: 1
}
},
required: [
"choices"
]
}
},
anyOf: [
{
// (re)note with text, files and poll are optional
properties: {
text: {
type: "string",
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: false
}
},
required: [
"text"
]
},
{
// (re)note with files, text and poll are optional
required: [
"fileIds"
]
},
{
// (re)note with files, text and poll are optional
required: [
"mediaIds"
]
},
{
// (re)note with poll, text and files are optional
properties: {
poll: {
type: "object",
nullable: false
}
},
required: [
"poll"
]
},
{
// pure renote
required: [
"renoteId"
]
}
]
};
export default define(meta, paramDef, async (ps, user)=>{
if (user.movedToUri != null) throw new ApiError(meta.errors.accountLocked);
if (typeof ps.scheduledAt === "number") {
const scheduledAt = new Date(ps.scheduledAt);
if (scheduledAt.getTime() <= Date.now()) {
throw new ApiError(meta.errors.cannotScheduleToPast);
}
const scheduledNote = await scheduleNote(user, ps, scheduledAt);
await enqueueScheduledNote(scheduledNote).catch((err)=>{
console.error("Failed to enqueue scheduled note", err);
});
return {
scheduledNote: {
id: scheduledNote.id,
scheduledAt: scheduledNote.scheduledAt.toISOString()
}
};
}
const note = await createNoteFromApiData(user, ps, new Date());
return {
createdNote: await Notes.pack(note, user)
};
});
@@ -0,0 +1,68 @@
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 { SECOND, HOUR } from "../../../../const.js";
import { getGroupActor } from "../../common/get-group-actor.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
kind: "write:notes",
limit: {
duration: HOUR,
max: 300,
minInterval: SECOND
},
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "490be23f-8c1f-4796-819f-94cb4f9d1630"
},
accessDenied: {
message: "Access denied.",
code: "ACCESS_DENIED",
id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9"
},
noSuchGroup: {
message: "No such group.",
code: "NO_SUCH_GROUP",
id: "4dc5f42e-ae8b-4b9c-88df-4f3bc0d87a9a"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
groupId: {
type: "string",
format: "misskey:id",
nullable: true
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const group = await getGroupActor(ps.groupId, user);
if (ps.groupId != null && group == null) throw new ApiError(meta.errors.noSuchGroup);
if (!(user.isAdmin || user.isModerator) && (group == null ? note.userId !== user.id : note.groupId !== group.id)) {
throw new ApiError(meta.errors.accessDenied);
}
// この操作を行うのが投稿者とは限らない(例えばモデレーター)ため
await deleteNote(await Users.findOneByOrFail({
id: note.userId
}), note);
});
@@ -0,0 +1,209 @@
import { Users, DriveFiles, Notes } from "../../../../models/index.js";
import { MAX_NOTE_TEXT_LENGTH } from "../../../../const.js";
import { ApiError } from "../../error.js";
import define from "../../define.js";
import { HOUR } from "../../../../const.js";
import editNote from "../../../../services/note/edit.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
limit: {
duration: HOUR,
max: 300
},
kind: "write:notes",
res: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
},
errors: {
cannotCreateAlreadyExpiredPoll: {
message: "Poll is already expired.",
code: "CANNOT_CREATE_ALREADY_EXPIRED_POLL",
id: "04da457d-b083-4055-9082-955525eda5a5"
},
accountLocked: {
message: "You migrated. Your account is now locked.",
code: "ACCOUNT_LOCKED",
id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3"
},
needsEditId: {
message: "You need to specify `editId`.",
code: "NEEDS_EDIT_ID",
id: "d697edc8-8c73-4de8-bded-35fd198b79e5"
},
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "eef6c173-3010-4a23-8674-7c4fcaeba719"
},
youAreNotTheAuthor: {
message: "You are not the author of this note.",
code: "YOU_ARE_NOT_THE_AUTHOR",
id: "c6e61685-411d-43d0-b90a-a448d2539001"
},
notLocalUser: {
message: "You are not a local user.",
code: "NOT_LOCAL_USER",
id: "b907f407-2aa0-4283-800b-a2c56290b822"
},
cannotAddPollToPost: {
message: "You cannot add a poll to a post.",
code: "CANNOT_ADD_POLL",
id: "e6a00055-21c8-4d69-a4d3-c26731cf6f18"
},
cannotRemovePollFromPost: {
message: "You cannot remove a poll from a post.",
code: "CANNOT_REMOVE_POLL",
id: "7c4889f1-cb50-4a36-a740-9d04f097479b"
}
}
};
export const paramDef = {
type: "object",
properties: {
editId: {
type: "string",
format: "misskey:id"
},
text: {
type: "string",
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: true
},
cw: {
type: "string",
nullable: true,
maxLength: 250
},
fileIds: {
type: "array",
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: {
type: "string",
format: "misskey:id"
}
},
poll: {
type: "object",
nullable: true,
properties: {
choices: {
type: "array",
uniqueItems: true,
minItems: 2,
maxItems: 10,
items: {
type: "string",
minLength: 1,
maxLength: 50
}
},
multiple: {
type: "boolean",
default: false
},
expiresAt: {
type: "integer",
nullable: true
},
expiredAfter: {
type: "integer",
nullable: true,
minimum: 1
}
},
required: [
"choices"
]
}
},
anyOf: [
{
// note with text, files and poll are optional
properties: {
text: {
type: "string",
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: false
}
},
required: [
"text"
]
},
{
// note with files, text and poll are optional
required: [
"fileIds"
]
},
{
// note with poll, text and files are optional
properties: {
poll: {
type: "object",
nullable: false
}
},
required: [
"poll"
]
}
]
};
export default define(meta, paramDef, async (ps, user)=>{
if (user.movedToUri != null) throw new ApiError(meta.errors.accountLocked);
if (!Users.isLocalUser(user)) {
throw new ApiError(meta.errors.notLocalUser);
}
if (!ps.editId) {
throw new ApiError(meta.errors.needsEditId);
}
let note = await Notes.findOneBy({
id: ps.editId
});
if (!note) {
throw new ApiError(meta.errors.noSuchNote);
}
if (note.userId !== user.id) {
throw new ApiError(meta.errors.youAreNotTheAuthor);
}
if (ps.poll?.expiresAt && new Date(ps.poll.expiresAt).getTime() < new Date().getTime()) {
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
}
if (!note.hasPoll && ps.poll) {
throw new ApiError(meta.errors.cannotAddPollToPost);
}
if (note.hasPoll && !ps.poll) {
throw new ApiError(meta.errors.cannotRemovePollFromPost);
}
let files = [];
const fileIds = ps.fileIds ?? null;
if (fileIds != null) {
files = await DriveFiles.createQueryBuilder("file").where("file.userId = :userId AND file.id IN (:...fileIds)", {
userId: user.id,
fileIds
}).orderBy('array_position(ARRAY[:...fileIds], "id"::text)').setParameters({
fileIds
}).getMany();
}
note = await editNote(user, note, {
text: ps.text,
cw: ps.cw,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null
} : null,
files: files
});
return Notes.pack(note, user);
});
@@ -0,0 +1,80 @@
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 { getGroupActor } from "../../../common/get-group-actor.js";
export const meta = {
tags: [
"notes",
"favorites"
],
requireCredential: true,
kind: "write:favorites",
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "6dd26674-e060-4816-909a-45ba3f4da458"
},
alreadyFavorited: {
message: "The note has already been marked as a favorite.",
code: "ALREADY_FAVORITED",
id: "a402c12b-34dd-41d2-97d8-4d2ffd96a1a6"
},
noSuchGroup: {
message: "No such group.",
code: "NO_SUCH_GROUP",
id: "02578569-44a6-4761-a112-d60dfc8764b3"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
groupId: {
type: "string",
format: "misskey:id",
nullable: true
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
// Get favoritee
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const group = await getGroupActor(ps.groupId, user);
if (ps.groupId != null && group == null) throw new ApiError(meta.errors.noSuchGroup);
// if already favorited
const exist = await NoteFavorites.exist({
where: {
noteId: note.id,
...group ? {
groupId: group.id
} : {
userId: user.id,
groupId: null
}
}
});
if (exist) {
throw new ApiError(meta.errors.alreadyFavorited);
}
// Create favorite
await NoteFavorites.insert({
id: genId(),
createdAt: new Date(),
noteId: note.id,
userId: user.id,
groupId: group?.id ?? null
});
});
@@ -0,0 +1,71 @@
import { NoteFavorites } from "../../../../../models/index.js";
import define from "../../../define.js";
import { ApiError } from "../../../error.js";
import { getNote } from "../../../common/getters.js";
import { getGroupActor } from "../../../common/get-group-actor.js";
export const meta = {
tags: [
"notes",
"favorites"
],
requireCredential: true,
kind: "write:favorites",
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "80848a2c-398f-4343-baa9-df1d57696c56"
},
notFavorited: {
message: "You have not marked that note a favorite.",
code: "NOT_FAVORITED",
id: "b625fc69-635e-45e9-86f4-dbefbef35af5"
},
noSuchGroup: {
message: "No such group.",
code: "NO_SUCH_GROUP",
id: "76417d51-a8c7-47d5-b7e2-28d89f0d438e"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
groupId: {
type: "string",
format: "misskey:id",
nullable: true
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
// Get favoritee
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const group = await getGroupActor(ps.groupId, user);
if (ps.groupId != null && group == null) throw new ApiError(meta.errors.noSuchGroup);
// if already favorited
const favorite = await NoteFavorites.findOneBy({
noteId: note.id,
...group ? {
groupId: group.id
} : {
userId: user.id,
groupId: null
}
});
if (favorite == null) {
throw new ApiError(meta.errors.notFavorited);
}
// Delete favorite
await NoteFavorites.delete(favorite.id);
});
@@ -0,0 +1,74 @@
import { Notes } from "../../../../models/index.js";
import define from "../../define.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
export const meta = {
tags: [
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
}
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
offset: {
type: "integer",
default: 0
},
origin: {
type: "string",
enum: [
"combined",
"local",
"remote"
],
default: "local"
},
days: {
type: "integer",
minimum: 1,
maximum: 365,
default: 3
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const max = 30;
const day = 1000 * 60 * 60 * 24 * ps.days;
const query = Notes.createQueryBuilder("note").addSelect("note.score").andWhere("note.score > 0").andWhere("note.createdAt > :date", {
date: new Date(Date.now() - day)
}).andWhere("note.visibility = 'public'").innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
switch(ps.origin){
case "local":
query.andWhere("note.userHost IS NULL");
break;
case "remote":
query.andWhere("note.userHost IS NOT NULL");
break;
}
if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
let notes = await query.orderBy("note.score", "DESC").take(max).getMany();
notes.sort((a, b)=>new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
notes = notes.slice(ps.offset, ps.offset + ps.limit);
return await Notes.packMany(notes, user);
});
@@ -0,0 +1,111 @@
import { fetchMeta } from "../../../../misc/fetch-meta.js";
import { Notes } 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 { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { generateRepliesQuery } from "../../common/generate-replies-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
import { generateExcludeMemorietQuery } from "../../common/generate-exclude-memoriet-query.js";
import { injectPromo } from "../../common/inject-promo.js";
export const meta = {
tags: [
"notes"
],
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
},
errors: {
gtlDisabled: {
message: "Global timeline has been disabled.",
code: "GTL_DISABLED",
id: "0332fc13-6ab2-4427-ae80-a9fadffd1a6b"
},
queryError: {
message: "Please follow more users.",
code: "QUERY_ERROR",
id: "620763f4-f621-4533-ab33-0577a1a3c343"
}
}
};
export const paramDef = {
type: "object",
properties: {
withFiles: {
type: "boolean",
default: false,
description: "Only show notes that have attached files."
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
},
sinceDate: {
type: "integer"
},
untilDate: {
type: "integer"
},
withReplies: {
type: "boolean",
default: false,
description: "Show replies in the timeline"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const m = await fetchMeta();
if (m.disableGlobalTimeline) {
if (user == null || !(user.isAdmin || user.isModerator)) {
throw new ApiError(meta.errors.gtlDisabled);
}
}
//#region Construct query
const query = makePaginationQuery(Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate).andWhere("note.visibility = 'public'").andWhere("note.channelId IS NULL").andWhere(`NOT ('adservice' = ANY(note.tags))`).innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
generateRepliesQuery(query, ps.withReplies, user);
if (user) {
generateMutedUserQuery(query, user);
generateBlockedUserQuery(query, user);
generateMutedUserRenotesQueryForNotes(query, user);
}
generateExcludeMemorietQuery(query);
if (ps.withFiles) {
query.andWhere("note.fileIds != '{}'");
}
query.andWhere("note.visibility != 'hidden'");
//#endregion
process.nextTick(()=>{
if (user) {
activeUsersChart.read(user);
}
});
try {
const notes = await query.take(ps.limit).getMany();
await injectPromo(notes, user);
return await Notes.packMany(notes, user);
} catch (error) {
throw new ApiError(meta.errors.queryError);
}
});
@@ -0,0 +1,162 @@
import { Brackets } from "typeorm";
import { fetchMeta } from "../../../../misc/fetch-meta.js";
import { Followings, Notes } 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 { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { generateRepliesQuery } from "../../common/generate-replies-query.js";
import { generateChannelQuery } from "../../common/generate-channel-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
import { generateListQuery } from "../../common/generate-list-query.js";
import { generateExcludeMemorietQuery } from "../../common/generate-exclude-memoriet-query.js";
import { injectPromo } from "../../common/inject-promo.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
},
errors: {
stlDisabled: {
message: "Hybrid timeline has been disabled.",
code: "STL_DISABLED",
id: "620763f4-f621-4533-ab33-0577a1a3c342"
},
queryError: {
message: "Please follow more users.",
code: "QUERY_ERROR",
id: "620763f4-f621-4533-ab33-0577a1a3c343"
}
}
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
},
sinceDate: {
type: "integer"
},
untilDate: {
type: "integer"
},
includeMyRenotes: {
type: "boolean",
default: true
},
includeRenotedMyNotes: {
type: "boolean",
default: true
},
includeLocalRenotes: {
type: "boolean",
default: true
},
withFiles: {
type: "boolean",
default: false,
description: "Only show notes that have attached files."
},
withReplies: {
type: "boolean",
default: false,
description: "Show replies in the timeline"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const m = await fetchMeta();
if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) {
throw new ApiError(meta.errors.stlDisabled);
}
//#region Construct query
const followingQuery = Followings.createQueryBuilder("following").select("following.followeeId").where("following.followerId = :followerId", {
followerId: user.id
});
const query = makePaginationQuery(Notes.createQueryBuilder("note"), 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)");
})).innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser").setParameters(followingQuery.getParameters());
generateListQuery(query, user);
generateChannelQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateBlockedUserQuery(query, user);
generateMutedUserRenotesQueryForNotes(query, user);
generateExcludeMemorietQuery(query);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets((qb)=>{
qb.orWhere("note.userId != :meId", {
meId: user.id
});
qb.orWhere("note.renoteId IS NULL");
qb.orWhere("note.text IS NOT NULL");
qb.orWhere("note.fileIds != '{}'");
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeRenotedMyNotes === false) {
query.andWhere(new Brackets((qb)=>{
qb.orWhere("note.renoteUserId != :meId", {
meId: user.id
});
qb.orWhere("note.renoteId IS NULL");
qb.orWhere("note.text IS NOT NULL");
qb.orWhere("note.fileIds != '{}'");
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeLocalRenotes === false) {
query.andWhere(new Brackets((qb)=>{
qb.orWhere("note.renoteUserHost IS NOT NULL");
qb.orWhere("note.renoteId IS NULL");
qb.orWhere("note.text IS NOT NULL");
qb.orWhere("note.fileIds != '{}'");
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.withFiles) {
query.andWhere("note.fileIds != '{}'");
}
query.andWhere("note.visibility != 'hidden'");
//#endregion
process.nextTick(()=>{
activeUsersChart.read(user);
});
try {
const notes = await query.take(ps.limit).getMany();
await injectPromo(notes, user);
return await Notes.packMany(notes, user);
} catch (error) {
throw new ApiError(meta.errors.queryError);
}
});
@@ -0,0 +1,75 @@
import { Notes } from "../../../../models/index.js";
import { MINUTE } from "../../../../const.js";
import { getIpHash } from "../../../../misc/get-ip-hash.js";
import define from "../../define.js";
import { getNote } from "../../common/getters.js";
import { ApiError } from "../../error.js";
import { limiter } from "../../limiter.js";
const serviceTags = {
video: "videoservice",
audio: "audioservice",
lua4frozen: "lua4frozen",
karaoke: "karaokeservice"
};
export const meta = {
tags: [
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "438ff599-8b8b-45d1-a72d-7e94d3476bd6",
httpStatusCode: 404
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
service: {
type: "string",
enum: [
"video",
"audio",
"lua4frozen",
"karaoke"
]
}
},
required: [
"noteId",
"service"
]
};
export default define(meta, paramDef, async (ps, user, token, file, cleanup, ip)=>{
const note = await getNote(ps.noteId, user, {
allowAdservice: true
}).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
if (!note.tags.includes(serviceTags[ps.service])) {
throw new ApiError(meta.errors.noSuchNote);
}
const actor = user ? user.id : getIpHash(ip ?? "");
const counted = await limiter({
key: `service-view:${ps.service}:${note.id}`,
duration: MINUTE,
max: 1
}, actor).then(()=>true, ()=>false);
if (counted) {
await Notes.increment({
id: note.id
}, "viewCount", 1);
}
return {
counted
};
});
@@ -0,0 +1,122 @@
import { Brackets } from "typeorm";
import { Notes } from "../../../../models/index.js";
import define from "../../define.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
function hasTag(note, tag) {
return note.tags?.some((x)=>x.toLowerCase() === tag.toLowerCase()) ?? false;
}
function searchableText(file) {
return `${file.name} ${file.comment ?? ""}`.toLowerCase();
}
function isMidiFile(file) {
const text = searchableText(file);
const type = file.type.toLowerCase();
return text.endsWith(".mid") || text.endsWith(".midi") || type === "audio/midi" || type === "audio/mid" || type === "audio/x-midi" || type === "audio/x-mid" || type === "application/midi" || type === "application/x-midi";
}
export function classifyKaraokeFile(file) {
const text = searchableText(file);
if (file.type.startsWith("video/")) return "video";
if (isMidiFile(file)) return "pitch";
if (text.includes("score") || text.includes("scoring") || text.includes("採点")) return "scoring";
if (text.includes("pitch") || text.includes("tempo") || text.includes("melody") || text.includes("part") || text.includes("音程") || text.includes("テンポ") || text.includes("パート")) return "pitch";
if (text.includes("lyrics") || text.includes("lyric") || text.endsWith(".lrc") || text.includes("歌詞")) return "lyrics";
if (file.type.startsWith("audio/")) return "accompaniment";
if (file.type.startsWith("text/") && (text.includes("info") || text.includes("metadata") || text.includes("song") || text.includes("楽曲情報"))) return "info";
return null;
}
function isKaraokeServiceNote(note) {
if (![
"public",
"home",
"followers"
].includes(note.visibility)) return false;
if (!hasTag(note, "KaraokeService")) return false;
const counts = {
accompaniment: 0,
lyrics: 0,
pitch: 0,
scoring: 0,
video: 0,
info: 0
};
const fallbackDataFiles = [];
for (const file of note.files ?? []){
const role = classifyKaraokeFile(file);
if (role == null) {
fallbackDataFiles.push(file);
continue;
}
counts[role]++;
}
for (const role of [
"lyrics",
"pitch",
"scoring"
]){
if (counts[role] === 0 && fallbackDataFiles.length > 0) {
fallbackDataFiles.shift();
counts[role]++;
}
}
return counts.accompaniment === 1 && counts.lyrics === 1 && counts.pitch === 1 && counts.scoring === 1 && counts.video <= 1 && counts.info <= 1 && fallbackDataFiles.length === 0;
}
export const meta = {
tags: [
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
}
};
export const paramDef = {
type: "object",
properties: {
query: {
type: "string",
default: ""
},
limit: {
type: "integer",
minimum: 1,
maximum: 50,
default: 20
},
offset: {
type: "integer",
minimum: 0,
default: 0
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const queryText = ps.query.trim();
const query = Notes.createQueryBuilder("note").innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser").where(`'{"karaokeservice"}' <@ note.tags`).andWhere("note.fileIds != '{}'");
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
if (queryText.length > 0) {
query.andWhere(new Brackets((qb)=>{
qb.where("note.text ILIKE :query", {
query: `%${queryText}%`
}).orWhere("user.usernameLower ILIKE :query", {
query: `%${queryText.toLowerCase()}%`
});
}));
}
const notes = await query.orderBy("note.createdAt", "DESC").skip(ps.offset).take(ps.limit * 4).getMany();
const packed = await Notes.packMany(notes, user);
return packed.filter((note)=>isKaraokeServiceNote(note)).slice(0, ps.limit);
});
@@ -0,0 +1,136 @@
import { Brackets } from "typeorm";
import { fetchMeta } from "../../../../misc/fetch-meta.js";
import { Notes } 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 { generateRepliesQuery } from "../../common/generate-replies-query.js";
import { generateChannelQuery } from "../../common/generate-channel-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
import { generateExcludeMemorietQuery } from "../../common/generate-exclude-memoriet-query.js";
import { injectPromo } from "../../common/inject-promo.js";
export const meta = {
tags: [
"notes"
],
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
},
errors: {
ltlDisabled: {
message: "Local timeline has been disabled.",
code: "LTL_DISABLED",
id: "45a6eb02-7695-4393-b023-dd3be9aaaefd"
},
queryError: {
message: "Please follow more users.",
code: "QUERY_ERROR",
id: "620763f4-f621-4533-ab33-0577a1a3c343"
}
}
};
export const paramDef = {
type: "object",
properties: {
withFiles: {
type: "boolean",
default: false,
description: "Only show notes that have attached files."
},
fileType: {
type: "array",
items: {
type: "string"
}
},
excludeNsfw: {
type: "boolean",
default: false
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
},
sinceDate: {
type: "integer"
},
untilDate: {
type: "integer"
},
withReplies: {
type: "boolean",
default: false,
description: "Show replies in the timeline"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const m = await fetchMeta();
if (m.disableLocalTimeline) {
if (user == null || !(user.isAdmin || user.isModerator)) {
throw new ApiError(meta.errors.ltlDisabled);
}
}
//#region Construct query
const query = makePaginationQuery(Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate).andWhere("note.visibility = 'public'").andWhere("note.userHost IS NULL").andWhere(`NOT ('adservice' = ANY(note.tags))`).innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
generateChannelQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
if (user) generateMutedUserRenotesQueryForNotes(query, user);
generateExcludeMemorietQuery(query);
if (ps.withFiles) {
query.andWhere("note.fileIds != '{}'");
}
if (ps.fileType != null) {
query.andWhere("note.fileIds != '{}'");
query.andWhere(new Brackets((qb)=>{
for (const type of ps.fileType){
const i = ps.fileType.indexOf(type);
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, {
[`type${i}`]: type
});
}
}));
if (ps.excludeNsfw) {
query.andWhere("note.cw IS NULL");
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
}
}
//#endregion
process.nextTick(()=>{
if (user) {
activeUsersChart.read(user);
}
});
try {
const notes = await query.take(ps.limit).getMany();
await injectPromo(notes, user);
return await Notes.packMany(notes, user);
} catch (error) {
throw new ApiError(meta.errors.queryError);
}
});
@@ -0,0 +1,66 @@
import { Brackets, In } from "typeorm";
import { Notes } from "../../../../models/index.js";
import define from "../../define.js";
function hasLuaFile(note) {
return note.files?.some((file)=>file.name.toLowerCase().endsWith(".lua")) ?? false;
}
export const meta = {
tags: [
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
}
};
export const paramDef = {
type: "object",
properties: {
query: {
type: "string",
default: ""
},
limit: {
type: "integer",
minimum: 1,
maximum: 50,
default: 20
},
offset: {
type: "integer",
minimum: 0,
default: 0
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const queryText = ps.query.trim();
const query = Notes.createQueryBuilder("note").innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser").where({
visibility: In([
"public",
"home"
])
}).andWhere("'{\"lua4frozen\"}' <@ note.tags").andWhere("note.fileIds != '{}'");
if (queryText.length > 0) {
query.andWhere(new Brackets((qb)=>{
qb.where("note.text ILIKE :query", {
query: `%${queryText}%`
}).orWhere("user.usernameLower ILIKE :query", {
query: `%${queryText.toLowerCase()}%`
});
}));
}
const notes = await query.orderBy("note.createdAt", "DESC").skip(ps.offset).take(ps.limit * 3).getMany();
const packed = await Notes.packMany(notes, user);
return packed.filter((note)=>hasLuaFile(note)).slice(0, ps.limit);
});
@@ -0,0 +1,93 @@
import { Brackets } from "typeorm";
import { Notes } from "../../../../models/index.js";
import define from "../../define.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
function hasTag(note, tag) {
return note.tags?.some((x)=>x.toLowerCase() === tag.toLowerCase()) ?? false;
}
function mediaServiceMatches(note, kind) {
const serviceTag = kind === "video" ? "VideoService" : kind === "audio" ? "AudioService" : "ImageService";
return [
"public",
"home",
"followers"
].includes(note.visibility) && hasTag(note, serviceTag) && hasValidMediaFiles(note.files ?? [], kind);
}
function hasValidMediaFiles(files, kind) {
const mediaFiles = files.filter((file)=>file.type.startsWith(`${kind}/`));
if (kind === "image") {
return mediaFiles.length >= 1 && files.every((file)=>file.type.startsWith("image/"));
}
return mediaFiles.length >= 1 && files.every((file)=>file.type.startsWith(`${kind}/`) || file.type.startsWith("image/"));
}
export const meta = {
tags: [
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
}
};
export const paramDef = {
type: "object",
properties: {
kind: {
type: "string",
enum: [
"video",
"audio",
"image"
]
},
query: {
type: "string",
default: ""
},
limit: {
type: "integer",
minimum: 1,
maximum: 50,
default: 20
},
offset: {
type: "integer",
minimum: 0,
default: 0
}
},
required: [
"kind"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const serviceTag = ps.kind === "video" ? "videoservice" : ps.kind === "audio" ? "audioservice" : "imageservice";
const queryText = ps.query.trim();
const query = Notes.createQueryBuilder("note").innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser").where(`'{"${serviceTag}"}' <@ note.tags`).andWhere("note.fileIds != '{}'");
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
if (queryText.length > 0) {
query.andWhere(new Brackets((qb)=>{
qb.where("note.text ILIKE :query", {
query: `%${queryText}%`
}).orWhere("user.usernameLower ILIKE :query", {
query: `%${queryText.toLowerCase()}%`
});
}));
}
const notes = await query.orderBy("note.createdAt", "DESC").skip(ps.offset).take(ps.limit * 3).getMany();
const packed = await Notes.packMany(notes, user);
return packed.filter((note)=>mediaServiceMatches(note, ps.kind)).slice(0, ps.limit);
});
@@ -0,0 +1,80 @@
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 { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedNoteThreadQuery } from "../../common/generate-muted-note-thread-query.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
}
};
export const paramDef = {
type: "object",
properties: {
following: {
type: "boolean",
default: false
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
},
visibility: {
type: "string"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const followingQuery = Followings.createQueryBuilder("following").select("following.followeeId").where("following.followerId = :followerId", {
followerId: user.id
});
const query = makePaginationQuery(Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId).andWhere(new Brackets((qb)=>{
qb.where(`'{"${user.id}"}' <@ note.mentions`).orWhere(`'{"${user.id}"}' <@ note.visibleUserIds`);
})).innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedNoteThreadQuery(query, user);
generateBlockedUserQuery(query, user);
if (ps.visibility) {
query.andWhere("note.visibility = :visibility", {
visibility: ps.visibility
});
}
if (ps.following) {
query.andWhere(`((note.userId IN (${followingQuery.getQuery()})) OR (note.userId = :meId))`, {
meId: user.id
});
query.setParameters(followingQuery.getParameters());
}
const notes = await query.take(ps.limit).getMany();
const packed = await Notes.packMany(notes, user);
read(user.id, packed);
return packed;
});
@@ -0,0 +1,72 @@
import { Brackets, In } from "typeorm";
import { Polls, Mutings, Notes, PollVotes } from "../../../../../models/index.js";
import define from "../../../define.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
}
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
offset: {
type: "integer",
default: 0
}
},
required: []
};
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(new Brackets((qb)=>{
qb.where("poll.expiresAt IS NULL").orWhere("poll.expiresAt > :now", {
now: new Date()
});
}));
//#region exclude arleady voted polls
const votedQuery = PollVotes.createQueryBuilder("vote").select("vote.noteId").where("vote.userId = :meId", {
meId: user.id
});
query.andWhere(`poll.noteId NOT IN (${votedQuery.getQuery()})`);
query.setParameters(votedQuery.getParameters());
//#endregion
//#region mute
const mutingQuery = Mutings.createQueryBuilder("muting").select("muting.muteeId").where("muting.muterId = :muterId", {
muterId: user.id
});
query.andWhere(`poll.userId NOT IN (${mutingQuery.getQuery()})`);
query.setParameters(mutingQuery.getParameters());
//#endregion
const polls = await query.orderBy("poll.noteId", "DESC").take(ps.limit).skip(ps.offset).getMany();
if (polls.length === 0) return [];
const notes = await Notes.find({
where: {
id: In(polls.map((poll)=>poll.noteId))
},
order: {
createdAt: "DESC"
}
});
return await Notes.packMany(notes, user, {
detail: true
});
});
@@ -0,0 +1,162 @@
import { Not } from "typeorm";
import { publishNoteStream } from "../../../../../services/stream.js";
import { createNotification } from "../../../../../services/create-notification.js";
import { deliver } from "../../../../../queue/index.js";
import { renderActivity } from "../../../../../remote/activitypub/renderer/index.js";
import renderVote from "../../../../../remote/activitypub/renderer/vote.js";
import { PollVotes, NoteWatchings, Users, Polls, Blockings } from "../../../../../models/index.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"
],
requireCredential: true,
kind: "write:votes",
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "ecafbd2e-c283-4d6d-aecb-1a0a33b75396"
},
noPoll: {
message: "The note does not attach a poll.",
code: "NO_POLL",
id: "5f979967-52d9-4314-a911-1c673727f92f"
},
invalidChoice: {
message: "Choice ID is invalid.",
code: "INVALID_CHOICE",
id: "e0cc9a04-f2e8-41e4-a5f1-4127293260cc"
},
alreadyVoted: {
message: "You have already voted.",
code: "ALREADY_VOTED",
id: "0963fc77-efac-419b-9424-b391608dc6d8"
},
alreadyExpired: {
message: "The poll is already expired.",
code: "ALREADY_EXPIRED",
id: "1022a357-b085-4054-9083-8f8de358337e"
},
youHaveBeenBlocked: {
message: "You cannot vote this poll because you have been blocked by this user.",
code: "YOU_HAVE_BEEN_BLOCKED",
id: "85a5377e-b1e9-4617-b0b9-5bea73331e49"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
choice: {
type: "integer"
}
},
required: [
"noteId",
"choice"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const createdAt = new Date();
// Get votee
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
if (!note.hasPoll) {
throw new ApiError(meta.errors.noPoll);
}
// Check blocking
if (note.userId !== user.id) {
const blocked = await Blockings.exist({
where: [
{
blockerId: note.userId,
blockeeId: user.id,
groupId: null
},
...note.groupId ? [
{
groupId: note.groupId,
blockeeId: user.id
}
] : []
]
});
if (blocked) {
throw new ApiError(meta.errors.youHaveBeenBlocked);
}
}
const poll = await Polls.findOneByOrFail({
noteId: note.id
});
if (poll.expiresAt && poll.expiresAt < createdAt) {
throw new ApiError(meta.errors.alreadyExpired);
}
if (poll.choices[ps.choice] == null) {
throw new ApiError(meta.errors.invalidChoice);
}
// if already voted
const exist = await PollVotes.findBy({
noteId: note.id,
userId: user.id
});
if (exist.length) {
if (poll.multiple) {
if (exist.some((x)=>x.choice === ps.choice)) {
throw new ApiError(meta.errors.alreadyVoted);
}
} else {
throw new ApiError(meta.errors.alreadyVoted);
}
}
// Create vote
const vote = await PollVotes.insert({
id: genId(),
createdAt,
noteId: note.id,
userId: user.id,
choice: ps.choice
}).then((x)=>PollVotes.findOneByOrFail(x.identifiers[0]));
// Increment votes count
const index = ps.choice + 1; // In SQL, array index is 1 based
await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
publishNoteStream(note.id, "pollVoted", {
choice: ps.choice,
userId: user.id
});
// Notify
createNotification(note.userId, "pollVote", {
notifierId: user.id,
noteId: note.id,
choice: ps.choice
});
// Fetch watchers
NoteWatchings.findBy({
noteId: note.id,
userId: Not(user.id)
}).then((watchers)=>{
for (const watcher of watchers){
createNotification(watcher.userId, "pollVote", {
notifierId: user.id,
noteId: note.id,
choice: ps.choice
});
}
});
// リモート投票の場合リプライ送信
if (note.userHost != null) {
const pollOwner = await Users.findOneByOrFail({
id: note.userId
});
deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox);
}
});
@@ -0,0 +1,98 @@
import { NoteReactions } from "../../../../models/index.js";
import define from "../../define.js";
import { ApiError } from "../../error.js";
import { getNote } from "../../common/getters.js";
export const meta = {
tags: [
"notes",
"reactions"
],
requireCredential: false,
requireCredentialPrivateMode: true,
allowGet: true,
cacheSec: 60,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "NoteReaction"
}
},
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "263fff3d-d0e1-4af4-bea7-8408059b451a"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
type: {
type: "string",
nullable: true
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
offset: {
type: "integer",
default: 0
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
// check note visibility
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const query = {
noteId: ps.noteId
};
if (ps.type) {
// ローカルリアクションはホスト名が . とされているが
// DB 上ではそうではないので、必要に応じて変換
const suffix = "@.:";
const type = ps.type.endsWith(suffix) ? `${ps.type.slice(0, ps.type.length - suffix.length)}:` : ps.type;
query.reaction = type;
}
const reactions = await NoteReactions.find({
where: query,
take: ps.limit,
skip: ps.offset,
order: {
id: -1
},
relations: [
"user",
"user.avatar",
"user.banner",
"note"
]
});
return await NoteReactions.packMany(reactions, user);
});
@@ -0,0 +1,79 @@
import createReaction from "../../../../../services/note/reaction/create.js";
import define from "../../../define.js";
import { getNote } from "../../../common/getters.js";
import { ApiError } from "../../../error.js";
import { getGroupActor } from "../../../common/get-group-actor.js";
export const meta = {
tags: [
"reactions",
"notes"
],
requireCredential: true,
kind: "write:reactions",
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "033d0620-5bfe-4027-965d-980b0c85a3ea"
},
alreadyReacted: {
message: "You are already reacting to that note.",
code: "ALREADY_REACTED",
id: "71efcf98-86d6-4e2b-b2ad-9d032369366b"
},
youHaveBeenBlocked: {
message: "You cannot react this note because you have been blocked by this user.",
code: "YOU_HAVE_BEEN_BLOCKED",
id: "20ef5475-9f38-4e4c-bd33-de6d979498ec"
},
accountLocked: {
message: "You migrated. Your account is now locked.",
code: "ACCOUNT_LOCKED",
id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3"
},
noSuchGroup: {
message: "No such group.",
code: "NO_SUCH_GROUP",
id: "8ad1dbe0-6498-45ac-9243-5864d80ed5c1"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
reaction: {
type: "string"
},
groupId: {
type: "string",
format: "misskey:id",
nullable: true
}
},
required: [
"noteId",
"reaction"
]
};
export default define(meta, paramDef, async (ps, user)=>{
if (user.movedToUri != null) throw new ApiError(meta.errors.accountLocked);
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const group = await getGroupActor(ps.groupId, user);
if (ps.groupId != null && group == null) throw new ApiError(meta.errors.noSuchGroup);
await createReaction({
...user,
groupId: group?.id ?? null
}, note, ps.reaction).catch((e)=>{
if (e.id === "51c42bb4-931a-456b-bff7-e5a8a70dd298") throw new ApiError(meta.errors.alreadyReacted);
if (e.id === "e70412a4-7197-4726-8e74-f3e0deb92aa7") throw new ApiError(meta.errors.youHaveBeenBlocked);
throw e;
});
return;
});
@@ -0,0 +1,68 @@
import deleteReaction from "../../../../../services/note/reaction/delete.js";
import define from "../../../define.js";
import { getNote } from "../../../common/getters.js";
import { ApiError } from "../../../error.js";
import { SECOND, HOUR } from "../../../../../const.js";
import { getGroupActor } from "../../../common/get-group-actor.js";
export const meta = {
tags: [
"reactions",
"notes"
],
requireCredential: true,
kind: "write:reactions",
limit: {
duration: HOUR,
max: 60,
minInterval: 3 * SECOND
},
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "764d9fce-f9f2-4a0e-92b1-6ceac9a7ad37"
},
notReacted: {
message: "You are not reacting to that note.",
code: "NOT_REACTED",
id: "92f4426d-4196-4125-aa5b-02943e2ec8fc"
},
noSuchGroup: {
message: "No such group.",
code: "NO_SUCH_GROUP",
id: "04a4f3d8-773e-43e4-a645-71da6bb03a6f"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
groupId: {
type: "string",
format: "misskey:id",
nullable: true
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const group = await getGroupActor(ps.groupId, user);
if (ps.groupId != null && group == null) throw new ApiError(meta.errors.noSuchGroup);
await deleteReaction({
...user,
groupId: group?.id ?? null
}, note).catch((e)=>{
if (e.id === "60527ec9-b4cb-4a88-a6bd-32d3ad26817d") throw new ApiError(meta.errors.notReacted);
throw e;
});
});
@@ -0,0 +1,138 @@
import { Brackets } from "typeorm";
import { fetchMeta } from "../../../../misc/fetch-meta.js";
import { Notes } 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 { generateRepliesQuery } from "../../common/generate-replies-query.js";
import { generateChannelQuery } from "../../common/generate-channel-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
import { generateExcludeMemorietQuery } from "../../common/generate-exclude-memoriet-query.js";
import { injectPromo } from "../../common/inject-promo.js";
export const meta = {
tags: [
"notes"
],
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
},
errors: {
rtlDisabled: {
message: "Recommended timeline has been disabled.",
code: "RTL_DISABLED",
id: "45a6eb02-7695-4393-b023-dd3be9aaaefe"
},
queryError: {
message: "Please follow more users.",
code: "QUERY_ERROR",
id: "620763f4-f621-4533-ab33-0577a1a3c343"
}
}
};
export const paramDef = {
type: "object",
properties: {
withFiles: {
type: "boolean",
default: false,
description: "Only show notes that have attached files."
},
fileType: {
type: "array",
items: {
type: "string"
}
},
excludeNsfw: {
type: "boolean",
default: false
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
},
sinceDate: {
type: "integer"
},
untilDate: {
type: "integer"
},
withReplies: {
type: "boolean",
default: false,
description: "Show replies in the timeline"
}
},
required: []
};
export default define(meta, paramDef, async (ps, user)=>{
const m = await fetchMeta();
if (m.disableRecommendedTimeline) {
if (user == null || !(user.isAdmin || user.isModerator)) {
throw new ApiError(meta.errors.rtlDisabled);
}
}
//#region Construct query
const query = makePaginationQuery(Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate).andWhere(`note.userHost IN (:...instances)`, {
instances: m.recommendedInstances
}).andWhere("note.visibility = 'public'").andWhere(`NOT ('adservice' = ANY(note.tags))`).innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
generateChannelQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
if (user) generateMutedUserRenotesQueryForNotes(query, user);
generateExcludeMemorietQuery(query);
if (ps.withFiles) {
query.andWhere("note.fileIds != '{}'");
}
if (ps.fileType != null) {
query.andWhere("note.fileIds != '{}'");
query.andWhere(new Brackets((qb)=>{
for (const type of ps.fileType){
const i = ps.fileType.indexOf(type);
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, {
[`type${i}`]: type
});
}
}));
if (ps.excludeNsfw) {
query.andWhere("note.cw IS NULL");
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
}
}
//#endregion
process.nextTick(()=>{
if (user) {
activeUsersChart.read(user);
}
});
try {
const notes = await query.take(ps.limit).getMany();
await injectPromo(notes, user);
return await Notes.packMany(notes, user);
} catch (error) {
throw new ApiError(meta.errors.queryError);
}
});
@@ -0,0 +1,83 @@
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 { generateBlockedUserQuery } from "../../common/generate-block-query.js";
export const meta = {
tags: [
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
},
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "12908022-2e21-46cd-ba6a-3edaf6093f46"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
userId: {
type: "string",
format: "misskey:id"
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
let query = makePaginationQuery(Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId).andWhere("note.renoteId = :renoteId", {
renoteId: note.id
}).innerJoinAndSelect("note.user", "user");
if (ps.userId) {
query.andWhere("user.id = :userId", {
userId: ps.userId
});
}
query.leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
const notes = await query.take(ps.limit).getMany();
return await Notes.packMany(notes, user);
});
@@ -0,0 +1,60 @@
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 { generateBlockedUserQuery } from "../../common/generate-block-query.js";
export const meta = {
tags: [
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const query = makePaginationQuery(Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId).andWhere("note.replyId = :replyId", {
replyId: ps.noteId
}).innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
const notes = await query.take(ps.limit).getMany();
return await Notes.packMany(notes, user);
});
@@ -0,0 +1,154 @@
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";
import { injectPromo } from "../../common/inject-promo.js";
export const meta = {
tags: [
"notes",
"hashtags"
],
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
}
};
export const paramDef = {
type: "object",
properties: {
reply: {
type: "boolean",
nullable: true,
default: null
},
renote: {
type: "boolean",
nullable: true,
default: null
},
withFiles: {
type: "boolean",
default: false,
description: "Only show notes that have attached files."
},
poll: {
type: "boolean",
nullable: true,
default: null
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
}
},
anyOf: [
{
properties: {
tag: {
type: "string",
minLength: 1
}
},
required: [
"tag"
]
},
{
properties: {
query: {
type: "array",
description: "The outer arrays are chained with OR, the inner arrays are chained with AND.",
items: {
type: "array",
items: {
type: "string",
minLength: 1
},
minItems: 1
},
minItems: 1
}
},
required: [
"query"
]
}
]
};
export default define(meta, paramDef, async (ps, me)=>{
const query = makePaginationQuery(Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId).innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
generateVisibilityQuery(query, me);
if (me) generateMutedUserQuery(query, me);
if (me) generateBlockedUserQuery(query, me);
try {
if (ps.tag) {
if (!safeForSql(normalizeForSearch(ps.tag))) throw "Injection";
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
} else {
query.andWhere(new Brackets((qb)=>{
for (const tags of ps.query){
qb.orWhere(new Brackets((qb)=>{
for (const tag of tags){
if (!safeForSql(normalizeForSearch(tag))) throw "Injection";
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
}
}));
}
}));
}
} catch (e) {
if (e.message === "Injection") return [];
throw e;
}
if (ps.reply != null) {
if (ps.reply) {
query.andWhere("note.replyId IS NOT NULL");
} else {
query.andWhere("note.replyId IS NULL");
}
}
if (ps.renote != null) {
if (ps.renote) {
query.andWhere("note.renoteId IS NOT NULL");
} else {
query.andWhere("note.renoteId IS NULL");
}
}
if (ps.withFiles) {
query.andWhere("note.fileIds != '{}'");
}
if (ps.poll != null) {
if (ps.poll) {
query.andWhere("note.hasPoll = TRUE");
} else {
query.andWhere("note.hasPoll = FALSE");
}
}
const notes = await query.take(ps.limit).getMany();
const preferredTag = ps.tag ?? ps.query?.flat()[0] ?? null;
await injectPromo(notes, me, preferredTag);
return await Notes.packMany(notes, me);
});
@@ -0,0 +1,91 @@
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 { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateFtsQuery } from "../../common/generate-fts-query.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
},
errors: {}
};
export const paramDef = {
type: "object",
properties: {
query: {
type: "string"
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
offset: {
type: "integer",
default: 0
},
host: {
type: "string",
nullable: true,
description: "The local host is represented with `null`."
},
userId: {
type: "string",
format: "misskey:id",
nullable: true,
default: null
},
channelId: {
type: "string",
format: "misskey:id",
nullable: true,
default: null
}
},
required: [
"query"
]
};
export default define(meta, paramDef, async (ps, me)=>{
const query = makePaginationQuery(Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId);
if (ps.userId) {
query.andWhere("note.userId = :userId", {
userId: ps.userId
});
} else if (ps.channelId) {
query.andWhere("note.channelId = :channelId", {
channelId: ps.channelId
});
}
query.innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
generateFtsQuery(query, ps.query);
generateVisibilityQuery(query, me);
generateMutedUserQuery(query, me);
generateBlockedUserQuery(query, me);
query.setParameter("meId", me.id);
return await query.take(ps.limit).getMany().then((notes)=>Notes.packMany(notes, me));
});
@@ -0,0 +1,65 @@
import { Notes } from "../../../../models/index.js";
import define from "../../define.js";
import { getNote } from "../../common/getters.js";
import { ApiError } from "../../error.js";
export const meta = {
tags: [
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
res: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
},
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "24fcbfc6-2e37-42b6-8388-c29b3861a08d",
httpStatusCode: 404
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user, {
allowAdservice: true
}).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const serviceTags = [
"videoservice",
"audioservice",
"lua4frozen",
"karaokeservice"
];
if (!serviceTags.some((tag)=>note.tags.includes(tag))) {
await Notes.increment({
id: note.id
}, "viewCount", 1);
note.viewCount += 1;
}
return await Notes.pack(note, user, {
// FIXME: packing with detail may throw an error if the reply or renote is not visible (#8774)
detail: true,
allowAdservice: true
}).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
});
@@ -0,0 +1,74 @@
import { NoteFavorites, NoteThreadMutings, NoteWatchings } from "../../../../models/index.js";
import { getNote } from "../../common/getters.js";
import define from "../../define.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
res: {
type: "object",
optional: false,
nullable: false,
properties: {
isFavorited: {
type: "boolean",
optional: false,
nullable: false
},
isWatching: {
type: "boolean",
optional: false,
nullable: false
},
isMutedThread: {
type: "boolean",
optional: false,
nullable: false
}
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user);
const [favorite, watching, threadMuting] = await Promise.all([
NoteFavorites.count({
where: {
userId: user.id,
noteId: note.id
},
take: 1
}),
NoteWatchings.count({
where: {
userId: user.id,
noteId: note.id
},
take: 1
}),
NoteThreadMutings.count({
where: {
userId: user.id,
threadId: note.threadId || note.id
},
take: 1
})
]);
return {
isFavorited: favorite !== 0,
isWatching: watching !== 0,
isMutedThread: threadMuting !== 0
};
});
@@ -0,0 +1,55 @@
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"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "5ff67ada-ed3b-2e71-8e87-a1a421e177d2"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const mutedNotes = await Notes.find({
where: [
{
id: note.threadId || note.id
},
{
threadId: note.threadId || note.id
}
]
});
await readNote(user.id, mutedNotes);
await NoteThreadMutings.insert({
id: genId(),
createdAt: new Date(),
threadId: note.threadId || note.id,
userId: user.id
});
});
@@ -0,0 +1,40 @@
import { NoteThreadMutings } from "../../../../../models/index.js";
import define from "../../../define.js";
import { getNote } from "../../../common/getters.js";
import { ApiError } from "../../../error.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "bddd57ac-ceb3-b29d-4334-86ea5fae481a"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
await NoteThreadMutings.delete({
threadId: note.threadId || note.id,
userId: user.id
});
});
@@ -0,0 +1,147 @@
import { Brackets } from "typeorm";
import { Notes } 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 { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { generateRepliesQuery } from "../../common/generate-replies-query.js";
import { generateChannelQuery } from "../../common/generate-channel-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import { generateMutedUserRenotesQueryForNotes } from "../../common/generated-muted-renote-query.js";
import { ApiError } from "../../error.js";
import { generateListQuery } from "../../common/generate-list-query.js";
import { generateFollowingQuery } from "../../common/generate-following-query.js";
import { generateExcludeMemorietQuery } from "../../common/generate-exclude-memoriet-query.js";
import { injectPromo } from "../../common/inject-promo.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
},
errors: {
queryError: {
message: "Please follow more users.",
code: "QUERY_ERROR",
id: "620763f4-f621-4533-ab33-0577a1a3c343"
}
}
};
export const paramDef = {
type: "object",
properties: {
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
},
sinceDate: {
type: "integer"
},
untilDate: {
type: "integer"
},
includeMyRenotes: {
type: "boolean",
default: true
},
includeRenotedMyNotes: {
type: "boolean",
default: true
},
includeLocalRenotes: {
type: "boolean",
default: true
},
withFiles: {
type: "boolean",
default: false,
description: "Only show notes that have attached files."
},
withReplies: {
type: "boolean",
default: false,
description: "Show replies in the timeline"
}
},
required: []
};
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).innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser");
await generateFollowingQuery(query, user);
generateListQuery(query, user);
generateChannelQuery(query, user);
generateRepliesQuery(query, ps.withReplies, user);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateBlockedUserQuery(query, user);
generateMutedUserRenotesQueryForNotes(query, user);
generateExcludeMemorietQuery(query);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets((qb)=>{
qb.orWhere("note.userId != :meId", {
meId: user.id
});
qb.orWhere("note.renoteId IS NULL");
qb.orWhere("note.text IS NOT NULL");
qb.orWhere("note.fileIds != '{}'");
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeRenotedMyNotes === false) {
query.andWhere(new Brackets((qb)=>{
qb.orWhere("note.renoteUserId != :meId", {
meId: user.id
});
qb.orWhere("note.renoteId IS NULL");
qb.orWhere("note.text IS NOT NULL");
qb.orWhere("note.fileIds != '{}'");
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeLocalRenotes === false) {
query.andWhere(new Brackets((qb)=>{
qb.orWhere("note.renoteUserHost IS NOT NULL");
qb.orWhere("note.renoteId IS NULL");
qb.orWhere("note.text IS NOT NULL");
qb.orWhere("note.fileIds != '{}'");
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.withFiles) {
query.andWhere("note.fileIds != '{}'");
}
query.andWhere("note.visibility != 'hidden'");
//#endregion
process.nextTick(()=>{
activeUsersChart.read(user);
});
try {
const notes = await query.take(ps.limit).getMany();
await injectPromo(notes, user);
return await Notes.packMany(notes, user);
} catch (error) {
throw new ApiError(meta.errors.queryError);
}
});
@@ -0,0 +1,111 @@
import { URLSearchParams } from "node:url";
import fetch from "node-fetch";
import config from "../../../../config/index.js";
import { getAgentByUrl } from "../../../../misc/fetch.js";
import { fetchMeta } from "../../../../misc/fetch-meta.js";
import { ApiError } from "../../error.js";
import { getNote } from "../../common/getters.js";
import define from "../../define.js";
export const meta = {
tags: [
"notes"
],
requireCredential: false,
requireCredentialPrivateMode: true,
res: {
type: "object",
optional: false,
nullable: false
},
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "bea9b03f-36e0-49c5-a4db-627a029f8971"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
targetLang: {
type: "string"
}
},
required: [
"noteId",
"targetLang"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
if (note.text == null) {
return 204;
}
const instance = await fetchMeta();
if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
return 204; // TODO: 良い感じのエラー返す
}
let targetLang = ps.targetLang;
if (targetLang.includes("-")) targetLang = targetLang.split("-")[0];
if (instance.libreTranslateApiUrl != null) {
const jsonBody = {
q: note.text,
source: "auto",
target: targetLang,
format: "text",
api_key: instance.libreTranslateApiKey ?? ""
};
const url = new URL(instance.libreTranslateApiUrl);
if (url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1);
}
if (!url.pathname.endsWith("/translate")) {
url.pathname += "/translate";
}
const res = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(jsonBody),
agent: getAgentByUrl,
size: 10 * 1024 * 1024
});
const json = await res.json();
return {
sourceLang: json.detectedLanguage?.language,
text: json.translatedText
};
}
const params = new URLSearchParams();
params.append("auth_key", instance.deeplAuthKey ?? "");
params.append("text", note.text);
params.append("target_lang", targetLang);
const endpoint = instance.deeplIsPro ? "https://api.deepl.com/v2/translate" : "https://api-free.deepl.com/v2/translate";
const res = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": config.userAgent,
Accept: "application/json, */*"
},
body: params,
size: 10 * 1024 * 1024,
// TODO
//timeout: 10000,
agent: getAgentByUrl
});
const json = await res.json();
return {
sourceLang: json.translations[0].detected_source_language,
text: json.translations[0].text
};
});
@@ -0,0 +1,66 @@
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 { SECOND, HOUR } from "../../../../const.js";
import { getGroupActor } from "../../common/get-group-actor.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
kind: "write:notes",
limit: {
duration: HOUR,
max: 300,
minInterval: SECOND
},
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "efd4a259-2442-496b-8dd7-b255aa1a160f"
},
noSuchGroup: {
message: "No such group.",
code: "NO_SUCH_GROUP",
id: "1e4d6ad6-bbc5-478a-9f4b-8a0f578c1e72"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
},
groupId: {
type: "string",
format: "misskey:id",
nullable: true
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const group = await getGroupActor(ps.groupId, user);
if (ps.groupId != null && group == null) throw new ApiError(meta.errors.noSuchGroup);
const renotes = await Notes.findBy({
userId: user.id,
renoteId: note.id,
groupId: group?.id ?? null
});
for (const note of renotes){
deleteNote(await Users.findOneByOrFail({
id: user.id
}), note);
}
});
@@ -0,0 +1,148 @@
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 { makePaginationQuery } from "../../common/make-pagination-query.js";
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
import { generateExcludeMemorietQuery } from "../../common/generate-exclude-memoriet-query.js";
export const meta = {
tags: [
"notes",
"lists"
],
requireCredential: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note"
}
},
errors: {
noSuchList: {
message: "No such list.",
code: "NO_SUCH_LIST",
id: "8fb1fbd5-e476-4c37-9fb0-43d55b63a2ff"
},
queryError: {
message: "Please follow more users.",
code: "QUERY_ERROR",
id: "620763f4-f621-4533-ab33-0577a1a3c343"
}
}
};
export const paramDef = {
type: "object",
properties: {
listId: {
type: "string",
format: "misskey:id"
},
limit: {
type: "integer",
minimum: 1,
maximum: 100,
default: 10
},
sinceId: {
type: "string",
format: "misskey:id"
},
untilId: {
type: "string",
format: "misskey:id"
},
sinceDate: {
type: "integer"
},
untilDate: {
type: "integer"
},
includeMyRenotes: {
type: "boolean",
default: true
},
includeRenotedMyNotes: {
type: "boolean",
default: true
},
includeLocalRenotes: {
type: "boolean",
default: true
},
withFiles: {
type: "boolean",
default: false,
description: "Only show notes that have attached files."
}
},
required: [
"listId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const list = await UserLists.findOneBy({
id: ps.listId,
userId: user.id
});
if (list == null) {
throw new ApiError(meta.errors.noSuchList);
}
//#region Construct query
const query = makePaginationQuery(Notes.createQueryBuilder("note"), ps.sinceId, ps.untilId).innerJoin(UserListJoinings.metadata.targetName, "userListJoining", "userListJoining.userId = note.userId").innerJoinAndSelect("note.user", "user").leftJoinAndSelect("note.reply", "reply").leftJoinAndSelect("note.renote", "renote").leftJoinAndSelect("reply.user", "replyUser").leftJoinAndSelect("renote.user", "renoteUser").andWhere("userListJoining.userListId = :userListId", {
userListId: list.id
});
generateVisibilityQuery(query, user);
generateExcludeMemorietQuery(query);
if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets((qb)=>{
qb.orWhere("note.userId != :meId", {
meId: user.id
});
qb.orWhere("note.renoteId IS NULL");
qb.orWhere("note.text IS NOT NULL");
qb.orWhere("note.fileIds != '{}'");
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeRenotedMyNotes === false) {
query.andWhere(new Brackets((qb)=>{
qb.orWhere("note.renoteUserId != :meId", {
meId: user.id
});
qb.orWhere("note.renoteId IS NULL");
qb.orWhere("note.text IS NOT NULL");
qb.orWhere("note.fileIds != '{}'");
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.includeLocalRenotes === false) {
query.andWhere(new Brackets((qb)=>{
qb.orWhere("note.renoteUserHost IS NOT NULL");
qb.orWhere("note.renoteId IS NULL");
qb.orWhere("note.text IS NOT NULL");
qb.orWhere("note.fileIds != '{}'");
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
}));
}
if (ps.withFiles) {
query.andWhere("note.fileIds != '{}'");
}
//#endregion
process.nextTick(()=>{
if (user) {
activeUsersChart.read(user);
}
});
try {
const notes = await query.take(ps.limit).getMany();
return await Notes.packMany(notes, user);
} catch (error) {
throw new ApiError(meta.errors.queryError);
}
});
@@ -0,0 +1,37 @@
import watch from "../../../../../services/note/watch.js";
import define from "../../../define.js";
import { getNote } from "../../../common/getters.js";
import { ApiError } from "../../../error.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "ea0e37a6-90a3-4f58-ba6b-c328ca206fc7"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
await watch(user.id, note);
});
@@ -0,0 +1,37 @@
import unwatch from "../../../../../services/note/unwatch.js";
import define from "../../../define.js";
import { getNote } from "../../../common/getters.js";
import { ApiError } from "../../../error.js";
export const meta = {
tags: [
"notes"
],
requireCredential: true,
kind: "write:account",
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "09b3695c-f72c-4731-a428-7cff825fc82e"
}
}
};
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id"
}
},
required: [
"noteId"
]
};
export default define(meta, paramDef, async (ps, user)=>{
const note = await getNote(ps.noteId, user).catch((err)=>{
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") throw new ApiError(meta.errors.noSuchNote);
throw err;
});
await unwatch(user.id, note);
});