Fixed 267U.pre2
This commit is contained in:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user