mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-25 03:17:38 -07:00
[mastodon-client] Code cleanup & reformat
This commit is contained in:
parent
2e7ac53c20
commit
4559b135cb
68 changed files with 4128 additions and 4139 deletions
|
@ -2,104 +2,111 @@ import { Entity } from "megalodon";
|
|||
import { convertId, IdType } from "../index.js";
|
||||
|
||||
function simpleConvert(data: any) {
|
||||
// copy the object to bypass weird pass by reference bugs
|
||||
const result = Object.assign({}, data);
|
||||
result.id = convertId(data.id, IdType.MastodonId);
|
||||
return result;
|
||||
// copy the object to bypass weird pass by reference bugs
|
||||
const result = Object.assign({}, data);
|
||||
result.id = convertId(data.id, IdType.MastodonId);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function convertAccount(account: Entity.Account | MastodonEntity.MutedAccount) {
|
||||
return simpleConvert(account);
|
||||
return simpleConvert(account);
|
||||
}
|
||||
|
||||
export function convertAnnouncement(announcement: Entity.Announcement) {
|
||||
return simpleConvert(announcement);
|
||||
return simpleConvert(announcement);
|
||||
}
|
||||
|
||||
export function convertAttachment(attachment: Entity.Attachment) {
|
||||
return simpleConvert(attachment);
|
||||
return simpleConvert(attachment);
|
||||
}
|
||||
|
||||
export function convertFilter(filter: Entity.Filter) {
|
||||
return simpleConvert(filter);
|
||||
return simpleConvert(filter);
|
||||
}
|
||||
|
||||
export function convertList(list: MastodonEntity.List) {
|
||||
return simpleConvert(list);
|
||||
return simpleConvert(list);
|
||||
}
|
||||
|
||||
export function convertFeaturedTag(tag: Entity.FeaturedTag) {
|
||||
return simpleConvert(tag);
|
||||
return simpleConvert(tag);
|
||||
}
|
||||
|
||||
export function convertNotification(notification: MastodonEntity.Notification) {
|
||||
notification.account = convertAccount(notification.account);
|
||||
notification.id = convertId(notification.id, IdType.MastodonId);
|
||||
if (notification.status)
|
||||
notification.status = convertStatus(notification.status);
|
||||
if (notification.reaction)
|
||||
notification.reaction = convertReaction(notification.reaction);
|
||||
return notification;
|
||||
notification.account = convertAccount(notification.account);
|
||||
notification.id = convertId(notification.id, IdType.MastodonId);
|
||||
if (notification.status)
|
||||
notification.status = convertStatus(notification.status);
|
||||
if (notification.reaction)
|
||||
notification.reaction = convertReaction(notification.reaction);
|
||||
return notification;
|
||||
}
|
||||
|
||||
export function convertPoll(poll: Entity.Poll) {
|
||||
return simpleConvert(poll);
|
||||
return simpleConvert(poll);
|
||||
}
|
||||
|
||||
export function convertReaction(reaction: MastodonEntity.Reaction) {
|
||||
if (reaction.accounts) {
|
||||
reaction.accounts = reaction.accounts.map(convertAccount);
|
||||
}
|
||||
return reaction;
|
||||
if (reaction.accounts) {
|
||||
reaction.accounts = reaction.accounts.map(convertAccount);
|
||||
}
|
||||
return reaction;
|
||||
}
|
||||
|
||||
export function convertRelationship(relationship: Entity.Relationship) {
|
||||
return simpleConvert(relationship);
|
||||
return simpleConvert(relationship);
|
||||
}
|
||||
|
||||
export function convertSearch(search: MastodonEntity.Search) {
|
||||
search.accounts = search.accounts.map(p => convertAccount(p));
|
||||
search.statuses = search.statuses.map(p => convertStatus(p));
|
||||
return search;
|
||||
search.accounts = search.accounts.map(p => convertAccount(p));
|
||||
search.statuses = search.statuses.map(p => convertStatus(p));
|
||||
return search;
|
||||
}
|
||||
|
||||
export function convertStatusSource(statusSource: MastodonEntity.StatusSource) {
|
||||
return simpleConvert(statusSource);
|
||||
return simpleConvert(statusSource);
|
||||
}
|
||||
|
||||
export function convertStatus(status: MastodonEntity.Status) {
|
||||
status.account = convertAccount(status.account);
|
||||
status.id = convertId(status.id, IdType.MastodonId);
|
||||
if (status.in_reply_to_account_id)
|
||||
status.in_reply_to_account_id = convertId(
|
||||
status.in_reply_to_account_id,
|
||||
IdType.MastodonId,
|
||||
);
|
||||
if (status.in_reply_to_id)
|
||||
status.in_reply_to_id = convertId(status.in_reply_to_id, IdType.MastodonId);
|
||||
status.media_attachments = status.media_attachments.map((attachment) =>
|
||||
convertAttachment(attachment),
|
||||
);
|
||||
status.mentions = status.mentions.map((mention) => ({
|
||||
...mention,
|
||||
id: convertId(mention.id, IdType.MastodonId),
|
||||
}));
|
||||
if (status.poll) status.poll = convertPoll(status.poll);
|
||||
if (status.reblog) status.reblog = convertStatus(status.reblog);
|
||||
if (status.quote) status.quote = convertStatus(status.quote);
|
||||
status.reactions = status.reactions.map(convertReaction);
|
||||
status.account = convertAccount(status.account);
|
||||
status.id = convertId(status.id, IdType.MastodonId);
|
||||
if (status.in_reply_to_account_id)
|
||||
status.in_reply_to_account_id = convertId(
|
||||
status.in_reply_to_account_id,
|
||||
IdType.MastodonId,
|
||||
);
|
||||
if (status.in_reply_to_id)
|
||||
status.in_reply_to_id = convertId(status.in_reply_to_id, IdType.MastodonId);
|
||||
status.media_attachments = status.media_attachments.map((attachment) =>
|
||||
convertAttachment(attachment),
|
||||
);
|
||||
status.mentions = status.mentions.map((mention) => ({
|
||||
...mention,
|
||||
id: convertId(mention.id, IdType.MastodonId),
|
||||
}));
|
||||
if (status.poll) status.poll = convertPoll(status.poll);
|
||||
if (status.reblog) status.reblog = convertStatus(status.reblog);
|
||||
if (status.quote) status.quote = convertStatus(status.quote);
|
||||
status.reactions = status.reactions.map(convertReaction);
|
||||
|
||||
return status;
|
||||
return status;
|
||||
}
|
||||
|
||||
export function convertStatusEdit(edit: MastodonEntity.StatusEdit) {
|
||||
edit.account = convertAccount(edit.account);
|
||||
edit.media_attachments = edit.media_attachments.map((attachment) =>
|
||||
convertAttachment(attachment),
|
||||
);
|
||||
if (edit.poll) edit.poll = convertPoll(edit.poll);
|
||||
return edit;
|
||||
edit.account = convertAccount(edit.account);
|
||||
edit.media_attachments = edit.media_attachments.map((attachment) =>
|
||||
convertAttachment(attachment),
|
||||
);
|
||||
if (edit.poll) edit.poll = convertPoll(edit.poll);
|
||||
return edit;
|
||||
}
|
||||
|
||||
export function convertConversation(conversation: MastodonEntity.Conversation) {
|
||||
conversation.id = convertId(conversation.id, IdType.MastodonId);
|
||||
conversation.accounts = conversation.accounts.map(convertAccount);
|
||||
if (conversation.last_status) {
|
||||
conversation.last_status = convertStatus(conversation.last_status);
|
||||
}
|
||||
conversation.id = convertId(conversation.id, IdType.MastodonId);
|
||||
conversation.accounts = conversation.accounts.map(convertAccount);
|
||||
if (conversation.last_status) {
|
||||
conversation.last_status = convertStatus(conversation.last_status);
|
||||
}
|
||||
|
||||
return conversation;
|
||||
return conversation;
|
||||
}
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
import { Packed } from "@/misc/schema.js";
|
||||
|
||||
export class FileConverter {
|
||||
public static encode(f: Packed<"DriveFile">): MastodonEntity.Attachment {
|
||||
return {
|
||||
id: f.id,
|
||||
type: this.encodefileType(f.type),
|
||||
url: f.url ?? "",
|
||||
remote_url: f.url,
|
||||
preview_url: f.thumbnailUrl,
|
||||
text_url: f.url,
|
||||
meta: {
|
||||
width: f.properties.width,
|
||||
height: f.properties.height,
|
||||
},
|
||||
description: f.comment,
|
||||
blurhash: f.blurhash,
|
||||
};
|
||||
}
|
||||
public static encode(f: Packed<"DriveFile">): MastodonEntity.Attachment {
|
||||
return {
|
||||
id: f.id,
|
||||
type: this.encodefileType(f.type),
|
||||
url: f.url ?? "",
|
||||
remote_url: f.url,
|
||||
preview_url: f.thumbnailUrl,
|
||||
text_url: f.url,
|
||||
meta: {
|
||||
width: f.properties.width,
|
||||
height: f.properties.height,
|
||||
},
|
||||
description: f.comment,
|
||||
blurhash: f.blurhash,
|
||||
};
|
||||
}
|
||||
|
||||
private static encodefileType(s: string): "unknown" | "image" | "gifv" | "video" | "audio" {
|
||||
if (s === "image/gif") {
|
||||
return "gifv";
|
||||
}
|
||||
if (s.includes("image")) {
|
||||
return "image";
|
||||
}
|
||||
if (s.includes("video")) {
|
||||
return "video";
|
||||
}
|
||||
if (s.includes("audio")) {
|
||||
return "audio";
|
||||
}
|
||||
return "unknown";
|
||||
};
|
||||
private static encodefileType(s: string): "unknown" | "image" | "gifv" | "video" | "audio" {
|
||||
if (s === "image/gif") {
|
||||
return "gifv";
|
||||
}
|
||||
if (s.includes("image")) {
|
||||
return "image";
|
||||
}
|
||||
if (s.includes("video")) {
|
||||
return "video";
|
||||
}
|
||||
if (s.includes("audio")) {
|
||||
return "audio";
|
||||
}
|
||||
return "unknown";
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,21 +3,21 @@ import config from "@/config/index.js";
|
|||
import { IMentionedRemoteUsers } from "@/models/entities/note.js";
|
||||
|
||||
export class MentionConverter {
|
||||
public static encode(u: User, m: IMentionedRemoteUsers): MastodonEntity.Mention {
|
||||
let acct = u.username;
|
||||
let acctUrl = `https://${u.host || config.host}/@${u.username}`;
|
||||
let url: string | null = null;
|
||||
if (u.host) {
|
||||
const info = m.find(r => r.username === u.username && r.host === u.host);
|
||||
acct = `${u.username}@${u.host}`;
|
||||
acctUrl = `https://${u.host}/@${u.username}`;
|
||||
if (info) url = info.url ?? info.uri;
|
||||
}
|
||||
return {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
acct: acct,
|
||||
url: url ?? acctUrl,
|
||||
};
|
||||
}
|
||||
public static encode(u: User, m: IMentionedRemoteUsers): MastodonEntity.Mention {
|
||||
let acct = u.username;
|
||||
let acctUrl = `https://${u.host || config.host}/@${u.username}`;
|
||||
let url: string | null = null;
|
||||
if (u.host) {
|
||||
const info = m.find(r => r.username === u.username && r.host === u.host);
|
||||
acct = `${u.username}@${u.host}`;
|
||||
acctUrl = `https://${u.host}/@${u.username}`;
|
||||
if (info) url = info.url ?? info.uri;
|
||||
}
|
||||
return {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
acct: acct,
|
||||
url: url ?? acctUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ILocalUser } from "@/models/entities/user.js";
|
||||
import {getNote, getUser} from "@/server/api/common/getters.js";
|
||||
import { getNote } from "@/server/api/common/getters.js";
|
||||
import { Note } from "@/models/entities/note.js";
|
||||
import config from "@/config/index.js";
|
||||
import mfm from "mfm-js";
|
||||
|
@ -23,75 +23,75 @@ export class NoteConverter {
|
|||
public static async encode(note: Note, user: ILocalUser | null, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Status> {
|
||||
const noteUser = note.user ?? UserHelpers.getUserCached(note.userId, cache);
|
||||
|
||||
if (!await Notes.isVisibleForMe(note, user?.id ?? null))
|
||||
throw new Error('Cannot encode note not visible for user');
|
||||
if (!await Notes.isVisibleForMe(note, user?.id ?? null))
|
||||
throw new Error('Cannot encode note not visible for user');
|
||||
|
||||
const host = Promise.resolve(noteUser).then(noteUser => noteUser.host ?? null);
|
||||
const host = Promise.resolve(noteUser).then(noteUser => noteUser.host ?? null);
|
||||
|
||||
const reactionEmojiNames = Object.keys(note.reactions)
|
||||
.filter((x) => x?.startsWith(":"))
|
||||
.map((x) => decodeReaction(x).reaction)
|
||||
.map((x) => x.replace(/:/g, ""));
|
||||
const reactionEmojiNames = Object.keys(note.reactions)
|
||||
.filter((x) => x?.startsWith(":"))
|
||||
.map((x) => decodeReaction(x).reaction)
|
||||
.map((x) => x.replace(/:/g, ""));
|
||||
|
||||
const noteEmoji = host.then(async host => populateEmojis(
|
||||
note.emojis.concat(reactionEmojiNames),
|
||||
host,
|
||||
));
|
||||
const noteEmoji = host.then(async host => populateEmojis(
|
||||
note.emojis.concat(reactionEmojiNames),
|
||||
host,
|
||||
));
|
||||
|
||||
const reactionCount = NoteReactions.countBy({noteId: note.id});
|
||||
const reactionCount = NoteReactions.countBy({noteId: note.id});
|
||||
|
||||
const reaction = user ? NoteReactions.findOneBy({
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
}) : null;
|
||||
const reaction = user ? NoteReactions.findOneBy({
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
}) : null;
|
||||
|
||||
const isFavorited = Promise.resolve(reaction).then(p => !!p);
|
||||
const isFavorited = Promise.resolve(reaction).then(p => !!p);
|
||||
|
||||
const isReblogged = user ? Notes.exist({
|
||||
where: {
|
||||
userId: user.id,
|
||||
renoteId: note.id,
|
||||
text: IsNull(),
|
||||
}
|
||||
}) : null;
|
||||
const isReblogged = user ? Notes.exist({
|
||||
where: {
|
||||
userId: user.id,
|
||||
renoteId: note.id,
|
||||
text: IsNull(),
|
||||
}
|
||||
}) : null;
|
||||
|
||||
const renote = note.renote ?? (note.renoteId ? getNote(note.renoteId, user) : null);
|
||||
const renote = note.renote ?? (note.renoteId ? getNote(note.renoteId, user) : null);
|
||||
|
||||
const isBookmarked = user ? NoteFavorites.exist({
|
||||
where: {
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
},
|
||||
take: 1,
|
||||
}) : false;
|
||||
const isBookmarked = user ? NoteFavorites.exist({
|
||||
where: {
|
||||
userId: user.id,
|
||||
noteId: note.id,
|
||||
},
|
||||
take: 1,
|
||||
}) : false;
|
||||
|
||||
const isMuted = user ? NoteThreadMutings.exist({
|
||||
where: {
|
||||
userId: user.id,
|
||||
threadId: note.threadId || note.id,
|
||||
}
|
||||
}) : false;
|
||||
const isMuted = user ? NoteThreadMutings.exist({
|
||||
where: {
|
||||
userId: user.id,
|
||||
threadId: note.threadId || note.id,
|
||||
}
|
||||
}) : false;
|
||||
|
||||
const files = DriveFiles.packMany(note.fileIds);
|
||||
const files = DriveFiles.packMany(note.fileIds);
|
||||
|
||||
const mentions = Promise.all(note.mentions.map(p =>
|
||||
UserHelpers.getUserCached(p, cache)
|
||||
.then(u => MentionConverter.encode(u, JSON.parse(note.mentionedRemoteUsers)))
|
||||
.catch(() => null)))
|
||||
.then(p => p.filter(m => m)) as Promise<MastodonEntity.Mention[]>;
|
||||
const mentions = Promise.all(note.mentions.map(p =>
|
||||
UserHelpers.getUserCached(p, cache)
|
||||
.then(u => MentionConverter.encode(u, JSON.parse(note.mentionedRemoteUsers)))
|
||||
.catch(() => null)))
|
||||
.then(p => p.filter(m => m)) as Promise<MastodonEntity.Mention[]>;
|
||||
|
||||
const text = Promise.resolve(renote).then(renote => {
|
||||
return renote && note.text !== null
|
||||
? note.text + `\n\nRE: ${renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`}`
|
||||
: note.text;
|
||||
});
|
||||
const text = Promise.resolve(renote).then(renote => {
|
||||
return renote && note.text !== null
|
||||
? note.text + `\n\nRE: ${renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`}`
|
||||
: note.text;
|
||||
});
|
||||
|
||||
const isPinned = user && note.userId === user.id
|
||||
? UserNotePinings.exist({ where: {userId: user.id, noteId: note.id } })
|
||||
: undefined;
|
||||
const isPinned = user && note.userId === user.id
|
||||
? UserNotePinings.exist({where: {userId: user.id, noteId: note.id}})
|
||||
: undefined;
|
||||
|
||||
// noinspection ES6MissingAwait
|
||||
return await awaitAll({
|
||||
return await awaitAll({
|
||||
id: note.id,
|
||||
uri: note.uri ? note.uri : `https://${config.host}/notes/${note.id}`,
|
||||
url: note.uri ? note.uri : `https://${config.host}/notes/${note.id}`,
|
||||
|
@ -104,9 +104,9 @@ export class NoteConverter {
|
|||
created_at: note.createdAt.toISOString(),
|
||||
// Remove reaction emojis with names containing @ from the emojis list.
|
||||
emojis: noteEmoji
|
||||
.then(noteEmoji => noteEmoji
|
||||
.filter((e) => e.name.indexOf("@") === -1)
|
||||
.map((e) => EmojiConverter.encode(e))),
|
||||
.then(noteEmoji => noteEmoji
|
||||
.filter((e) => e.name.indexOf("@") === -1)
|
||||
.map((e) => EmojiConverter.encode(e))),
|
||||
replies_count: note.repliesCount,
|
||||
reblogs_count: note.renoteCount,
|
||||
favourites_count: reactionCount,
|
||||
|
@ -128,12 +128,12 @@ export class NoteConverter {
|
|||
reactions: [], //FIXME: this.mapReactions(n.emojis, n.reactions, n.myReaction),
|
||||
bookmarked: isBookmarked,
|
||||
quote: Promise.resolve(renote).then(renote => renote && note.text !== null ? this.encode(renote, user, cache) : null),
|
||||
edited_at: note.updatedAt?.toISOString()
|
||||
edited_at: note.updatedAt?.toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
public static async encodeMany(notes: Note[], user: ILocalUser | null, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Status[]> {
|
||||
const encoded = notes.map(n => this.encode(n, user, cache));
|
||||
return Promise.all(encoded);
|
||||
}
|
||||
public static async encodeMany(notes: Note[], user: ILocalUser | null, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Status[]> {
|
||||
const encoded = notes.map(n => this.encode(n, user, cache));
|
||||
return Promise.all(encoded);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,71 +10,71 @@ import { getNote } from "@/server/api/common/getters.js";
|
|||
type NotificationType = typeof notificationTypes[number];
|
||||
|
||||
export class NotificationConverter {
|
||||
public static async encode(notification: Notification, localUser: ILocalUser, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Notification> {
|
||||
if (notification.notifieeId !== localUser.id) throw new Error('User is not recipient of notification');
|
||||
public static async encode(notification: Notification, localUser: ILocalUser, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Notification> {
|
||||
if (notification.notifieeId !== localUser.id) throw new Error('User is not recipient of notification');
|
||||
|
||||
//TODO: Test this (poll ended etc)
|
||||
const account = notification.notifierId
|
||||
? UserHelpers.getUserCached(notification.notifierId, cache).then(p => UserConverter.encode(p))
|
||||
: UserConverter.encode(localUser);
|
||||
//TODO: Test this (poll ended etc)
|
||||
const account = notification.notifierId
|
||||
? UserHelpers.getUserCached(notification.notifierId, cache).then(p => UserConverter.encode(p))
|
||||
: UserConverter.encode(localUser);
|
||||
|
||||
let result = {
|
||||
id: notification.id,
|
||||
account: account,
|
||||
created_at: notification.createdAt.toISOString(),
|
||||
type: this.encodeNotificationType(notification.type),
|
||||
};
|
||||
let result = {
|
||||
id: notification.id,
|
||||
account: account,
|
||||
created_at: notification.createdAt.toISOString(),
|
||||
type: this.encodeNotificationType(notification.type),
|
||||
};
|
||||
|
||||
if (notification.note) {
|
||||
const isPureRenote = notification.note.renoteId !== null && notification.note.text === null;
|
||||
const encodedNote = isPureRenote
|
||||
? getNote(notification.note.renoteId!, localUser).then(note => NoteConverter.encode(note, localUser, cache))
|
||||
: NoteConverter.encode(notification.note, localUser, cache);
|
||||
result = Object.assign(result, {
|
||||
status: encodedNote,
|
||||
});
|
||||
if (result.type === 'poll') {
|
||||
result = Object.assign(result, {
|
||||
account: encodedNote.then(p => p.account),
|
||||
});
|
||||
}
|
||||
if (notification.reaction) {
|
||||
//FIXME: Implement reactions;
|
||||
}
|
||||
}
|
||||
return awaitAll(result);
|
||||
}
|
||||
if (notification.note) {
|
||||
const isPureRenote = notification.note.renoteId !== null && notification.note.text === null;
|
||||
const encodedNote = isPureRenote
|
||||
? getNote(notification.note.renoteId!, localUser).then(note => NoteConverter.encode(note, localUser, cache))
|
||||
: NoteConverter.encode(notification.note, localUser, cache);
|
||||
result = Object.assign(result, {
|
||||
status: encodedNote,
|
||||
});
|
||||
if (result.type === 'poll') {
|
||||
result = Object.assign(result, {
|
||||
account: encodedNote.then(p => p.account),
|
||||
});
|
||||
}
|
||||
if (notification.reaction) {
|
||||
//FIXME: Implement reactions;
|
||||
}
|
||||
}
|
||||
return awaitAll(result);
|
||||
}
|
||||
|
||||
public static async encodeMany(notifications: Notification[], localUser: ILocalUser, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Notification[]> {
|
||||
const encoded = notifications.map(u => this.encode(u, localUser, cache));
|
||||
return Promise.all(encoded)
|
||||
.then(p => p.filter(n => n !== null) as MastodonEntity.Notification[]);
|
||||
}
|
||||
public static async encodeMany(notifications: Notification[], localUser: ILocalUser, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Notification[]> {
|
||||
const encoded = notifications.map(u => this.encode(u, localUser, cache));
|
||||
return Promise.all(encoded)
|
||||
.then(p => p.filter(n => n !== null) as MastodonEntity.Notification[]);
|
||||
}
|
||||
|
||||
private static encodeNotificationType(t: NotificationType): MastodonEntity.NotificationType {
|
||||
//FIXME: Implement custom notification for followRequestAccepted
|
||||
//FIXME: Implement mastodon notification type 'update' on misskey side
|
||||
switch (t) {
|
||||
case "follow":
|
||||
return 'follow';
|
||||
case "mention":
|
||||
case "reply":
|
||||
return 'mention'
|
||||
case "renote":
|
||||
return 'reblog';
|
||||
case "quote":
|
||||
return 'reblog';
|
||||
case "reaction":
|
||||
return 'favourite';
|
||||
case "pollEnded":
|
||||
return 'poll';
|
||||
case "receiveFollowRequest":
|
||||
return 'follow_request';
|
||||
case "followRequestAccepted":
|
||||
case "pollVote":
|
||||
case "groupInvited":
|
||||
case "app":
|
||||
throw new Error(`Notification type ${t} not supported`);
|
||||
}
|
||||
}
|
||||
private static encodeNotificationType(t: NotificationType): MastodonEntity.NotificationType {
|
||||
//FIXME: Implement custom notification for followRequestAccepted
|
||||
//FIXME: Implement mastodon notification type 'update' on misskey side
|
||||
switch (t) {
|
||||
case "follow":
|
||||
return 'follow';
|
||||
case "mention":
|
||||
case "reply":
|
||||
return 'mention'
|
||||
case "renote":
|
||||
return 'reblog';
|
||||
case "quote":
|
||||
return 'reblog';
|
||||
case "reaction":
|
||||
return 'favourite';
|
||||
case "pollEnded":
|
||||
return 'poll';
|
||||
case "receiveFollowRequest":
|
||||
return 'follow_request';
|
||||
case "followRequestAccepted":
|
||||
case "pollVote":
|
||||
case "groupInvited":
|
||||
case "app":
|
||||
throw new Error(`Notification type ${t} not supported`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +1,37 @@
|
|||
type Choice = {
|
||||
text: string
|
||||
votes: number
|
||||
isVoted: boolean
|
||||
text: string
|
||||
votes: number
|
||||
isVoted: boolean
|
||||
}
|
||||
|
||||
type Poll = {
|
||||
multiple: boolean
|
||||
expiresAt: Date | null
|
||||
choices: Array<Choice>
|
||||
multiple: boolean
|
||||
expiresAt: Date | null
|
||||
choices: Array<Choice>
|
||||
}
|
||||
|
||||
export class PollConverter {
|
||||
public static encode(p: Poll, noteId: string): MastodonEntity.Poll {
|
||||
const now = new Date();
|
||||
const count = p.choices.reduce((sum, choice) => sum + choice.votes, 0);
|
||||
return {
|
||||
id: noteId,
|
||||
expires_at: p.expiresAt?.toISOString() ?? null,
|
||||
expired: p.expiresAt == null ? false : now > p.expiresAt,
|
||||
multiple: p.multiple,
|
||||
votes_count: count,
|
||||
options: p.choices.map((c) => this.encodeChoice(c)),
|
||||
voted: p.choices.some((c) => c.isVoted),
|
||||
own_votes: p.choices
|
||||
.filter((c) => c.isVoted)
|
||||
.map((c) => p.choices.indexOf(c)),
|
||||
};
|
||||
}
|
||||
public static encode(p: Poll, noteId: string): MastodonEntity.Poll {
|
||||
const now = new Date();
|
||||
const count = p.choices.reduce((sum, choice) => sum + choice.votes, 0);
|
||||
return {
|
||||
id: noteId,
|
||||
expires_at: p.expiresAt?.toISOString() ?? null,
|
||||
expired: p.expiresAt == null ? false : now > p.expiresAt,
|
||||
multiple: p.multiple,
|
||||
votes_count: count,
|
||||
options: p.choices.map((c) => this.encodeChoice(c)),
|
||||
voted: p.choices.some((c) => c.isVoted),
|
||||
own_votes: p.choices
|
||||
.filter((c) => c.isVoted)
|
||||
.map((c) => p.choices.indexOf(c)),
|
||||
};
|
||||
}
|
||||
|
||||
private static encodeChoice(c: Choice): MastodonEntity.PollOption {
|
||||
return {
|
||||
title: c.text,
|
||||
votes_count: c.votes,
|
||||
};
|
||||
}
|
||||
private static encodeChoice(c: Choice): MastodonEntity.PollOption {
|
||||
return {
|
||||
title: c.text,
|
||||
votes_count: c.votes,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,72 +10,72 @@ import { AccountCache, UserHelpers } from "@/server/api/mastodon/helpers/user.js
|
|||
import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js";
|
||||
|
||||
type Field = {
|
||||
name: string;
|
||||
value: string;
|
||||
verified?: boolean;
|
||||
name: string;
|
||||
value: string;
|
||||
verified?: boolean;
|
||||
};
|
||||
|
||||
export class UserConverter {
|
||||
public static async encode(u: User, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Account> {
|
||||
return cache.locks.acquire(u.id, async () => {
|
||||
const cacheHit = cache.accounts.find(p => p.id == u.id);
|
||||
if (cacheHit) return cacheHit;
|
||||
public static async encode(u: User, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Account> {
|
||||
return cache.locks.acquire(u.id, async () => {
|
||||
const cacheHit = cache.accounts.find(p => p.id == u.id);
|
||||
if (cacheHit) return cacheHit;
|
||||
|
||||
let acct = u.username;
|
||||
let acctUrl = `https://${u.host || config.host}/@${u.username}`;
|
||||
if (u.host) {
|
||||
acct = `${u.username}@${u.host}`;
|
||||
acctUrl = `https://${u.host}/@${u.username}`;
|
||||
}
|
||||
const profile = UserProfiles.findOneBy({userId: u.id});
|
||||
const bio = profile.then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? "")) ?? escapeMFM(profile?.description ?? ""));
|
||||
const avatar = u.avatarId
|
||||
? (DriveFiles.findOneBy({ id: u.avatarId }))
|
||||
.then(p => p?.url ?? Users.getIdenticonUrl(u.id))
|
||||
: Users.getIdenticonUrl(u.id);
|
||||
const banner = u.bannerId
|
||||
? (DriveFiles.findOneBy({ id: u.bannerId }))
|
||||
.then(p => p?.url ?? `${config.url}/static-assets/transparent.png`)
|
||||
: `${config.url}/static-assets/transparent.png`;
|
||||
let acct = u.username;
|
||||
let acctUrl = `https://${u.host || config.host}/@${u.username}`;
|
||||
if (u.host) {
|
||||
acct = `${u.username}@${u.host}`;
|
||||
acctUrl = `https://${u.host}/@${u.username}`;
|
||||
}
|
||||
const profile = UserProfiles.findOneBy({userId: u.id});
|
||||
const bio = profile.then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? "")) ?? escapeMFM(profile?.description ?? ""));
|
||||
const avatar = u.avatarId
|
||||
? (DriveFiles.findOneBy({id: u.avatarId}))
|
||||
.then(p => p?.url ?? Users.getIdenticonUrl(u.id))
|
||||
: Users.getIdenticonUrl(u.id);
|
||||
const banner = u.bannerId
|
||||
? (DriveFiles.findOneBy({id: u.bannerId}))
|
||||
.then(p => p?.url ?? `${config.url}/static-assets/transparent.png`)
|
||||
: `${config.url}/static-assets/transparent.png`;
|
||||
|
||||
return awaitAll({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
acct: acct,
|
||||
display_name: u.name || u.username,
|
||||
locked: u.isLocked,
|
||||
created_at: u.createdAt.toISOString(),
|
||||
followers_count: u.followersCount,
|
||||
following_count: u.followingCount,
|
||||
statuses_count: u.notesCount,
|
||||
note: bio,
|
||||
url: u.uri ?? acctUrl,
|
||||
avatar: avatar,
|
||||
avatar_static: avatar,
|
||||
header: banner,
|
||||
header_static: banner,
|
||||
emojis: populateEmojis(u.emojis, u.host).then(emoji => emoji.map((e) => EmojiConverter.encode(e))),
|
||||
moved: null, //FIXME
|
||||
fields: profile.then(profile => profile?.fields.map(p => this.encodeField(p)) ?? []),
|
||||
bot: u.isBot,
|
||||
discoverable: u.isExplorable
|
||||
}).then(p => {
|
||||
cache.accounts.push(p);
|
||||
return p;
|
||||
});
|
||||
});
|
||||
}
|
||||
return awaitAll({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
acct: acct,
|
||||
display_name: u.name || u.username,
|
||||
locked: u.isLocked,
|
||||
created_at: u.createdAt.toISOString(),
|
||||
followers_count: u.followersCount,
|
||||
following_count: u.followingCount,
|
||||
statuses_count: u.notesCount,
|
||||
note: bio,
|
||||
url: u.uri ?? acctUrl,
|
||||
avatar: avatar,
|
||||
avatar_static: avatar,
|
||||
header: banner,
|
||||
header_static: banner,
|
||||
emojis: populateEmojis(u.emojis, u.host).then(emoji => emoji.map((e) => EmojiConverter.encode(e))),
|
||||
moved: null, //FIXME
|
||||
fields: profile.then(profile => profile?.fields.map(p => this.encodeField(p)) ?? []),
|
||||
bot: u.isBot,
|
||||
discoverable: u.isExplorable
|
||||
}).then(p => {
|
||||
cache.accounts.push(p);
|
||||
return p;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static async encodeMany(users: User[], cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Account[]> {
|
||||
const encoded = users.map(u => this.encode(u, cache));
|
||||
return Promise.all(encoded);
|
||||
}
|
||||
public static async encodeMany(users: User[], cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Account[]> {
|
||||
const encoded = users.map(u => this.encode(u, cache));
|
||||
return Promise.all(encoded);
|
||||
}
|
||||
|
||||
private static encodeField(f: Field): MastodonEntity.Field {
|
||||
return {
|
||||
name: f.name,
|
||||
value: MfmHelpers.toHtml(mfm.parse(f.value)) ?? escapeMFM(f.value),
|
||||
verified_at: f.verified ? (new Date()).toISOString() : null,
|
||||
}
|
||||
}
|
||||
private static encodeField(f: Field): MastodonEntity.Field {
|
||||
return {
|
||||
name: f.name,
|
||||
value: MfmHelpers.toHtml(mfm.parse(f.value)) ?? escapeMFM(f.value),
|
||||
verified_at: f.verified ? (new Date()).toISOString() : null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ type IceshrimpVisibility = "public" | "home" | "followers" | "specified" | "hidd
|
|||
type MastodonVisibility = "public" | "unlisted" | "private" | "direct";
|
||||
|
||||
export class VisibilityConverter {
|
||||
public static encode (v: IceshrimpVisibility): MastodonVisibility {
|
||||
public static encode(v: IceshrimpVisibility): MastodonVisibility {
|
||||
switch (v) {
|
||||
case "public":
|
||||
return v;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,70 +3,70 @@ import { AuthHelpers } from "@/server/api/mastodon/helpers/auth.js";
|
|||
import { convertId, IdType } from "@/misc/convert-id.js";
|
||||
|
||||
const readScope = [
|
||||
"read:account",
|
||||
"read:drive",
|
||||
"read:blocks",
|
||||
"read:favorites",
|
||||
"read:following",
|
||||
"read:messaging",
|
||||
"read:mutes",
|
||||
"read:notifications",
|
||||
"read:reactions",
|
||||
"read:pages",
|
||||
"read:page-likes",
|
||||
"read:user-groups",
|
||||
"read:channels",
|
||||
"read:gallery",
|
||||
"read:gallery-likes",
|
||||
"read:account",
|
||||
"read:drive",
|
||||
"read:blocks",
|
||||
"read:favorites",
|
||||
"read:following",
|
||||
"read:messaging",
|
||||
"read:mutes",
|
||||
"read:notifications",
|
||||
"read:reactions",
|
||||
"read:pages",
|
||||
"read:page-likes",
|
||||
"read:user-groups",
|
||||
"read:channels",
|
||||
"read:gallery",
|
||||
"read:gallery-likes",
|
||||
];
|
||||
const writeScope = [
|
||||
"write:account",
|
||||
"write:drive",
|
||||
"write:blocks",
|
||||
"write:favorites",
|
||||
"write:following",
|
||||
"write:messaging",
|
||||
"write:mutes",
|
||||
"write:notes",
|
||||
"write:notifications",
|
||||
"write:reactions",
|
||||
"write:votes",
|
||||
"write:pages",
|
||||
"write:page-likes",
|
||||
"write:user-groups",
|
||||
"write:channels",
|
||||
"write:gallery",
|
||||
"write:gallery-likes",
|
||||
"write:account",
|
||||
"write:drive",
|
||||
"write:blocks",
|
||||
"write:favorites",
|
||||
"write:following",
|
||||
"write:messaging",
|
||||
"write:mutes",
|
||||
"write:notes",
|
||||
"write:notifications",
|
||||
"write:reactions",
|
||||
"write:votes",
|
||||
"write:pages",
|
||||
"write:page-likes",
|
||||
"write:user-groups",
|
||||
"write:channels",
|
||||
"write:gallery",
|
||||
"write:gallery-likes",
|
||||
];
|
||||
|
||||
export function setupEndpointsAuth(router: Router): void {
|
||||
router.post("/v1/apps", async (ctx) => {
|
||||
const body: any = ctx.request.body || ctx.request.query;
|
||||
try {
|
||||
let scope = body.scopes;
|
||||
if (typeof scope === "string") scope = scope.split(" ");
|
||||
const pushScope = new Set<string>();
|
||||
for (const s of scope) {
|
||||
if (s.match(/^read/)) for (const r of readScope) pushScope.add(r);
|
||||
if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r);
|
||||
}
|
||||
const scopeArr = Array.from(pushScope);
|
||||
router.post("/v1/apps", async (ctx) => {
|
||||
const body: any = ctx.request.body || ctx.request.query;
|
||||
try {
|
||||
let scope = body.scopes;
|
||||
if (typeof scope === "string") scope = scope.split(" ");
|
||||
const pushScope = new Set<string>();
|
||||
for (const s of scope) {
|
||||
if (s.match(/^read/)) for (const r of readScope) pushScope.add(r);
|
||||
if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r);
|
||||
}
|
||||
const scopeArr = Array.from(pushScope);
|
||||
|
||||
const red = body.redirect_uris;
|
||||
const appData = await AuthHelpers.registerApp(body['client_name'], scopeArr, red, body['website']);
|
||||
const returns = {
|
||||
id: convertId(appData.id, IdType.MastodonId),
|
||||
name: appData.name,
|
||||
website: body.website,
|
||||
redirect_uri: red,
|
||||
client_id: Buffer.from(appData.url ?? "").toString("base64"),
|
||||
client_secret: appData.clientSecret,
|
||||
};
|
||||
ctx.body = returns;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
const red = body.redirect_uris;
|
||||
const appData = await AuthHelpers.registerApp(body['client_name'], scopeArr, red, body['website']);
|
||||
const returns = {
|
||||
id: convertId(appData.id, IdType.MastodonId),
|
||||
name: appData.name,
|
||||
website: body.website,
|
||||
redirect_uri: red,
|
||||
client_id: Buffer.from(appData.url ?? "").toString("base64"),
|
||||
client_secret: appData.clientSecret,
|
||||
};
|
||||
ctx.body = returns;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,90 +1,89 @@
|
|||
import megalodon, { MegalodonInterface } from "megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { getClient } from "../index.js";
|
||||
import { IdType, convertId } from "../../index.js";
|
||||
import { convertId, IdType } from "../../index.js";
|
||||
import { convertFilter } from "../converters.js";
|
||||
|
||||
export function setupEndpointsFilter(router: Router): void {
|
||||
router.get("/v1/filters", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.getFilters();
|
||||
ctx.body = data.data.map((filter) => convertFilter(filter));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/filters", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.getFilters();
|
||||
ctx.body = data.data.map((filter) => convertFilter(filter));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/filters/:id", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.getFilter(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
);
|
||||
ctx.body = convertFilter(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/filters/:id", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.getFilter(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
);
|
||||
ctx.body = convertFilter(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/v1/filters", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.createFilter(body.phrase, body.context, body);
|
||||
ctx.body = convertFilter(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.post("/v1/filters", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.createFilter(body.phrase, body.context, body);
|
||||
ctx.body = convertFilter(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/v1/filters/:id", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.updateFilter(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
body.phrase,
|
||||
body.context,
|
||||
);
|
||||
ctx.body = convertFilter(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.post("/v1/filters/:id", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.updateFilter(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
body.phrase,
|
||||
body.context,
|
||||
);
|
||||
ctx.body = convertFilter(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/v1/filters/:id", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.deleteFilter(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.delete("/v1/filters/:id", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const data = await client.deleteFilter(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,181 +1,172 @@
|
|||
import Router from "@koa/router";
|
||||
import { getClient } from "../index.js";
|
||||
import { ParsedUrlQuery } from "querystring";
|
||||
import {
|
||||
convertAccount,
|
||||
convertConversation,
|
||||
convertList,
|
||||
convertStatus,
|
||||
} from "../converters.js";
|
||||
import { convertAccount, convertList, } from "../converters.js";
|
||||
import { convertId, IdType } from "../../index.js";
|
||||
import authenticate from "@/server/api/authenticate.js";
|
||||
import { TimelineHelpers } from "@/server/api/mastodon/helpers/timeline.js";
|
||||
import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
|
||||
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
||||
import { convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "@/server/api/mastodon/endpoints/timeline.js";
|
||||
import { ListHelpers } from "@/server/api/mastodon/helpers/list.js";
|
||||
import { UserConverter } from "@/server/api/mastodon/converters/user.js";
|
||||
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||
|
||||
export function setupEndpointsList(router: Router): void {
|
||||
router.get("/v1/lists", async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
router.get("/v1/lists", async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = await ListHelpers.getLists(user)
|
||||
.then(p => p.map(list => convertList(list)));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id",
|
||||
async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
ctx.body = await ListHelpers.getLists(user)
|
||||
.then(p => p.map(list => convertList(list)));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id",
|
||||
async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
|
||||
ctx.body = await ListHelpers.getList(user, id)
|
||||
.then(p => convertList(p));
|
||||
} catch (e: any) {
|
||||
ctx.status = 404;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post("/v1/lists", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.createList((ctx.request.body as any).title);
|
||||
ctx.body = convertList(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.put<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.updateList(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
(ctx.request.body as any).title,
|
||||
);
|
||||
ctx.body = convertList(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.deleteList(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
ctx.body = await ListHelpers.getList(user, id)
|
||||
.then(p => convertList(p));
|
||||
} catch (e: any) {
|
||||
ctx.status = 404;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post("/v1/lists", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.createList((ctx.request.body as any).title);
|
||||
ctx.body = convertList(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.put<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.updateList(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
(ctx.request.body as any).title,
|
||||
);
|
||||
ctx.body = convertList(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.deleteList(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
|
||||
const res = await ListHelpers.getListUsers(user, id, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const accounts = await UserConverter.encodeMany(res.data);
|
||||
ctx.body = accounts.map(account => convertAccount(account));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res);
|
||||
} catch (e: any) {
|
||||
ctx.status = 404;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.addAccountsToList(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
(ctx.query.account_ids as string[]).map((id) =>
|
||||
convertId(id, IdType.IceshrimpId),
|
||||
),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.deleteAccountsFromList(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
(ctx.query.account_ids as string[]).map((id) =>
|
||||
convertId(id, IdType.IceshrimpId),
|
||||
),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
|
||||
const res = await ListHelpers.getListUsers(user, id, args.max_id, args.since_id, args.min_id, args.limit);
|
||||
const accounts = await UserConverter.encodeMany(res.data);
|
||||
ctx.body = accounts.map(account => convertAccount(account));
|
||||
PaginationHelpers.appendLinkPaginationHeader(args, ctx, res);
|
||||
} catch (e: any) {
|
||||
ctx.status = 404;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.addAccountsToList(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
(ctx.query.account_ids as string[]).map((id) =>
|
||||
convertId(id, IdType.IceshrimpId),
|
||||
),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.delete<{ Params: { id: string } }>(
|
||||
"/v1/lists/:id/accounts",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.deleteAccountsFromList(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
(ctx.query.account_ids as string[]).map((id) =>
|
||||
convertId(id, IdType.IceshrimpId),
|
||||
),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,85 +7,85 @@ import { MediaHelpers } from "@/server/api/mastodon/helpers/media.js";
|
|||
import { FileConverter } from "@/server/api/mastodon/converters/file.js";
|
||||
|
||||
export function setupEndpointsMedia(router: Router, fileRouter: Router, upload: multer.Instance): void {
|
||||
router.get<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
router.get<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const file = await MediaHelpers.getMediaPacked(user, id);
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const file = await MediaHelpers.getMediaPacked(user, id);
|
||||
|
||||
if (!file) {
|
||||
ctx.status = 404;
|
||||
ctx.body = { error: "File not found" };
|
||||
return;
|
||||
}
|
||||
if (!file) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {error: "File not found"};
|
||||
return;
|
||||
}
|
||||
|
||||
const attachment = FileConverter.encode(file);
|
||||
ctx.body = convertAttachment(attachment);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 500;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.put<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
const attachment = FileConverter.encode(file);
|
||||
ctx.body = convertAttachment(attachment);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 500;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.put<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const file = await MediaHelpers.getMedia(user, id);
|
||||
const id = convertId(ctx.params.id, IdType.IceshrimpId);
|
||||
const file = await MediaHelpers.getMedia(user, id);
|
||||
|
||||
if (!file) {
|
||||
ctx.status = 404;
|
||||
ctx.body = { error: "File not found" };
|
||||
return;
|
||||
}
|
||||
if (!file) {
|
||||
ctx.status = 404;
|
||||
ctx.body = {error: "File not found"};
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await MediaHelpers.updateMedia(user, file, ctx.request.body)
|
||||
.then(p => FileConverter.encode(p));
|
||||
ctx.body = convertAttachment(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
const result = await MediaHelpers.updateMedia(user, file, ctx.request.body)
|
||||
.then(p => FileConverter.encode(p));
|
||||
ctx.body = convertAttachment(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
fileRouter.post(["/v2/media", "/v1/media"], upload.single("file"), async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
fileRouter.post(["/v2/media", "/v1/media"], upload.single("file"), async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await ctx.file;
|
||||
if (!file) {
|
||||
ctx.body = { error: "No image" };
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
const result = await MediaHelpers.uploadMedia(user, file, ctx.request.body)
|
||||
.then(p => FileConverter.encode(p));
|
||||
ctx.body = convertAttachment(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 500;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
const file = await ctx.file;
|
||||
if (!file) {
|
||||
ctx.body = {error: "No image"};
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
const result = await MediaHelpers.uploadMedia(user, file, ctx.request.body)
|
||||
.then(p => FileConverter.encode(p));
|
||||
ctx.body = convertAttachment(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 500;
|
||||
ctx.body = {error: e.message};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,70 +1,70 @@
|
|||
import { Entity } from "megalodon";
|
||||
import config from "@/config/index.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { Users, Notes } from "@/models/index.js";
|
||||
import { Notes, Users } from "@/models/index.js";
|
||||
import { IsNull } from "typeorm";
|
||||
import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from "@/const.js";
|
||||
import { FILE_TYPE_BROWSERSAFE, MAX_NOTE_TEXT_LENGTH } from "@/const.js";
|
||||
|
||||
// TODO: add iceshrimp features
|
||||
export async function getInstance(
|
||||
response: Entity.Instance,
|
||||
contact: Entity.Account,
|
||||
response: Entity.Instance,
|
||||
contact: Entity.Account,
|
||||
) {
|
||||
const [meta, totalUsers, totalStatuses] = await Promise.all([
|
||||
fetchMeta(true),
|
||||
Users.count({ where: { host: IsNull() } }),
|
||||
Notes.count({ where: { userHost: IsNull() } }),
|
||||
]);
|
||||
const [meta, totalUsers, totalStatuses] = await Promise.all([
|
||||
fetchMeta(true),
|
||||
Users.count({where: {host: IsNull()}}),
|
||||
Notes.count({where: {userHost: IsNull()}}),
|
||||
]);
|
||||
|
||||
return {
|
||||
uri: response.uri,
|
||||
title: response.title || "Iceshrimp",
|
||||
short_description:
|
||||
response.description?.substring(0, 50) || "See real server website",
|
||||
description:
|
||||
response.description ||
|
||||
"This is a vanilla Iceshrimp Instance. It doesn't seem to have a description.",
|
||||
email: response.email || "",
|
||||
version: `4.1.0 (compatible; Iceshrimp ${config.version})`,
|
||||
urls: response.urls,
|
||||
stats: {
|
||||
user_count: await totalUsers,
|
||||
status_count: await totalStatuses,
|
||||
domain_count: response.stats.domain_count,
|
||||
},
|
||||
thumbnail: response.thumbnail || "/static-assets/transparent.png",
|
||||
languages: meta.langs,
|
||||
registrations: !meta.disableRegistration || response.registrations,
|
||||
approval_required: !response.registrations,
|
||||
invites_enabled: response.registrations,
|
||||
configuration: {
|
||||
accounts: {
|
||||
max_featured_tags: 20,
|
||||
},
|
||||
statuses: {
|
||||
max_characters: MAX_NOTE_TEXT_LENGTH,
|
||||
max_media_attachments: 16,
|
||||
characters_reserved_per_url: response.uri.length,
|
||||
},
|
||||
media_attachments: {
|
||||
supported_mime_types: FILE_TYPE_BROWSERSAFE,
|
||||
image_size_limit: 10485760,
|
||||
image_matrix_limit: 16777216,
|
||||
video_size_limit: 41943040,
|
||||
video_frame_rate_limit: 60,
|
||||
video_matrix_limit: 2304000,
|
||||
},
|
||||
polls: {
|
||||
max_options: 10,
|
||||
max_characters_per_option: 50,
|
||||
min_expiration: 50,
|
||||
max_expiration: 2629746,
|
||||
},
|
||||
reactions: {
|
||||
max_reactions: 1,
|
||||
},
|
||||
},
|
||||
contact_account: contact,
|
||||
rules: [],
|
||||
};
|
||||
return {
|
||||
uri: response.uri,
|
||||
title: response.title || "Iceshrimp",
|
||||
short_description:
|
||||
response.description?.substring(0, 50) || "See real server website",
|
||||
description:
|
||||
response.description ||
|
||||
"This is a vanilla Iceshrimp Instance. It doesn't seem to have a description.",
|
||||
email: response.email || "",
|
||||
version: `4.1.0 (compatible; Iceshrimp ${config.version})`,
|
||||
urls: response.urls,
|
||||
stats: {
|
||||
user_count: await totalUsers,
|
||||
status_count: await totalStatuses,
|
||||
domain_count: response.stats.domain_count,
|
||||
},
|
||||
thumbnail: response.thumbnail || "/static-assets/transparent.png",
|
||||
languages: meta.langs,
|
||||
registrations: !meta.disableRegistration || response.registrations,
|
||||
approval_required: !response.registrations,
|
||||
invites_enabled: response.registrations,
|
||||
configuration: {
|
||||
accounts: {
|
||||
max_featured_tags: 20,
|
||||
},
|
||||
statuses: {
|
||||
max_characters: MAX_NOTE_TEXT_LENGTH,
|
||||
max_media_attachments: 16,
|
||||
characters_reserved_per_url: response.uri.length,
|
||||
},
|
||||
media_attachments: {
|
||||
supported_mime_types: FILE_TYPE_BROWSERSAFE,
|
||||
image_size_limit: 10485760,
|
||||
image_matrix_limit: 16777216,
|
||||
video_size_limit: 41943040,
|
||||
video_frame_rate_limit: 60,
|
||||
video_matrix_limit: 2304000,
|
||||
},
|
||||
polls: {
|
||||
max_options: 10,
|
||||
max_characters_per_option: 50,
|
||||
min_expiration: 50,
|
||||
max_expiration: 2629746,
|
||||
},
|
||||
reactions: {
|
||||
max_reactions: 1,
|
||||
},
|
||||
},
|
||||
contact_account: contact,
|
||||
rules: [],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,136 +1,131 @@
|
|||
import Router from "@koa/router";
|
||||
import { getClient } from "@/server/api/mastodon/index.js";
|
||||
import { convertId, IdType } from "@/misc/convert-id.js";
|
||||
import {
|
||||
convertAccount,
|
||||
convertAnnouncement,
|
||||
convertAttachment,
|
||||
convertFilter
|
||||
} from "@/server/api/mastodon/converters.js";
|
||||
import { convertAccount, convertAnnouncement, convertFilter } from "@/server/api/mastodon/converters.js";
|
||||
import { Users } from "@/models/index.js";
|
||||
import { getInstance } from "@/server/api/mastodon/endpoints/meta.js";
|
||||
import { IsNull } from "typeorm";
|
||||
|
||||
export function setupEndpointsMisc(router: Router): void {
|
||||
router.get("/v1/custom_emojis", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getInstanceCustomEmojis();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/custom_emojis", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getInstanceCustomEmojis();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/instance", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getInstance();
|
||||
const admin = await Users.findOne({
|
||||
where: {
|
||||
host: IsNull(),
|
||||
isAdmin: true,
|
||||
isDeleted: false,
|
||||
isSuspended: false,
|
||||
},
|
||||
order: { id: "ASC" },
|
||||
});
|
||||
const contact =
|
||||
admin == null
|
||||
? null
|
||||
: convertAccount((await client.getAccount(admin.id)).data);
|
||||
ctx.body = await getInstance(data.data, contact);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/instance", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getInstance();
|
||||
const admin = await Users.findOne({
|
||||
where: {
|
||||
host: IsNull(),
|
||||
isAdmin: true,
|
||||
isDeleted: false,
|
||||
isSuspended: false,
|
||||
},
|
||||
order: {id: "ASC"},
|
||||
});
|
||||
const contact =
|
||||
admin == null
|
||||
? null
|
||||
: convertAccount((await client.getAccount(admin.id)).data);
|
||||
ctx.body = await getInstance(data.data, contact);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/announcements", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getInstanceAnnouncements();
|
||||
ctx.body = data.data.map((announcement) =>
|
||||
convertAnnouncement(announcement),
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/announcements", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getInstanceAnnouncements();
|
||||
ctx.body = data.data.map((announcement) =>
|
||||
convertAnnouncement(announcement),
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/announcements/:id/dismiss",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.dismissInstanceAnnouncement(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.post<{ Params: { id: string } }>(
|
||||
"/v1/announcements/:id/dismiss",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.dismissInstanceAnnouncement(
|
||||
convertId(ctx.params.id, IdType.IceshrimpId),
|
||||
);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.get("/v1/filters", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getFilters();
|
||||
ctx.body = data.data.map((filter) => convertFilter(filter));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/filters", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getFilters();
|
||||
ctx.body = data.data.map((filter) => convertFilter(filter));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/trends", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getInstanceTrends();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/trends", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getInstanceTrends();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/preferences", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getPreferences();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/preferences", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getPreferences();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,95 +8,95 @@ import { NotificationHelpers } from "@/server/api/mastodon/helpers/notification.
|
|||
import { NotificationConverter } from "@/server/api/mastodon/converters/notification.js";
|
||||
|
||||
export function setupEndpointsNotifications(router: Router): void {
|
||||
router.get("/v1/notifications", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
router.get("/v1/notifications", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)), ['types[]', 'exclude_types[]']);
|
||||
const data = NotificationHelpers.getNotifications(user, args.max_id, args.since_id, args.min_id, args.limit, args['types[]'], args['exclude_types[]'], args.account_id)
|
||||
.then(p => NotificationConverter.encodeMany(p, user, cache))
|
||||
.then(p => p.map(n => convertNotification(n)));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)), ['types[]', 'exclude_types[]']);
|
||||
const data = NotificationHelpers.getNotifications(user, args.max_id, args.since_id, args.min_id, args.limit, args['types[]'], args['exclude_types[]'], args.account_id)
|
||||
.then(p => NotificationConverter.encodeMany(p, user, cache))
|
||||
.then(p => p.map(n => convertNotification(n)));
|
||||
|
||||
ctx.body = await data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
ctx.body = await data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/v1/notifications/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
router.get("/v1/notifications/:id", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = await NotificationHelpers.getNotification(convertId(ctx.params.id, IdType.IceshrimpId), user);
|
||||
if (notification === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
const notification = await NotificationHelpers.getNotification(convertId(ctx.params.id, IdType.IceshrimpId), user);
|
||||
if (notification === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = convertNotification(await NotificationConverter.encode(notification, user));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
ctx.body = convertNotification(await NotificationConverter.encode(notification, user));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/v1/notifications/clear", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
router.post("/v1/notifications/clear", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
await NotificationHelpers.clearAllNotifications(user);
|
||||
ctx.body = {};
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
await NotificationHelpers.clearAllNotifications(user);
|
||||
ctx.body = {};
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/v1/notifications/:id/dismiss", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
router.post("/v1/notifications/:id/dismiss", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? null;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = await NotificationHelpers.getNotification(convertId(ctx.params.id, IdType.IceshrimpId), user);
|
||||
if (notification === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
const notification = await NotificationHelpers.getNotification(convertId(ctx.params.id, IdType.IceshrimpId), user);
|
||||
if (notification === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
await NotificationHelpers.dismissNotification(notification.id, user);
|
||||
ctx.body = {};
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
await NotificationHelpers.dismissNotification(notification.id, user);
|
||||
ctx.body = {};
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import megalodon, { MegalodonInterface } from "megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { getClient } from "../index.js";
|
||||
import axios from "axios";
|
||||
import { Converter } from "megalodon";
|
||||
import Router from "@koa/router";
|
||||
import axios from "axios";
|
||||
import { argsToBools, convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "./timeline.js";
|
||||
import { convertAccount, convertSearch, convertStatus } from "../converters.js";
|
||||
import authenticate from "@/server/api/authenticate.js";
|
||||
|
@ -10,137 +8,139 @@ import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
|||
import { SearchHelpers } from "@/server/api/mastodon/helpers/search.js";
|
||||
|
||||
export function setupEndpointsSearch(router: Router): void {
|
||||
router.get("/v1/search", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
router.get("/v1/search", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query), ['resolve', 'following', 'exclude_unreviewed'])));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const result = await SearchHelpers.search(user, args.q, args.type, args.resolve, args.following, args.account_id, args['exclude_unreviewed'], args.max_id, args.min_id, args.limit, args.offset, cache);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query), ['resolve', 'following', 'exclude_unreviewed'])));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const result = await SearchHelpers.search(user, args.q, args.type, args.resolve, args.following, args.account_id, args['exclude_unreviewed'], args.max_id, args.min_id, args.limit, args.offset, cache);
|
||||
|
||||
ctx.body = {
|
||||
...convertSearch(result),
|
||||
hashtags: result.hashtags.map(p => p.name),
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
router.get("/v2/search", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
ctx.body = {
|
||||
...convertSearch(result),
|
||||
hashtags: result.hashtags.map(p => p.name),
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 400;
|
||||
ctx.body = {error: e.message};
|
||||
}
|
||||
});
|
||||
router.get("/v2/search", async (ctx) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query), ['resolve', 'following', 'exclude_unreviewed'])));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const result = await SearchHelpers.search(user, args.q, args.type, args.resolve, args.following, args.account_id, args['exclude_unreviewed'], args.max_id, args.min_id, args.limit, args.offset, cache);
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query), ['resolve', 'following', 'exclude_unreviewed'])));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const result = await SearchHelpers.search(user, args.q, args.type, args.resolve, args.following, args.account_id, args['exclude_unreviewed'], args.max_id, args.min_id, args.limit, args.offset, cache);
|
||||
|
||||
ctx.body = convertSearch(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 400;
|
||||
ctx.body = { error: e.message };
|
||||
}
|
||||
});
|
||||
router.get("/v1/trends/statuses", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
try {
|
||||
const data = await getHighlight(
|
||||
BASE_URL,
|
||||
ctx.request.hostname,
|
||||
accessTokens,
|
||||
);
|
||||
ctx.body = data.map((status) => convertStatus(status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v2/suggestions", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
try {
|
||||
const query: any = ctx.query;
|
||||
let data = await getFeaturedUser(
|
||||
BASE_URL,
|
||||
ctx.request.hostname,
|
||||
accessTokens,
|
||||
query.limit || 20,
|
||||
);
|
||||
data = data.map((suggestion) => {
|
||||
suggestion.account = convertAccount(suggestion.account);
|
||||
return suggestion;
|
||||
});
|
||||
console.log(data);
|
||||
ctx.body = data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
ctx.body = convertSearch(result);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 400;
|
||||
ctx.body = {error: e.message};
|
||||
}
|
||||
});
|
||||
router.get("/v1/trends/statuses", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
try {
|
||||
const data = await getHighlight(
|
||||
BASE_URL,
|
||||
ctx.request.hostname,
|
||||
accessTokens,
|
||||
);
|
||||
ctx.body = data.map((status) => convertStatus(status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v2/suggestions", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
try {
|
||||
const query: any = ctx.query;
|
||||
let data = await getFeaturedUser(
|
||||
BASE_URL,
|
||||
ctx.request.hostname,
|
||||
accessTokens,
|
||||
query.limit || 20,
|
||||
);
|
||||
data = data.map((suggestion) => {
|
||||
suggestion.account = convertAccount(suggestion.account);
|
||||
return suggestion;
|
||||
});
|
||||
console.log(data);
|
||||
ctx.body = data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getHighlight(
|
||||
BASE_URL: string,
|
||||
domain: string,
|
||||
accessTokens: string | undefined,
|
||||
BASE_URL: string,
|
||||
domain: string,
|
||||
accessTokens: string | undefined,
|
||||
) {
|
||||
const accessTokenArr = accessTokens?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
try {
|
||||
const api = await axios.post(`${BASE_URL}/api/notes/featured`, {
|
||||
i: accessToken,
|
||||
});
|
||||
const data: MisskeyEntity.Note[] = api.data;
|
||||
return data.map((note) => new Converter(BASE_URL).note(note, domain));
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
console.log(e.response.data);
|
||||
return [];
|
||||
}
|
||||
const accessTokenArr = accessTokens?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
try {
|
||||
const api = await axios.post(`${BASE_URL}/api/notes/featured`, {
|
||||
i: accessToken,
|
||||
});
|
||||
const data: MisskeyEntity.Note[] = api.data;
|
||||
return data.map((note) => new Converter(BASE_URL).note(note, domain));
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
console.log(e.response.data);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getFeaturedUser(
|
||||
BASE_URL: string,
|
||||
host: string,
|
||||
accessTokens: string | undefined,
|
||||
limit: number,
|
||||
BASE_URL: string,
|
||||
host: string,
|
||||
accessTokens: string | undefined,
|
||||
limit: number,
|
||||
) {
|
||||
const accessTokenArr = accessTokens?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
try {
|
||||
const api = await axios.post(`${BASE_URL}/api/users`, {
|
||||
i: accessToken,
|
||||
limit,
|
||||
origin: "local",
|
||||
sort: "+follower",
|
||||
state: "alive",
|
||||
});
|
||||
const data: MisskeyEntity.UserDetail[] = api.data;
|
||||
console.log(data);
|
||||
return data.map((u) => {
|
||||
return {
|
||||
source: "past_interactions",
|
||||
account: new Converter(BASE_URL).userDetail(u, host),
|
||||
};
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
console.log(e.response.data);
|
||||
return [];
|
||||
}
|
||||
const accessTokenArr = accessTokens?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
try {
|
||||
const api = await axios.post(`${BASE_URL}/api/users`, {
|
||||
i: accessToken,
|
||||
limit,
|
||||
origin: "local",
|
||||
sort: "+follower",
|
||||
state: "alive",
|
||||
});
|
||||
const data: MisskeyEntity.UserDetail[] = api.data;
|
||||
console.log(data);
|
||||
return data.map((u) => {
|
||||
return {
|
||||
source: "past_interactions",
|
||||
account: new Converter(BASE_URL).userDetail(u, host),
|
||||
};
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
console.log(e.response.data);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,7 @@
|
|||
import Router from "@koa/router";
|
||||
import { getClient } from "../index.js";
|
||||
import { ParsedUrlQuery } from "querystring";
|
||||
import {
|
||||
convertAccount,
|
||||
convertConversation,
|
||||
convertList,
|
||||
convertStatus,
|
||||
} from "../converters.js";
|
||||
import { convertConversation, convertStatus, } from "../converters.js";
|
||||
import { convertId, IdType } from "../../index.js";
|
||||
import authenticate from "@/server/api/authenticate.js";
|
||||
import { TimelineHelpers } from "@/server/api/mastodon/helpers/timeline.js";
|
||||
|
@ -14,161 +9,161 @@ import { NoteConverter } from "@/server/api/mastodon/converters/note.js";
|
|||
import { UserHelpers } from "@/server/api/mastodon/helpers/user.js";
|
||||
|
||||
export function limitToInt(q: ParsedUrlQuery, additional: string[] = []) {
|
||||
let object: any = q;
|
||||
if (q.limit)
|
||||
if (typeof q.limit === "string") object.limit = parseInt(q.limit, 10);
|
||||
if (q.offset)
|
||||
if (typeof q.offset === "string") object.offset = parseInt(q.offset, 10);
|
||||
for (const key of additional)
|
||||
if (typeof q[key] === "string") object[key] = parseInt(<string>q[key], 10);
|
||||
return object;
|
||||
let object: any = q;
|
||||
if (q.limit)
|
||||
if (typeof q.limit === "string") object.limit = parseInt(q.limit, 10);
|
||||
if (q.offset)
|
||||
if (typeof q.offset === "string") object.offset = parseInt(q.offset, 10);
|
||||
for (const key of additional)
|
||||
if (typeof q[key] === "string") object[key] = parseInt(<string>q[key], 10);
|
||||
return object;
|
||||
}
|
||||
|
||||
export function argsToBools(q: ParsedUrlQuery, additional: string[] = []) {
|
||||
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
|
||||
const toBoolean = (value: string) =>
|
||||
!["0", "f", "F", "false", "FALSE", "off", "OFF"].includes(value);
|
||||
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
|
||||
const toBoolean = (value: string) =>
|
||||
!["0", "f", "F", "false", "FALSE", "off", "OFF"].includes(value);
|
||||
|
||||
// Keys taken from:
|
||||
// - https://docs.joinmastodon.org/methods/accounts/#statuses
|
||||
// - https://docs.joinmastodon.org/methods/timelines/#public
|
||||
// - https://docs.joinmastodon.org/methods/timelines/#tag
|
||||
let keys = ['only_media', 'exclude_replies', 'exclude_reblogs', 'pinned', 'local', 'remote']
|
||||
let object: any = q;
|
||||
// Keys taken from:
|
||||
// - https://docs.joinmastodon.org/methods/accounts/#statuses
|
||||
// - https://docs.joinmastodon.org/methods/timelines/#public
|
||||
// - https://docs.joinmastodon.org/methods/timelines/#tag
|
||||
let keys = ['only_media', 'exclude_replies', 'exclude_reblogs', 'pinned', 'local', 'remote']
|
||||
let object: any = q;
|
||||
|
||||
for (const key of keys)
|
||||
if (q[key] && typeof q[key] === "string")
|
||||
object[key] = toBoolean(<string>q[key]);
|
||||
for (const key of keys)
|
||||
if (q[key] && typeof q[key] === "string")
|
||||
object[key] = toBoolean(<string>q[key]);
|
||||
|
||||
return object;
|
||||
return object;
|
||||
}
|
||||
|
||||
export function convertPaginationArgsIds(q: ParsedUrlQuery) {
|
||||
if (typeof q.min_id === "string")
|
||||
q.min_id = convertId(q.min_id, IdType.IceshrimpId);
|
||||
if (typeof q.max_id === "string")
|
||||
q.max_id = convertId(q.max_id, IdType.IceshrimpId);
|
||||
if (typeof q.since_id === "string")
|
||||
q.since_id = convertId(q.since_id, IdType.IceshrimpId);
|
||||
return q;
|
||||
if (typeof q.min_id === "string")
|
||||
q.min_id = convertId(q.min_id, IdType.IceshrimpId);
|
||||
if (typeof q.max_id === "string")
|
||||
q.max_id = convertId(q.max_id, IdType.IceshrimpId);
|
||||
if (typeof q.since_id === "string")
|
||||
q.since_id = convertId(q.since_id, IdType.IceshrimpId);
|
||||
return q;
|
||||
}
|
||||
|
||||
export function normalizeUrlQuery(q: ParsedUrlQuery, arrayKeys: string[] = []): any {
|
||||
const dict: any = {};
|
||||
const dict: any = {};
|
||||
|
||||
for (const k in q) {
|
||||
if (arrayKeys.includes(k))
|
||||
dict[k] = Array.isArray(q[k]) ? q[k] : [q[k]];
|
||||
else
|
||||
dict[k] = Array.isArray(q[k]) ? q[k]?.at(-1) : q[k];
|
||||
}
|
||||
for (const k in q) {
|
||||
if (arrayKeys.includes(k))
|
||||
dict[k] = Array.isArray(q[k]) ? q[k] : [q[k]];
|
||||
else
|
||||
dict[k] = Array.isArray(q[k]) ? q[k]?.at(-1) : q[k];
|
||||
}
|
||||
|
||||
return dict;
|
||||
return dict;
|
||||
}
|
||||
|
||||
export function setupEndpointsTimeline(router: Router): void {
|
||||
router.get("/v1/timelines/public", async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
router.get("/v1/timelines/public", async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const tl = await TimelineHelpers.getPublicTimeline(user, args.max_id, args.since_id, args.min_id, args.limit, args.only_media, args.local, args.remote)
|
||||
.then(n => NoteConverter.encodeMany(n, user, cache));
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = tl.map(s => convertStatus(s));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { hashtag: string } }>(
|
||||
"/v1/timelines/tag/:hashtag",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getTagTimeline(
|
||||
ctx.params.hashtag,
|
||||
convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))),
|
||||
);
|
||||
ctx.body = data.data.map((status) => convertStatus(status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get("/v1/timelines/home", async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const tl = await TimelineHelpers.getPublicTimeline(user, args.max_id, args.since_id, args.min_id, args.limit, args.only_media, args.local, args.remote)
|
||||
.then(n => NoteConverter.encodeMany(n, user, cache));
|
||||
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
ctx.body = tl.map(s => convertStatus(s));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { hashtag: string } }>(
|
||||
"/v1/timelines/tag/:hashtag",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getTagTimeline(
|
||||
ctx.params.hashtag,
|
||||
convertPaginationArgsIds(argsToBools(limitToInt(ctx.query))),
|
||||
);
|
||||
ctx.body = data.data.map((status) => convertStatus(status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get("/v1/timelines/home", async (ctx, reply) => {
|
||||
try {
|
||||
const auth = await authenticate(ctx.headers.authorization, null);
|
||||
const user = auth[0] ?? undefined;
|
||||
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const tl = await TimelineHelpers.getHomeTimeline(user, args.max_id, args.since_id, args.min_id, args.limit)
|
||||
.then(n => NoteConverter.encodeMany(n, user, cache));
|
||||
if (!user) {
|
||||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = tl.map(s => convertStatus(s));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { listId: string } }>(
|
||||
"/v1/timelines/list/:listId",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getListTimeline(
|
||||
convertId(ctx.params.listId, IdType.IceshrimpId),
|
||||
convertPaginationArgsIds(limitToInt(ctx.query)),
|
||||
);
|
||||
ctx.body = data.data.map((status) => convertStatus(status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get("/v1/conversations", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getConversationTimeline(
|
||||
convertPaginationArgsIds(limitToInt(ctx.query)),
|
||||
);
|
||||
ctx.body = data.data.map((conversation) =>
|
||||
convertConversation(conversation),
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
const args = normalizeUrlQuery(convertPaginationArgsIds(limitToInt(ctx.query)));
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const tl = await TimelineHelpers.getHomeTimeline(user, args.max_id, args.since_id, args.min_id, args.limit)
|
||||
.then(n => NoteConverter.encodeMany(n, user, cache));
|
||||
|
||||
ctx.body = tl.map(s => convertStatus(s));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get<{ Params: { listId: string } }>(
|
||||
"/v1/timelines/list/:listId",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getListTimeline(
|
||||
convertId(ctx.params.listId, IdType.IceshrimpId),
|
||||
convertPaginationArgsIds(limitToInt(ctx.query)),
|
||||
);
|
||||
ctx.body = data.data.map((status) => convertStatus(status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get("/v1/conversations", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getConversationTimeline(
|
||||
convertPaginationArgsIds(limitToInt(ctx.query)),
|
||||
);
|
||||
ctx.body = data.data.map((conversation) =>
|
||||
convertConversation(conversation),
|
||||
);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,31 +2,31 @@
|
|||
/// <reference path="source.ts" />
|
||||
/// <reference path="field.ts" />
|
||||
namespace MastodonEntity {
|
||||
export type Account = {
|
||||
id: string;
|
||||
username: string;
|
||||
acct: string;
|
||||
display_name: string;
|
||||
locked: boolean;
|
||||
created_at: string;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
statuses_count: number;
|
||||
note: string;
|
||||
url: string;
|
||||
avatar: string;
|
||||
avatar_static: string;
|
||||
header: string;
|
||||
header_static: string;
|
||||
emojis: Array<Emoji>;
|
||||
moved: Account | null;
|
||||
fields: Array<Field>;
|
||||
bot: boolean | null;
|
||||
discoverable: boolean;
|
||||
source?: Source;
|
||||
};
|
||||
export type Account = {
|
||||
id: string;
|
||||
username: string;
|
||||
acct: string;
|
||||
display_name: string;
|
||||
locked: boolean;
|
||||
created_at: string;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
statuses_count: number;
|
||||
note: string;
|
||||
url: string;
|
||||
avatar: string;
|
||||
avatar_static: string;
|
||||
header: string;
|
||||
header_static: string;
|
||||
emojis: Array<Emoji>;
|
||||
moved: Account | null;
|
||||
fields: Array<Field>;
|
||||
bot: boolean | null;
|
||||
discoverable: boolean;
|
||||
source?: Source;
|
||||
};
|
||||
|
||||
export type MutedAccount = Account | {
|
||||
mute_expires_at: string | null;
|
||||
}
|
||||
export type MutedAccount = Account | {
|
||||
mute_expires_at: string | null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
namespace MastodonEntity {
|
||||
export type Activity = {
|
||||
week: string;
|
||||
statuses: string;
|
||||
logins: string;
|
||||
registrations: string;
|
||||
};
|
||||
export type Activity = {
|
||||
week: string;
|
||||
statuses: string;
|
||||
logins: string;
|
||||
registrations: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,32 +3,32 @@
|
|||
/// <reference path="reaction.ts" />
|
||||
|
||||
namespace MastodonEntity {
|
||||
export type Announcement = {
|
||||
id: string;
|
||||
content: string;
|
||||
starts_at: string | null;
|
||||
ends_at: string | null;
|
||||
published: boolean;
|
||||
all_day: boolean;
|
||||
published_at: string;
|
||||
updated_at: string;
|
||||
read?: boolean;
|
||||
mentions: Array<AnnouncementAccount>;
|
||||
statuses: Array<AnnouncementStatus>;
|
||||
tags: Array<Tag>;
|
||||
emojis: Array<Emoji>;
|
||||
reactions: Array<Reaction>;
|
||||
};
|
||||
export type Announcement = {
|
||||
id: string;
|
||||
content: string;
|
||||
starts_at: string | null;
|
||||
ends_at: string | null;
|
||||
published: boolean;
|
||||
all_day: boolean;
|
||||
published_at: string;
|
||||
updated_at: string;
|
||||
read?: boolean;
|
||||
mentions: Array<AnnouncementAccount>;
|
||||
statuses: Array<AnnouncementStatus>;
|
||||
tags: Array<Tag>;
|
||||
emojis: Array<Emoji>;
|
||||
reactions: Array<Reaction>;
|
||||
};
|
||||
|
||||
export type AnnouncementAccount = {
|
||||
id: string;
|
||||
username: string;
|
||||
url: string;
|
||||
acct: string;
|
||||
};
|
||||
export type AnnouncementAccount = {
|
||||
id: string;
|
||||
username: string;
|
||||
url: string;
|
||||
acct: string;
|
||||
};
|
||||
|
||||
export type AnnouncementStatus = {
|
||||
id: string;
|
||||
url: string;
|
||||
};
|
||||
export type AnnouncementStatus = {
|
||||
id: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace MastodonEntity {
|
||||
export type Application = {
|
||||
name: string;
|
||||
website?: string | null;
|
||||
vapid_key?: string | null;
|
||||
};
|
||||
export type Application = {
|
||||
name: string;
|
||||
website?: string | null;
|
||||
vapid_key?: string | null;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
/// <reference path="attachment.ts" />
|
||||
namespace MastodonEntity {
|
||||
export type AsyncAttachment = {
|
||||
id: string;
|
||||
type: "unknown" | "image" | "gifv" | "video" | "audio";
|
||||
url: string | null;
|
||||
remote_url: string | null;
|
||||
preview_url: string;
|
||||
text_url: string | null;
|
||||
meta: Meta | null;
|
||||
description: string | null;
|
||||
blurhash: string | null;
|
||||
};
|
||||
export type AsyncAttachment = {
|
||||
id: string;
|
||||
type: "unknown" | "image" | "gifv" | "video" | "audio";
|
||||
url: string | null;
|
||||
remote_url: string | null;
|
||||
preview_url: string;
|
||||
text_url: string | null;
|
||||
meta: Meta | null;
|
||||
description: string | null;
|
||||
blurhash: string | null;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,49 +1,49 @@
|
|||
namespace MastodonEntity {
|
||||
export type Sub = {
|
||||
// For Image, Gifv, and Video
|
||||
width?: number;
|
||||
height?: number;
|
||||
size?: string;
|
||||
aspect?: number;
|
||||
export type Sub = {
|
||||
// For Image, Gifv, and Video
|
||||
width?: number;
|
||||
height?: number;
|
||||
size?: string;
|
||||
aspect?: number;
|
||||
|
||||
// For Gifv and Video
|
||||
frame_rate?: string;
|
||||
// For Gifv and Video
|
||||
frame_rate?: string;
|
||||
|
||||
// For Audio, Gifv, and Video
|
||||
duration?: number;
|
||||
bitrate?: number;
|
||||
};
|
||||
// For Audio, Gifv, and Video
|
||||
duration?: number;
|
||||
bitrate?: number;
|
||||
};
|
||||
|
||||
export type Focus = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
export type Focus = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Meta = {
|
||||
original?: Sub;
|
||||
small?: Sub;
|
||||
focus?: Focus;
|
||||
length?: string;
|
||||
duration?: number;
|
||||
fps?: number;
|
||||
size?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
aspect?: number;
|
||||
audio_encode?: string;
|
||||
audio_bitrate?: string;
|
||||
audio_channel?: string;
|
||||
};
|
||||
export type Meta = {
|
||||
original?: Sub;
|
||||
small?: Sub;
|
||||
focus?: Focus;
|
||||
length?: string;
|
||||
duration?: number;
|
||||
fps?: number;
|
||||
size?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
aspect?: number;
|
||||
audio_encode?: string;
|
||||
audio_bitrate?: string;
|
||||
audio_channel?: string;
|
||||
};
|
||||
|
||||
export type Attachment = {
|
||||
id: string;
|
||||
type: "unknown" | "image" | "gifv" | "video" | "audio";
|
||||
url: string;
|
||||
remote_url: string | null;
|
||||
preview_url: string | null;
|
||||
text_url: string | null;
|
||||
meta: Meta | null;
|
||||
description: string | null;
|
||||
blurhash: string | null;
|
||||
};
|
||||
export type Attachment = {
|
||||
id: string;
|
||||
type: "unknown" | "image" | "gifv" | "video" | "audio";
|
||||
url: string;
|
||||
remote_url: string | null;
|
||||
preview_url: string | null;
|
||||
text_url: string | null;
|
||||
meta: Meta | null;
|
||||
description: string | null;
|
||||
blurhash: string | null;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
namespace MastodonEntity {
|
||||
export type Card = {
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
type: "link" | "photo" | "video" | "rich";
|
||||
image?: string;
|
||||
author_name?: string;
|
||||
author_url?: string;
|
||||
provider_name?: string;
|
||||
provider_url?: string;
|
||||
html?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
export type Card = {
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
type: "link" | "photo" | "video" | "rich";
|
||||
image?: string;
|
||||
author_name?: string;
|
||||
author_url?: string;
|
||||
provider_name?: string;
|
||||
provider_url?: string;
|
||||
html?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/// <reference path="status.ts" />
|
||||
|
||||
namespace MastodonEntity {
|
||||
export type Context = {
|
||||
ancestors: Array<Status>;
|
||||
descendants: Array<Status>;
|
||||
};
|
||||
export type Context = {
|
||||
ancestors: Array<Status>;
|
||||
descendants: Array<Status>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
/// <reference path="status.ts" />
|
||||
|
||||
namespace MastodonEntity {
|
||||
export type Conversation = {
|
||||
id: string;
|
||||
accounts: Array<Account>;
|
||||
last_status: Status | null;
|
||||
unread: boolean;
|
||||
};
|
||||
export type Conversation = {
|
||||
id: string;
|
||||
accounts: Array<Account>;
|
||||
last_status: Status | null;
|
||||
unread: boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
namespace MastodonEntity {
|
||||
export type Emoji = {
|
||||
shortcode: string;
|
||||
static_url: string;
|
||||
url: string;
|
||||
visible_in_picker: boolean;
|
||||
category: string;
|
||||
};
|
||||
export type Emoji = {
|
||||
shortcode: string;
|
||||
static_url: string;
|
||||
url: string;
|
||||
visible_in_picker: boolean;
|
||||
category: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
namespace MastodonEntity {
|
||||
export type FeaturedTag = {
|
||||
id: string;
|
||||
name: string;
|
||||
statuses_count: number;
|
||||
last_status_at: string;
|
||||
};
|
||||
export type FeaturedTag = {
|
||||
id: string;
|
||||
name: string;
|
||||
statuses_count: number;
|
||||
last_status_at: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace MastodonEntity {
|
||||
export type Field = {
|
||||
name: string;
|
||||
value: string;
|
||||
verified_at: string | null;
|
||||
};
|
||||
export type Field = {
|
||||
name: string;
|
||||
value: string;
|
||||
verified_at: string | null;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
namespace MastodonEntity {
|
||||
export type Filter = {
|
||||
id: string;
|
||||
phrase: string;
|
||||
context: Array<FilterContext>;
|
||||
expires_at: string | null;
|
||||
irreversible: boolean;
|
||||
whole_word: boolean;
|
||||
};
|
||||
export type Filter = {
|
||||
id: string;
|
||||
phrase: string;
|
||||
context: Array<FilterContext>;
|
||||
expires_at: string | null;
|
||||
irreversible: boolean;
|
||||
whole_word: boolean;
|
||||
};
|
||||
|
||||
export type FilterContext = string;
|
||||
export type FilterContext = string;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace MastodonEntity {
|
||||
export type History = {
|
||||
day: string;
|
||||
uses: number;
|
||||
accounts: number;
|
||||
};
|
||||
export type History = {
|
||||
day: string;
|
||||
uses: number;
|
||||
accounts: number;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
namespace MastodonEntity {
|
||||
export type IdentityProof = {
|
||||
provider: string;
|
||||
provider_username: string;
|
||||
updated_at: string;
|
||||
proof_url: string;
|
||||
profile_url: string;
|
||||
};
|
||||
export type IdentityProof = {
|
||||
provider: string;
|
||||
provider_username: string;
|
||||
updated_at: string;
|
||||
proof_url: string;
|
||||
profile_url: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,39 +3,39 @@
|
|||
/// <reference path="stats.ts" />
|
||||
|
||||
namespace MastodonEntity {
|
||||
export type Instance = {
|
||||
uri: string;
|
||||
title: string;
|
||||
description: string;
|
||||
email: string;
|
||||
version: string;
|
||||
thumbnail: string | null;
|
||||
urls: URLs;
|
||||
stats: Stats;
|
||||
languages: Array<string>;
|
||||
contact_account: Account | null;
|
||||
max_toot_chars?: number;
|
||||
registrations?: boolean;
|
||||
configuration?: {
|
||||
statuses: {
|
||||
max_characters: number;
|
||||
max_media_attachments: number;
|
||||
characters_reserved_per_url: number;
|
||||
};
|
||||
media_attachments: {
|
||||
supported_mime_types: Array<string>;
|
||||
image_size_limit: number;
|
||||
image_matrix_limit: number;
|
||||
video_size_limit: number;
|
||||
video_frame_limit: number;
|
||||
video_matrix_limit: number;
|
||||
};
|
||||
polls: {
|
||||
max_options: number;
|
||||
max_characters_per_option: number;
|
||||
min_expiration: number;
|
||||
max_expiration: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
export type Instance = {
|
||||
uri: string;
|
||||
title: string;
|
||||
description: string;
|
||||
email: string;
|
||||
version: string;
|
||||
thumbnail: string | null;
|
||||
urls: URLs;
|
||||
stats: Stats;
|
||||
languages: Array<string>;
|
||||
contact_account: Account | null;
|
||||
max_toot_chars?: number;
|
||||
registrations?: boolean;
|
||||
configuration?: {
|
||||
statuses: {
|
||||
max_characters: number;
|
||||
max_media_attachments: number;
|
||||
characters_reserved_per_url: number;
|
||||
};
|
||||
media_attachments: {
|
||||
supported_mime_types: Array<string>;
|
||||
image_size_limit: number;
|
||||
image_matrix_limit: number;
|
||||
video_size_limit: number;
|
||||
video_frame_limit: number;
|
||||
video_matrix_limit: number;
|
||||
};
|
||||
polls: {
|
||||
max_options: number;
|
||||
max_characters_per_option: number;
|
||||
min_expiration: number;
|
||||
max_expiration: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
namespace MastodonEntity {
|
||||
export type List = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
export type List = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
namespace MastodonEntity {
|
||||
export type Marker = {
|
||||
home?: {
|
||||
last_read_id: string;
|
||||
version: number;
|
||||
updated_at: string;
|
||||
};
|
||||
notifications?: {
|
||||
last_read_id: string;
|
||||
version: number;
|
||||
updated_at: string;
|
||||
unread_count?: number;
|
||||
};
|
||||
};
|
||||
export type Marker = {
|
||||
home?: {
|
||||
last_read_id: string;
|
||||
version: number;
|
||||
updated_at: string;
|
||||
};
|
||||
notifications?: {
|
||||
last_read_id: string;
|
||||
version: number;
|
||||
updated_at: string;
|
||||
unread_count?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
namespace MastodonEntity {
|
||||
export type Mention = {
|
||||
id: string;
|
||||
username: string;
|
||||
url: string;
|
||||
acct: string;
|
||||
};
|
||||
export type Mention = {
|
||||
id: string;
|
||||
username: string;
|
||||
url: string;
|
||||
acct: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,14 +2,22 @@
|
|||
/// <reference path="status.ts" />
|
||||
|
||||
namespace MastodonEntity {
|
||||
export type Notification = {
|
||||
account: Account;
|
||||
created_at: string;
|
||||
id: string;
|
||||
status?: Status;
|
||||
reaction?: Reaction;
|
||||
type: NotificationType;
|
||||
};
|
||||
export type Notification = {
|
||||
account: Account;
|
||||
created_at: string;
|
||||
id: string;
|
||||
status?: Status;
|
||||
reaction?: Reaction;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
export type NotificationType = 'follow' | 'favourite' | 'reblog' | 'mention' | 'reaction' | 'follow_request' | 'status' | 'poll';
|
||||
export type NotificationType =
|
||||
'follow'
|
||||
| 'favourite'
|
||||
| 'reblog'
|
||||
| 'mention'
|
||||
| 'reaction'
|
||||
| 'follow_request'
|
||||
| 'status'
|
||||
| 'poll';
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ namespace OAuth {
|
|||
export class AppData {
|
||||
public url: string | null;
|
||||
public session_token: string | null;
|
||||
|
||||
constructor(
|
||||
public id: string,
|
||||
public name: string,
|
||||
|
@ -54,9 +55,11 @@ namespace OAuth {
|
|||
get redirectUri() {
|
||||
return this.redirect_uri;
|
||||
}
|
||||
|
||||
get clientId() {
|
||||
return this.client_id;
|
||||
}
|
||||
|
||||
get clientSecret() {
|
||||
return this.client_secret;
|
||||
}
|
||||
|
@ -64,6 +67,7 @@ namespace OAuth {
|
|||
|
||||
export class TokenData {
|
||||
public _scope: string;
|
||||
|
||||
constructor(
|
||||
public access_token: string,
|
||||
public token_type: string,
|
||||
|
@ -96,21 +100,26 @@ namespace OAuth {
|
|||
get accessToken() {
|
||||
return this.access_token;
|
||||
}
|
||||
|
||||
get tokenType() {
|
||||
return this.token_type;
|
||||
}
|
||||
|
||||
get scope() {
|
||||
return this._scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application ID
|
||||
*/
|
||||
get createdAt() {
|
||||
return this.created_at;
|
||||
}
|
||||
|
||||
get expiresIn() {
|
||||
return this.expires_in;
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth Refresh Token
|
||||
*/
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
/// <reference path="poll_option.ts" />
|
||||
|
||||
namespace MastodonEntity {
|
||||
export type Poll = {
|
||||
id: string;
|
||||
expires_at: string | null;
|
||||
expired: boolean;
|
||||
multiple: boolean;
|
||||
votes_count: number;
|
||||
options: Array<PollOption>;
|
||||
voted: boolean;
|
||||
own_votes: Array<number>;
|
||||
};
|
||||
export type Poll = {
|
||||
id: string;
|
||||
expires_at: string | null;
|
||||
expired: boolean;
|
||||
multiple: boolean;
|
||||
votes_count: number;
|
||||
options: Array<PollOption>;
|
||||
voted: boolean;
|
||||
own_votes: Array<number>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
namespace MastodonEntity {
|
||||
export type PollOption = {
|
||||
title: string;
|
||||
votes_count: number | null;
|
||||
};
|
||||
export type PollOption = {
|
||||
title: string;
|
||||
votes_count: number | null;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
namespace MastodonEntity {
|
||||
export type Preferences = {
|
||||
"posting:default:visibility": "public" | "unlisted" | "private" | "direct";
|
||||
"posting:default:sensitive": boolean;
|
||||
"posting:default:language": string | null;
|
||||
"reading:expand:media": "default" | "show_all" | "hide_all";
|
||||
"reading:expand:spoilers": boolean;
|
||||
};
|
||||
export type Preferences = {
|
||||
"posting:default:visibility": "public" | "unlisted" | "private" | "direct";
|
||||
"posting:default:sensitive": boolean;
|
||||
"posting:default:language": string | null;
|
||||
"reading:expand:media": "default" | "show_all" | "hide_all";
|
||||
"reading:expand:spoilers": boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
namespace MastodonEntity {
|
||||
export type Alerts = {
|
||||
follow: boolean;
|
||||
favourite: boolean;
|
||||
mention: boolean;
|
||||
reblog: boolean;
|
||||
poll: boolean;
|
||||
};
|
||||
export type Alerts = {
|
||||
follow: boolean;
|
||||
favourite: boolean;
|
||||
mention: boolean;
|
||||
reblog: boolean;
|
||||
poll: boolean;
|
||||
};
|
||||
|
||||
export type PushSubscription = {
|
||||
id: string;
|
||||
endpoint: string;
|
||||
server_key: string;
|
||||
alerts: Alerts;
|
||||
};
|
||||
export type PushSubscription = {
|
||||
id: string;
|
||||
endpoint: string;
|
||||
server_key: string;
|
||||
alerts: Alerts;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/// <reference path="account.ts" />
|
||||
|
||||
namespace MastodonEntity {
|
||||
export type Reaction = {
|
||||
count: number;
|
||||
me: boolean;
|
||||
name: string;
|
||||
url?: string;
|
||||
static_url?: string;
|
||||
accounts?: Array<Account>;
|
||||
};
|
||||
export type Reaction = {
|
||||
count: number;
|
||||
me: boolean;
|
||||
name: string;
|
||||
url?: string;
|
||||
static_url?: string;
|
||||
accounts?: Array<Account>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
namespace MastodonEntity {
|
||||
export type Relationship = {
|
||||
id: string;
|
||||
following: boolean;
|
||||
followed_by: boolean;
|
||||
blocking: boolean;
|
||||
blocked_by: boolean;
|
||||
muting: boolean;
|
||||
muting_notifications: boolean;
|
||||
requested: boolean;
|
||||
domain_blocking: boolean;
|
||||
showing_reblogs: boolean;
|
||||
endorsed: boolean;
|
||||
notifying: boolean;
|
||||
note: string;
|
||||
};
|
||||
export type Relationship = {
|
||||
id: string;
|
||||
following: boolean;
|
||||
followed_by: boolean;
|
||||
blocking: boolean;
|
||||
blocked_by: boolean;
|
||||
muting: boolean;
|
||||
muting_notifications: boolean;
|
||||
requested: boolean;
|
||||
domain_blocking: boolean;
|
||||
showing_reblogs: boolean;
|
||||
endorsed: boolean;
|
||||
notifying: boolean;
|
||||
note: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
namespace MastodonEntity {
|
||||
export type Report = {
|
||||
id: string;
|
||||
action_taken: string;
|
||||
comment: string;
|
||||
account_id: string;
|
||||
status_ids: Array<string>;
|
||||
};
|
||||
export type Report = {
|
||||
id: string;
|
||||
action_taken: string;
|
||||
comment: string;
|
||||
account_id: string;
|
||||
status_ids: Array<string>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
/// <reference path="tag.ts" />
|
||||
|
||||
namespace MastodonEntity {
|
||||
export type Search = {
|
||||
accounts: Array<Account>;
|
||||
statuses: Array<Status>;
|
||||
hashtags: Array<Tag>;
|
||||
};
|
||||
export type Search = {
|
||||
accounts: Array<Account>;
|
||||
statuses: Array<Status>;
|
||||
hashtags: Array<Tag>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/// <reference path="attachment.ts" />
|
||||
/// <reference path="status_params.ts" />
|
||||
namespace MastodonEntity {
|
||||
export type ScheduledStatus = {
|
||||
id: string;
|
||||
scheduled_at: string;
|
||||
params: StatusParams;
|
||||
media_attachments: Array<Attachment>;
|
||||
};
|
||||
export type ScheduledStatus = {
|
||||
id: string;
|
||||
scheduled_at: string;
|
||||
params: StatusParams;
|
||||
media_attachments: Array<Attachment>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/// <reference path="field.ts" />
|
||||
namespace MastodonEntity {
|
||||
export type Source = {
|
||||
privacy: string | null;
|
||||
sensitive: boolean | null;
|
||||
language: string | null;
|
||||
note: string;
|
||||
fields: Array<Field>;
|
||||
};
|
||||
export type Source = {
|
||||
privacy: string | null;
|
||||
sensitive: boolean | null;
|
||||
language: string | null;
|
||||
note: string;
|
||||
fields: Array<Field>;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace MastodonEntity {
|
||||
export type Stats = {
|
||||
user_count: number;
|
||||
status_count: number;
|
||||
domain_count: number;
|
||||
};
|
||||
export type Stats = {
|
||||
user_count: number;
|
||||
status_count: number;
|
||||
domain_count: number;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,67 +9,67 @@
|
|||
/// <reference path="reaction.ts" />
|
||||
|
||||
namespace MastodonEntity {
|
||||
export type Status = {
|
||||
id: string;
|
||||
uri: string;
|
||||
url: string;
|
||||
account: Account;
|
||||
in_reply_to_id: string | null;
|
||||
in_reply_to_account_id: string | null;
|
||||
reblog: Status | null;
|
||||
content: string | undefined;
|
||||
text: string | null | undefined;
|
||||
created_at: string;
|
||||
emojis: Emoji[];
|
||||
replies_count: number;
|
||||
reblogs_count: number;
|
||||
favourites_count: number;
|
||||
reblogged: boolean | null;
|
||||
favourited: boolean | null;
|
||||
muted: boolean | null;
|
||||
sensitive: boolean;
|
||||
spoiler_text: string;
|
||||
visibility: "public" | "unlisted" | "private" | "direct";
|
||||
media_attachments: Array<Attachment>;
|
||||
mentions: Array<Mention>;
|
||||
tags: Array<Tag>;
|
||||
card: Card | null;
|
||||
poll: Poll | null;
|
||||
application: Application | null;
|
||||
language: string | null;
|
||||
pinned: boolean | undefined;
|
||||
reactions: Array<Reaction>;
|
||||
quote: Status | null;
|
||||
bookmarked: boolean;
|
||||
edited_at: string | null;
|
||||
};
|
||||
export type Status = {
|
||||
id: string;
|
||||
uri: string;
|
||||
url: string;
|
||||
account: Account;
|
||||
in_reply_to_id: string | null;
|
||||
in_reply_to_account_id: string | null;
|
||||
reblog: Status | null;
|
||||
content: string | undefined;
|
||||
text: string | null | undefined;
|
||||
created_at: string;
|
||||
emojis: Emoji[];
|
||||
replies_count: number;
|
||||
reblogs_count: number;
|
||||
favourites_count: number;
|
||||
reblogged: boolean | null;
|
||||
favourited: boolean | null;
|
||||
muted: boolean | null;
|
||||
sensitive: boolean;
|
||||
spoiler_text: string;
|
||||
visibility: "public" | "unlisted" | "private" | "direct";
|
||||
media_attachments: Array<Attachment>;
|
||||
mentions: Array<Mention>;
|
||||
tags: Array<Tag>;
|
||||
card: Card | null;
|
||||
poll: Poll | null;
|
||||
application: Application | null;
|
||||
language: string | null;
|
||||
pinned: boolean | undefined;
|
||||
reactions: Array<Reaction>;
|
||||
quote: Status | null;
|
||||
bookmarked: boolean;
|
||||
edited_at: string | null;
|
||||
};
|
||||
|
||||
export type StatusCreationRequest = {
|
||||
text?: string,
|
||||
media_ids?: string[],
|
||||
poll?: {
|
||||
options: string[],
|
||||
expires_in: number,
|
||||
multiple: boolean
|
||||
},
|
||||
in_reply_to_id?: string,
|
||||
sensitive?: boolean,
|
||||
spoiler_text?: string,
|
||||
visibility?: string,
|
||||
language?: string,
|
||||
scheduled_at?: Date
|
||||
}
|
||||
export type StatusCreationRequest = {
|
||||
text?: string,
|
||||
media_ids?: string[],
|
||||
poll?: {
|
||||
options: string[],
|
||||
expires_in: number,
|
||||
multiple: boolean
|
||||
},
|
||||
in_reply_to_id?: string,
|
||||
sensitive?: boolean,
|
||||
spoiler_text?: string,
|
||||
visibility?: string,
|
||||
language?: string,
|
||||
scheduled_at?: Date
|
||||
}
|
||||
|
||||
export type StatusEditRequest = {
|
||||
text?: string,
|
||||
media_ids?: string[],
|
||||
poll?: {
|
||||
options: string[],
|
||||
expires_in: number,
|
||||
multiple: boolean
|
||||
},
|
||||
sensitive?: boolean,
|
||||
spoiler_text?: string,
|
||||
language?: string
|
||||
}
|
||||
export type StatusEditRequest = {
|
||||
text?: string,
|
||||
media_ids?: string[],
|
||||
poll?: {
|
||||
options: string[],
|
||||
expires_in: number,
|
||||
multiple: boolean
|
||||
},
|
||||
sensitive?: boolean,
|
||||
spoiler_text?: string,
|
||||
language?: string
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,14 +9,14 @@
|
|||
/// <reference path="reaction.ts" />
|
||||
|
||||
namespace MastodonEntity {
|
||||
export type StatusEdit = {
|
||||
account: Account;
|
||||
content: string;
|
||||
created_at: string;
|
||||
emojis: Emoji[];
|
||||
sensitive: boolean;
|
||||
spoiler_text: string;
|
||||
media_attachments: Array<Attachment>;
|
||||
poll: Poll | null;
|
||||
};
|
||||
export type StatusEdit = {
|
||||
account: Account;
|
||||
content: string;
|
||||
created_at: string;
|
||||
emojis: Emoji[];
|
||||
sensitive: boolean;
|
||||
spoiler_text: string;
|
||||
media_attachments: Array<Attachment>;
|
||||
poll: Poll | null;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
namespace MastodonEntity {
|
||||
export type StatusParams = {
|
||||
text: string;
|
||||
in_reply_to_id: string | null;
|
||||
media_ids: Array<string> | null;
|
||||
sensitive: boolean | null;
|
||||
spoiler_text: string | null;
|
||||
visibility: "public" | "unlisted" | "private" | "direct";
|
||||
scheduled_at: string | null;
|
||||
application_id: string;
|
||||
};
|
||||
export type StatusParams = {
|
||||
text: string;
|
||||
in_reply_to_id: string | null;
|
||||
media_ids: Array<string> | null;
|
||||
sensitive: boolean | null;
|
||||
spoiler_text: string | null;
|
||||
visibility: "public" | "unlisted" | "private" | "direct";
|
||||
scheduled_at: string | null;
|
||||
application_id: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace MastodonEntity {
|
||||
export type StatusSource = {
|
||||
id: string;
|
||||
text: string;
|
||||
spoiler_text: string;
|
||||
};
|
||||
export type StatusSource = {
|
||||
id: string;
|
||||
text: string;
|
||||
spoiler_text: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/// <reference path="history.ts" />
|
||||
|
||||
namespace MastodonEntity {
|
||||
export type Tag = {
|
||||
name: string;
|
||||
url: string;
|
||||
history: Array<History> | null;
|
||||
following?: boolean;
|
||||
};
|
||||
export type Tag = {
|
||||
name: string;
|
||||
url: string;
|
||||
history: Array<History> | null;
|
||||
following?: boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
namespace MastodonEntity {
|
||||
export type Token = {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
scope: string;
|
||||
created_at: number;
|
||||
};
|
||||
export type Token = {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
scope: string;
|
||||
created_at: number;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
namespace MastodonEntity {
|
||||
export type URLs = {
|
||||
streaming_api: string;
|
||||
};
|
||||
export type URLs = {
|
||||
streaming_api: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,47 +4,47 @@ import { LinkPaginationObject } from "@/server/api/mastodon/helpers/user.js";
|
|||
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||
|
||||
export class ListHelpers {
|
||||
public static async getLists(user: ILocalUser): Promise<MastodonEntity.List[]> {
|
||||
return UserLists.findBy({userId: user.id}).then(p => p.map(list => {
|
||||
return {
|
||||
id: list.id,
|
||||
title: list.name
|
||||
}
|
||||
}));
|
||||
}
|
||||
public static async getLists(user: ILocalUser): Promise<MastodonEntity.List[]> {
|
||||
return UserLists.findBy({userId: user.id}).then(p => p.map(list => {
|
||||
return {
|
||||
id: list.id,
|
||||
title: list.name
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public static async getList(user: ILocalUser, id: string): Promise<MastodonEntity.List> {
|
||||
return UserLists.findOneByOrFail({userId: user.id, id: id}).then(list => {
|
||||
return {
|
||||
id: list.id,
|
||||
title: list.name
|
||||
}
|
||||
});
|
||||
}
|
||||
public static async getList(user: ILocalUser, id: string): Promise<MastodonEntity.List> {
|
||||
return UserLists.findOneByOrFail({userId: user.id, id: id}).then(list => {
|
||||
return {
|
||||
id: list.id,
|
||||
title: list.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static async getListUsers(user: ILocalUser, id: string, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
const list = await UserLists.findOneByOrFail({userId: user.id, id: id});
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
UserListJoinings.createQueryBuilder('member'),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("member.userListId = :listId", {listId: id})
|
||||
.innerJoinAndSelect("member.user", "user");
|
||||
public static async getListUsers(user: ILocalUser, id: string, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
const list = await UserLists.findOneByOrFail({userId: user.id, id: id});
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
UserListJoinings.createQueryBuilder('member'),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("member.userListId = :listId", {listId: id})
|
||||
.innerJoinAndSelect("member.user", "user");
|
||||
|
||||
return query.take(limit).getMany().then(async p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.user)
|
||||
.filter(p => p) as User[];
|
||||
return query.take(limit).getMany().then(async p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.user)
|
||||
.filter(p => p) as User[];
|
||||
|
||||
return {
|
||||
data: users,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
data: users,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,32 +6,32 @@ import { Packed } from "@/misc/schema.js";
|
|||
import { DriveFile } from "@/models/entities/drive-file.js";
|
||||
|
||||
export class MediaHelpers {
|
||||
public static async uploadMedia(user: ILocalUser, file: multer.File, body: any): Promise<Packed<"DriveFile">> {
|
||||
return await addFile({
|
||||
user: user,
|
||||
path: file.path,
|
||||
name: file.originalname !== null && file.originalname !== 'file' ? file.originalname : undefined,
|
||||
comment: body?.description ?? undefined,
|
||||
sensitive: false, //FIXME: this needs to be updated on from composing a post with the media attached
|
||||
})
|
||||
.then(p => DriveFiles.pack(p));
|
||||
}
|
||||
public static async uploadMedia(user: ILocalUser, file: multer.File, body: any): Promise<Packed<"DriveFile">> {
|
||||
return await addFile({
|
||||
user: user,
|
||||
path: file.path,
|
||||
name: file.originalname !== null && file.originalname !== 'file' ? file.originalname : undefined,
|
||||
comment: body?.description ?? undefined,
|
||||
sensitive: false, //FIXME: this needs to be updated on from composing a post with the media attached
|
||||
})
|
||||
.then(p => DriveFiles.pack(p));
|
||||
}
|
||||
|
||||
public static async updateMedia(user: ILocalUser, file: DriveFile, body: any): Promise<Packed<"DriveFile">> {
|
||||
await DriveFiles.update(file.id, {
|
||||
comment: body?.description ?? undefined
|
||||
});
|
||||
public static async updateMedia(user: ILocalUser, file: DriveFile, body: any): Promise<Packed<"DriveFile">> {
|
||||
await DriveFiles.update(file.id, {
|
||||
comment: body?.description ?? undefined
|
||||
});
|
||||
|
||||
return DriveFiles.findOneByOrFail({id: file.id, userId: user.id})
|
||||
.then(p => DriveFiles.pack(p));
|
||||
}
|
||||
return DriveFiles.findOneByOrFail({id: file.id, userId: user.id})
|
||||
.then(p => DriveFiles.pack(p));
|
||||
}
|
||||
|
||||
public static async getMediaPacked(user: ILocalUser, id: string): Promise<Packed<"DriveFile"> | null> {
|
||||
return this.getMedia(user, id)
|
||||
.then(p => p ? DriveFiles.pack(p) : null);
|
||||
}
|
||||
public static async getMediaPacked(user: ILocalUser, id: string): Promise<Packed<"DriveFile"> | null> {
|
||||
return this.getMedia(user, id)
|
||||
.then(p => p ? DriveFiles.pack(p) : null);
|
||||
}
|
||||
|
||||
public static async getMedia(user: ILocalUser, id: string): Promise<DriveFile | null> {
|
||||
return DriveFiles.findOneBy({id: id, userId: user.id});
|
||||
}
|
||||
public static async getMedia(user: ILocalUser, id: string): Promise<DriveFile | null> {
|
||||
return DriveFiles.findOneBy({id: id, userId: user.id});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
|
||||
import {
|
||||
DriveFiles,
|
||||
Metas, NoteEdits,
|
||||
NoteFavorites,
|
||||
NoteReactions,
|
||||
Notes,
|
||||
UserNotePinings
|
||||
} from "@/models/index.js";
|
||||
import { DriveFiles, Metas, NoteEdits, NoteFavorites, NoteReactions, Notes, UserNotePinings } from "@/models/index.js";
|
||||
import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js";
|
||||
import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js";
|
||||
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
|
||||
|
@ -32,358 +25,358 @@ import { FileConverter } from "@/server/api/mastodon/converters/file.js";
|
|||
import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js";
|
||||
|
||||
export class NoteHelpers {
|
||||
public static async getDefaultReaction(): Promise<string> {
|
||||
return Metas.createQueryBuilder()
|
||||
.select('"defaultReaction"')
|
||||
.execute()
|
||||
.then(p => p[0].defaultReaction);
|
||||
}
|
||||
public static async getDefaultReaction(): Promise<string> {
|
||||
return Metas.createQueryBuilder()
|
||||
.select('"defaultReaction"')
|
||||
.execute()
|
||||
.then(p => p[0].defaultReaction);
|
||||
}
|
||||
|
||||
public static async reactToNote(note: Note, user: ILocalUser, reaction: string): Promise<Note> {
|
||||
await createReaction(user, note, reaction);
|
||||
return getNote(note.id, user);
|
||||
}
|
||||
public static async reactToNote(note: Note, user: ILocalUser, reaction: string): Promise<Note> {
|
||||
await createReaction(user, note, reaction);
|
||||
return getNote(note.id, user);
|
||||
}
|
||||
|
||||
public static async removeReactFromNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
await deleteReaction(user, note);
|
||||
return getNote(note.id, user);
|
||||
}
|
||||
public static async removeReactFromNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
await deleteReaction(user, note);
|
||||
return getNote(note.id, user);
|
||||
}
|
||||
|
||||
public static async reblogNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
const data = {
|
||||
createdAt: new Date(),
|
||||
files: [],
|
||||
renote: note
|
||||
};
|
||||
return await createNote(user, data);
|
||||
}
|
||||
public static async reblogNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
const data = {
|
||||
createdAt: new Date(),
|
||||
files: [],
|
||||
renote: note
|
||||
};
|
||||
return await createNote(user, data);
|
||||
}
|
||||
|
||||
public static async unreblogNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
return Notes.findBy({
|
||||
userId: user.id,
|
||||
renoteId: note.id,
|
||||
})
|
||||
.then(p => p.map(n => deleteNote(user, n)))
|
||||
.then(p => Promise.all(p))
|
||||
.then(_ => getNote(note.id, user));
|
||||
}
|
||||
public static async unreblogNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
return Notes.findBy({
|
||||
userId: user.id,
|
||||
renoteId: note.id,
|
||||
})
|
||||
.then(p => p.map(n => deleteNote(user, n)))
|
||||
.then(p => Promise.all(p))
|
||||
.then(_ => getNote(note.id, user));
|
||||
}
|
||||
|
||||
public static async bookmarkNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
const bookmarked = await NoteFavorites.exist({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
public static async bookmarkNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
const bookmarked = await NoteFavorites.exist({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!bookmarked) {
|
||||
await NoteFavorites.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
if (!bookmarked) {
|
||||
await NoteFavorites.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
public static async unbookmarkNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
return NoteFavorites.findOneBy({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
})
|
||||
.then(p => p !== null ? NoteFavorites.delete(p.id) : null)
|
||||
.then(_ => note);
|
||||
}
|
||||
public static async unbookmarkNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
return NoteFavorites.findOneBy({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
})
|
||||
.then(p => p !== null ? NoteFavorites.delete(p.id) : null)
|
||||
.then(_ => note);
|
||||
}
|
||||
|
||||
public static async pinNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
const pinned = await UserNotePinings.exist({
|
||||
where: {
|
||||
userId: user.id,
|
||||
noteId: note.id
|
||||
}
|
||||
});
|
||||
public static async pinNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
const pinned = await UserNotePinings.exist({
|
||||
where: {
|
||||
userId: user.id,
|
||||
noteId: note.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!pinned) {
|
||||
await addPinned(user, note.id);
|
||||
}
|
||||
if (!pinned) {
|
||||
await addPinned(user, note.id);
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
public static async unpinNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
const pinned = await UserNotePinings.exist({
|
||||
where: {
|
||||
userId: user.id,
|
||||
noteId: note.id
|
||||
}
|
||||
});
|
||||
public static async unpinNote(note: Note, user: ILocalUser): Promise<Note> {
|
||||
const pinned = await UserNotePinings.exist({
|
||||
where: {
|
||||
userId: user.id,
|
||||
noteId: note.id
|
||||
}
|
||||
});
|
||||
|
||||
if (pinned) {
|
||||
await removePinned(user, note.id);
|
||||
}
|
||||
if (pinned) {
|
||||
await removePinned(user, note.id);
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
public static async deleteNote(note: Note, user: ILocalUser): Promise<MastodonEntity.Status> {
|
||||
if (user.id !== note.userId) throw new Error("Can't delete someone elses note");
|
||||
const status = await NoteConverter.encode(note, user);
|
||||
await deleteNote(user, note);
|
||||
status.content = undefined;
|
||||
return status;
|
||||
}
|
||||
public static async deleteNote(note: Note, user: ILocalUser): Promise<MastodonEntity.Status> {
|
||||
if (user.id !== note.userId) throw new Error("Can't delete someone elses note");
|
||||
const status = await NoteConverter.encode(note, user);
|
||||
await deleteNote(user, note);
|
||||
status.content = undefined;
|
||||
return status;
|
||||
}
|
||||
|
||||
public static async getNoteFavoritedBy(note: Note, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
NoteReactions.createQueryBuilder("reaction"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("reaction.noteId = :noteId", {noteId: note.id})
|
||||
.innerJoinAndSelect("reaction.user", "user");
|
||||
public static async getNoteFavoritedBy(note: Note, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
NoteReactions.createQueryBuilder("reaction"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("reaction.noteId = :noteId", {noteId: note.id})
|
||||
.innerJoinAndSelect("reaction.user", "user");
|
||||
|
||||
return query.take(limit).getMany().then(async p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.user)
|
||||
.filter(p => p) as User[];
|
||||
return query.take(limit).getMany().then(async p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.user)
|
||||
.filter(p => p) as User[];
|
||||
|
||||
return {
|
||||
data: users,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
data: users,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getNoteEditHistory(note: Note): Promise<MastodonEntity.StatusEdit[]> {
|
||||
if (!note.updatedAt) return [];
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const account = Promise.resolve(note.user ?? await UserHelpers.getUserCached(note.userId, cache))
|
||||
.then(p => UserConverter.encode(p, cache));
|
||||
const edits = await NoteEdits.find({where: {noteId: note.id}, order: {id: "ASC"}});
|
||||
const history: Promise<MastodonEntity.StatusEdit>[] = [];
|
||||
if (edits.length < 1) return [];
|
||||
public static async getNoteEditHistory(note: Note): Promise<MastodonEntity.StatusEdit[]> {
|
||||
if (!note.updatedAt) return [];
|
||||
const cache = UserHelpers.getFreshAccountCache();
|
||||
const account = Promise.resolve(note.user ?? await UserHelpers.getUserCached(note.userId, cache))
|
||||
.then(p => UserConverter.encode(p, cache));
|
||||
const edits = await NoteEdits.find({where: {noteId: note.id}, order: {id: "ASC"}});
|
||||
const history: Promise<MastodonEntity.StatusEdit>[] = [];
|
||||
if (edits.length < 1) return [];
|
||||
|
||||
const curr = {
|
||||
id: note.id,
|
||||
noteId: note.id,
|
||||
note: note,
|
||||
text: note.text,
|
||||
cw: note.cw,
|
||||
fileIds: note.fileIds,
|
||||
updatedAt: note.updatedAt
|
||||
}
|
||||
const curr = {
|
||||
id: note.id,
|
||||
noteId: note.id,
|
||||
note: note,
|
||||
text: note.text,
|
||||
cw: note.cw,
|
||||
fileIds: note.fileIds,
|
||||
updatedAt: note.updatedAt
|
||||
}
|
||||
|
||||
edits.push(curr);
|
||||
edits.push(curr);
|
||||
|
||||
let lastDate = note.createdAt;
|
||||
for (const edit of edits) {
|
||||
const files = DriveFiles.packMany(edit.fileIds);
|
||||
const item = {
|
||||
account: account,
|
||||
content: MfmHelpers.toHtml(mfm.parse(edit.text ?? ''), JSON.parse(note.mentionedRemoteUsers)) ?? '',
|
||||
created_at: lastDate.toISOString(),
|
||||
emojis: [],
|
||||
sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false),
|
||||
spoiler_text: edit.cw ?? '',
|
||||
poll: null,
|
||||
media_attachments: files.then(files => files.length > 0 ? files.map((f) => FileConverter.encode(f)) : [])
|
||||
};
|
||||
lastDate = edit.updatedAt;
|
||||
history.unshift(awaitAll(item));
|
||||
}
|
||||
let lastDate = note.createdAt;
|
||||
for (const edit of edits) {
|
||||
const files = DriveFiles.packMany(edit.fileIds);
|
||||
const item = {
|
||||
account: account,
|
||||
content: MfmHelpers.toHtml(mfm.parse(edit.text ?? ''), JSON.parse(note.mentionedRemoteUsers)) ?? '',
|
||||
created_at: lastDate.toISOString(),
|
||||
emojis: [],
|
||||
sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false),
|
||||
spoiler_text: edit.cw ?? '',
|
||||
poll: null,
|
||||
media_attachments: files.then(files => files.length > 0 ? files.map((f) => FileConverter.encode(f)) : [])
|
||||
};
|
||||
lastDate = edit.updatedAt;
|
||||
history.unshift(awaitAll(item));
|
||||
}
|
||||
|
||||
return Promise.all(history);
|
||||
}
|
||||
return Promise.all(history);
|
||||
}
|
||||
|
||||
public static getNoteSource(note: Note): MastodonEntity.StatusSource {
|
||||
return {
|
||||
id: note.id,
|
||||
text: note.text ?? '',
|
||||
spoiler_text: note.cw ?? ''
|
||||
}
|
||||
}
|
||||
public static getNoteSource(note: Note): MastodonEntity.StatusSource {
|
||||
return {
|
||||
id: note.id,
|
||||
text: note.text ?? '',
|
||||
spoiler_text: note.cw ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
public static async getNoteRebloggedBy(note: Note, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("note.renoteId = :noteId", {noteId: note.id})
|
||||
.andWhere("note.text IS NULL") // We don't want to count quotes as renotes
|
||||
.innerJoinAndSelect("note.user", "user");
|
||||
public static async getNoteRebloggedBy(note: Note, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("note.renoteId = :noteId", {noteId: note.id})
|
||||
.andWhere("note.text IS NULL") // We don't want to count quotes as renotes
|
||||
.innerJoinAndSelect("note.user", "user");
|
||||
|
||||
return query.take(limit).getMany().then(async p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.user)
|
||||
.filter(p => p) as User[];
|
||||
return query.take(limit).getMany().then(async p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.user)
|
||||
.filter(p => p) as User[];
|
||||
|
||||
return {
|
||||
data: users,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
data: users,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getNoteDescendants(note: Note | string, user: ILocalUser | null, limit: number = 10, depth: number = 2): Promise<Note[]> {
|
||||
const noteId = typeof note === "string" ? note : note.id;
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder("note"))
|
||||
.andWhere(
|
||||
"note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))",
|
||||
{noteId, depth, limit},
|
||||
);
|
||||
public static async getNoteDescendants(note: Note | string, user: ILocalUser | null, limit: number = 10, depth: number = 2): Promise<Note[]> {
|
||||
const noteId = typeof note === "string" ? note : note.id;
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder("note"))
|
||||
.andWhere(
|
||||
"note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))",
|
||||
{noteId, depth, limit},
|
||||
);
|
||||
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
}
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
}
|
||||
|
||||
return query.getMany().then(p => p.reverse());
|
||||
}
|
||||
return query.getMany().then(p => p.reverse());
|
||||
}
|
||||
|
||||
public static async getNoteAncestors(rootNote: Note, user: ILocalUser | null, limit: number = 10): Promise<Note[]> {
|
||||
const notes = new Array<Note>;
|
||||
for (let i = 0; i < limit; i++) {
|
||||
const currentNote = notes.at(-1) ?? rootNote;
|
||||
if (!currentNote.replyId) break;
|
||||
const nextNote = await getNote(currentNote.replyId, user).catch((e) => {
|
||||
if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") return null;
|
||||
throw e;
|
||||
});
|
||||
if (nextNote && await Notes.isVisibleForMe(nextNote, user?.id ?? null)) notes.push(nextNote);
|
||||
else break;
|
||||
}
|
||||
public static async getNoteAncestors(rootNote: Note, user: ILocalUser | null, limit: number = 10): Promise<Note[]> {
|
||||
const notes = new Array<Note>;
|
||||
for (let i = 0; i < limit; i++) {
|
||||
const currentNote = notes.at(-1) ?? rootNote;
|
||||
if (!currentNote.replyId) break;
|
||||
const nextNote = await getNote(currentNote.replyId, user).catch((e) => {
|
||||
if (e.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24") return null;
|
||||
throw e;
|
||||
});
|
||||
if (nextNote && await Notes.isVisibleForMe(nextNote, user?.id ?? null)) notes.push(nextNote);
|
||||
else break;
|
||||
}
|
||||
|
||||
return notes.reverse();
|
||||
}
|
||||
return notes.reverse();
|
||||
}
|
||||
|
||||
public static async createNote(request: MastodonEntity.StatusCreationRequest, user: ILocalUser): Promise<Note> {
|
||||
const files = request.media_ids && request.media_ids.length > 0
|
||||
? DriveFiles.findByIds(request.media_ids)
|
||||
: [];
|
||||
public static async createNote(request: MastodonEntity.StatusCreationRequest, user: ILocalUser): Promise<Note> {
|
||||
const files = request.media_ids && request.media_ids.length > 0
|
||||
? DriveFiles.findByIds(request.media_ids)
|
||||
: [];
|
||||
|
||||
const reply = request.in_reply_to_id ? await getNote(request.in_reply_to_id, user) : undefined;
|
||||
const visibility = request.visibility ?? UserHelpers.getDefaultNoteVisibility(user);
|
||||
const reply = request.in_reply_to_id ? await getNote(request.in_reply_to_id, user) : undefined;
|
||||
const visibility = request.visibility ?? UserHelpers.getDefaultNoteVisibility(user);
|
||||
|
||||
const data = {
|
||||
createdAt: new Date(),
|
||||
files: files,
|
||||
poll: request.poll
|
||||
? {
|
||||
choices: request.poll.options,
|
||||
multiple: request.poll.multiple,
|
||||
expiresAt: request.poll.expires_in && request.poll.expires_in > 0 ? new Date(new Date().getTime() + (request.poll.expires_in * 1000)) : null,
|
||||
}
|
||||
: undefined,
|
||||
text: request.text,
|
||||
reply: reply,
|
||||
cw: request.spoiler_text,
|
||||
visibility: visibility,
|
||||
visibleUsers: Promise.resolve(visibility).then(p => p === 'specified' ? this.extractMentions(request.text ?? '', user) : undefined)
|
||||
}
|
||||
const data = {
|
||||
createdAt: new Date(),
|
||||
files: files,
|
||||
poll: request.poll
|
||||
? {
|
||||
choices: request.poll.options,
|
||||
multiple: request.poll.multiple,
|
||||
expiresAt: request.poll.expires_in && request.poll.expires_in > 0 ? new Date(new Date().getTime() + (request.poll.expires_in * 1000)) : null,
|
||||
}
|
||||
: undefined,
|
||||
text: request.text,
|
||||
reply: reply,
|
||||
cw: request.spoiler_text,
|
||||
visibility: visibility,
|
||||
visibleUsers: Promise.resolve(visibility).then(p => p === 'specified' ? this.extractMentions(request.text ?? '', user) : undefined)
|
||||
}
|
||||
|
||||
return createNote(user, await awaitAll(data));
|
||||
}
|
||||
return createNote(user, await awaitAll(data));
|
||||
}
|
||||
|
||||
public static async editNote(request: MastodonEntity.StatusEditRequest, note: Note, user: ILocalUser): Promise<Note> {
|
||||
const files = request.media_ids && request.media_ids.length > 0
|
||||
? DriveFiles.findByIds(request.media_ids)
|
||||
: [];
|
||||
public static async editNote(request: MastodonEntity.StatusEditRequest, note: Note, user: ILocalUser): Promise<Note> {
|
||||
const files = request.media_ids && request.media_ids.length > 0
|
||||
? DriveFiles.findByIds(request.media_ids)
|
||||
: [];
|
||||
|
||||
const data = {
|
||||
files: files,
|
||||
poll: request.poll
|
||||
? {
|
||||
choices: request.poll.options,
|
||||
multiple: request.poll.multiple,
|
||||
expiresAt: request.poll.expires_in && request.poll.expires_in > 0 ? new Date(new Date().getTime() + (request.poll.expires_in * 1000)) : null,
|
||||
}
|
||||
: undefined,
|
||||
text: request.text,
|
||||
cw: request.spoiler_text
|
||||
}
|
||||
const data = {
|
||||
files: files,
|
||||
poll: request.poll
|
||||
? {
|
||||
choices: request.poll.options,
|
||||
multiple: request.poll.multiple,
|
||||
expiresAt: request.poll.expires_in && request.poll.expires_in > 0 ? new Date(new Date().getTime() + (request.poll.expires_in * 1000)) : null,
|
||||
}
|
||||
: undefined,
|
||||
text: request.text,
|
||||
cw: request.spoiler_text
|
||||
}
|
||||
|
||||
return editNote(user, note, await awaitAll(data));
|
||||
}
|
||||
return editNote(user, note, await awaitAll(data));
|
||||
}
|
||||
|
||||
public static async extractMentions(text: string, user: ILocalUser): Promise<User[]> {
|
||||
return extractMentionedUsers(user, mfm.parse(text)!);
|
||||
}
|
||||
public static async extractMentions(text: string, user: ILocalUser): Promise<User[]> {
|
||||
return extractMentionedUsers(user, mfm.parse(text)!);
|
||||
}
|
||||
|
||||
public static normalizeComposeOptions(body: any): MastodonEntity.StatusCreationRequest {
|
||||
const result: MastodonEntity.StatusCreationRequest = {};
|
||||
public static normalizeComposeOptions(body: any): MastodonEntity.StatusCreationRequest {
|
||||
const result: MastodonEntity.StatusCreationRequest = {};
|
||||
|
||||
if (body.status !== null)
|
||||
result.text = body.status;
|
||||
if (body.spoiler_text !== null)
|
||||
result.spoiler_text = body.spoiler_text;
|
||||
if (body.visibility !== null)
|
||||
result.visibility = VisibilityConverter.decode(body.visibility);
|
||||
if (body.language !== null)
|
||||
result.language = body.language;
|
||||
if (body.scheduled_at !== null)
|
||||
result.scheduled_at = new Date(Date.parse(body.scheduled_at));
|
||||
if (body.in_reply_to_id)
|
||||
result.in_reply_to_id = convertId(body.in_reply_to_id, IdType.IceshrimpId);
|
||||
if (body.media_ids)
|
||||
result.media_ids = body.media_ids && body.media_ids.length > 0
|
||||
? this.normalizeToArray(body.media_ids)
|
||||
.map(p => convertId(p, IdType.IceshrimpId))
|
||||
: undefined;
|
||||
if (body.status !== null)
|
||||
result.text = body.status;
|
||||
if (body.spoiler_text !== null)
|
||||
result.spoiler_text = body.spoiler_text;
|
||||
if (body.visibility !== null)
|
||||
result.visibility = VisibilityConverter.decode(body.visibility);
|
||||
if (body.language !== null)
|
||||
result.language = body.language;
|
||||
if (body.scheduled_at !== null)
|
||||
result.scheduled_at = new Date(Date.parse(body.scheduled_at));
|
||||
if (body.in_reply_to_id)
|
||||
result.in_reply_to_id = convertId(body.in_reply_to_id, IdType.IceshrimpId);
|
||||
if (body.media_ids)
|
||||
result.media_ids = body.media_ids && body.media_ids.length > 0
|
||||
? this.normalizeToArray(body.media_ids)
|
||||
.map(p => convertId(p, IdType.IceshrimpId))
|
||||
: undefined;
|
||||
|
||||
if (body.poll) {
|
||||
result.poll = {
|
||||
expires_in: parseInt(body.poll.expires_in, 10),
|
||||
options: body.poll.options,
|
||||
multiple: !!body.poll.multiple,
|
||||
}
|
||||
}
|
||||
if (body.poll) {
|
||||
result.poll = {
|
||||
expires_in: parseInt(body.poll.expires_in, 10),
|
||||
options: body.poll.options,
|
||||
multiple: !!body.poll.multiple,
|
||||
}
|
||||
}
|
||||
|
||||
result.sensitive = !!body.sensitive;
|
||||
result.sensitive = !!body.sensitive;
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static normalizeEditOptions(body: any): MastodonEntity.StatusEditRequest {
|
||||
const result: MastodonEntity.StatusEditRequest = {};
|
||||
public static normalizeEditOptions(body: any): MastodonEntity.StatusEditRequest {
|
||||
const result: MastodonEntity.StatusEditRequest = {};
|
||||
|
||||
if (body.status !== null)
|
||||
result.text = body.status;
|
||||
if (body.spoiler_text !== null)
|
||||
result.spoiler_text = body.spoiler_text;
|
||||
if (body.language !== null)
|
||||
result.language = body.language;
|
||||
if (body.media_ids)
|
||||
result.media_ids = body.media_ids && body.media_ids.length > 0
|
||||
? this.normalizeToArray(body.media_ids)
|
||||
.map(p => convertId(p, IdType.IceshrimpId))
|
||||
: undefined;
|
||||
if (body.status !== null)
|
||||
result.text = body.status;
|
||||
if (body.spoiler_text !== null)
|
||||
result.spoiler_text = body.spoiler_text;
|
||||
if (body.language !== null)
|
||||
result.language = body.language;
|
||||
if (body.media_ids)
|
||||
result.media_ids = body.media_ids && body.media_ids.length > 0
|
||||
? this.normalizeToArray(body.media_ids)
|
||||
.map(p => convertId(p, IdType.IceshrimpId))
|
||||
: undefined;
|
||||
|
||||
if (body.poll) {
|
||||
result.poll = {
|
||||
expires_in: parseInt(body.poll.expires_in, 10),
|
||||
options: body.poll.options,
|
||||
multiple: !!body.poll.multiple,
|
||||
}
|
||||
}
|
||||
if (body.poll) {
|
||||
result.poll = {
|
||||
expires_in: parseInt(body.poll.expires_in, 10),
|
||||
options: body.poll.options,
|
||||
multiple: !!body.poll.multiple,
|
||||
}
|
||||
}
|
||||
|
||||
result.sensitive = !!body.sensitive;
|
||||
result.sensitive = !!body.sensitive;
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static normalizeToArray<T>(subject: T | T[]) {
|
||||
return Array.isArray(subject) ? subject : [subject];
|
||||
}
|
||||
public static normalizeToArray<T>(subject: T | T[]) {
|
||||
return Array.isArray(subject) ? subject : [subject];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,19 +2,20 @@ import { ILocalUser } from "@/models/entities/user.js";
|
|||
import { Notifications } from "@/models/index.js";
|
||||
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||
import { Notification } from "@/models/entities/notification.js";
|
||||
|
||||
export class NotificationHelpers {
|
||||
public static async getNotifications(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 15, types: string[] | undefined, excludeTypes: string[] | undefined, accountId: string | undefined): Promise<Notification[]> {
|
||||
if (limit > 30) limit = 30;
|
||||
if (types && excludeTypes) throw new Error("types and exclude_types can not be used simultaneously");
|
||||
if (types && excludeTypes) throw new Error("types and exclude_types can not be used simultaneously");
|
||||
|
||||
let requestedTypes = types
|
||||
? this.decodeTypes(types)
|
||||
: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest'];
|
||||
let requestedTypes = types
|
||||
? this.decodeTypes(types)
|
||||
: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest'];
|
||||
|
||||
if (excludeTypes) {
|
||||
const excludedTypes = this.decodeTypes(excludeTypes);
|
||||
requestedTypes = requestedTypes.filter(p => !excludedTypes.includes(p));
|
||||
}
|
||||
if (excludeTypes) {
|
||||
const excludedTypes = this.decodeTypes(excludeTypes);
|
||||
requestedTypes = requestedTypes.filter(p => !excludedTypes.includes(p));
|
||||
}
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notifications.createQueryBuilder("notification"),
|
||||
|
@ -22,30 +23,30 @@ export class NotificationHelpers {
|
|||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("notification.notifieeId = :userId", { userId: user.id })
|
||||
.andWhere("notification.type IN (:...types)", { types: requestedTypes });
|
||||
.andWhere("notification.notifieeId = :userId", {userId: user.id})
|
||||
.andWhere("notification.type IN (:...types)", {types: requestedTypes});
|
||||
|
||||
if (accountId !== undefined)
|
||||
query.andWhere("notification.notifierId = :notifierId", { notifierId: accountId });
|
||||
if (accountId !== undefined)
|
||||
query.andWhere("notification.notifierId = :notifierId", {notifierId: accountId});
|
||||
|
||||
query.leftJoinAndSelect("notification.note", "note");
|
||||
query.leftJoinAndSelect("notification.note", "note");
|
||||
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined);
|
||||
}
|
||||
|
||||
public static async getNotification(id: string, user: ILocalUser): Promise<Notification | null> {
|
||||
return Notifications.findOneBy({id: id, notifieeId: user.id});
|
||||
}
|
||||
public static async getNotification(id: string, user: ILocalUser): Promise<Notification | null> {
|
||||
return Notifications.findOneBy({id: id, notifieeId: user.id});
|
||||
}
|
||||
|
||||
public static async dismissNotification(id: string, user: ILocalUser): Promise<void> {
|
||||
const result = await Notifications.update({id: id, notifieeId: user.id}, {isRead: true});
|
||||
}
|
||||
public static async dismissNotification(id: string, user: ILocalUser): Promise<void> {
|
||||
const result = await Notifications.update({id: id, notifieeId: user.id}, {isRead: true});
|
||||
}
|
||||
|
||||
public static async clearAllNotifications(user: ILocalUser): Promise<void> {
|
||||
await Notifications.update({notifieeId: user.id}, {isRead: true});
|
||||
}
|
||||
public static async clearAllNotifications(user: ILocalUser): Promise<void> {
|
||||
await Notifications.update({notifieeId: user.id}, {isRead: true});
|
||||
}
|
||||
|
||||
private static decodeTypes(types: string[]) {
|
||||
private static decodeTypes(types: string[]) {
|
||||
const result: string[] = [];
|
||||
if (types.includes('follow')) result.push('follow');
|
||||
if (types.includes('mention')) result.push('mention', 'reply');
|
||||
|
@ -53,6 +54,6 @@ export class NotificationHelpers {
|
|||
if (types.includes('favourite')) result.push('reaction');
|
||||
if (types.includes('poll')) result.push('pollEnded');
|
||||
if (types.includes('follow_request')) result.push('receiveFollowRequest');
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,81 +3,82 @@ import config from "@/config/index.js";
|
|||
import { convertId, IdType } from "../../index.js";
|
||||
|
||||
export class PaginationHelpers {
|
||||
public static makePaginationQuery<T extends ObjectLiteral>(
|
||||
q: SelectQueryBuilder<T>,
|
||||
sinceId?: string,
|
||||
maxId?: string,
|
||||
minId?: string,
|
||||
idField: string = "id"
|
||||
) {
|
||||
if (sinceId && minId) throw new Error("Can't user both sinceId and minId params");
|
||||
public static makePaginationQuery<T extends ObjectLiteral>(
|
||||
q: SelectQueryBuilder<T>,
|
||||
sinceId?: string,
|
||||
maxId?: string,
|
||||
minId?: string,
|
||||
idField: string = "id"
|
||||
) {
|
||||
if (sinceId && minId) throw new Error("Can't user both sinceId and minId params");
|
||||
|
||||
if (sinceId && maxId) {
|
||||
q.andWhere(`${q.alias}.${idField} > :sinceId`, { sinceId: sinceId });
|
||||
q.andWhere(`${q.alias}.${idField} < :maxId`, { maxId: maxId });
|
||||
q.orderBy(`${q.alias}.${idField}`, "DESC");
|
||||
} if (minId && maxId) {
|
||||
q.andWhere(`${q.alias}.${idField} > :minId`, { minId: minId });
|
||||
q.andWhere(`${q.alias}.${idField} < :maxId`, { maxId: maxId });
|
||||
q.orderBy(`${q.alias}.${idField}`, "ASC");
|
||||
} else if (sinceId) {
|
||||
q.andWhere(`${q.alias}.${idField} > :sinceId`, { sinceId: sinceId });
|
||||
q.orderBy(`${q.alias}.${idField}`, "DESC");
|
||||
} else if (minId) {
|
||||
q.andWhere(`${q.alias}.${idField} > :minId`, { minId: minId });
|
||||
q.orderBy(`${q.alias}.${idField}`, "ASC");
|
||||
} else if (maxId) {
|
||||
q.andWhere(`${q.alias}.${idField} < :maxId`, { maxId: maxId });
|
||||
q.orderBy(`${q.alias}.${idField}`, "DESC");
|
||||
} else {
|
||||
q.orderBy(`${q.alias}.${idField}`, "DESC");
|
||||
}
|
||||
return q;
|
||||
}
|
||||
if (sinceId && maxId) {
|
||||
q.andWhere(`${q.alias}.${idField} > :sinceId`, {sinceId: sinceId});
|
||||
q.andWhere(`${q.alias}.${idField} < :maxId`, {maxId: maxId});
|
||||
q.orderBy(`${q.alias}.${idField}`, "DESC");
|
||||
}
|
||||
if (minId && maxId) {
|
||||
q.andWhere(`${q.alias}.${idField} > :minId`, {minId: minId});
|
||||
q.andWhere(`${q.alias}.${idField} < :maxId`, {maxId: maxId});
|
||||
q.orderBy(`${q.alias}.${idField}`, "ASC");
|
||||
} else if (sinceId) {
|
||||
q.andWhere(`${q.alias}.${idField} > :sinceId`, {sinceId: sinceId});
|
||||
q.orderBy(`${q.alias}.${idField}`, "DESC");
|
||||
} else if (minId) {
|
||||
q.andWhere(`${q.alias}.${idField} > :minId`, {minId: minId});
|
||||
q.orderBy(`${q.alias}.${idField}`, "ASC");
|
||||
} else if (maxId) {
|
||||
q.andWhere(`${q.alias}.${idField} < :maxId`, {maxId: maxId});
|
||||
q.orderBy(`${q.alias}.${idField}`, "DESC");
|
||||
} else {
|
||||
q.orderBy(`${q.alias}.${idField}`, "DESC");
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param query
|
||||
* @param limit
|
||||
* @param reverse whether the result needs to be .reverse()'d. Set this to true when the parameter minId is not undefined in the original request.
|
||||
*/
|
||||
public static async execQuery<T extends ObjectLiteral>(query: SelectQueryBuilder<T>, limit: number, reverse: boolean): Promise<T[]> {
|
||||
// We fetch more than requested because some may be filtered out, and if there's less than
|
||||
// requested, the pagination stops.
|
||||
const found = [];
|
||||
const take = Math.floor(limit * 1.5);
|
||||
let skip = 0;
|
||||
try {
|
||||
while (found.length < limit) {
|
||||
const notes = await query.take(take).skip(skip).getMany();
|
||||
found.push(...notes);
|
||||
skip += take;
|
||||
if (notes.length < take) break;
|
||||
}
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param query
|
||||
* @param limit
|
||||
* @param reverse whether the result needs to be .reverse()'d. Set this to true when the parameter minId is not undefined in the original request.
|
||||
*/
|
||||
public static async execQuery<T extends ObjectLiteral>(query: SelectQueryBuilder<T>, limit: number, reverse: boolean): Promise<T[]> {
|
||||
// We fetch more than requested because some may be filtered out, and if there's less than
|
||||
// requested, the pagination stops.
|
||||
const found = [];
|
||||
const take = Math.floor(limit * 1.5);
|
||||
let skip = 0;
|
||||
try {
|
||||
while (found.length < limit) {
|
||||
const notes = await query.take(take).skip(skip).getMany();
|
||||
found.push(...notes);
|
||||
skip += take;
|
||||
if (notes.length < take) break;
|
||||
}
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (found.length > limit) {
|
||||
found.length = limit;
|
||||
}
|
||||
if (found.length > limit) {
|
||||
found.length = limit;
|
||||
}
|
||||
|
||||
return reverse ? found.reverse() : found;
|
||||
}
|
||||
return reverse ? found.reverse() : found;
|
||||
}
|
||||
|
||||
public static appendLinkPaginationHeader(args: any, ctx: any, res: any): void {
|
||||
const link: string[] = [];
|
||||
const limit = args.limit ?? 40;
|
||||
if (res.maxId) {
|
||||
const l = `<${config.url}/api${ctx.path}?limit=${limit}&max_id=${convertId(res.maxId, IdType.MastodonId)}>; rel="next"`;
|
||||
link.push(l);
|
||||
}
|
||||
if (res.minId) {
|
||||
const l = `<${config.url}/api${ctx.path}?limit=${limit}&min_id=${convertId(res.minId, IdType.MastodonId)}>; rel="prev"`;
|
||||
link.push(l);
|
||||
}
|
||||
if (link.length > 0){
|
||||
ctx.response.append('Link', link.join(', '));
|
||||
}
|
||||
}
|
||||
public static appendLinkPaginationHeader(args: any, ctx: any, res: any): void {
|
||||
const link: string[] = [];
|
||||
const limit = args.limit ?? 40;
|
||||
if (res.maxId) {
|
||||
const l = `<${config.url}/api${ctx.path}?limit=${limit}&max_id=${convertId(res.maxId, IdType.MastodonId)}>; rel="next"`;
|
||||
link.push(l);
|
||||
}
|
||||
if (res.minId) {
|
||||
const l = `<${config.url}/api${ctx.path}?limit=${limit}&min_id=${convertId(res.minId, IdType.MastodonId)}>; rel="prev"`;
|
||||
link.push(l);
|
||||
}
|
||||
if (link.length > 0) {
|
||||
ctx.response.append('Link', link.join(', '));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ import { Note } from "@/models/entities/note.js";
|
|||
import { populatePoll } from "@/models/repositories/note.js";
|
||||
import { PollConverter } from "@/server/api/mastodon/converters/poll.js";
|
||||
import { ILocalUser, IRemoteUser } from "@/models/entities/user.js";
|
||||
import { getNote } from "@/server/api/common/getters.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
import { Blockings, NoteWatchings, Polls, PollVotes, Users } from "@/models/index.js";
|
||||
import { genId } from "@/misc/gen-id.js";
|
||||
import { publishNoteStream } from "@/services/stream.js";
|
||||
|
@ -11,7 +9,6 @@ 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 { meta } from "@/server/api/endpoints/notes/polls/vote.js";
|
||||
import { Not } from "typeorm";
|
||||
|
||||
export class PollHelpers {
|
||||
|
@ -34,7 +31,7 @@ export class PollHelpers {
|
|||
if (block) throw new Error('You are blocked by the poll author');
|
||||
}
|
||||
|
||||
const poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||
const poll = await Polls.findOneByOrFail({noteId: note.id});
|
||||
|
||||
if (poll.expiresAt && poll.expiresAt < createdAt) throw new Error('Poll is expired');
|
||||
|
||||
|
|
|
@ -22,389 +22,384 @@ import { resolveUser } from "@/remote/resolve-user.js";
|
|||
import { createNote } from "@/remote/activitypub/models/note.js";
|
||||
import { getUser } from "@/server/api/common/getters.js";
|
||||
import config from "@/config/index.js";
|
||||
import { Hashtag } from "@/models/entities/hashtag.js";
|
||||
|
||||
export class SearchHelpers {
|
||||
public static async search(user: ILocalUser, q: string | undefined, type: string | undefined, resolve: boolean = false, following: boolean = false, accountId: string | undefined, excludeUnreviewed: boolean = false, maxId: string | undefined, minId: string | undefined, limit: number = 20, offset: number | undefined, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Search> {
|
||||
if (q === undefined || q.trim().length === 0) throw new Error('Search query cannot be empty');
|
||||
if (limit > 40) limit = 40;
|
||||
const notes = type === 'statuses' || !type ? this.searchNotes(user, q, resolve, following, accountId, maxId, minId, limit, offset) : [];
|
||||
const users = type === 'accounts' || !type ? this.searchUsers(user, q, resolve, following, maxId, minId, limit, offset) : [];
|
||||
const tags = type === 'hashtags' || !type ? this.searchTags(q, excludeUnreviewed, limit, offset) : [];
|
||||
public static async search(user: ILocalUser, q: string | undefined, type: string | undefined, resolve: boolean = false, following: boolean = false, accountId: string | undefined, excludeUnreviewed: boolean = false, maxId: string | undefined, minId: string | undefined, limit: number = 20, offset: number | undefined, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<MastodonEntity.Search> {
|
||||
if (q === undefined || q.trim().length === 0) throw new Error('Search query cannot be empty');
|
||||
if (limit > 40) limit = 40;
|
||||
const notes = type === 'statuses' || !type ? this.searchNotes(user, q, resolve, following, accountId, maxId, minId, limit, offset) : [];
|
||||
const users = type === 'accounts' || !type ? this.searchUsers(user, q, resolve, following, maxId, minId, limit, offset) : [];
|
||||
const tags = type === 'hashtags' || !type ? this.searchTags(q, excludeUnreviewed, limit, offset) : [];
|
||||
|
||||
const result = {
|
||||
statuses: Promise.resolve(notes).then(p => NoteConverter.encodeMany(p, user, cache)),
|
||||
accounts: Promise.resolve(users).then(p => UserConverter.encodeMany(p, cache)),
|
||||
hashtags: Promise.resolve(tags)
|
||||
};
|
||||
const result = {
|
||||
statuses: Promise.resolve(notes).then(p => NoteConverter.encodeMany(p, user, cache)),
|
||||
accounts: Promise.resolve(users).then(p => UserConverter.encodeMany(p, cache)),
|
||||
hashtags: Promise.resolve(tags)
|
||||
};
|
||||
|
||||
return awaitAll(result);
|
||||
}
|
||||
return awaitAll(result);
|
||||
}
|
||||
|
||||
private static async searchUsers(user: ILocalUser, q: string, resolve: boolean, following: boolean, maxId: string | undefined, minId: string | undefined, limit: number, offset: number | undefined): Promise<User[]> {
|
||||
if (resolve) {
|
||||
try {
|
||||
if (q.startsWith('https://') || q.startsWith('http://')) {
|
||||
// try resolving locally first
|
||||
const dbResolver = new DbResolver();
|
||||
const dbResult = await dbResolver.getUserFromApId(q);
|
||||
if (dbResult) return [dbResult];
|
||||
private static async searchUsers(user: ILocalUser, q: string, resolve: boolean, following: boolean, maxId: string | undefined, minId: string | undefined, limit: number, offset: number | undefined): Promise<User[]> {
|
||||
if (resolve) {
|
||||
try {
|
||||
if (q.startsWith('https://') || q.startsWith('http://')) {
|
||||
// try resolving locally first
|
||||
const dbResolver = new DbResolver();
|
||||
const dbResult = await dbResolver.getUserFromApId(q);
|
||||
if (dbResult) return [dbResult];
|
||||
|
||||
// ask remote
|
||||
const resolver = new Resolver();
|
||||
resolver.setUser(user);
|
||||
const object = await resolver.resolve(q);
|
||||
if (q !== object.id) {
|
||||
const result = await dbResolver.getUserFromApId(getApId(object));
|
||||
if (result) return [result];
|
||||
}
|
||||
return isActor(object) ? Promise.all([createPerson(getApId(object), resolver.reset())]) : [];
|
||||
}
|
||||
else {
|
||||
let match = q.match(/^@?(?<user>[a-zA-Z0-9_]+)@(?<host>[a-zA-Z0-9-.]+\.[a-zA-Z0-9-]+)$/);
|
||||
if (!match) match = q.match(/^@(?<user>[a-zA-Z0-9_]+)$/)
|
||||
if (match) {
|
||||
// check if user is already in database
|
||||
const dbResult = await Users.findOneBy({usernameLower: match.groups!.user.toLowerCase(), host: match.groups?.host ?? IsNull()});
|
||||
if (dbResult) return [dbResult];
|
||||
// ask remote
|
||||
const resolver = new Resolver();
|
||||
resolver.setUser(user);
|
||||
const object = await resolver.resolve(q);
|
||||
if (q !== object.id) {
|
||||
const result = await dbResolver.getUserFromApId(getApId(object));
|
||||
if (result) return [result];
|
||||
}
|
||||
return isActor(object) ? Promise.all([createPerson(getApId(object), resolver.reset())]) : [];
|
||||
} else {
|
||||
let match = q.match(/^@?(?<user>[a-zA-Z0-9_]+)@(?<host>[a-zA-Z0-9-.]+\.[a-zA-Z0-9-]+)$/);
|
||||
if (!match) match = q.match(/^@(?<user>[a-zA-Z0-9_]+)$/)
|
||||
if (match) {
|
||||
// check if user is already in database
|
||||
const dbResult = await Users.findOneBy({usernameLower: match.groups!.user.toLowerCase(), host: match.groups?.host ?? IsNull()});
|
||||
if (dbResult) return [dbResult];
|
||||
|
||||
const result = await resolveUser(match.groups!.user.toLowerCase(), match.groups?.host ?? null);
|
||||
if (result) return [result];
|
||||
const result = await resolveUser(match.groups!.user.toLowerCase(), match.groups?.host ?? null);
|
||||
if (result) return [result];
|
||||
|
||||
// no matches found
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e: any) {
|
||||
console.log(`[mastodon-client] resolve user '${q}' failed: ${e.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// no matches found
|
||||
return [];
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log(`[mastodon-client] resolve user '${q}' failed: ${e.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Users.createQueryBuilder("user"),
|
||||
undefined,
|
||||
minId,
|
||||
maxId,
|
||||
);
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Users.createQueryBuilder("user"),
|
||||
undefined,
|
||||
minId,
|
||||
maxId,
|
||||
);
|
||||
|
||||
if (following) {
|
||||
const followingQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :followerId", {followerId: user.id});
|
||||
if (following) {
|
||||
const followingQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :followerId", {followerId: user.id});
|
||||
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(`user.id IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id});
|
||||
}),
|
||||
);
|
||||
}
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(`user.id IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where("user.name ILIKE :q", { q: `%${sqlLikeEscape(q)}%` });
|
||||
qb.orWhere("user.usernameLower ILIKE :q", { q: `%${sqlLikeEscape(q)}%` });
|
||||
})
|
||||
);
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where("user.name ILIKE :q", {q: `%${sqlLikeEscape(q)}%`});
|
||||
qb.orWhere("user.usernameLower ILIKE :q", {q: `%${sqlLikeEscape(q)}%`});
|
||||
})
|
||||
);
|
||||
|
||||
query.orderBy({'user.notesCount': 'DESC'});
|
||||
query.orderBy({'user.notesCount': 'DESC'});
|
||||
|
||||
return query.skip(offset ?? 0).take(limit).getMany().then(p => minId ? p.reverse() : p);
|
||||
}
|
||||
return query.skip(offset ?? 0).take(limit).getMany().then(p => minId ? p.reverse() : p);
|
||||
}
|
||||
|
||||
private static async searchNotes(user: ILocalUser, q: string, resolve: boolean, following: boolean, accountId: string | undefined, maxId: string | undefined, minId: string | undefined, limit: number, offset: number | undefined): Promise<Note[]> {
|
||||
if (accountId && following) throw new Error("The 'following' and 'accountId' parameters cannot be used simultaneously");
|
||||
private static async searchNotes(user: ILocalUser, q: string, resolve: boolean, following: boolean, accountId: string | undefined, maxId: string | undefined, minId: string | undefined, limit: number, offset: number | undefined): Promise<Note[]> {
|
||||
if (accountId && following) throw new Error("The 'following' and 'accountId' parameters cannot be used simultaneously");
|
||||
|
||||
if (resolve) {
|
||||
try {
|
||||
if (q.startsWith('https://') || q.startsWith('http://')) {
|
||||
// try resolving locally first
|
||||
const dbResolver = new DbResolver();
|
||||
const dbResult = await dbResolver.getNoteFromApId(q);
|
||||
if (dbResult) return [dbResult];
|
||||
if (resolve) {
|
||||
try {
|
||||
if (q.startsWith('https://') || q.startsWith('http://')) {
|
||||
// try resolving locally first
|
||||
const dbResolver = new DbResolver();
|
||||
const dbResult = await dbResolver.getNoteFromApId(q);
|
||||
if (dbResult) return [dbResult];
|
||||
|
||||
// ask remote
|
||||
const resolver = new Resolver();
|
||||
resolver.setUser(user);
|
||||
const object = await resolver.resolve(q);
|
||||
if (q !== object.id) {
|
||||
const result = await dbResolver.getNoteFromApId(getApId(object));
|
||||
if (result) return [result];
|
||||
}
|
||||
// ask remote
|
||||
const resolver = new Resolver();
|
||||
resolver.setUser(user);
|
||||
const object = await resolver.resolve(q);
|
||||
if (q !== object.id) {
|
||||
const result = await dbResolver.getNoteFromApId(getApId(object));
|
||||
if (result) return [result];
|
||||
}
|
||||
|
||||
return isPost(object) ? createNote(getApId(object), resolver.reset(), true).then(p => p ? [p] : []) : [];
|
||||
}
|
||||
}
|
||||
catch (e: any) {
|
||||
console.log(`[mastodon-client] resolve note '${q}' failed: ${e.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return isPost(object) ? createNote(getApId(object), resolver.reset(), true).then(p => p ? [p] : []) : [];
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log(`[mastodon-client] resolve note '${q}' failed: ${e.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Try sonic search first, unless we have advanced filters
|
||||
if (sonic && !accountId && !following) {
|
||||
let start = offset ?? 0;
|
||||
const chunkSize = 100;
|
||||
// Try sonic search first, unless we have advanced filters
|
||||
if (sonic && !accountId && !following) {
|
||||
let start = offset ?? 0;
|
||||
const chunkSize = 100;
|
||||
|
||||
// Use sonic to fetch and step through all search results that could match the requirements
|
||||
const ids = [];
|
||||
while (true) {
|
||||
const results = await sonic.search.query(
|
||||
sonic.collection,
|
||||
sonic.bucket,
|
||||
q,
|
||||
{
|
||||
limit: chunkSize,
|
||||
offset: start,
|
||||
},
|
||||
);
|
||||
// Use sonic to fetch and step through all search results that could match the requirements
|
||||
const ids = [];
|
||||
while (true) {
|
||||
const results = await sonic.search.query(
|
||||
sonic.collection,
|
||||
sonic.bucket,
|
||||
q,
|
||||
{
|
||||
limit: chunkSize,
|
||||
offset: start,
|
||||
},
|
||||
);
|
||||
|
||||
start += chunkSize;
|
||||
start += chunkSize;
|
||||
|
||||
if (results.length === 0) {
|
||||
break;
|
||||
}
|
||||
if (results.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const res = results
|
||||
.map((k) => JSON.parse(k))
|
||||
.filter((key) => {
|
||||
if (minId && key.id < minId) return false;
|
||||
if (maxId && key.id > maxId) return false;
|
||||
return true;
|
||||
})
|
||||
.map((key) => key.id);
|
||||
const res = results
|
||||
.map((k) => JSON.parse(k))
|
||||
.filter((key) => {
|
||||
if (minId && key.id < minId) return false;
|
||||
if (maxId && key.id > maxId) return false;
|
||||
return true;
|
||||
})
|
||||
.map((key) => key.id);
|
||||
|
||||
ids.push(...res);
|
||||
}
|
||||
ids.push(...res);
|
||||
}
|
||||
|
||||
// Sort all the results by note id DESC (newest first)
|
||||
ids.sort((a, b) => b - a);
|
||||
// Sort all the results by note id DESC (newest first)
|
||||
ids.sort((a, b) => b - a);
|
||||
|
||||
// Fetch the notes from the database until we have enough to satisfy the limit
|
||||
start = 0;
|
||||
const found = [];
|
||||
while (found.length < limit && start < ids.length) {
|
||||
const chunk = ids.slice(start, start + chunkSize);
|
||||
// Fetch the notes from the database until we have enough to satisfy the limit
|
||||
start = 0;
|
||||
const found = [];
|
||||
while (found.length < limit && start < ids.length) {
|
||||
const chunk = ids.slice(start, start + chunkSize);
|
||||
|
||||
const query = Notes.createQueryBuilder("note")
|
||||
.where({id: In(chunk)})
|
||||
.orderBy({id: "DESC"})
|
||||
const query = Notes.createQueryBuilder("note")
|
||||
.where({id: In(chunk)})
|
||||
.orderBy({id: "DESC"})
|
||||
|
||||
generateVisibilityQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
|
||||
if (!accountId) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
}
|
||||
if (!accountId) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
}
|
||||
|
||||
if (following) {
|
||||
const followingQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :followerId", {followerId: user.id});
|
||||
if (following) {
|
||||
const followingQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :followerId", {followerId: user.id});
|
||||
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id});
|
||||
}),
|
||||
)
|
||||
}
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id});
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const notes: Note[] = await query.getMany();
|
||||
const notes: Note[] = await query.getMany();
|
||||
|
||||
found.push(...notes);
|
||||
start += chunkSize;
|
||||
}
|
||||
found.push(...notes);
|
||||
start += chunkSize;
|
||||
}
|
||||
|
||||
// If we have more results than the limit, trim them
|
||||
if (found.length > limit) {
|
||||
found.length = limit;
|
||||
}
|
||||
// If we have more results than the limit, trim them
|
||||
if (found.length > limit) {
|
||||
found.length = limit;
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
// Try meilisearch next
|
||||
else if (meilisearch) {
|
||||
let start = 0;
|
||||
const chunkSize = 100;
|
||||
return found;
|
||||
}
|
||||
// Try meilisearch next
|
||||
else if (meilisearch) {
|
||||
let start = 0;
|
||||
const chunkSize = 100;
|
||||
|
||||
// Use meilisearch to fetch and step through all search results that could match the requirements
|
||||
const ids = [];
|
||||
if (accountId) {
|
||||
const acc = await getUser(accountId);
|
||||
const append = acc.host !== null ? `from:${acc.usernameLower}@${acc.host} ` : `from:${acc.usernameLower}`;
|
||||
q = append + q;
|
||||
}
|
||||
if (following) {
|
||||
q = `filter:following ${q}`;
|
||||
}
|
||||
while (true) {
|
||||
const results = await meilisearch.search(q, chunkSize, start, user);
|
||||
// Use meilisearch to fetch and step through all search results that could match the requirements
|
||||
const ids = [];
|
||||
if (accountId) {
|
||||
const acc = await getUser(accountId);
|
||||
const append = acc.host !== null ? `from:${acc.usernameLower}@${acc.host} ` : `from:${acc.usernameLower}`;
|
||||
q = append + q;
|
||||
}
|
||||
if (following) {
|
||||
q = `filter:following ${q}`;
|
||||
}
|
||||
while (true) {
|
||||
const results = await meilisearch.search(q, chunkSize, start, user);
|
||||
|
||||
start += chunkSize;
|
||||
start += chunkSize;
|
||||
|
||||
if (results.hits.length === 0) {
|
||||
break;
|
||||
}
|
||||
if (results.hits.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
//TODO test this, it's the same logic the mk api uses but it seems, we need to make .hits already be a MeilisearchNote[] instead of forcing type checks to pass
|
||||
const res = (results.hits as MeilisearchNote[])
|
||||
.filter((key: MeilisearchNote) => {
|
||||
if (accountId && key.userId !== accountId) return false;
|
||||
if (minId && key.id < minId) return false;
|
||||
if (maxId && key.id > maxId) return false;
|
||||
return true;
|
||||
})
|
||||
.map((key) => key.id);
|
||||
//TODO test this, it's the same logic the mk api uses but it seems, we need to make .hits already be a MeilisearchNote[] instead of forcing type checks to pass
|
||||
const res = (results.hits as MeilisearchNote[])
|
||||
.filter((key: MeilisearchNote) => {
|
||||
if (accountId && key.userId !== accountId) return false;
|
||||
if (minId && key.id < minId) return false;
|
||||
if (maxId && key.id > maxId) return false;
|
||||
return true;
|
||||
})
|
||||
.map((key) => key.id);
|
||||
|
||||
ids.push(...res);
|
||||
}
|
||||
ids.push(...res);
|
||||
}
|
||||
|
||||
// Sort all the results by note id DESC (newest first)
|
||||
//FIXME: fix this sort function (is it even necessary?)
|
||||
//ids.sort((a, b) => b - a);
|
||||
// Sort all the results by note id DESC (newest first)
|
||||
//FIXME: fix this sort function (is it even necessary?)
|
||||
//ids.sort((a, b) => b - a);
|
||||
|
||||
// Fetch the notes from the database until we have enough to satisfy the limit
|
||||
start = 0;
|
||||
const found = [];
|
||||
while (found.length < limit && start < ids.length) {
|
||||
const chunk = ids.slice(start, start + chunkSize);
|
||||
// Fetch the notes from the database until we have enough to satisfy the limit
|
||||
start = 0;
|
||||
const found = [];
|
||||
while (found.length < limit && start < ids.length) {
|
||||
const chunk = ids.slice(start, start + chunkSize);
|
||||
|
||||
const query = Notes.createQueryBuilder("note")
|
||||
.where({id: In(chunk)})
|
||||
.orderBy({id: "DESC"})
|
||||
const query = Notes.createQueryBuilder("note")
|
||||
.where({id: In(chunk)})
|
||||
.orderBy({id: "DESC"})
|
||||
|
||||
generateVisibilityQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
|
||||
if (!accountId) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
}
|
||||
if (!accountId) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
}
|
||||
|
||||
const notes: Note[] = await query.getMany();
|
||||
const notes: Note[] = await query.getMany();
|
||||
|
||||
found.push(...notes);
|
||||
start += chunkSize;
|
||||
}
|
||||
found.push(...notes);
|
||||
start += chunkSize;
|
||||
}
|
||||
|
||||
// If we have more results than the limit, trim them
|
||||
if (found.length > limit) {
|
||||
found.length = limit;
|
||||
}
|
||||
// If we have more results than the limit, trim them
|
||||
if (found.length > limit) {
|
||||
found.length = limit;
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
else if (es) {
|
||||
const userQuery =
|
||||
accountId != null
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
userId: accountId,
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
return found;
|
||||
} else if (es) {
|
||||
const userQuery =
|
||||
accountId != null
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
userId: accountId,
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const result = await es.search({
|
||||
index: config.elasticsearch.index || "misskey_note",
|
||||
body: {
|
||||
size: limit,
|
||||
from: offset,
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
simple_query_string: {
|
||||
fields: ["text"],
|
||||
query: q.toLowerCase(),
|
||||
default_operator: "and",
|
||||
},
|
||||
},
|
||||
...userQuery,
|
||||
],
|
||||
},
|
||||
},
|
||||
sort: [
|
||||
{
|
||||
_doc: "desc",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const result = await es.search({
|
||||
index: config.elasticsearch.index || "misskey_note",
|
||||
body: {
|
||||
size: limit,
|
||||
from: offset,
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
simple_query_string: {
|
||||
fields: ["text"],
|
||||
query: q.toLowerCase(),
|
||||
default_operator: "and",
|
||||
},
|
||||
},
|
||||
...userQuery,
|
||||
],
|
||||
},
|
||||
},
|
||||
sort: [
|
||||
{
|
||||
_doc: "desc",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const hits = result.body.hits.hits.map((hit: any) => hit._id);
|
||||
const hits = result.body.hits.hits.map((hit: any) => hit._id);
|
||||
|
||||
if (hits.length === 0) return [];
|
||||
if (hits.length === 0) return [];
|
||||
|
||||
// Fetch found notes
|
||||
const notes = await Notes.find({
|
||||
where: {
|
||||
id: In(hits),
|
||||
},
|
||||
order: {
|
||||
id: -1,
|
||||
},
|
||||
});
|
||||
// Fetch found notes
|
||||
const notes = await Notes.find({
|
||||
where: {
|
||||
id: In(hits),
|
||||
},
|
||||
order: {
|
||||
id: -1,
|
||||
},
|
||||
});
|
||||
|
||||
//TODO: test this
|
||||
//FIXME: implement pagination
|
||||
return notes;
|
||||
}
|
||||
//TODO: test this
|
||||
//FIXME: implement pagination
|
||||
return notes;
|
||||
}
|
||||
|
||||
// Fallback to database query
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
undefined,
|
||||
minId,
|
||||
maxId,
|
||||
);
|
||||
// Fallback to database query
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
undefined,
|
||||
minId,
|
||||
maxId,
|
||||
);
|
||||
|
||||
if (accountId) {
|
||||
query.andWhere("note.userId = :userId", { userId: accountId });
|
||||
}
|
||||
if (accountId) {
|
||||
query.andWhere("note.userId = :userId", {userId: accountId});
|
||||
}
|
||||
|
||||
if (following) {
|
||||
const followingQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :followerId", {followerId: user.id});
|
||||
if (following) {
|
||||
const followingQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :followerId", {followerId: user.id});
|
||||
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id});
|
||||
}),
|
||||
)
|
||||
}
|
||||
query.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id});
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
query
|
||||
.andWhere("note.text ILIKE :q", { q: `%${sqlLikeEscape(q)}%` })
|
||||
.leftJoinAndSelect("note.renote", "renote");
|
||||
query
|
||||
.andWhere("note.text ILIKE :q", {q: `%${sqlLikeEscape(q)}%`})
|
||||
.leftJoinAndSelect("note.renote", "renote");
|
||||
|
||||
|
||||
generateVisibilityQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
|
||||
if (!accountId) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
}
|
||||
if (!accountId) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
}
|
||||
|
||||
return query.skip(offset ?? 0).take(limit).getMany().then(p => minId ? p.reverse() : p);
|
||||
}
|
||||
return query.skip(offset ?? 0).take(limit).getMany().then(p => minId ? p.reverse() : p);
|
||||
}
|
||||
|
||||
private static async searchTags(q: string, excludeUnreviewed: boolean, limit: number, offset: number | undefined): Promise<MastodonEntity.Tag[]> {
|
||||
const tags = Hashtags.createQueryBuilder('tag')
|
||||
.select('tag.name')
|
||||
.distinctOn(['tag.name'])
|
||||
.where("tag.name ILIKE :q", { q: `%${sqlLikeEscape(q)}%` })
|
||||
.orderBy({'tag.name': 'ASC'})
|
||||
.skip(offset ?? 0).take(limit).getMany();
|
||||
private static async searchTags(q: string, excludeUnreviewed: boolean, limit: number, offset: number | undefined): Promise<MastodonEntity.Tag[]> {
|
||||
const tags = Hashtags.createQueryBuilder('tag')
|
||||
.select('tag.name')
|
||||
.distinctOn(['tag.name'])
|
||||
.where("tag.name ILIKE :q", {q: `%${sqlLikeEscape(q)}%`})
|
||||
.orderBy({'tag.name': 'ASC'})
|
||||
.skip(offset ?? 0).take(limit).getMany();
|
||||
|
||||
return tags.then(p => p.map(tag => {
|
||||
return {
|
||||
name: tag.name,
|
||||
url: `${config.url}/tags/${tag.name}`,
|
||||
history: null
|
||||
};
|
||||
}));
|
||||
}
|
||||
return tags.then(p => p.map(tag => {
|
||||
return {
|
||||
name: tag.name,
|
||||
url: `${config.url}/tags/${tag.name}`,
|
||||
history: null
|
||||
};
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,78 +15,78 @@ import { meta } from "@/server/api/endpoints/notes/global-timeline.js";
|
|||
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||
|
||||
export class TimelineHelpers {
|
||||
public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<Note[]> {
|
||||
if (limit > 40) limit = 40;
|
||||
public static async getHomeTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<Note[]> {
|
||||
if (limit > 40) limit = 40;
|
||||
|
||||
const followingQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :followerId", {followerId: user.id});
|
||||
const followingQuery = Followings.createQueryBuilder("following")
|
||||
.select("following.followeeId")
|
||||
.where("following.followerId = :followerId", {followerId: user.id});
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id});
|
||||
}),
|
||||
)
|
||||
.leftJoinAndSelect("note.renote", "renote");
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere(
|
||||
new Brackets((qb) => {
|
||||
qb.where(`note.userId IN (${followingQuery.getQuery()} UNION ALL VALUES (:meId))`, {meId: user.id});
|
||||
}),
|
||||
)
|
||||
.leftJoinAndSelect("note.renote", "renote");
|
||||
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, true, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
generateMutedUserRenotesQueryForNotes(query, user);
|
||||
generateChannelQuery(query, user);
|
||||
generateRepliesQuery(query, true, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
generateMutedUserRenotesQueryForNotes(query, user);
|
||||
|
||||
query.andWhere("note.visibility != 'hidden'");
|
||||
query.andWhere("note.visibility != 'specified'");
|
||||
query.andWhere("note.visibility != 'hidden'");
|
||||
query.andWhere("note.visibility != 'specified'");
|
||||
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined);
|
||||
}
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined);
|
||||
}
|
||||
|
||||
public static async getPublicTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, local: boolean = false, remote: boolean = false): Promise<Note[]> {
|
||||
if (limit > 40) limit = 40;
|
||||
public static async getPublicTimeline(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, local: boolean = false, remote: boolean = false): Promise<Note[]> {
|
||||
if (limit > 40) limit = 40;
|
||||
|
||||
const m = await fetchMeta();
|
||||
if (m.disableGlobalTimeline) {
|
||||
if (user == null || !(user.isAdmin || user.isModerator)) {
|
||||
throw new ApiError(meta.errors.gtlDisabled);
|
||||
}
|
||||
}
|
||||
const m = await fetchMeta();
|
||||
if (m.disableGlobalTimeline) {
|
||||
if (user == null || !(user.isAdmin || user.isModerator)) {
|
||||
throw new ApiError(meta.errors.gtlDisabled);
|
||||
}
|
||||
}
|
||||
|
||||
if (local && remote) {
|
||||
throw new Error("local and remote are mutually exclusive options");
|
||||
}
|
||||
if (local && remote) {
|
||||
throw new Error("local and remote are mutually exclusive options");
|
||||
}
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("note.visibility = 'public'");
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("note.visibility = 'public'");
|
||||
|
||||
if (remote) query.andWhere("note.userHost IS NOT NULL");
|
||||
if (local) query.andWhere("note.userHost IS NULL");
|
||||
if (!local) query.andWhere("note.channelId IS NULL");
|
||||
if (remote) query.andWhere("note.userHost IS NOT NULL");
|
||||
if (local) query.andWhere("note.userHost IS NULL");
|
||||
if (!local) query.andWhere("note.channelId IS NULL");
|
||||
|
||||
query.leftJoinAndSelect("note.renote", "renote");
|
||||
query.leftJoinAndSelect("note.renote", "renote");
|
||||
|
||||
generateRepliesQuery(query, true, user);
|
||||
if (user) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
generateMutedUserRenotesQueryForNotes(query, user);
|
||||
}
|
||||
generateRepliesQuery(query, true, user);
|
||||
if (user) {
|
||||
generateMutedUserQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
generateBlockedUserQuery(query, user);
|
||||
generateMutedUserRenotesQueryForNotes(query, user);
|
||||
}
|
||||
|
||||
if (onlyMedia) query.andWhere("note.fileIds != '{}'");
|
||||
if (onlyMedia) query.andWhere("note.fileIds != '{}'");
|
||||
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined);
|
||||
}
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import { Note } from "@/models/entities/note.js";
|
||||
import { ILocalUser, User } from "@/models/entities/user.js";
|
||||
import {
|
||||
Blockings,
|
||||
Followings,
|
||||
FollowRequests,
|
||||
Mutings,
|
||||
NoteFavorites,
|
||||
NoteReactions,
|
||||
Notes,
|
||||
NoteWatchings, RegistryItems, UserNotePinings,
|
||||
UserProfiles,
|
||||
Users
|
||||
Blockings,
|
||||
Followings,
|
||||
FollowRequests,
|
||||
Mutings,
|
||||
NoteFavorites,
|
||||
NoteReactions,
|
||||
Notes,
|
||||
NoteWatchings,
|
||||
RegistryItems,
|
||||
UserNotePinings,
|
||||
UserProfiles,
|
||||
Users
|
||||
} from "@/models/index.js";
|
||||
import { generateRepliesQuery } from "@/server/api/common/generate-replies-query.js";
|
||||
import { generateVisibilityQuery } from "@/server/api/common/generate-visibility-query.js";
|
||||
import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js";
|
||||
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
|
||||
import Entity from "megalodon/src/entity.js";
|
||||
import AsyncLock from "async-lock";
|
||||
import { getUser } from "@/server/api/common/getters.js";
|
||||
import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js";
|
||||
|
@ -34,459 +34,457 @@ import acceptFollowRequest from "@/services/following/requests/accept.js";
|
|||
import { rejectFollowRequest } from "@/services/following/reject.js";
|
||||
import { Brackets, IsNull } from "typeorm";
|
||||
import { VisibilityConverter } from "@/server/api/mastodon/converters/visibility.js";
|
||||
import { UserProfile } from "@/models/entities/user-profile.js";
|
||||
|
||||
export type AccountCache = {
|
||||
locks: AsyncLock;
|
||||
accounts: MastodonEntity.Account[];
|
||||
users: User[];
|
||||
locks: AsyncLock;
|
||||
accounts: MastodonEntity.Account[];
|
||||
users: User[];
|
||||
};
|
||||
|
||||
export type LinkPaginationObject<T> = {
|
||||
data: T;
|
||||
maxId?: string | undefined;
|
||||
minId?: string | undefined;
|
||||
data: T;
|
||||
maxId?: string | undefined;
|
||||
minId?: string | undefined;
|
||||
}
|
||||
|
||||
export type updateCredsData = {
|
||||
display_name: string;
|
||||
note: string;
|
||||
locked: boolean;
|
||||
bot: boolean;
|
||||
discoverable: boolean;
|
||||
display_name: string;
|
||||
note: string;
|
||||
locked: boolean;
|
||||
bot: boolean;
|
||||
discoverable: boolean;
|
||||
}
|
||||
|
||||
type RelationshipType = 'followers' | 'following';
|
||||
|
||||
export class UserHelpers {
|
||||
public static async followUser(target: User, localUser: ILocalUser, reblogs: boolean, notify: boolean): Promise<MastodonEntity.Relationship> {
|
||||
//FIXME: implement reblogs & notify params
|
||||
const following = await Followings.exist({where: {followerId: localUser.id, followeeId: target.id}});
|
||||
const requested = await FollowRequests.exist({where: {followerId: localUser.id, followeeId: target.id}});
|
||||
if (!following && !requested)
|
||||
await createFollowing(localUser, target);
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async unfollowUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const following = await Followings.exist({where: {followerId: localUser.id, followeeId: target.id}});
|
||||
const requested = await FollowRequests.exist({where: {followerId: localUser.id, followeeId: target.id}});
|
||||
if (following)
|
||||
await deleteFollowing(localUser, target);
|
||||
if (requested)
|
||||
await cancelFollowRequest(target, localUser);
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async blockUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const blocked = await Blockings.exist({where: {blockerId: localUser.id, blockeeId: target.id}});
|
||||
if (!blocked)
|
||||
await createBlocking(localUser, target);
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async unblockUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const blocked = await Blockings.exist({where: {blockerId: localUser.id, blockeeId: target.id}});
|
||||
if (blocked)
|
||||
await deleteBlocking(localUser, target);
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async muteUser(target: User, localUser: ILocalUser, notifications: boolean = true, duration: number = 0): Promise<MastodonEntity.Relationship> {
|
||||
//FIXME: respect notifications parameter
|
||||
const muted = await Mutings.exist({where: {muterId: localUser.id, muteeId: target.id}});
|
||||
if (!muted) {
|
||||
await Mutings.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: duration === 0 ? null : new Date(new Date().getTime() + (duration * 1000)),
|
||||
muterId: localUser.id,
|
||||
muteeId: target.id,
|
||||
} as Muting);
|
||||
|
||||
publishUserEvent(localUser.id, "mute", target);
|
||||
|
||||
NoteWatchings.delete({
|
||||
userId: localUser.id,
|
||||
noteUserId: target.id,
|
||||
});
|
||||
}
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async unmuteUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const muting = await Mutings.findOneBy({muterId: localUser.id, muteeId: target.id});
|
||||
if (muting) {
|
||||
await Mutings.delete({
|
||||
id: muting.id,
|
||||
});
|
||||
|
||||
publishUserEvent(localUser.id, "unmute", target);
|
||||
}
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async acceptFollowRequest(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const pending = await FollowRequests.exist({where: {followerId: target.id, followeeId: localUser.id}});
|
||||
if (pending)
|
||||
await acceptFollowRequest(localUser, target);
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async rejectFollowRequest(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const pending = await FollowRequests.exist({where: {followerId: target.id, followeeId: localUser.id}});
|
||||
if (pending)
|
||||
await rejectFollowRequest(localUser, target);
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async updateCredentials(user: ILocalUser, formData: updateCredsData): Promise<MastodonEntity.Account> {
|
||||
//FIXME: Actually implement this
|
||||
//FIXME: handle multipart avatar & header image upload
|
||||
//FIXME: handle field attributes
|
||||
const obj: any = {};
|
||||
|
||||
if (formData.display_name) obj.name = formData.display_name;
|
||||
if (formData.note) obj.description = formData.note;
|
||||
if (formData.locked) obj.isLocked = formData.locked;
|
||||
if (formData.bot) obj.isBot = formData.bot;
|
||||
if (formData.discoverable) obj.isExplorable = formData.discoverable;
|
||||
|
||||
return this.verifyCredentials(user);
|
||||
}
|
||||
|
||||
public static async verifyCredentials(user: ILocalUser): Promise<MastodonEntity.Account> {
|
||||
const acct = UserConverter.encode(user);
|
||||
const profile = UserProfiles.findOneByOrFail({userId: user.id});
|
||||
const privacy = this.getDefaultNoteVisibility(user);
|
||||
return acct.then(acct => {
|
||||
const source = {
|
||||
note: acct.note,
|
||||
fields: acct.fields,
|
||||
privacy: privacy.then(p => VisibilityConverter.encode(p)),
|
||||
sensitive: profile.then(p => p.alwaysMarkNsfw),
|
||||
language: profile.then(p => p.lang ?? ''),
|
||||
};
|
||||
|
||||
const result = {
|
||||
...acct,
|
||||
source: awaitAll(source)
|
||||
};
|
||||
|
||||
return awaitAll(result);
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserFromAcct(acct: string): Promise<User | null> {
|
||||
const split = acct.toLowerCase().split('@');
|
||||
if (split.length > 2) throw new Error('Invalid acct');
|
||||
return Users.findOneBy({usernameLower: split[0], host: split[1] ?? IsNull()});
|
||||
}
|
||||
|
||||
public static async getUserMutes(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<LinkPaginationObject<MastodonEntity.MutedAccount[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Mutings.createQueryBuilder("muting"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
);
|
||||
|
||||
query.andWhere("muting.muterId = :userId", {userId: user.id})
|
||||
.innerJoinAndSelect("muting.mutee", "mutee");
|
||||
|
||||
return query.take(limit).getMany().then(async p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.mutee)
|
||||
.filter(p => p) as User[];
|
||||
|
||||
const result = await UserConverter.encodeMany(users, cache)
|
||||
.then(res => res.map(m => {
|
||||
const muting = p.find(acc => acc.muteeId === m.id);
|
||||
return {
|
||||
...m,
|
||||
mute_expires_at: muting?.expiresAt?.toISOString() ?? null
|
||||
} as MastodonEntity.MutedAccount
|
||||
}));
|
||||
|
||||
return {
|
||||
data: result,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserBlocks(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Blockings.createQueryBuilder("blocking"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
);
|
||||
|
||||
query.andWhere("blocking.blockerId = :userId", {userId: user.id})
|
||||
.innerJoinAndSelect("blocking.blockee", "blockee");
|
||||
|
||||
return query.take(limit).getMany().then(p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.blockee)
|
||||
.filter(p => p) as User[];
|
||||
|
||||
return {
|
||||
data: users,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserFollowRequests(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
FollowRequests.createQueryBuilder("request"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
);
|
||||
|
||||
query.andWhere("request.followeeId = :userId", {userId: user.id})
|
||||
.innerJoinAndSelect("request.follower", "follower");
|
||||
|
||||
return query.take(limit).getMany().then(p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.follower)
|
||||
.filter(p => p) as User[];
|
||||
|
||||
return {
|
||||
data: users,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserStatuses(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, excludeReplies: boolean = false, excludeReblogs: boolean = false, pinned: boolean = false, tagged: string | undefined): Promise<Note[]> {
|
||||
if (limit > 40) limit = 40;
|
||||
|
||||
if (tagged !== undefined) {
|
||||
//FIXME respect tagged
|
||||
return [];
|
||||
}
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("note.userId = :userId");
|
||||
|
||||
if (pinned) {
|
||||
const sq = UserNotePinings.createQueryBuilder("pin")
|
||||
.select("pin.noteId")
|
||||
.where("pin.userId = :userId");
|
||||
query.andWhere(`note.id IN (${sq.getQuery()})`);
|
||||
}
|
||||
|
||||
if (excludeReblogs) {
|
||||
query.andWhere(
|
||||
new Brackets(qb => {
|
||||
qb.where('note.renoteId IS NULL')
|
||||
.orWhere('note.text IS NOT NULL');
|
||||
}));
|
||||
}
|
||||
|
||||
if (excludeReplies) {
|
||||
query.leftJoin("note", "thread", "note.threadId = thread.id")
|
||||
.andWhere(
|
||||
new Brackets(qb => {
|
||||
qb.where("note.replyId IS NULL")
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb.where('note.mentions = :mentions', {mentions: []})
|
||||
.andWhere('thread.userId = :userId')
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
query.leftJoinAndSelect("note.renote", "renote");
|
||||
|
||||
generateVisibilityQuery(query, localUser);
|
||||
if (localUser) {
|
||||
generateMutedUserQuery(query, localUser, user);
|
||||
generateBlockedUserQuery(query, localUser);
|
||||
}
|
||||
|
||||
if (onlyMedia) query.andWhere("note.fileIds != '{}'");
|
||||
|
||||
query.andWhere("note.visibility != 'hidden'");
|
||||
query.andWhere("note.visibility != 'specified'");
|
||||
|
||||
query.setParameters({ userId: user.id });
|
||||
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined);
|
||||
}
|
||||
|
||||
public static async getUserBookmarks(localUser: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<LinkPaginationObject<Note[]>> {
|
||||
if (limit > 40) limit = 40;
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
NoteFavorites.createQueryBuilder("favorite"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("favorite.userId = :meId", { meId: localUser.id })
|
||||
.leftJoinAndSelect("favorite.note", "note");
|
||||
|
||||
generateVisibilityQuery(query, localUser);
|
||||
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined)
|
||||
.then(res => {
|
||||
return {
|
||||
data: res.map(p => p.note as Note),
|
||||
maxId: res.map(p => p.id).at(-1),
|
||||
minId: res.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserFavorites(localUser: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<LinkPaginationObject<Note[]>> {
|
||||
if (limit > 40) limit = 40;
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
NoteReactions.createQueryBuilder("reaction"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("reaction.userId = :meId", { meId: localUser.id })
|
||||
.leftJoinAndSelect("reaction.note", "note");
|
||||
|
||||
generateVisibilityQuery(query, localUser);
|
||||
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined)
|
||||
.then(res => {
|
||||
return {
|
||||
data: res.map(p => p.note as Note),
|
||||
maxId: res.map(p => p.id).at(-1),
|
||||
minId: res.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static async getUserRelationships(type: RelationshipType, user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
if (profile.ffVisibility === "private") {
|
||||
if (!localUser || user.id !== localUser.id) return { data: [] };
|
||||
}
|
||||
else if (profile.ffVisibility === "followers") {
|
||||
if (!localUser) return { data: [] };
|
||||
if (user.id !== localUser.id) {
|
||||
const isFollowed = await Followings.exist({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: localUser.id,
|
||||
},
|
||||
});
|
||||
if (!isFollowed) return { data: [] };
|
||||
}
|
||||
}
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Followings.createQueryBuilder("following"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
);
|
||||
|
||||
if (type === "followers") {
|
||||
query.andWhere("following.followeeId = :userId", {userId: user.id})
|
||||
.innerJoinAndSelect("following.follower", "follower");
|
||||
} else {
|
||||
query.andWhere("following.followerId = :userId", {userId: user.id})
|
||||
.innerJoinAndSelect("following.followee", "followee");
|
||||
}
|
||||
|
||||
return query.take(limit).getMany().then(p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
|
||||
return {
|
||||
data: p.map(p => type === "followers" ? p.follower : p.followee).filter(p => p) as User[],
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserFollowers(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
return this.getUserRelationships('followers', user, localUser, maxId, sinceId, minId, limit);
|
||||
}
|
||||
|
||||
public static async getUserFollowing(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
return this.getUserRelationships('following', user, localUser, maxId, sinceId, minId, limit);
|
||||
}
|
||||
|
||||
public static async getUserRelationhipToMany(targetIds: string[], localUserId: string): Promise<MastodonEntity.Relationship[]> {
|
||||
return Promise.all(targetIds.map(targetId => this.getUserRelationshipTo(targetId, localUserId)));
|
||||
}
|
||||
|
||||
public static async getUserRelationshipTo(targetId: string, localUserId: string): Promise<MastodonEntity.Relationship> {
|
||||
const relation = await Users.getRelation(localUserId, targetId);
|
||||
const response = {
|
||||
id: targetId,
|
||||
following: relation.isFollowing,
|
||||
followed_by: relation.isFollowed,
|
||||
blocking: relation.isBlocking,
|
||||
blocked_by: relation.isBlocked,
|
||||
muting: relation.isMuted,
|
||||
muting_notifications: relation.isMuted,
|
||||
requested: relation.hasPendingFollowRequestFromYou,
|
||||
domain_blocking: false, //FIXME
|
||||
showing_reblogs: !relation.isRenoteMuted,
|
||||
endorsed: false,
|
||||
notifying: false, //FIXME
|
||||
note: '' //FIXME
|
||||
}
|
||||
|
||||
return awaitAll(response);
|
||||
}
|
||||
|
||||
public static async getUserCached(id: string, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<User> {
|
||||
return cache.locks.acquire(id, async () => {
|
||||
const cacheHit = cache.users.find(p => p.id == id);
|
||||
if (cacheHit) return cacheHit;
|
||||
return getUser(id).then(p => {
|
||||
cache.users.push(p);
|
||||
return p;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static getFreshAccountCache(): AccountCache {
|
||||
return {
|
||||
locks: new AsyncLock(),
|
||||
accounts: [],
|
||||
users: [],
|
||||
};
|
||||
}
|
||||
|
||||
public static getDefaultNoteVisibility(user: ILocalUser): Promise<string> {
|
||||
return RegistryItems.findOneBy({domain: IsNull(), userId: user.id, key: 'defaultNoteVisibility', scope: '{client,base}'}).then(p => p?.value ?? 'public')
|
||||
}
|
||||
public static async followUser(target: User, localUser: ILocalUser, reblogs: boolean, notify: boolean): Promise<MastodonEntity.Relationship> {
|
||||
//FIXME: implement reblogs & notify params
|
||||
const following = await Followings.exist({where: {followerId: localUser.id, followeeId: target.id}});
|
||||
const requested = await FollowRequests.exist({where: {followerId: localUser.id, followeeId: target.id}});
|
||||
if (!following && !requested)
|
||||
await createFollowing(localUser, target);
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async unfollowUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const following = await Followings.exist({where: {followerId: localUser.id, followeeId: target.id}});
|
||||
const requested = await FollowRequests.exist({where: {followerId: localUser.id, followeeId: target.id}});
|
||||
if (following)
|
||||
await deleteFollowing(localUser, target);
|
||||
if (requested)
|
||||
await cancelFollowRequest(target, localUser);
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async blockUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const blocked = await Blockings.exist({where: {blockerId: localUser.id, blockeeId: target.id}});
|
||||
if (!blocked)
|
||||
await createBlocking(localUser, target);
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async unblockUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const blocked = await Blockings.exist({where: {blockerId: localUser.id, blockeeId: target.id}});
|
||||
if (blocked)
|
||||
await deleteBlocking(localUser, target);
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async muteUser(target: User, localUser: ILocalUser, notifications: boolean = true, duration: number = 0): Promise<MastodonEntity.Relationship> {
|
||||
//FIXME: respect notifications parameter
|
||||
const muted = await Mutings.exist({where: {muterId: localUser.id, muteeId: target.id}});
|
||||
if (!muted) {
|
||||
await Mutings.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: duration === 0 ? null : new Date(new Date().getTime() + (duration * 1000)),
|
||||
muterId: localUser.id,
|
||||
muteeId: target.id,
|
||||
} as Muting);
|
||||
|
||||
publishUserEvent(localUser.id, "mute", target);
|
||||
|
||||
NoteWatchings.delete({
|
||||
userId: localUser.id,
|
||||
noteUserId: target.id,
|
||||
});
|
||||
}
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async unmuteUser(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const muting = await Mutings.findOneBy({muterId: localUser.id, muteeId: target.id});
|
||||
if (muting) {
|
||||
await Mutings.delete({
|
||||
id: muting.id,
|
||||
});
|
||||
|
||||
publishUserEvent(localUser.id, "unmute", target);
|
||||
}
|
||||
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async acceptFollowRequest(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const pending = await FollowRequests.exist({where: {followerId: target.id, followeeId: localUser.id}});
|
||||
if (pending)
|
||||
await acceptFollowRequest(localUser, target);
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async rejectFollowRequest(target: User, localUser: ILocalUser): Promise<MastodonEntity.Relationship> {
|
||||
const pending = await FollowRequests.exist({where: {followerId: target.id, followeeId: localUser.id}});
|
||||
if (pending)
|
||||
await rejectFollowRequest(localUser, target);
|
||||
return this.getUserRelationshipTo(target.id, localUser.id);
|
||||
}
|
||||
|
||||
public static async updateCredentials(user: ILocalUser, formData: updateCredsData): Promise<MastodonEntity.Account> {
|
||||
//FIXME: Actually implement this
|
||||
//FIXME: handle multipart avatar & header image upload
|
||||
//FIXME: handle field attributes
|
||||
const obj: any = {};
|
||||
|
||||
if (formData.display_name) obj.name = formData.display_name;
|
||||
if (formData.note) obj.description = formData.note;
|
||||
if (formData.locked) obj.isLocked = formData.locked;
|
||||
if (formData.bot) obj.isBot = formData.bot;
|
||||
if (formData.discoverable) obj.isExplorable = formData.discoverable;
|
||||
|
||||
return this.verifyCredentials(user);
|
||||
}
|
||||
|
||||
public static async verifyCredentials(user: ILocalUser): Promise<MastodonEntity.Account> {
|
||||
const acct = UserConverter.encode(user);
|
||||
const profile = UserProfiles.findOneByOrFail({userId: user.id});
|
||||
const privacy = this.getDefaultNoteVisibility(user);
|
||||
return acct.then(acct => {
|
||||
const source = {
|
||||
note: acct.note,
|
||||
fields: acct.fields,
|
||||
privacy: privacy.then(p => VisibilityConverter.encode(p)),
|
||||
sensitive: profile.then(p => p.alwaysMarkNsfw),
|
||||
language: profile.then(p => p.lang ?? ''),
|
||||
};
|
||||
|
||||
const result = {
|
||||
...acct,
|
||||
source: awaitAll(source)
|
||||
};
|
||||
|
||||
return awaitAll(result);
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserFromAcct(acct: string): Promise<User | null> {
|
||||
const split = acct.toLowerCase().split('@');
|
||||
if (split.length > 2) throw new Error('Invalid acct');
|
||||
return Users.findOneBy({usernameLower: split[0], host: split[1] ?? IsNull()});
|
||||
}
|
||||
|
||||
public static async getUserMutes(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<LinkPaginationObject<MastodonEntity.MutedAccount[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Mutings.createQueryBuilder("muting"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
);
|
||||
|
||||
query.andWhere("muting.muterId = :userId", {userId: user.id})
|
||||
.innerJoinAndSelect("muting.mutee", "mutee");
|
||||
|
||||
return query.take(limit).getMany().then(async p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.mutee)
|
||||
.filter(p => p) as User[];
|
||||
|
||||
const result = await UserConverter.encodeMany(users, cache)
|
||||
.then(res => res.map(m => {
|
||||
const muting = p.find(acc => acc.muteeId === m.id);
|
||||
return {
|
||||
...m,
|
||||
mute_expires_at: muting?.expiresAt?.toISOString() ?? null
|
||||
} as MastodonEntity.MutedAccount
|
||||
}));
|
||||
|
||||
return {
|
||||
data: result,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserBlocks(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Blockings.createQueryBuilder("blocking"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
);
|
||||
|
||||
query.andWhere("blocking.blockerId = :userId", {userId: user.id})
|
||||
.innerJoinAndSelect("blocking.blockee", "blockee");
|
||||
|
||||
return query.take(limit).getMany().then(p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.blockee)
|
||||
.filter(p => p) as User[];
|
||||
|
||||
return {
|
||||
data: users,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserFollowRequests(user: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
FollowRequests.createQueryBuilder("request"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
);
|
||||
|
||||
query.andWhere("request.followeeId = :userId", {userId: user.id})
|
||||
.innerJoinAndSelect("request.follower", "follower");
|
||||
|
||||
return query.take(limit).getMany().then(p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
const users = p
|
||||
.map(p => p.follower)
|
||||
.filter(p => p) as User[];
|
||||
|
||||
return {
|
||||
data: users,
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserStatuses(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20, onlyMedia: boolean = false, excludeReplies: boolean = false, excludeReblogs: boolean = false, pinned: boolean = false, tagged: string | undefined): Promise<Note[]> {
|
||||
if (limit > 40) limit = 40;
|
||||
|
||||
if (tagged !== undefined) {
|
||||
//FIXME respect tagged
|
||||
return [];
|
||||
}
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Notes.createQueryBuilder("note"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("note.userId = :userId");
|
||||
|
||||
if (pinned) {
|
||||
const sq = UserNotePinings.createQueryBuilder("pin")
|
||||
.select("pin.noteId")
|
||||
.where("pin.userId = :userId");
|
||||
query.andWhere(`note.id IN (${sq.getQuery()})`);
|
||||
}
|
||||
|
||||
if (excludeReblogs) {
|
||||
query.andWhere(
|
||||
new Brackets(qb => {
|
||||
qb.where('note.renoteId IS NULL')
|
||||
.orWhere('note.text IS NOT NULL');
|
||||
}));
|
||||
}
|
||||
|
||||
if (excludeReplies) {
|
||||
query.leftJoin("note", "thread", "note.threadId = thread.id")
|
||||
.andWhere(
|
||||
new Brackets(qb => {
|
||||
qb.where("note.replyId IS NULL")
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb.where('note.mentions = :mentions', {mentions: []})
|
||||
.andWhere('thread.userId = :userId')
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
query.leftJoinAndSelect("note.renote", "renote");
|
||||
|
||||
generateVisibilityQuery(query, localUser);
|
||||
if (localUser) {
|
||||
generateMutedUserQuery(query, localUser, user);
|
||||
generateBlockedUserQuery(query, localUser);
|
||||
}
|
||||
|
||||
if (onlyMedia) query.andWhere("note.fileIds != '{}'");
|
||||
|
||||
query.andWhere("note.visibility != 'hidden'");
|
||||
query.andWhere("note.visibility != 'specified'");
|
||||
|
||||
query.setParameters({userId: user.id});
|
||||
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined);
|
||||
}
|
||||
|
||||
public static async getUserBookmarks(localUser: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<LinkPaginationObject<Note[]>> {
|
||||
if (limit > 40) limit = 40;
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
NoteFavorites.createQueryBuilder("favorite"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("favorite.userId = :meId", {meId: localUser.id})
|
||||
.leftJoinAndSelect("favorite.note", "note");
|
||||
|
||||
generateVisibilityQuery(query, localUser);
|
||||
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined)
|
||||
.then(res => {
|
||||
return {
|
||||
data: res.map(p => p.note as Note),
|
||||
maxId: res.map(p => p.id).at(-1),
|
||||
minId: res.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserFavorites(localUser: ILocalUser, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 20): Promise<LinkPaginationObject<Note[]>> {
|
||||
if (limit > 40) limit = 40;
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
NoteReactions.createQueryBuilder("reaction"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
)
|
||||
.andWhere("reaction.userId = :meId", {meId: localUser.id})
|
||||
.leftJoinAndSelect("reaction.note", "note");
|
||||
|
||||
generateVisibilityQuery(query, localUser);
|
||||
|
||||
return PaginationHelpers.execQuery(query, limit, minId !== undefined)
|
||||
.then(res => {
|
||||
return {
|
||||
data: res.map(p => p.note as Note),
|
||||
maxId: res.map(p => p.id).at(-1),
|
||||
minId: res.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static async getUserRelationships(type: RelationshipType, user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
if (limit > 80) limit = 80;
|
||||
|
||||
const profile = await UserProfiles.findOneByOrFail({userId: user.id});
|
||||
if (profile.ffVisibility === "private") {
|
||||
if (!localUser || user.id !== localUser.id) return {data: []};
|
||||
} else if (profile.ffVisibility === "followers") {
|
||||
if (!localUser) return {data: []};
|
||||
if (user.id !== localUser.id) {
|
||||
const isFollowed = await Followings.exist({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: localUser.id,
|
||||
},
|
||||
});
|
||||
if (!isFollowed) return {data: []};
|
||||
}
|
||||
}
|
||||
|
||||
const query = PaginationHelpers.makePaginationQuery(
|
||||
Followings.createQueryBuilder("following"),
|
||||
sinceId,
|
||||
maxId,
|
||||
minId
|
||||
);
|
||||
|
||||
if (type === "followers") {
|
||||
query.andWhere("following.followeeId = :userId", {userId: user.id})
|
||||
.innerJoinAndSelect("following.follower", "follower");
|
||||
} else {
|
||||
query.andWhere("following.followerId = :userId", {userId: user.id})
|
||||
.innerJoinAndSelect("following.followee", "followee");
|
||||
}
|
||||
|
||||
return query.take(limit).getMany().then(p => {
|
||||
if (minId !== undefined) p = p.reverse();
|
||||
|
||||
return {
|
||||
data: p.map(p => type === "followers" ? p.follower : p.followee).filter(p => p) as User[],
|
||||
maxId: p.map(p => p.id).at(-1),
|
||||
minId: p.map(p => p.id)[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserFollowers(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
return this.getUserRelationships('followers', user, localUser, maxId, sinceId, minId, limit);
|
||||
}
|
||||
|
||||
public static async getUserFollowing(user: User, localUser: ILocalUser | null, maxId: string | undefined, sinceId: string | undefined, minId: string | undefined, limit: number = 40): Promise<LinkPaginationObject<User[]>> {
|
||||
return this.getUserRelationships('following', user, localUser, maxId, sinceId, minId, limit);
|
||||
}
|
||||
|
||||
public static async getUserRelationhipToMany(targetIds: string[], localUserId: string): Promise<MastodonEntity.Relationship[]> {
|
||||
return Promise.all(targetIds.map(targetId => this.getUserRelationshipTo(targetId, localUserId)));
|
||||
}
|
||||
|
||||
public static async getUserRelationshipTo(targetId: string, localUserId: string): Promise<MastodonEntity.Relationship> {
|
||||
const relation = await Users.getRelation(localUserId, targetId);
|
||||
const response = {
|
||||
id: targetId,
|
||||
following: relation.isFollowing,
|
||||
followed_by: relation.isFollowed,
|
||||
blocking: relation.isBlocking,
|
||||
blocked_by: relation.isBlocked,
|
||||
muting: relation.isMuted,
|
||||
muting_notifications: relation.isMuted,
|
||||
requested: relation.hasPendingFollowRequestFromYou,
|
||||
domain_blocking: false, //FIXME
|
||||
showing_reblogs: !relation.isRenoteMuted,
|
||||
endorsed: false,
|
||||
notifying: false, //FIXME
|
||||
note: '' //FIXME
|
||||
}
|
||||
|
||||
return awaitAll(response);
|
||||
}
|
||||
|
||||
public static async getUserCached(id: string, cache: AccountCache = UserHelpers.getFreshAccountCache()): Promise<User> {
|
||||
return cache.locks.acquire(id, async () => {
|
||||
const cacheHit = cache.users.find(p => p.id == id);
|
||||
if (cacheHit) return cacheHit;
|
||||
return getUser(id).then(p => {
|
||||
cache.users.push(p);
|
||||
return p;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static getFreshAccountCache(): AccountCache {
|
||||
return {
|
||||
locks: new AsyncLock(),
|
||||
accounts: [],
|
||||
users: [],
|
||||
};
|
||||
}
|
||||
|
||||
public static getDefaultNoteVisibility(user: ILocalUser): Promise<string> {
|
||||
return RegistryItems.findOneBy({domain: IsNull(), userId: user.id, key: 'defaultNoteVisibility', scope: '{client,base}'}).then(p => p?.value ?? 'public')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,43 +14,43 @@ import multer from "@koa/multer";
|
|||
import { setupEndpointsList } from "@/server/api/mastodon/endpoints/list.js";
|
||||
|
||||
export function getClient(
|
||||
BASE_URL: string,
|
||||
authorization: string | undefined,
|
||||
BASE_URL: string,
|
||||
authorization: string | undefined,
|
||||
): MegalodonInterface {
|
||||
const accessTokenArr = authorization?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator(BASE_URL, accessToken) as MegalodonInterface;
|
||||
return client;
|
||||
const accessTokenArr = authorization?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator(BASE_URL, accessToken) as MegalodonInterface;
|
||||
return client;
|
||||
}
|
||||
|
||||
export function setupMastodonApi(router: Router, fileRouter: Router, upload: multer.Instance): void {
|
||||
router.use(
|
||||
koaBody({
|
||||
multipart: true,
|
||||
urlencoded: true,
|
||||
}),
|
||||
);
|
||||
router.use(
|
||||
koaBody({
|
||||
multipart: true,
|
||||
urlencoded: true,
|
||||
}),
|
||||
);
|
||||
|
||||
router.use(async (ctx, next) => {
|
||||
if (ctx.request.query) {
|
||||
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
|
||||
ctx.request.body = ctx.request.query;
|
||||
} else {
|
||||
ctx.request.body = { ...ctx.request.body, ...ctx.request.query };
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
router.use(async (ctx, next) => {
|
||||
if (ctx.request.query) {
|
||||
if (!ctx.request.body || Object.keys(ctx.request.body).length === 0) {
|
||||
ctx.request.body = ctx.request.query;
|
||||
} else {
|
||||
ctx.request.body = {...ctx.request.body, ...ctx.request.query};
|
||||
}
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
setupEndpointsAuth(router);
|
||||
setupEndpointsAccount(router);
|
||||
setupEndpointsStatus(router);
|
||||
setupEndpointsFilter(router);
|
||||
setupEndpointsTimeline(router);
|
||||
setupEndpointsNotifications(router);
|
||||
setupEndpointsSearch(router);
|
||||
setupEndpointsMedia(router, fileRouter, upload);
|
||||
setupEndpointsList(router);
|
||||
setupEndpointsMisc(router);
|
||||
setupEndpointsAuth(router);
|
||||
setupEndpointsAccount(router);
|
||||
setupEndpointsStatus(router);
|
||||
setupEndpointsFilter(router);
|
||||
setupEndpointsTimeline(router);
|
||||
setupEndpointsNotifications(router);
|
||||
setupEndpointsSearch(router);
|
||||
setupEndpointsMedia(router, fileRouter, upload);
|
||||
setupEndpointsList(router);
|
||||
setupEndpointsMisc(router);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue