72
packages/backend/src/server/api/endpoints/notes/children.ts
Normal file
72
packages/backend/src/server/api/endpoints/notes/children.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Notes } from '@/models/index';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where(`note.replyId = :noteId`, { noteId: ps.noteId })
|
||||
.orWhere(new Brackets(qb => { qb
|
||||
.where(`note.renoteId = :noteId`, { noteId: ps.noteId })
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where(`note.text IS NOT NULL`)
|
||||
.orWhere(`note.fileIds != '{}'`)
|
||||
.orWhere(`note.hasPoll = TRUE`);
|
||||
}));
|
||||
}));
|
||||
}))
|
||||
.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);
|
||||
});
|
55
packages/backend/src/server/api/endpoints/notes/clips.ts
Normal file
55
packages/backend/src/server/api/endpoints/notes/clips.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ClipNotes, Clips } from '@/models/index';
|
||||
import { getNote } from '../../common/getters';
|
||||
import { ApiError } from '../../error';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
tags: ['clips', 'notes'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: '47db1a1c-b0af-458d-8fb4-986e4efafe1e'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
const clipNotes = await ClipNotes.find({
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
const clips = await Clips.find({
|
||||
id: In(clipNotes.map(x => x.clipId)),
|
||||
isPublic: true
|
||||
});
|
||||
|
||||
return await Promise.all(clips.map(x => Clips.pack(x)));
|
||||
});
|
@ -0,0 +1,81 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { getNote } from '../../common/getters';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { Notes } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
offset: {
|
||||
validator: $.optional.num.min(0),
|
||||
default: 0
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: 'e1035875-9551-45ec-afa8-1ded1fcb53c8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
const conversation: Note[] = [];
|
||||
let i = 0;
|
||||
|
||||
async function get(id: any) {
|
||||
i++;
|
||||
const p = await Notes.findOne(id);
|
||||
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);
|
||||
});
|
299
packages/backend/src/server/api/endpoints/notes/create.ts
Normal file
299
packages/backend/src/server/api/endpoints/notes/create.ts
Normal file
@ -0,0 +1,299 @@
|
||||
import $ from 'cafy';
|
||||
import * as ms from 'ms';
|
||||
import { length } from 'stringz';
|
||||
import create from '@/services/note/create';
|
||||
import define from '../../define';
|
||||
import { fetchMeta } from '@/misc/fetch-meta';
|
||||
import { ApiError } from '../../error';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index';
|
||||
import { DriveFile } from '@/models/entities/drive-file';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits';
|
||||
import { noteVisibilities } from '../../../../types';
|
||||
import { Channel } from '@/models/entities/channel';
|
||||
|
||||
let maxNoteTextLength = 500;
|
||||
|
||||
setInterval(() => {
|
||||
fetchMeta().then(m => {
|
||||
maxNoteTextLength = m.maxNoteTextLength;
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 300
|
||||
},
|
||||
|
||||
kind: 'write:notes',
|
||||
|
||||
params: {
|
||||
visibility: {
|
||||
validator: $.optional.str.or(noteVisibilities as unknown as string[]),
|
||||
default: 'public',
|
||||
},
|
||||
|
||||
visibleUserIds: {
|
||||
validator: $.optional.arr($.type(ID)).unique().min(0),
|
||||
},
|
||||
|
||||
text: {
|
||||
validator: $.optional.nullable.str.pipe(text =>
|
||||
text.trim() != ''
|
||||
&& length(text.trim()) <= maxNoteTextLength
|
||||
&& Array.from(text.trim()).length <= DB_MAX_NOTE_TEXT_LENGTH // DB limit
|
||||
),
|
||||
default: null,
|
||||
},
|
||||
|
||||
cw: {
|
||||
validator: $.optional.nullable.str.pipe(Notes.validateCw),
|
||||
},
|
||||
|
||||
viaMobile: {
|
||||
validator: $.optional.bool,
|
||||
default: false,
|
||||
},
|
||||
|
||||
localOnly: {
|
||||
validator: $.optional.bool,
|
||||
default: false,
|
||||
},
|
||||
|
||||
noExtractMentions: {
|
||||
validator: $.optional.bool,
|
||||
default: false,
|
||||
},
|
||||
|
||||
noExtractHashtags: {
|
||||
validator: $.optional.bool,
|
||||
default: false,
|
||||
},
|
||||
|
||||
noExtractEmojis: {
|
||||
validator: $.optional.bool,
|
||||
default: false,
|
||||
},
|
||||
|
||||
fileIds: {
|
||||
validator: $.optional.arr($.type(ID)).unique().range(1, 4),
|
||||
},
|
||||
|
||||
mediaIds: {
|
||||
validator: $.optional.arr($.type(ID)).unique().range(1, 4),
|
||||
deprecated: true,
|
||||
},
|
||||
|
||||
replyId: {
|
||||
validator: $.optional.nullable.type(ID),
|
||||
},
|
||||
|
||||
renoteId: {
|
||||
validator: $.optional.nullable.type(ID),
|
||||
},
|
||||
|
||||
channelId: {
|
||||
validator: $.optional.nullable.type(ID),
|
||||
},
|
||||
|
||||
poll: {
|
||||
validator: $.optional.nullable.obj({
|
||||
choices: $.arr($.str)
|
||||
.unique()
|
||||
.range(2, 10)
|
||||
.each(c => c.length > 0 && c.length < 50),
|
||||
multiple: $.optional.bool,
|
||||
expiresAt: $.optional.nullable.num.int(),
|
||||
expiredAfter: $.optional.nullable.num.int().min(1)
|
||||
}).strict(),
|
||||
ref: 'poll'
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
createdNote: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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'
|
||||
},
|
||||
|
||||
contentRequired: {
|
||||
message: 'Content required. You need to set text, fileIds, renoteId or poll.',
|
||||
code: 'CONTENT_REQUIRED',
|
||||
id: '6f57e42b-c348-439b-bc45-993995cc515a'
|
||||
},
|
||||
|
||||
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'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
let visibleUsers: User[] = [];
|
||||
if (ps.visibleUserIds) {
|
||||
visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id))))
|
||||
.filter(x => x != null) as User[];
|
||||
}
|
||||
|
||||
let files: DriveFile[] = [];
|
||||
const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
|
||||
if (fileIds != null) {
|
||||
files = (await Promise.all(fileIds.map(fileId =>
|
||||
DriveFiles.findOne({
|
||||
id: fileId,
|
||||
userId: user.id
|
||||
})
|
||||
))).filter(file => file != null) as DriveFile[];
|
||||
}
|
||||
|
||||
let renote: Note | undefined;
|
||||
if (ps.renoteId != null) {
|
||||
// Fetch renote to note
|
||||
renote = await Notes.findOne(ps.renoteId);
|
||||
|
||||
if (renote == null) {
|
||||
throw new ApiError(meta.errors.noSuchRenoteTarget);
|
||||
} else if (renote.renoteId && !renote.text && !renote.fileIds) {
|
||||
throw new ApiError(meta.errors.cannotReRenote);
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
if (renote.userId !== user.id) {
|
||||
const block = await Blockings.findOne({
|
||||
blockerId: renote.userId,
|
||||
blockeeId: user.id,
|
||||
});
|
||||
if (block) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reply: Note | undefined;
|
||||
if (ps.replyId != null) {
|
||||
// Fetch reply
|
||||
reply = await Notes.findOne(ps.replyId);
|
||||
|
||||
if (reply == null) {
|
||||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||
}
|
||||
|
||||
// 返信対象が引用でないRenoteだったらエラー
|
||||
if (reply.renoteId && !reply.text && !reply.fileIds) {
|
||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
if (reply.userId !== user.id) {
|
||||
const block = await Blockings.findOne({
|
||||
blockerId: reply.userId,
|
||||
blockeeId: user.id,
|
||||
});
|
||||
if (block) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.poll) {
|
||||
if (typeof ps.poll.expiresAt === 'number') {
|
||||
if (ps.poll.expiresAt < Date.now())
|
||||
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
|
||||
} else if (typeof ps.poll.expiredAfter === 'number') {
|
||||
ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter;
|
||||
}
|
||||
}
|
||||
|
||||
// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
|
||||
if (!(ps.text || files.length || renote || ps.poll)) {
|
||||
throw new ApiError(meta.errors.contentRequired);
|
||||
}
|
||||
|
||||
let channel: Channel | undefined;
|
||||
if (ps.channelId != null) {
|
||||
channel = await Channels.findOne(ps.channelId);
|
||||
|
||||
if (channel == null) {
|
||||
throw new ApiError(meta.errors.noSuchChannel);
|
||||
}
|
||||
}
|
||||
|
||||
// 投稿を作成
|
||||
const note = await create(user, {
|
||||
createdAt: new Date(),
|
||||
files: files,
|
||||
poll: ps.poll ? {
|
||||
choices: ps.poll.choices,
|
||||
multiple: ps.poll.multiple || false,
|
||||
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null
|
||||
} : undefined,
|
||||
text: ps.text || undefined,
|
||||
reply,
|
||||
renote,
|
||||
cw: ps.cw,
|
||||
viaMobile: ps.viaMobile,
|
||||
localOnly: ps.localOnly,
|
||||
visibility: ps.visibility,
|
||||
visibleUsers,
|
||||
channel,
|
||||
apMentions: ps.noExtractMentions ? [] : undefined,
|
||||
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
||||
apEmojis: ps.noExtractEmojis ? [] : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
createdNote: await Notes.pack(note, user)
|
||||
};
|
||||
});
|
56
packages/backend/src/server/api/endpoints/notes/delete.ts
Normal file
56
packages/backend/src/server/api/endpoints/notes/delete.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import deleteNote from '@/services/note/delete';
|
||||
import define from '../../define';
|
||||
import * as ms from 'ms';
|
||||
import { getNote } from '../../common/getters';
|
||||
import { ApiError } from '../../error';
|
||||
import { Users } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:notes',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 300,
|
||||
minInterval: ms('1sec')
|
||||
},
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!user.isAdmin && !user.isModerator && (note.userId !== user.id)) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
// この操作を行うのが投稿者とは限らない(例えばモデレーター)ため
|
||||
await deleteNote(await Users.findOneOrFail(note.userId), note);
|
||||
});
|
@ -0,0 +1,61 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { getNote } from '../../../common/getters';
|
||||
import { NoteFavorites } from '@/models/index';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'favorites'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:favorites',
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
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'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
// Get favoritee
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
// if already favorited
|
||||
const exist = await NoteFavorites.findOne({
|
||||
noteId: note.id,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
throw new ApiError(meta.errors.alreadyFavorited);
|
||||
}
|
||||
|
||||
// Create favorite
|
||||
await NoteFavorites.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
noteId: note.id,
|
||||
userId: user.id
|
||||
});
|
||||
});
|
@ -0,0 +1,55 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { getNote } from '../../../common/getters';
|
||||
import { NoteFavorites } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'favorites'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:favorites',
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
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'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
// Get favoritee
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
// if already favorited
|
||||
const exist = await NoteFavorites.findOne({
|
||||
noteId: note.id,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
throw new ApiError(meta.errors.notFavorited);
|
||||
}
|
||||
|
||||
// Delete favorite
|
||||
await NoteFavorites.delete(exist.id);
|
||||
});
|
64
packages/backend/src/server/api/endpoints/notes/featured.ts
Normal file
64
packages/backend/src/server/api/endpoints/notes/featured.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { Notes } from '@/models/index';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10,
|
||||
},
|
||||
|
||||
offset: {
|
||||
validator: $.optional.num.min(0),
|
||||
default: 0
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const max = 30;
|
||||
const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
|
||||
|
||||
const query = Notes.createQueryBuilder('note')
|
||||
.addSelect('note.score')
|
||||
.where('note.userHost IS NULL')
|
||||
.andWhere(`note.score > 0`)
|
||||
.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
|
||||
.andWhere(`note.visibility = 'public'`)
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
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,101 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { fetchMeta } from '@/misc/fetch-meta';
|
||||
import { ApiError } from '../../error';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { Notes } from '@/models/index';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { activeUsersChart } from '@/services/chart/index';
|
||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
params: {
|
||||
withFiles: {
|
||||
validator: $.optional.bool,
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
sinceDate: {
|
||||
validator: $.optional.num
|
||||
},
|
||||
|
||||
untilDate: {
|
||||
validator: $.optional.num
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
gtlDisabled: {
|
||||
message: 'Global timeline has been disabled.',
|
||||
code: 'GTL_DISABLED',
|
||||
id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6b'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, 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')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
generateRepliesQuery(query, user);
|
||||
if (user) generateMutedUserQuery(query, user);
|
||||
if (user) generateMutedNoteQuery(query, user);
|
||||
if (user) generateBlockedUserQuery(query, user);
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.take(ps.limit!).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (user) {
|
||||
activeUsersChart.update(user);
|
||||
}
|
||||
});
|
||||
|
||||
return await Notes.packMany(timeline, user);
|
||||
});
|
@ -0,0 +1,158 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { fetchMeta } from '@/misc/fetch-meta';
|
||||
import { ApiError } from '../../error';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { Followings, Notes } from '@/models/index';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { activeUsersChart } from '@/services/chart/index';
|
||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||
import { generateChannelQuery } from '../../common/generate-channel-query';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10,
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
sinceDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
|
||||
untilDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
|
||||
includeMyRenotes: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
|
||||
includeRenotedMyNotes: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
|
||||
includeLocalRenotes: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
|
||||
withFiles: {
|
||||
validator: $.optional.bool,
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
stlDisabled: {
|
||||
message: 'Hybrid timeline has been disabled.',
|
||||
code: 'STL_DISABLED',
|
||||
id: '620763f4-f621-4533-ab33-0577a1a3c342'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, 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());
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
|
||||
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
|
||||
|
||||
const timeline = await query.take(ps.limit!).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (user) {
|
||||
activeUsersChart.update(user);
|
||||
}
|
||||
});
|
||||
|
||||
return await Notes.packMany(timeline, user);
|
||||
});
|
@ -0,0 +1,129 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { fetchMeta } from '@/misc/fetch-meta';
|
||||
import { ApiError } from '../../error';
|
||||
import { Notes } from '@/models/index';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { activeUsersChart } from '@/services/chart/index';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||
import { generateChannelQuery } from '../../common/generate-channel-query';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
params: {
|
||||
withFiles: {
|
||||
validator: $.optional.bool,
|
||||
},
|
||||
|
||||
fileType: {
|
||||
validator: $.optional.arr($.str),
|
||||
},
|
||||
|
||||
excludeNsfw: {
|
||||
validator: $.optional.bool,
|
||||
default: false,
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
sinceDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
|
||||
untilDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
ltlDisabled: {
|
||||
message: 'Local timeline has been disabled.',
|
||||
code: 'LTL_DISABLED',
|
||||
id: '45a6eb02-7695-4393-b023-dd3be9aaaefd'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, 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\') 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');
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) generateMutedUserQuery(query, user);
|
||||
if (user) generateMutedNoteQuery(query, user);
|
||||
if (user) generateBlockedUserQuery(query, user);
|
||||
|
||||
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
|
||||
|
||||
const timeline = await query.take(ps.limit!).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (user) {
|
||||
activeUsersChart.update(user);
|
||||
}
|
||||
});
|
||||
|
||||
return await Notes.packMany(timeline, user);
|
||||
});
|
88
packages/backend/src/server/api/endpoints/notes/mentions.ts
Normal file
88
packages/backend/src/server/api/endpoints/notes/mentions.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import read from '@/services/note/read';
|
||||
import { Notes, Followings } from '@/models/index';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
params: {
|
||||
following: {
|
||||
validator: $.optional.bool,
|
||||
default: false
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
visibility: {
|
||||
validator: $.optional.str,
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, 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 mentions = await query.take(ps.limit!).getMany();
|
||||
|
||||
read(user.id, mentions);
|
||||
|
||||
return await Notes.packMany(mentions, user);
|
||||
});
|
@ -0,0 +1,77 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { Polls, Mutings, Notes, PollVotes } from '@/models/index';
|
||||
import { Brackets, In } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
offset: {
|
||||
validator: $.optional.num.min(0),
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, 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.take(ps.limit!).skip(ps.offset).getMany();
|
||||
|
||||
if (polls.length === 0) return [];
|
||||
|
||||
const notes = await Notes.find({
|
||||
id: In(polls.map(poll => poll.noteId))
|
||||
});
|
||||
|
||||
return await Notes.packMany(notes, user, {
|
||||
detail: true
|
||||
});
|
||||
});
|
170
packages/backend/src/server/api/endpoints/notes/polls/vote.ts
Normal file
170
packages/backend/src/server/api/endpoints/notes/polls/vote.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import { publishNoteStream } from '@/services/stream';
|
||||
import { createNotification } from '@/services/create-notification';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { getNote } from '../../../common/getters';
|
||||
import { deliver } from '@/queue/index';
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index';
|
||||
import renderVote from '@/remote/activitypub/renderer/vote';
|
||||
import { deliverQuestionUpdate } from '@/services/note/polls/update';
|
||||
import { PollVotes, NoteWatchings, Users, Polls, Blockings } from '@/models/index';
|
||||
import { Not } from 'typeorm';
|
||||
import { IRemoteUser } from '@/models/entities/user';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:votes',
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
choice: {
|
||||
validator: $.num
|
||||
},
|
||||
},
|
||||
|
||||
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 default define(meta, async (ps, user) => {
|
||||
const createdAt = new Date();
|
||||
|
||||
// Get votee
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!note.hasPoll) {
|
||||
throw new ApiError(meta.errors.noPoll);
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const block = await Blockings.findOne({
|
||||
blockerId: note.userId,
|
||||
blockeeId: user.id,
|
||||
});
|
||||
if (block) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
||||
const poll = await Polls.findOneOrFail({ 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.find({
|
||||
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.findOneOrFail(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.find({
|
||||
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.findOneOrFail(note.userId) as IRemoteUser;
|
||||
|
||||
deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox);
|
||||
}
|
||||
|
||||
// リモートフォロワーにUpdate配信
|
||||
deliverQuestionUpdate(note.id);
|
||||
});
|
90
packages/backend/src/server/api/endpoints/notes/reactions.ts
Normal file
90
packages/backend/src/server/api/endpoints/notes/reactions.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { getNote } from '../../common/getters';
|
||||
import { ApiError } from '../../error';
|
||||
import { NoteReactions } from '@/models/index';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
import { NoteReaction } from '@/models/entities/note-reaction';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'reactions'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
type: {
|
||||
validator: $.optional.nullable.str,
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
offset: {
|
||||
validator: $.optional.num,
|
||||
default: 0
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'NoteReaction',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: '263fff3d-d0e1-4af4-bea7-8408059b451a'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
const query = {
|
||||
noteId: note.id
|
||||
} as DeepPartial<NoteReaction>;
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user)));
|
||||
});
|
@ -0,0 +1,57 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import createReaction from '@/services/note/reaction/create';
|
||||
import define from '../../../define';
|
||||
import { getNote } from '../../../common/getters';
|
||||
import { ApiError } from '../../../error';
|
||||
|
||||
export const meta = {
|
||||
tags: ['reactions', 'notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:reactions',
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
reaction: {
|
||||
validator: $.str,
|
||||
}
|
||||
},
|
||||
|
||||
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'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
await createReaction(user, 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,52 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import * as ms from 'ms';
|
||||
import deleteReaction from '@/services/note/reaction/delete';
|
||||
import { getNote } from '../../../common/getters';
|
||||
import { ApiError } from '../../../error';
|
||||
|
||||
export const meta = {
|
||||
tags: ['reactions', 'notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:reactions',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 60,
|
||||
minInterval: ms('3sec')
|
||||
},
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
},
|
||||
|
||||
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'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
await deleteReaction(user, note).catch(e => {
|
||||
if (e.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted);
|
||||
throw e;
|
||||
});
|
||||
});
|
76
packages/backend/src/server/api/endpoints/notes/renotes.ts
Normal file
76
packages/backend/src/server/api/endpoints/notes/renotes.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { getNote } from '../../common/getters';
|
||||
import { ApiError } from '../../error';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { Notes } from '@/models/index';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: '12908022-2e21-46cd-ba6a-3edaf6093f46'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere(`note.renoteId = :renoteId`, { renoteId: note.id })
|
||||
.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 renotes = await query.take(ps.limit!).getMany();
|
||||
|
||||
return await Notes.packMany(renotes, user);
|
||||
});
|
61
packages/backend/src/server/api/endpoints/notes/replies.ts
Normal file
61
packages/backend/src/server/api/endpoints/notes/replies.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { Notes } from '@/models/index';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, 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 timeline = await query.take(ps.limit!).getMany();
|
||||
|
||||
return await Notes.packMany(timeline, user);
|
||||
});
|
134
packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
Normal file
134
packages/backend/src/server/api/endpoints/notes/search-by-tag.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { Notes } from '@/models/index';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { safeForSql } from '@/misc/safe-for-sql';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'hashtags'],
|
||||
|
||||
params: {
|
||||
tag: {
|
||||
validator: $.optional.str,
|
||||
},
|
||||
|
||||
query: {
|
||||
validator: $.optional.arr($.arr($.str)),
|
||||
},
|
||||
|
||||
reply: {
|
||||
validator: $.optional.nullable.bool,
|
||||
default: null,
|
||||
},
|
||||
|
||||
renote: {
|
||||
validator: $.optional.nullable.bool,
|
||||
default: null,
|
||||
},
|
||||
|
||||
withFiles: {
|
||||
validator: $.optional.bool,
|
||||
},
|
||||
|
||||
poll: {
|
||||
validator: $.optional.nullable.bool,
|
||||
default: null,
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, 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(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(tag)) throw 'Injection';
|
||||
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e === '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');
|
||||
}
|
||||
}
|
||||
|
||||
// Search notes
|
||||
const notes = await query.take(ps.limit!).getMany();
|
||||
|
||||
return await Notes.packMany(notes, me);
|
||||
});
|
152
packages/backend/src/server/api/endpoints/notes/search.ts
Normal file
152
packages/backend/src/server/api/endpoints/notes/search.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import $ from 'cafy';
|
||||
import es from '../../../../db/elasticsearch';
|
||||
import define from '../../define';
|
||||
import { Notes } from '@/models/index';
|
||||
import { In } from 'typeorm';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import config from '@/config/index';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
query: {
|
||||
validator: $.str
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
host: {
|
||||
validator: $.optional.nullable.str,
|
||||
default: undefined
|
||||
},
|
||||
|
||||
userId: {
|
||||
validator: $.optional.nullable.type(ID),
|
||||
default: null
|
||||
},
|
||||
|
||||
channelId: {
|
||||
validator: $.optional.nullable.type(ID),
|
||||
default: null
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
if (es == null) {
|
||||
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
|
||||
.andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
|
||||
.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);
|
||||
|
||||
const notes = await query.take(ps.limit!).getMany();
|
||||
|
||||
return await Notes.packMany(notes, me);
|
||||
} else {
|
||||
const userQuery = ps.userId != null ? [{
|
||||
term: {
|
||||
userId: ps.userId
|
||||
}
|
||||
}] : [];
|
||||
|
||||
const hostQuery = ps.userId == null ?
|
||||
ps.host === null ? [{
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'userHost'
|
||||
}
|
||||
}
|
||||
}
|
||||
}] : ps.host !== undefined ? [{
|
||||
term: {
|
||||
userHost: ps.host
|
||||
}
|
||||
}] : []
|
||||
: [];
|
||||
|
||||
const result = await es.search({
|
||||
index: config.elasticsearch.index || 'misskey_note',
|
||||
body: {
|
||||
size: ps.limit!,
|
||||
from: ps.offset,
|
||||
query: {
|
||||
bool: {
|
||||
must: [{
|
||||
simple_query_string: {
|
||||
fields: ['text'],
|
||||
query: ps.query.toLowerCase(),
|
||||
default_operator: 'and'
|
||||
},
|
||||
}, ...hostQuery, ...userQuery]
|
||||
}
|
||||
},
|
||||
sort: [{
|
||||
_doc: 'desc'
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
const hits = result.body.hits.hits.map((hit: any) => hit._id);
|
||||
|
||||
if (hits.length === 0) return [];
|
||||
|
||||
// Fetch found notes
|
||||
const notes = await Notes.find({
|
||||
where: {
|
||||
id: In(hits)
|
||||
},
|
||||
order: {
|
||||
id: -1
|
||||
}
|
||||
});
|
||||
|
||||
return await Notes.packMany(notes, me);
|
||||
}
|
||||
});
|
43
packages/backend/src/server/api/endpoints/notes/show.ts
Normal file
43
packages/backend/src/server/api/endpoints/notes/show.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { getNote } from '../../common/getters';
|
||||
import { ApiError } from '../../error';
|
||||
import { Notes } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
return await Notes.pack(note, user, {
|
||||
detail: true
|
||||
});
|
||||
});
|
69
packages/backend/src/server/api/endpoints/notes/state.ts
Normal file
69
packages/backend/src/server/api/endpoints/notes/state.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
isFavorited: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
isWatching: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
isMutedThread: {
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await Notes.findOneOrFail(ps.noteId);
|
||||
|
||||
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,54 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { getNote } from '../../../common/getters';
|
||||
import { ApiError } from '../../../error';
|
||||
import { Notes, NoteThreadMutings } from '@/models';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
import readNote from '@/services/note/read';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: '5ff67ada-ed3b-2e71-8e87-a1a421e177d2'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
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 $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { getNote } from '../../../common/getters';
|
||||
import { ApiError } from '../../../error';
|
||||
import { NoteThreadMutings } from '@/models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: 'bddd57ac-ceb3-b29d-4334-86ea5fae481a'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
await NoteThreadMutings.delete({
|
||||
threadId: note.threadId || note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
});
|
150
packages/backend/src/server/api/endpoints/notes/timeline.ts
Normal file
150
packages/backend/src/server/api/endpoints/notes/timeline.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { Notes, Followings } from '@/models/index';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
|
||||
import { activeUsersChart } from '@/services/chart/index';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||
import { generateChannelQuery } from '../../common/generate-channel-query';
|
||||
import { generateBlockedUserQuery } from '../../common/generate-block-query';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10,
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
sinceDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
|
||||
untilDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
|
||||
includeMyRenotes: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
|
||||
includeRenotedMyNotes: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
|
||||
includeLocalRenotes: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
|
||||
withFiles: {
|
||||
validator: $.optional.bool,
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const hasFollowing = (await Followings.count({
|
||||
where: {
|
||||
followerId: user.id,
|
||||
},
|
||||
take: 1
|
||||
})) !== 0;
|
||||
|
||||
//#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 = :meId', { meId: user.id });
|
||||
if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
|
||||
}))
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.setParameters(followingQuery.getParameters());
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
|
||||
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
|
||||
|
||||
const timeline = await query.take(ps.limit!).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (user) {
|
||||
activeUsersChart.update(user);
|
||||
}
|
||||
});
|
||||
|
||||
return await Notes.packMany(timeline, user);
|
||||
});
|
89
packages/backend/src/server/api/endpoints/notes/translate.ts
Normal file
89
packages/backend/src/server/api/endpoints/notes/translate.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { getNote } from '../../common/getters';
|
||||
import { ApiError } from '../../error';
|
||||
import fetch from 'node-fetch';
|
||||
import config from '@/config/index';
|
||||
import { getAgentByUrl } from '@/misc/fetch';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { fetchMeta } from '@/misc/fetch-meta';
|
||||
import { Notes } from '@/models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: false as const,
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
targetLang: {
|
||||
validator: $.str,
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: 'bea9b03f-36e0-49c5-a4db-627a029f8971'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
if (!(await Notes.isVisibleForMe(note, user ? user.id : null))) {
|
||||
return 204; // TODO: 良い感じのエラー返す
|
||||
}
|
||||
|
||||
if (note.text == null) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
const instance = await fetchMeta();
|
||||
|
||||
if (instance.deeplAuthKey == null) {
|
||||
return 204; // TODO: 良い感じのエラー返す
|
||||
}
|
||||
|
||||
let targetLang = ps.targetLang;
|
||||
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
|
||||
|
||||
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,
|
||||
timeout: 10000,
|
||||
agent: getAgentByUrl,
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
return {
|
||||
sourceLang: json.translations[0].detected_source_language,
|
||||
text: json.translations[0].text
|
||||
};
|
||||
});
|
52
packages/backend/src/server/api/endpoints/notes/unrenote.ts
Normal file
52
packages/backend/src/server/api/endpoints/notes/unrenote.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import deleteNote from '@/services/note/delete';
|
||||
import define from '../../define';
|
||||
import * as ms from 'ms';
|
||||
import { getNote } from '../../common/getters';
|
||||
import { ApiError } from '../../error';
|
||||
import { Notes, Users } from '@/models/index';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:notes',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 300,
|
||||
minInterval: ms('1sec')
|
||||
},
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: 'efd4a259-2442-496b-8dd7-b255aa1a160f'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
const renotes = await Notes.find({
|
||||
userId: user.id,
|
||||
renoteId: note.id
|
||||
});
|
||||
|
||||
for (const note of renotes) {
|
||||
deleteNote(await Users.findOneOrFail(user.id), note);
|
||||
}
|
||||
});
|
@ -0,0 +1,147 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { UserLists, UserListJoinings, Notes } from '@/models/index';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query';
|
||||
import { activeUsersChart } from '@/services/chart/index';
|
||||
import { Brackets } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'lists'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
params: {
|
||||
listId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.optional.num.range(1, 100),
|
||||
default: 10,
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.optional.type(ID),
|
||||
},
|
||||
|
||||
sinceDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
|
||||
untilDate: {
|
||||
validator: $.optional.num,
|
||||
},
|
||||
|
||||
includeMyRenotes: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
|
||||
includeRenotedMyNotes: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
|
||||
includeLocalRenotes: {
|
||||
validator: $.optional.bool,
|
||||
default: true,
|
||||
},
|
||||
|
||||
withFiles: {
|
||||
validator: $.optional.bool,
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_LIST',
|
||||
id: '8fb1fbd5-e476-4c37-9fb0-43d55b63a2ff'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const list = await UserLists.findOne({
|
||||
id: ps.listId,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (list == null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
const listQuery = UserListJoinings.createQueryBuilder('joining')
|
||||
.select('joining.userId')
|
||||
.where('joining.userListId = :userListId', { userListId: list.id });
|
||||
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere(`note.userId IN (${ listQuery.getQuery() })`)
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.setParameters(listQuery.getParameters());
|
||||
|
||||
generateVisibilityQuery(query, user);
|
||||
|
||||
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
|
||||
|
||||
const timeline = await query.take(ps.limit!).getMany();
|
||||
|
||||
activeUsersChart.update(user);
|
||||
|
||||
return await Notes.packMany(timeline, user);
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import watch from '@/services/note/watch';
|
||||
import { getNote } from '../../../common/getters';
|
||||
import { ApiError } from '../../../error';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: 'ea0e37a6-90a3-4f58-ba6b-c328ca206fc7'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
await watch(user.id, note);
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import unwatch from '@/services/note/unwatch';
|
||||
import { getNote } from '../../../common/getters';
|
||||
import { ApiError } from '../../../error';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: true as const,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
params: {
|
||||
noteId: {
|
||||
validator: $.type(ID),
|
||||
}
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchNote: {
|
||||
message: 'No such note.',
|
||||
code: 'NO_SUCH_NOTE',
|
||||
id: '09b3695c-f72c-4731-a428-7cff825fc82e'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const note = await getNote(ps.noteId).catch(e => {
|
||||
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||
throw e;
|
||||
});
|
||||
|
||||
await unwatch(user.id, note);
|
||||
});
|
Reference in New Issue
Block a user