iceshrimp/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts

145 lines
3.5 KiB
TypeScript

import { Brackets } from "typeorm";
import { Notes } from "@/models/index.js";
import { safeForSql } from "@/misc/safe-for-sql.js";
import { normalizeForSearch } from "@/misc/normalize-for-search.js";
import define from "../../define.js";
import { makePaginationQuery } from "../../common/make-pagination-query.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
export const meta = {
tags: ["notes", "hashtags"],
requireCredentialPrivateMode: true,
res: {
type: "array",
optional: false,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "Note",
},
},
} as const;
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"],
},
],
} as const;
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(ps.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();
return await Notes.packMany(notes, me);
});