mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-04-28 17:46:56 +00:00
Merge branch 'develop' into merge/2024-02-03
This commit is contained in:
commit
feb80ee992
42 changed files with 1707 additions and 1351 deletions
|
@ -100,6 +100,8 @@ url: https://example.tld/
|
||||||
|
|
||||||
# The port that your Misskey server should listen on.
|
# The port that your Misskey server should listen on.
|
||||||
port: 3000
|
port: 3000
|
||||||
|
# the address to bind to, defaults to "every address"
|
||||||
|
# address: '0.0.0.0'
|
||||||
|
|
||||||
# You can also use UNIX domain socket.
|
# You can also use UNIX domain socket.
|
||||||
# socket: /path/to/misskey.sock
|
# socket: /path/to/misskey.sock
|
||||||
|
|
24
locales/index.d.ts
vendored
24
locales/index.d.ts
vendored
|
@ -8538,6 +8538,30 @@ export interface Locale extends ILocale {
|
||||||
* 違反を報告する
|
* 違反を報告する
|
||||||
*/
|
*/
|
||||||
"write:report-abuse": string;
|
"write:report-abuse": string;
|
||||||
|
/**
|
||||||
|
* Approve new users
|
||||||
|
*/
|
||||||
|
"write:admin:approve-user": string;
|
||||||
|
/**
|
||||||
|
* Decline new users
|
||||||
|
*/
|
||||||
|
"write:admin:decline-user": string;
|
||||||
|
/**
|
||||||
|
* Mark users as NSFW
|
||||||
|
*/
|
||||||
|
"write:admin:nsfw-user": string;
|
||||||
|
/**
|
||||||
|
* Mark users an not NSFW
|
||||||
|
*/
|
||||||
|
"write:admin:unnsfw-user": string;
|
||||||
|
/**
|
||||||
|
* Silence users
|
||||||
|
*/
|
||||||
|
"write:admin:silence-user": string;
|
||||||
|
/**
|
||||||
|
* Un-silence users
|
||||||
|
*/
|
||||||
|
"write:admin:unsilence-user": string;
|
||||||
/**
|
/**
|
||||||
* View your list of scheduled notes
|
* View your list of scheduled notes
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -18,6 +18,7 @@ import type { Config } from '@/config.js';
|
||||||
import { showMachineInfo } from '@/misc/show-machine-info.js';
|
import { showMachineInfo } from '@/misc/show-machine-info.js';
|
||||||
import { envOption } from '@/env.js';
|
import { envOption } from '@/env.js';
|
||||||
import { jobQueue, server } from './common.js';
|
import { jobQueue, server } from './common.js';
|
||||||
|
import * as net from 'node:net';
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
|
@ -141,7 +142,8 @@ export async function masterMain() {
|
||||||
if (envOption.onlyQueue) {
|
if (envOption.onlyQueue) {
|
||||||
bootLogger.succ('Queue started', null, true);
|
bootLogger.succ('Queue started', null, true);
|
||||||
} else {
|
} else {
|
||||||
bootLogger.succ(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on port ${config.port} on ${config.url}`, null, true);
|
const addressString = net.isIPv6(config.address) ? `[${config.address}]` : config.address;
|
||||||
|
bootLogger.succ(config.socket ? `Now listening on socket ${config.socket} on ${config.url}` : `Now listening on ${addressString}:${config.port} on ${config.url}`, null, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
|
||||||
type Source = {
|
type Source = {
|
||||||
url?: string;
|
url?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
address?: string;
|
||||||
socket?: string;
|
socket?: string;
|
||||||
chmodSocket?: string;
|
chmodSocket?: string;
|
||||||
disableHsts?: boolean;
|
disableHsts?: boolean;
|
||||||
|
@ -133,6 +134,7 @@ type Source = {
|
||||||
export type Config = {
|
export type Config = {
|
||||||
url: string;
|
url: string;
|
||||||
port: number;
|
port: number;
|
||||||
|
address: string;
|
||||||
socket: string | undefined;
|
socket: string | undefined;
|
||||||
chmodSocket: string | undefined;
|
chmodSocket: string | undefined;
|
||||||
disableHsts: boolean | undefined;
|
disableHsts: boolean | undefined;
|
||||||
|
@ -309,6 +311,7 @@ export function loadConfig(): Config {
|
||||||
setupPassword: config.setupPassword,
|
setupPassword: config.setupPassword,
|
||||||
url: url.origin,
|
url: url.origin,
|
||||||
port: config.port ?? parseInt(process.env.PORT ?? '3000', 10),
|
port: config.port ?? parseInt(process.env.PORT ?? '3000', 10),
|
||||||
|
address: config.address ?? '0.0.0.0',
|
||||||
socket: config.socket,
|
socket: config.socket,
|
||||||
chmodSocket: config.chmodSocket,
|
chmodSocket: config.chmodSocket,
|
||||||
disableHsts: config.disableHsts,
|
disableHsts: config.disableHsts,
|
||||||
|
@ -511,7 +514,7 @@ function applyEnvOverrides(config: Source) {
|
||||||
|
|
||||||
// these are all the settings that can be overridden
|
// these are all the settings that can be overridden
|
||||||
|
|
||||||
_apply_top([['url', 'port', 'socket', 'chmodSocket', 'disableHsts', 'id', 'dbReplications']]);
|
_apply_top([['url', 'port', 'address', 'socket', 'chmodSocket', 'disableHsts', 'id', 'dbReplications']]);
|
||||||
_apply_top(['db', ['host', 'port', 'db', 'user', 'pass', 'disableCache']]);
|
_apply_top(['db', ['host', 'port', 'db', 'user', 'pass', 'disableCache']]);
|
||||||
_apply_top(['dbSlaves', Array.from((config.dbSlaves ?? []).keys()), ['host', 'port', 'db', 'user', 'pass']]);
|
_apply_top(['dbSlaves', Array.from((config.dbSlaves ?? []).keys()), ['host', 'port', 'db', 'user', 'pass']]);
|
||||||
_apply_top([
|
_apply_top([
|
||||||
|
|
|
@ -185,7 +185,7 @@ export class ApRequestService {
|
||||||
* @param url URL to fetch
|
* @param url URL to fetch
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<unknown> {
|
public async signedGet(url: string, user: { id: MiUser['id'] }, followAlternate?: boolean): Promise<object> {
|
||||||
const _followAlternate = followAlternate ?? true;
|
const _followAlternate = followAlternate ?? true;
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
|
@ -239,7 +239,18 @@ export class ApRequestService {
|
||||||
try {
|
try {
|
||||||
document.documentElement.innerHTML = html;
|
document.documentElement.innerHTML = html;
|
||||||
|
|
||||||
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
|
// Search for any matching value in priority order:
|
||||||
|
// 1. Type=AP > Type=none > Type=anything
|
||||||
|
// 2. Alternate > Canonical
|
||||||
|
// 3. Page order (fallback)
|
||||||
|
const alternate =
|
||||||
|
document.querySelector('head > link[href][rel="alternate"][type="application/activity+json"]') ??
|
||||||
|
document.querySelector('head > link[href][rel="canonical"][type="application/activity+json"]') ??
|
||||||
|
document.querySelector('head > link[href][rel="alternate"]:not([type])') ??
|
||||||
|
document.querySelector('head > link[href][rel="canonical"]:not([type])') ??
|
||||||
|
document.querySelector('head > link[href][rel="alternate"]') ??
|
||||||
|
document.querySelector('head > link[href][rel="canonical"]');
|
||||||
|
|
||||||
if (alternate) {
|
if (alternate) {
|
||||||
const href = alternate.getAttribute('href');
|
const href = alternate.getAttribute('href');
|
||||||
if (href && this.utilityService.punyHostPSLDomain(url) === this.utilityService.punyHostPSLDomain(href)) {
|
if (href && this.utilityService.punyHostPSLDomain(url) === this.utilityService.punyHostPSLDomain(href)) {
|
||||||
|
|
|
@ -56,10 +56,17 @@ export function getOneApId(value: ApObject): string {
|
||||||
return getApId(firstOne);
|
return getApId(firstOne);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal AP payload - just an object with optional ID.
|
||||||
|
*/
|
||||||
|
export interface ObjectWithId {
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get ActivityStreams Object id
|
* Get ActivityStreams Object id
|
||||||
*/
|
*/
|
||||||
export function getApId(value: string | IObject | [string | IObject]): string {
|
export function getApId(value: string | ObjectWithId | [string | ObjectWithId]): string {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
value = fromTuple(value);
|
value = fromTuple(value);
|
||||||
|
|
||||||
|
@ -71,7 +78,7 @@ export function getApId(value: string | IObject | [string | IObject]): string {
|
||||||
/**
|
/**
|
||||||
* Get ActivityStreams Object id, or null if not present
|
* Get ActivityStreams Object id, or null if not present
|
||||||
*/
|
*/
|
||||||
export function getNullableApId(value: string | IObject | [string | IObject]): string | null {
|
export function getNullableApId(value: string | ObjectWithId | [string | ObjectWithId]): string | null {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
value = fromTuple(value);
|
value = fromTuple(value);
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,8 @@ export type UserRelation = {
|
||||||
isBlocked: boolean
|
isBlocked: boolean
|
||||||
isMuted: boolean
|
isMuted: boolean
|
||||||
isRenoteMuted: boolean
|
isRenoteMuted: boolean
|
||||||
|
isInstanceMuted?: boolean
|
||||||
|
memo?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -182,6 +184,9 @@ export class UserEntityService implements OnModuleInit {
|
||||||
isBlocked,
|
isBlocked,
|
||||||
isMuted,
|
isMuted,
|
||||||
isRenoteMuted,
|
isRenoteMuted,
|
||||||
|
host,
|
||||||
|
memo,
|
||||||
|
mutedInstances,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.followingsRepository.findOneBy({
|
this.followingsRepository.findOneBy({
|
||||||
followerId: me,
|
followerId: me,
|
||||||
|
@ -229,8 +234,25 @@ export class UserEntityService implements OnModuleInit {
|
||||||
muteeId: target,
|
muteeId: target,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
this.usersRepository.createQueryBuilder('u')
|
||||||
|
.select('u.host')
|
||||||
|
.where({ id: target })
|
||||||
|
.getRawOne<{ u_host: string }>()
|
||||||
|
.then(it => it?.u_host ?? null),
|
||||||
|
this.userMemosRepository.createQueryBuilder('m')
|
||||||
|
.select('m.memo')
|
||||||
|
.where({ userId: me, targetUserId: target })
|
||||||
|
.getRawOne<{ m_memo: string | null }>()
|
||||||
|
.then(it => it?.m_memo ?? null),
|
||||||
|
this.userProfilesRepository.createQueryBuilder('p')
|
||||||
|
.select('p.mutedInstances')
|
||||||
|
.where({ userId: me })
|
||||||
|
.getRawOne<{ p_mutedInstances: string[] }>()
|
||||||
|
.then(it => it?.p_mutedInstances ?? []),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const isInstanceMuted = !!host && mutedInstances.includes(host);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: target,
|
id: target,
|
||||||
following,
|
following,
|
||||||
|
@ -242,6 +264,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
isBlocked,
|
isBlocked,
|
||||||
isMuted,
|
isMuted,
|
||||||
isRenoteMuted,
|
isRenoteMuted,
|
||||||
|
isInstanceMuted,
|
||||||
|
memo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,6 +280,9 @@ export class UserEntityService implements OnModuleInit {
|
||||||
blockees,
|
blockees,
|
||||||
muters,
|
muters,
|
||||||
renoteMuters,
|
renoteMuters,
|
||||||
|
hosts,
|
||||||
|
memos,
|
||||||
|
mutedInstances,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.followingsRepository.findBy({ followerId: me })
|
this.followingsRepository.findBy({ followerId: me })
|
||||||
.then(f => new Map(f.map(it => [it.followeeId, it]))),
|
.then(f => new Map(f.map(it => [it.followeeId, it]))),
|
||||||
|
@ -294,6 +321,27 @@ export class UserEntityService implements OnModuleInit {
|
||||||
.where('m.muterId = :me', { me })
|
.where('m.muterId = :me', { me })
|
||||||
.getRawMany<{ m_muteeId: string }>()
|
.getRawMany<{ m_muteeId: string }>()
|
||||||
.then(it => it.map(it => it.m_muteeId)),
|
.then(it => it.map(it => it.m_muteeId)),
|
||||||
|
this.usersRepository.createQueryBuilder('u')
|
||||||
|
.select(['u.id', 'u.host'])
|
||||||
|
.where({ id: In(targets) } )
|
||||||
|
.getRawMany<{ m_id: string, m_host: string }>()
|
||||||
|
.then(it => it.reduce((map, it) => {
|
||||||
|
map[it.m_id] = it.m_host;
|
||||||
|
return map;
|
||||||
|
}, {} as Record<string, string>)),
|
||||||
|
this.userMemosRepository.createQueryBuilder('m')
|
||||||
|
.select(['m.targetUserId', 'm.memo'])
|
||||||
|
.where({ userId: me, targetUserId: In(targets) })
|
||||||
|
.getRawMany<{ m_targetUserId: string, m_memo: string | null }>()
|
||||||
|
.then(it => it.reduce((map, it) => {
|
||||||
|
map[it.m_targetUserId] = it.m_memo;
|
||||||
|
return map;
|
||||||
|
}, {} as Record<string, string | null>)),
|
||||||
|
this.userProfilesRepository.createQueryBuilder('p')
|
||||||
|
.select('p.mutedInstances')
|
||||||
|
.where({ userId: me })
|
||||||
|
.getRawOne<{ p_mutedInstances: string[] }>()
|
||||||
|
.then(it => it?.p_mutedInstances ?? []),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return new Map(
|
return new Map(
|
||||||
|
@ -313,6 +361,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
isBlocked: blockees.includes(target),
|
isBlocked: blockees.includes(target),
|
||||||
isMuted: muters.includes(target),
|
isMuted: muters.includes(target),
|
||||||
isRenoteMuted: renoteMuters.includes(target),
|
isRenoteMuted: renoteMuters.includes(target),
|
||||||
|
isInstanceMuted: mutedInstances.includes(hosts[target]),
|
||||||
|
memo: memos[target] ?? null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -18,8 +18,8 @@ type Context = {
|
||||||
|
|
||||||
type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
|
type Level = 'error' | 'success' | 'warning' | 'debug' | 'info';
|
||||||
|
|
||||||
type Data = DataElement | DataElement[];
|
export type Data = DataElement | DataElement[];
|
||||||
type DataElement = Record<string, unknown> | Error | string | null;
|
export type DataElement = Record<string, unknown> | Error | string | null;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default class Logger {
|
export default class Logger {
|
||||||
|
|
|
@ -27,6 +27,8 @@ import { StreamingApiServerService } from './api/StreamingApiServerService.js';
|
||||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||||
import { ClientServerService } from './web/ClientServerService.js';
|
import { ClientServerService } from './web/ClientServerService.js';
|
||||||
import { MastoConverters } from './api/mastodon/converters.js';
|
import { MastoConverters } from './api/mastodon/converters.js';
|
||||||
|
import { MastodonLogger } from './api/mastodon/MastodonLogger.js';
|
||||||
|
import { MastodonDataService } from './api/mastodon/MastodonDataService.js';
|
||||||
import { FeedService } from './web/FeedService.js';
|
import { FeedService } from './web/FeedService.js';
|
||||||
import { UrlPreviewService } from './web/UrlPreviewService.js';
|
import { UrlPreviewService } from './web/UrlPreviewService.js';
|
||||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||||
|
@ -103,6 +105,8 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||||
MastodonApiServerService,
|
MastodonApiServerService,
|
||||||
OAuth2ProviderService,
|
OAuth2ProviderService,
|
||||||
MastoConverters,
|
MastoConverters,
|
||||||
|
MastodonLogger,
|
||||||
|
MastodonDataService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ServerService,
|
ServerService,
|
||||||
|
|
|
@ -262,7 +262,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
fastify.listen({ port: this.config.port, host: '0.0.0.0' });
|
fastify.listen({ port: this.config.port, host: this.config.address });
|
||||||
}
|
}
|
||||||
|
|
||||||
await fastify.ready();
|
await fastify.ready();
|
||||||
|
|
|
@ -4,11 +4,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import ms from 'ms';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
|
import { isActor, isPost, getApId, getNullableApId, ObjectWithId } from '@/core/activitypub/type.js';
|
||||||
import type { SchemaType } from '@/misc/json-schema.js';
|
import type { SchemaType } from '@/misc/json-schema.js';
|
||||||
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
|
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
|
||||||
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
||||||
|
@ -18,6 +17,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
|
||||||
|
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
|
||||||
|
@ -27,9 +28,10 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
kind: 'read:account',
|
kind: 'read:account',
|
||||||
|
|
||||||
|
// Up to 30 calls, then 1 per 1/2 second
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1minute'),
|
|
||||||
max: 30,
|
max: 30,
|
||||||
|
dripRate: 500,
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
@ -120,6 +122,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
private apNoteService: ApNoteService,
|
private apNoteService: ApNoteService,
|
||||||
|
private readonly apRequestService: ApRequestService,
|
||||||
|
private readonly instanceActorService: InstanceActorService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const object = await this.fetchAny(ps.uri, me);
|
const object = await this.fetchAny(ps.uri, me);
|
||||||
|
@ -146,6 +150,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
]));
|
]));
|
||||||
if (local != null) return local;
|
if (local != null) return local;
|
||||||
|
|
||||||
|
// No local object found with that uri.
|
||||||
|
// Before we fetch, resolve the URI in case it has a cross-origin redirect or anything like that.
|
||||||
|
// Resolver.resolve() uses strict verification, which is overly paranoid for a user-provided lookup.
|
||||||
|
uri = await this.resolveCanonicalUri(uri); // eslint-disable-line no-param-reassign
|
||||||
|
if (!this.utilityService.isFederationAllowedUri(uri)) return null;
|
||||||
|
|
||||||
const host = this.utilityService.extractDbHost(uri);
|
const host = this.utilityService.extractDbHost(uri);
|
||||||
|
|
||||||
// local object, not found in db? fail
|
// local object, not found in db? fail
|
||||||
|
@ -228,4 +238,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves an arbitrary URI to its canonical, post-redirect form.
|
||||||
|
*/
|
||||||
|
private async resolveCanonicalUri(uri: string): Promise<string> {
|
||||||
|
const user = await this.instanceActorService.getInstanceActor();
|
||||||
|
const res = await this.apRequestService.signedGet(uri, user, true) as ObjectWithId;
|
||||||
|
return getNullableApId(res) ?? uri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||||
.andWhere('note.visibility = \'public\'')
|
.andWhere("note.visibility IN ('public', 'home')") // keep in sync with NoteCreateService call to `hashtagService.updateHashtags()`
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user')
|
||||||
.leftJoinAndSelect('note.reply', 'reply')
|
.leftJoinAndSelect('note.reply', 'reply')
|
||||||
.leftJoinAndSelect('note.renote', 'renote')
|
.leftJoinAndSelect('note.renote', 'renote')
|
||||||
|
|
|
@ -58,6 +58,14 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isInstanceMuted: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
memo: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -103,6 +111,14 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isInstanceMuted: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
memo: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
|
import type { MiNote, NotesRepository } from '@/models/_.js';
|
||||||
|
import type { MiLocalUser } from '@/models/User.js';
|
||||||
|
import { ApiError } from '../error.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility service for accessing data with Mastodon semantics
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class MastodonDataService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.notesRepository)
|
||||||
|
private readonly notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@Inject(QueryService)
|
||||||
|
private readonly queryService: QueryService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a note in the context of the current user, and throws an exception if not found.
|
||||||
|
*/
|
||||||
|
public async requireNote(noteId: string, me?: MiLocalUser | null): Promise<MiNote> {
|
||||||
|
const note = await this.getNote(noteId, me);
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
throw new ApiError({
|
||||||
|
message: 'No such note.',
|
||||||
|
code: 'NO_SUCH_NOTE',
|
||||||
|
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
|
||||||
|
kind: 'client',
|
||||||
|
httpStatusCode: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a note in the context of the current user.
|
||||||
|
*/
|
||||||
|
public async getNote(noteId: string, me?: MiLocalUser | null): Promise<MiNote | null> {
|
||||||
|
// Root query: note + required dependencies
|
||||||
|
const query = this.notesRepository
|
||||||
|
.createQueryBuilder('note')
|
||||||
|
.where('note.id = :noteId', { noteId })
|
||||||
|
.innerJoinAndSelect('note.user', 'user');
|
||||||
|
|
||||||
|
// Restrict visibility
|
||||||
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
|
if (me) {
|
||||||
|
this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query.getOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks where the current user has made a reblog / boost / pure renote of a given target note.
|
||||||
|
*/
|
||||||
|
public async hasReblog(noteId: string, me: MiLocalUser | null | undefined): Promise<boolean> {
|
||||||
|
if (!me) return false;
|
||||||
|
|
||||||
|
return await this.notesRepository.existsBy({
|
||||||
|
// Reblog of the target note by me
|
||||||
|
userId: me.id,
|
||||||
|
renoteId: noteId,
|
||||||
|
|
||||||
|
// That is pure (not a quote)
|
||||||
|
text: IsNull(),
|
||||||
|
cw: IsNull(),
|
||||||
|
replyId: IsNull(),
|
||||||
|
hasPoll: false,
|
||||||
|
fileIds: '{}',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
39
packages/backend/src/server/api/mastodon/MastodonLogger.ts
Normal file
39
packages/backend/src/server/api/mastodon/MastodonLogger.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import Logger, { Data } from '@/logger.js';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MastodonLogger {
|
||||||
|
public readonly logger: Logger;
|
||||||
|
|
||||||
|
constructor(loggerService: LoggerService) {
|
||||||
|
this.logger = loggerService.getLogger('masto-api');
|
||||||
|
}
|
||||||
|
|
||||||
|
public error(endpoint: string, error: Data): void {
|
||||||
|
this.logger.error(`Error in mastodon API endpoint ${endpoint}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getErrorData(error: unknown): Data {
|
||||||
|
if (error == null) return {};
|
||||||
|
if (typeof(error) === 'string') return error;
|
||||||
|
if (typeof(error) === 'object') {
|
||||||
|
if ('response' in error) {
|
||||||
|
if (typeof(error.response) === 'object' && error.response) {
|
||||||
|
if ('data' in error.response) {
|
||||||
|
if (typeof(error.response.data) === 'object' && error.response.data) {
|
||||||
|
return error.response.data as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return error as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
return { error };
|
||||||
|
}
|
|
@ -9,18 +9,32 @@ import mfm from '@transfem-org/sfm-js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { MfmService } from '@/core/MfmService.js';
|
import { MfmService } from '@/core/MfmService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
import { IMentionedRemoteUsers, MiNote } from '@/models/Note.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import type { NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import type { NoteEditRepository, UserProfilesRepository } from '@/models/_.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GetterService } from '../GetterService.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
|
import { MastodonDataService } from '@/server/api/mastodon/MastodonDataService.js';
|
||||||
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
|
|
||||||
export enum IdConvertType {
|
// Missing from Megalodon apparently
|
||||||
MastodonId,
|
// https://docs.joinmastodon.org/entities/StatusEdit/
|
||||||
SharkeyId,
|
export interface StatusEdit {
|
||||||
|
content: string;
|
||||||
|
spoiler_text: string;
|
||||||
|
sensitive: boolean;
|
||||||
|
created_at: string;
|
||||||
|
account: MastodonEntity.Account;
|
||||||
|
poll?: {
|
||||||
|
options: {
|
||||||
|
title: string;
|
||||||
|
}[]
|
||||||
|
},
|
||||||
|
media_attachments: MastodonEntity.Attachment[],
|
||||||
|
emojis: MastodonEntity.Emoji[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const escapeMFM = (text: string): string => text
|
export const escapeMFM = (text: string): string => text
|
||||||
|
@ -36,27 +50,25 @@ export const escapeMFM = (text: string): string => text
|
||||||
export class MastoConverters {
|
export class MastoConverters {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private readonly config: Config,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private readonly userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.noteEditRepository)
|
@Inject(DI.noteEditRepository)
|
||||||
private noteEditRepository: NoteEditRepository,
|
private readonly noteEditRepository: NoteEditRepository,
|
||||||
|
|
||||||
private mfmService: MfmService,
|
private readonly mfmService: MfmService,
|
||||||
private getterService: GetterService,
|
private readonly getterService: GetterService,
|
||||||
private customEmojiService: CustomEmojiService,
|
private readonly customEmojiService: CustomEmojiService,
|
||||||
private idService: IdService,
|
private readonly idService: IdService,
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private readonly driveFileEntityService: DriveFileEntityService,
|
||||||
) {
|
private readonly mastodonDataService: MastodonDataService,
|
||||||
}
|
) {}
|
||||||
|
|
||||||
private encode(u: MiUser, m: IMentionedRemoteUsers): Entity.Mention {
|
private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.Mention {
|
||||||
let acct = u.username;
|
let acct = u.username;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
let acctUrl = `https://${u.host || this.config.host}/@${u.username}`;
|
let acctUrl = `https://${u.host || this.config.host}/@${u.username}`;
|
||||||
let url: string | null = null;
|
let url: string | null = null;
|
||||||
if (u.host) {
|
if (u.host) {
|
||||||
|
@ -89,7 +101,11 @@ export class MastoConverters {
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
public encodeFile(f: any): Entity.Attachment {
|
public encodeFile(f: Packed<'DriveFile'>): MastodonEntity.Attachment {
|
||||||
|
const { width, height } = f.properties;
|
||||||
|
const size = (width && height) ? `${width}x${height}` : undefined;
|
||||||
|
const aspect = (width && height) ? (width / height) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: f.id,
|
id: f.id,
|
||||||
type: this.fileType(f.type),
|
type: this.fileType(f.type),
|
||||||
|
@ -98,11 +114,19 @@ export class MastoConverters {
|
||||||
preview_url: f.thumbnailUrl,
|
preview_url: f.thumbnailUrl,
|
||||||
text_url: f.url,
|
text_url: f.url,
|
||||||
meta: {
|
meta: {
|
||||||
width: f.properties.width,
|
original: {
|
||||||
height: f.properties.height,
|
width,
|
||||||
|
height,
|
||||||
|
size,
|
||||||
|
aspect,
|
||||||
},
|
},
|
||||||
description: f.comment ? f.comment : null,
|
width,
|
||||||
blurhash: f.blurhash ? f.blurhash : null,
|
height,
|
||||||
|
size,
|
||||||
|
aspect,
|
||||||
|
},
|
||||||
|
description: f.comment ?? null,
|
||||||
|
blurhash: f.blurhash ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +136,7 @@ export class MastoConverters {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async encodeField(f: Entity.Field): Promise<Entity.Field> {
|
private async encodeField(f: Entity.Field): Promise<MastodonEntity.Field> {
|
||||||
return {
|
return {
|
||||||
name: f.name,
|
name: f.name,
|
||||||
value: await this.mfmService.toMastoApiHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value),
|
value: await this.mfmService.toMastoApiHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value),
|
||||||
|
@ -120,7 +144,7 @@ export class MastoConverters {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async convertAccount(account: Entity.Account | MiUser) {
|
public async convertAccount(account: Entity.Account | MiUser): Promise<MastodonEntity.Account> {
|
||||||
const user = await this.getUser(account.id);
|
const user = await this.getUser(account.id);
|
||||||
const profile = await this.userProfilesRepository.findOneBy({ userId: user.id });
|
const profile = await this.userProfilesRepository.findOneBy({ userId: user.id });
|
||||||
const emojis = await this.customEmojiService.populateEmojis(user.emojis, user.host ? user.host : this.config.host);
|
const emojis = await this.customEmojiService.populateEmojis(user.emojis, user.host ? user.host : this.config.host);
|
||||||
|
@ -137,6 +161,7 @@ export class MastoConverters {
|
||||||
});
|
});
|
||||||
const fqn = `${user.username}@${user.host ?? this.config.hostname}`;
|
const fqn = `${user.username}@${user.host ?? this.config.hostname}`;
|
||||||
let acct = user.username;
|
let acct = user.username;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
let acctUrl = `https://${user.host || this.config.host}/@${user.username}`;
|
let acctUrl = `https://${user.host || this.config.host}/@${user.username}`;
|
||||||
const acctUri = `https://${this.config.host}/users/${user.id}`;
|
const acctUri = `https://${this.config.host}/users/${user.id}`;
|
||||||
if (user.host) {
|
if (user.host) {
|
||||||
|
@ -166,19 +191,23 @@ export class MastoConverters {
|
||||||
fields: Promise.all(profile?.fields.map(async p => this.encodeField(p)) ?? []),
|
fields: Promise.all(profile?.fields.map(async p => this.encodeField(p)) ?? []),
|
||||||
bot: user.isBot,
|
bot: user.isBot,
|
||||||
discoverable: user.isExplorable,
|
discoverable: user.isExplorable,
|
||||||
|
noindex: user.noindex,
|
||||||
|
group: null,
|
||||||
|
suspended: user.isSuspended,
|
||||||
|
limited: user.isSilenced,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getEdits(id: string) {
|
public async getEdits(id: string, me?: MiLocalUser | null) {
|
||||||
const note = await this.getterService.getNote(id);
|
const note = await this.mastodonDataService.getNote(id, me);
|
||||||
if (!note) {
|
if (!note) {
|
||||||
return {};
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteUser = await this.getUser(note.userId).then(async (p) => await this.convertAccount(p));
|
const noteUser = await this.getUser(note.userId).then(async (p) => await this.convertAccount(p));
|
||||||
const edits = await this.noteEditRepository.find({ where: { noteId: note.id }, order: { id: 'ASC' } });
|
const edits = await this.noteEditRepository.find({ where: { noteId: note.id }, order: { id: 'ASC' } });
|
||||||
const history: Promise<any>[] = [];
|
const history: Promise<StatusEdit>[] = [];
|
||||||
|
|
||||||
|
// TODO this looks wrong, according to mastodon docs
|
||||||
let lastDate = this.idService.parse(note.id).date;
|
let lastDate = this.idService.parse(note.id).date;
|
||||||
for (const edit of edits) {
|
for (const edit of edits) {
|
||||||
const files = this.driveFileEntityService.packManyByIds(edit.fileIds);
|
const files = this.driveFileEntityService.packManyByIds(edit.fileIds);
|
||||||
|
@ -187,9 +216,8 @@ export class MastoConverters {
|
||||||
content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''),
|
content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''),
|
||||||
created_at: lastDate.toISOString(),
|
created_at: lastDate.toISOString(),
|
||||||
emojis: [],
|
emojis: [],
|
||||||
sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false),
|
sensitive: edit.cw != null && edit.cw.length > 0,
|
||||||
spoiler_text: edit.cw ?? '',
|
spoiler_text: edit.cw ?? '',
|
||||||
poll: null,
|
|
||||||
media_attachments: files.then(files => files.length > 0 ? files.map((f) => this.encodeFile(f)) : []),
|
media_attachments: files.then(files => files.length > 0 ? files.map((f) => this.encodeFile(f)) : []),
|
||||||
};
|
};
|
||||||
lastDate = edit.updatedAt;
|
lastDate = edit.updatedAt;
|
||||||
|
@ -199,15 +227,16 @@ export class MastoConverters {
|
||||||
return await Promise.all(history);
|
return await Promise.all(history);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async convertReblog(status: Entity.Status | null): Promise<any> {
|
private async convertReblog(status: Entity.Status | null, me?: MiLocalUser | null): Promise<MastodonEntity.Status | null> {
|
||||||
if (!status) return null;
|
if (!status) return null;
|
||||||
return await this.convertStatus(status);
|
return await this.convertStatus(status, me);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async convertStatus(status: Entity.Status) {
|
public async convertStatus(status: Entity.Status, me?: MiLocalUser | null): Promise<MastodonEntity.Status> {
|
||||||
const convertedAccount = this.convertAccount(status.account);
|
const convertedAccount = this.convertAccount(status.account);
|
||||||
const note = await this.getterService.getNote(status.id);
|
const note = await this.mastodonDataService.requireNote(status.id, me);
|
||||||
const noteUser = await this.getUser(status.account.id);
|
const noteUser = await this.getUser(status.account.id);
|
||||||
|
const mentionedRemoteUsers = JSON.parse(note.mentionedRemoteUsers);
|
||||||
|
|
||||||
const emojis = await this.customEmojiService.populateEmojis(note.emojis, noteUser.host ? noteUser.host : this.config.host);
|
const emojis = await this.customEmojiService.populateEmojis(note.emojis, noteUser.host ? noteUser.host : this.config.host);
|
||||||
const emoji: Entity.Emoji[] = [];
|
const emoji: Entity.Emoji[] = [];
|
||||||
|
@ -224,7 +253,7 @@ export class MastoConverters {
|
||||||
|
|
||||||
const mentions = Promise.all(note.mentions.map(p =>
|
const mentions = Promise.all(note.mentions.map(p =>
|
||||||
this.getUser(p)
|
this.getUser(p)
|
||||||
.then(u => this.encode(u, JSON.parse(note.mentionedRemoteUsers)))
|
.then(u => this.encode(u, mentionedRemoteUsers))
|
||||||
.catch(() => null)))
|
.catch(() => null)))
|
||||||
.then(p => p.filter(m => m)) as Promise<Entity.Mention[]>;
|
.then(p => p.filter(m => m)) as Promise<Entity.Mention[]>;
|
||||||
|
|
||||||
|
@ -235,20 +264,26 @@ export class MastoConverters {
|
||||||
} as Entity.Tag;
|
} as Entity.Tag;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isQuote = note.renoteId && note.text ? true : false;
|
// This must mirror the usual isQuote / isPureRenote logic used elsewhere.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
const isQuote = note.renoteId && (note.text || note.cw || note.fileIds.length > 0 || note.hasPoll || note.replyId);
|
||||||
|
|
||||||
const renote = note.renoteId ? this.getterService.getNote(note.renoteId) : null;
|
const renote: Promise<MiNote> | null = note.renoteId ? this.mastodonDataService.requireNote(note.renoteId, me) : null;
|
||||||
|
|
||||||
const quoteUri = Promise.resolve(renote).then(renote => {
|
const quoteUri = Promise.resolve(renote).then(renote => {
|
||||||
if (!renote || !isQuote) return null;
|
if (!renote || !isQuote) return null;
|
||||||
return renote.url ?? renote.uri ?? `${this.config.url}/notes/${renote.id}`;
|
return renote.url ?? renote.uri ?? `${this.config.url}/notes/${renote.id}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = note.text !== null
|
const text = note.text;
|
||||||
? quoteUri.then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri))
|
const content = text !== null
|
||||||
.then(p => p ?? escapeMFM(note.text!))
|
? quoteUri
|
||||||
|
.then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(text), mentionedRemoteUsers, false, quoteUri))
|
||||||
|
.then(p => p ?? escapeMFM(text))
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
const reblogged = await this.mastodonDataService.hasReblog(note.id, me);
|
||||||
|
|
||||||
// noinspection ES6MissingAwait
|
// noinspection ES6MissingAwait
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
|
@ -257,7 +292,7 @@ export class MastoConverters {
|
||||||
account: convertedAccount,
|
account: convertedAccount,
|
||||||
in_reply_to_id: note.replyId,
|
in_reply_to_id: note.replyId,
|
||||||
in_reply_to_account_id: note.replyUserId,
|
in_reply_to_account_id: note.replyUserId,
|
||||||
reblog: !isQuote ? await this.convertReblog(status.reblog) : null,
|
reblog: !isQuote ? await this.convertReblog(status.reblog, me) : null,
|
||||||
content: content,
|
content: content,
|
||||||
content_type: 'text/x.misskeymarkdown',
|
content_type: 'text/x.misskeymarkdown',
|
||||||
text: note.text,
|
text: note.text,
|
||||||
|
@ -266,34 +301,51 @@ export class MastoConverters {
|
||||||
replies_count: note.repliesCount,
|
replies_count: note.repliesCount,
|
||||||
reblogs_count: note.renoteCount,
|
reblogs_count: note.renoteCount,
|
||||||
favourites_count: status.favourites_count,
|
favourites_count: status.favourites_count,
|
||||||
reblogged: false,
|
reblogged,
|
||||||
favourited: status.favourited,
|
favourited: status.favourited,
|
||||||
muted: status.muted,
|
muted: status.muted,
|
||||||
sensitive: status.sensitive,
|
sensitive: status.sensitive,
|
||||||
spoiler_text: note.cw ? note.cw : '',
|
spoiler_text: note.cw ?? '',
|
||||||
visibility: status.visibility,
|
visibility: status.visibility,
|
||||||
media_attachments: status.media_attachments,
|
media_attachments: status.media_attachments.map(a => convertAttachment(a)),
|
||||||
mentions: mentions,
|
mentions: mentions,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
card: null, //FIXME
|
card: null, //FIXME
|
||||||
poll: status.poll ?? null,
|
poll: status.poll ?? null,
|
||||||
application: null, //FIXME
|
application: null, //FIXME
|
||||||
language: null, //FIXME
|
language: null, //FIXME
|
||||||
pinned: false,
|
pinned: false, //FIXME
|
||||||
reactions: status.emoji_reactions,
|
reactions: status.emoji_reactions,
|
||||||
emoji_reactions: status.emoji_reactions,
|
emoji_reactions: status.emoji_reactions,
|
||||||
bookmarked: false,
|
bookmarked: false, //FIXME
|
||||||
quote: isQuote ? await this.convertReblog(status.reblog) : null,
|
quote: isQuote ? await this.convertReblog(status.reblog, me) : null,
|
||||||
// optional chaining cannot be used, as it evaluates to undefined, not null
|
edited_at: note.updatedAt?.toISOString() ?? null,
|
||||||
edited_at: note.updatedAt ? note.updatedAt.toISOString() : null,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async convertConversation(conversation: Entity.Conversation, me?: MiLocalUser | null): Promise<MastodonEntity.Conversation> {
|
||||||
|
return {
|
||||||
|
id: conversation.id,
|
||||||
|
accounts: await Promise.all(conversation.accounts.map(a => this.convertAccount(a))),
|
||||||
|
last_status: conversation.last_status ? await this.convertStatus(conversation.last_status, me) : null,
|
||||||
|
unread: conversation.unread,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async convertNotification(notification: Entity.Notification, me?: MiLocalUser | null): Promise<MastodonEntity.Notification> {
|
||||||
|
return {
|
||||||
|
account: await this.convertAccount(notification.account),
|
||||||
|
created_at: notification.created_at,
|
||||||
|
id: notification.id,
|
||||||
|
status: notification.status ? await this.convertStatus(notification.status, me) : undefined,
|
||||||
|
type: notification.type,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function simpleConvert(data: any) {
|
function simpleConvert<T>(data: T): T {
|
||||||
// copy the object to bypass weird pass by reference bugs
|
// copy the object to bypass weird pass by reference bugs
|
||||||
const result = Object.assign({}, data);
|
return Object.assign({}, data);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertAccount(account: Entity.Account) {
|
export function convertAccount(account: Entity.Account) {
|
||||||
|
@ -302,8 +354,30 @@ export function convertAccount(account: Entity.Account) {
|
||||||
export function convertAnnouncement(announcement: Entity.Announcement) {
|
export function convertAnnouncement(announcement: Entity.Announcement) {
|
||||||
return simpleConvert(announcement);
|
return simpleConvert(announcement);
|
||||||
}
|
}
|
||||||
export function convertAttachment(attachment: Entity.Attachment) {
|
export function convertAttachment(attachment: Entity.Attachment): MastodonEntity.Attachment {
|
||||||
return simpleConvert(attachment);
|
const { width, height } = attachment.meta?.original ?? attachment.meta ?? {};
|
||||||
|
const size = (width && height) ? `${width}x${height}` : undefined;
|
||||||
|
const aspect = (width && height) ? (width / height) : undefined;
|
||||||
|
return {
|
||||||
|
...attachment,
|
||||||
|
meta: attachment.meta ? {
|
||||||
|
...attachment.meta,
|
||||||
|
original: {
|
||||||
|
...attachment.meta.original,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
size,
|
||||||
|
aspect,
|
||||||
|
frame_rate: String(attachment.meta.fps),
|
||||||
|
duration: attachment.meta.duration,
|
||||||
|
bitrate: attachment.meta.audio_bitrate ? parseInt(attachment.meta.audio_bitrate) : undefined,
|
||||||
|
},
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
size,
|
||||||
|
aspect,
|
||||||
|
} : null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export function convertFilter(filter: Entity.Filter) {
|
export function convertFilter(filter: Entity.Filter) {
|
||||||
return simpleConvert(filter);
|
return simpleConvert(filter);
|
||||||
|
@ -315,45 +389,40 @@ export function convertFeaturedTag(tag: Entity.FeaturedTag) {
|
||||||
return simpleConvert(tag);
|
return simpleConvert(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertNotification(notification: Entity.Notification) {
|
|
||||||
notification.account = convertAccount(notification.account);
|
|
||||||
if (notification.status) notification.status = convertStatus(notification.status);
|
|
||||||
return notification;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function convertPoll(poll: Entity.Poll) {
|
export function convertPoll(poll: Entity.Poll) {
|
||||||
return simpleConvert(poll);
|
return simpleConvert(poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
export function convertReaction(reaction: Entity.Reaction) {
|
export function convertReaction(reaction: Entity.Reaction) {
|
||||||
if (reaction.accounts) {
|
if (reaction.accounts) {
|
||||||
reaction.accounts = reaction.accounts.map(convertAccount);
|
reaction.accounts = reaction.accounts.map(convertAccount);
|
||||||
}
|
}
|
||||||
return reaction;
|
return reaction;
|
||||||
}
|
}
|
||||||
export function convertRelationship(relationship: Entity.Relationship) {
|
|
||||||
return simpleConvert(relationship);
|
// Megalodon sometimes returns broken / stubbed relationship data
|
||||||
}
|
export function convertRelationship(relationship: Partial<Entity.Relationship> & { id: string }): MastodonEntity.Relationship {
|
||||||
|
return {
|
||||||
export function convertStatus(status: Entity.Status) {
|
id: relationship.id,
|
||||||
status.account = convertAccount(status.account);
|
following: relationship.following ?? false,
|
||||||
status.media_attachments = status.media_attachments.map((attachment) =>
|
showing_reblogs: relationship.showing_reblogs ?? true,
|
||||||
convertAttachment(attachment),
|
notifying: relationship.notifying ?? true,
|
||||||
);
|
languages: [],
|
||||||
if (status.poll) status.poll = convertPoll(status.poll);
|
followed_by: relationship.followed_by ?? false,
|
||||||
if (status.reblog) status.reblog = convertStatus(status.reblog);
|
blocking: relationship.blocking ?? false,
|
||||||
|
blocked_by: relationship.blocked_by ?? false,
|
||||||
return status;
|
muting: relationship.muting ?? false,
|
||||||
|
muting_notifications: relationship.muting_notifications ?? false,
|
||||||
|
requested: relationship.requested ?? false,
|
||||||
|
requested_by: relationship.requested_by ?? false,
|
||||||
|
domain_blocking: relationship.domain_blocking ?? false,
|
||||||
|
endorsed: relationship.endorsed ?? false,
|
||||||
|
note: relationship.note ?? '',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
export function convertStatusSource(status: Entity.StatusSource) {
|
export function convertStatusSource(status: Entity.StatusSource) {
|
||||||
return simpleConvert(status);
|
return simpleConvert(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertConversation(conversation: Entity.Conversation) {
|
|
||||||
conversation.accounts = conversation.accounts.map(convertAccount);
|
|
||||||
if (conversation.last_status) {
|
|
||||||
conversation.last_status = convertStatus(conversation.last_status);
|
|
||||||
}
|
|
||||||
|
|
||||||
return conversation;
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,49 +3,32 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
|
||||||
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { MastoConverters, convertRelationship } from '../converters.js';
|
import { MastoConverters, convertRelationship } from '../converters.js';
|
||||||
import { argsToBools, limitToInt } from './timeline.js';
|
|
||||||
import type { MegalodonInterface } from 'megalodon';
|
import type { MegalodonInterface } from 'megalodon';
|
||||||
import type { FastifyRequest } from 'fastify';
|
import type { FastifyRequest } from 'fastify';
|
||||||
import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
|
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
import type { Config } from '@/config.js';
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
const relationshipModel = {
|
export interface ApiAccountMastodonRoute {
|
||||||
id: '',
|
Params: { id?: string },
|
||||||
following: false,
|
Querystring: TimelineArgs & { acct?: string },
|
||||||
followed_by: false,
|
Body: { notifications?: boolean }
|
||||||
delivery_following: false,
|
}
|
||||||
blocking: false,
|
|
||||||
blocked_by: false,
|
|
||||||
muting: false,
|
|
||||||
muting_notifications: false,
|
|
||||||
requested: false,
|
|
||||||
domain_blocking: false,
|
|
||||||
showing_reblogs: false,
|
|
||||||
endorsed: false,
|
|
||||||
notifying: false,
|
|
||||||
note: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApiAccountMastodon {
|
export class ApiAccountMastodon {
|
||||||
private request: FastifyRequest;
|
constructor(
|
||||||
private client: MegalodonInterface;
|
private readonly request: FastifyRequest<ApiAccountMastodonRoute>,
|
||||||
private BASE_URL: string;
|
private readonly client: MegalodonInterface,
|
||||||
|
private readonly me: MiLocalUser | null,
|
||||||
constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoconverter: MastoConverters) {
|
private readonly mastoConverters: MastoConverters,
|
||||||
this.request = request;
|
) {}
|
||||||
this.client = client;
|
|
||||||
this.BASE_URL = BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async verifyCredentials() {
|
public async verifyCredentials() {
|
||||||
try {
|
|
||||||
const data = await this.client.verifyAccountCredentials();
|
const data = await this.client.verifyAccountCredentials();
|
||||||
const acct = await this.mastoconverter.convertAccount(data.data);
|
const acct = await this.mastoConverters.convertAccount(data.data);
|
||||||
const newAcct = Object.assign({}, acct, {
|
return Object.assign({}, acct, {
|
||||||
source: {
|
source: {
|
||||||
note: acct.note,
|
note: acct.note,
|
||||||
fields: acct.fields,
|
fields: acct.fields,
|
||||||
|
@ -54,222 +37,115 @@ export class ApiAccountMastodon {
|
||||||
language: '',
|
language: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return newAcct;
|
|
||||||
} catch (e: any) {
|
|
||||||
/* console.error(e);
|
|
||||||
console.error(e.response.data); */
|
|
||||||
return e.response;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async lookup() {
|
public async lookup() {
|
||||||
try {
|
if (!this.request.query.acct) throw new Error('Missing required property "acct"');
|
||||||
const data = await this.client.search((this.request.query as any).acct, { type: 'accounts' });
|
const data = await this.client.search(this.request.query.acct, { type: 'accounts' });
|
||||||
return this.mastoconverter.convertAccount(data.data.accounts[0]);
|
return this.mastoConverters.convertAccount(data.data.accounts[0]);
|
||||||
} catch (e: any) {
|
|
||||||
/* console.error(e)
|
|
||||||
console.error(e.response.data); */
|
|
||||||
return e.response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getRelationships(users: [string]) {
|
|
||||||
try {
|
|
||||||
relationshipModel.id = users.toString() || '1';
|
|
||||||
|
|
||||||
if (!(users.length > 0)) {
|
|
||||||
return [relationshipModel];
|
|
||||||
}
|
|
||||||
|
|
||||||
const reqIds = [];
|
|
||||||
for (let i = 0; i < users.length; i++) {
|
|
||||||
reqIds.push(users[i]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getRelationships(reqIds: string[]) {
|
||||||
const data = await this.client.getRelationships(reqIds);
|
const data = await this.client.getRelationships(reqIds);
|
||||||
return data.data.map((relationship) => convertRelationship(relationship));
|
return data.data.map(relationship => convertRelationship(relationship));
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getStatuses() {
|
public async getStatuses() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.getAccountStatuses((this.request.params as any).id, argsToBools(limitToInt(this.request.query as any)));
|
const data = await this.client.getAccountStatuses(this.request.params.id, parseTimelineArgs(this.request.query));
|
||||||
return await Promise.all(data.data.map(async (status) => await this.mastoconverter.convertStatus(status)));
|
return await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, this.me)));
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFollowers() {
|
public async getFollowers() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.getAccountFollowers(
|
const data = await this.client.getAccountFollowers(
|
||||||
(this.request.params as any).id,
|
this.request.params.id,
|
||||||
limitToInt(this.request.query as any),
|
parseTimelineArgs(this.request.query),
|
||||||
);
|
);
|
||||||
return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
|
return await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFollowing() {
|
public async getFollowing() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.getAccountFollowing(
|
const data = await this.client.getAccountFollowing(
|
||||||
(this.request.params as any).id,
|
this.request.params.id,
|
||||||
limitToInt(this.request.query as any),
|
parseTimelineArgs(this.request.query),
|
||||||
);
|
);
|
||||||
return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
|
return await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addFollow() {
|
public async addFollow() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.followAccount( (this.request.params as any).id );
|
const data = await this.client.followAccount(this.request.params.id);
|
||||||
const acct = convertRelationship(data.data);
|
const acct = convertRelationship(data.data);
|
||||||
acct.following = true;
|
acct.following = true;
|
||||||
return acct;
|
return acct;
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rmFollow() {
|
public async rmFollow() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.unfollowAccount( (this.request.params as any).id );
|
const data = await this.client.unfollowAccount(this.request.params.id);
|
||||||
const acct = convertRelationship(data.data);
|
const acct = convertRelationship(data.data);
|
||||||
acct.following = false;
|
acct.following = false;
|
||||||
return acct;
|
return acct;
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addBlock() {
|
public async addBlock() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.blockAccount( (this.request.params as any).id );
|
const data = await this.client.blockAccount(this.request.params.id);
|
||||||
return convertRelationship(data.data);
|
return convertRelationship(data.data);
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rmBlock() {
|
public async rmBlock() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.unblockAccount( (this.request.params as any).id );
|
const data = await this.client.unblockAccount(this.request.params.id);
|
||||||
return convertRelationship(data.data);
|
return convertRelationship(data.data);
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addMute() {
|
public async addMute() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.muteAccount(
|
const data = await this.client.muteAccount(
|
||||||
(this.request.params as any).id,
|
this.request.params.id,
|
||||||
this.request.body as any,
|
this.request.body.notifications ?? true,
|
||||||
);
|
);
|
||||||
return convertRelationship(data.data);
|
return convertRelationship(data.data);
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rmMute() {
|
public async rmMute() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.unmuteAccount( (this.request.params as any).id );
|
const data = await this.client.unmuteAccount(this.request.params.id);
|
||||||
return convertRelationship(data.data);
|
return convertRelationship(data.data);
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBookmarks() {
|
public async getBookmarks() {
|
||||||
try {
|
const data = await this.client.getBookmarks(parseTimelineArgs(this.request.query));
|
||||||
const data = await this.client.getBookmarks( limitToInt(this.request.query as any) );
|
return Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, this.me)));
|
||||||
return data.data.map((status) => this.mastoconverter.convertStatus(status));
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFavourites() {
|
public async getFavourites() {
|
||||||
try {
|
const data = await this.client.getFavourites(parseTimelineArgs(this.request.query));
|
||||||
const data = await this.client.getFavourites( limitToInt(this.request.query as any) );
|
return Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, this.me)));
|
||||||
return data.data.map((status) => this.mastoconverter.convertStatus(status));
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMutes() {
|
public async getMutes() {
|
||||||
try {
|
const data = await this.client.getMutes(parseTimelineArgs(this.request.query));
|
||||||
const data = await this.client.getMutes( limitToInt(this.request.query as any) );
|
return Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
|
||||||
return data.data.map((account) => this.mastoconverter.convertAccount(account));
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBlocks() {
|
public async getBlocks() {
|
||||||
try {
|
const data = await this.client.getBlocks(parseTimelineArgs(this.request.query));
|
||||||
const data = await this.client.getBlocks( limitToInt(this.request.query as any) );
|
return Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
|
||||||
return data.data.map((account) => this.mastoconverter.convertAccount(account));
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async acceptFollow() {
|
public async acceptFollow() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.acceptFollowRequest( (this.request.params as any).id );
|
const data = await this.client.acceptFollowRequest(this.request.params.id);
|
||||||
return convertRelationship(data.data);
|
return convertRelationship(data.data);
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rejectFollow() {
|
public async rejectFollow() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.rejectFollowRequest( (this.request.params as any).id );
|
const data = await this.client.rejectFollowRequest(this.request.params.id);
|
||||||
return convertRelationship(data.data);
|
return convertRelationship(data.data);
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
console.error(e.response.data);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,36 +44,54 @@ const writeScope = [
|
||||||
'write:gallery-likes',
|
'write:gallery-likes',
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function ApiAuthMastodon(request: FastifyRequest, client: MegalodonInterface) {
|
export interface AuthPayload {
|
||||||
const body: any = request.body || request.query;
|
scopes?: string | string[],
|
||||||
try {
|
redirect_uris?: string,
|
||||||
|
client_name?: string,
|
||||||
|
website?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not entirely right, but it gets TypeScript to work so *shrug*
|
||||||
|
export type AuthMastodonRoute = { Body?: AuthPayload, Querystring: AuthPayload };
|
||||||
|
|
||||||
|
export async function ApiAuthMastodon(request: FastifyRequest<AuthMastodonRoute>, client: MegalodonInterface) {
|
||||||
|
const body = request.body ?? request.query;
|
||||||
|
if (!body.scopes) throw new Error('Missing required payload "scopes"');
|
||||||
|
if (!body.redirect_uris) throw new Error('Missing required payload "redirect_uris"');
|
||||||
|
if (!body.client_name) throw new Error('Missing required payload "client_name"');
|
||||||
|
|
||||||
let scope = body.scopes;
|
let scope = body.scopes;
|
||||||
if (typeof scope === 'string') scope = scope.split(' ') || scope.split('+');
|
if (typeof scope === 'string') {
|
||||||
|
scope = scope.split(/[ +]/g);
|
||||||
|
}
|
||||||
|
|
||||||
const pushScope = new Set<string>();
|
const pushScope = new Set<string>();
|
||||||
for (const s of scope) {
|
for (const s of scope) {
|
||||||
if (s.match(/^read/)) for (const r of readScope) pushScope.add(r);
|
if (s.match(/^read/)) {
|
||||||
if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r);
|
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 red = body.redirect_uris;
|
||||||
const appData = await client.registerApp(body.client_name, {
|
const appData = await client.registerApp(body.client_name, {
|
||||||
scopes: scopeArr,
|
scopes: Array.from(pushScope),
|
||||||
redirect_uris: red,
|
redirect_uris: red,
|
||||||
website: body.website,
|
website: body.website,
|
||||||
});
|
});
|
||||||
const returns = {
|
|
||||||
|
return {
|
||||||
id: Math.floor(Math.random() * 100).toString(),
|
id: Math.floor(Math.random() * 100).toString(),
|
||||||
name: appData.name,
|
name: appData.name,
|
||||||
website: body.website,
|
website: body.website,
|
||||||
redirect_uri: red,
|
redirect_uri: red,
|
||||||
client_id: Buffer.from(appData.url || '').toString('base64'),
|
client_id: Buffer.from(appData.url || '').toString('base64'), // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
client_secret: appData.clientSecret,
|
client_secret: appData.clientSecret,
|
||||||
};
|
};
|
||||||
|
|
||||||
return returns;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,68 +3,73 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { toBoolean } from '@/server/api/mastodon/timelineArgs.js';
|
||||||
import { convertFilter } from '../converters.js';
|
import { convertFilter } from '../converters.js';
|
||||||
import type { MegalodonInterface } from 'megalodon';
|
import type { MegalodonInterface } from 'megalodon';
|
||||||
import type { FastifyRequest } from 'fastify';
|
import type { FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
export class ApiFilterMastodon {
|
export interface ApiFilterMastodonRoute {
|
||||||
private request: FastifyRequest;
|
Params: {
|
||||||
private client: MegalodonInterface;
|
id?: string,
|
||||||
|
},
|
||||||
constructor(request: FastifyRequest, client: MegalodonInterface) {
|
Body: {
|
||||||
this.request = request;
|
phrase?: string,
|
||||||
this.client = client;
|
context?: string[],
|
||||||
|
irreversible?: string,
|
||||||
|
whole_word?: string,
|
||||||
|
expires_in?: string,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiFilterMastodon {
|
||||||
|
constructor(
|
||||||
|
private readonly request: FastifyRequest<ApiFilterMastodonRoute>,
|
||||||
|
private readonly client: MegalodonInterface,
|
||||||
|
) {}
|
||||||
|
|
||||||
public async getFilters() {
|
public async getFilters() {
|
||||||
try {
|
|
||||||
const data = await this.client.getFilters();
|
const data = await this.client.getFilters();
|
||||||
return data.data.map((filter) => convertFilter(filter));
|
return data.data.map((filter) => convertFilter(filter));
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFilter() {
|
public async getFilter() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.getFilter( (this.request.params as any).id );
|
const data = await this.client.getFilter(this.request.params.id);
|
||||||
return convertFilter(data.data);
|
return convertFilter(data.data);
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createFilter() {
|
public async createFilter() {
|
||||||
try {
|
if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
|
||||||
const body: any = this.request.body;
|
if (!this.request.body.context) throw new Error('Missing required payload "context"');
|
||||||
const data = await this.client.createFilter(body.pharse, body.context, body);
|
const options = {
|
||||||
|
phrase: this.request.body.phrase,
|
||||||
|
context: this.request.body.context,
|
||||||
|
irreversible: toBoolean(this.request.body.irreversible),
|
||||||
|
whole_word: toBoolean(this.request.body.whole_word),
|
||||||
|
expires_in: this.request.body.expires_in,
|
||||||
|
};
|
||||||
|
const data = await this.client.createFilter(this.request.body.phrase, this.request.body.context, options);
|
||||||
return convertFilter(data.data);
|
return convertFilter(data.data);
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateFilter() {
|
public async updateFilter() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const body: any = this.request.body;
|
if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
|
||||||
const data = await this.client.updateFilter((this.request.params as any).id, body.pharse, body.context);
|
if (!this.request.body.context) throw new Error('Missing required payload "context"');
|
||||||
|
const options = {
|
||||||
|
phrase: this.request.body.phrase,
|
||||||
|
context: this.request.body.context,
|
||||||
|
irreversible: toBoolean(this.request.body.irreversible),
|
||||||
|
whole_word: toBoolean(this.request.body.whole_word),
|
||||||
|
expires_in: this.request.body.expires_in,
|
||||||
|
};
|
||||||
|
const data = await this.client.updateFilter(this.request.params.id, this.request.body.phrase, this.request.body.context, options);
|
||||||
return convertFilter(data.data);
|
return convertFilter(data.data);
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rmFilter() {
|
public async rmFilter() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.deleteFilter( (this.request.params as any).id );
|
const data = await this.client.deleteFilter(this.request.params.id);
|
||||||
return data.data;
|
return data.data;
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiMeta } from '@/models/Meta.js';
|
import type { MiMeta } from '@/models/Meta.js';
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||||
export async function getInstance(
|
export async function getInstance(
|
||||||
response: Entity.Instance,
|
response: Entity.Instance,
|
||||||
contact: Entity.Account,
|
contact: Entity.Account,
|
||||||
|
@ -17,11 +18,8 @@ export async function getInstance(
|
||||||
return {
|
return {
|
||||||
uri: config.url,
|
uri: config.url,
|
||||||
title: meta.name || 'Sharkey',
|
title: meta.name || 'Sharkey',
|
||||||
short_description:
|
short_description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
||||||
meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
||||||
description:
|
|
||||||
meta.description ||
|
|
||||||
'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
|
||||||
email: response.email || '',
|
email: response.email || '',
|
||||||
version: `3.0.0 (compatible; Sharkey ${config.version})`,
|
version: `3.0.0 (compatible; Sharkey ${config.version})`,
|
||||||
urls: response.urls,
|
urls: response.urls,
|
||||||
|
|
|
@ -3,73 +3,56 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { convertNotification } from '../converters.js';
|
import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
|
||||||
import type { MegalodonInterface, Entity } from 'megalodon';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
|
import { MastoConverters } from '@/server/api/mastodon/converters.js';
|
||||||
|
import type { MegalodonInterface } from 'megalodon';
|
||||||
import type { FastifyRequest } from 'fastify';
|
import type { FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
function toLimitToInt(q: any) {
|
export interface ApiNotifyMastodonRoute {
|
||||||
if (q.limit) if (typeof q.limit === 'string') q.limit = parseInt(q.limit, 10);
|
Params: {
|
||||||
return q;
|
id?: string,
|
||||||
|
},
|
||||||
|
Querystring: TimelineArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiNotifyMastodon {
|
export class ApiNotifyMastodon {
|
||||||
private request: FastifyRequest;
|
constructor(
|
||||||
private client: MegalodonInterface;
|
private readonly request: FastifyRequest<ApiNotifyMastodonRoute>,
|
||||||
|
private readonly client: MegalodonInterface,
|
||||||
constructor(request: FastifyRequest, client: MegalodonInterface) {
|
private readonly me: MiLocalUser | null,
|
||||||
this.request = request;
|
private readonly mastoConverters: MastoConverters,
|
||||||
this.client = client;
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
public async getNotifications() {
|
public async getNotifications() {
|
||||||
try {
|
const data = await this.client.getNotifications(parseTimelineArgs(this.request.query));
|
||||||
const data = await this.client.getNotifications( toLimitToInt(this.request.query) );
|
return Promise.all(data.data.map(async n => {
|
||||||
const notifs = data.data;
|
const converted = await this.mastoConverters.convertNotification(n, this.me);
|
||||||
const processed = notifs.map((n: Entity.Notification) => {
|
if (converted.type === 'reaction') {
|
||||||
const convertedn = convertNotification(n);
|
converted.type = 'favourite';
|
||||||
if (convertedn.type !== 'follow' && convertedn.type !== 'follow_request') {
|
|
||||||
if (convertedn.type === 'reaction') convertedn.type = 'favourite';
|
|
||||||
return convertedn;
|
|
||||||
} else {
|
|
||||||
return convertedn;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return processed;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
}
|
||||||
|
return converted;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNotification() {
|
public async getNotification() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.getNotification( (this.request.params as any).id );
|
const data = await this.client.getNotification(this.request.params.id);
|
||||||
const notif = convertNotification(data.data);
|
const converted = await this.mastoConverters.convertNotification(data.data, this.me);
|
||||||
if (notif.type !== 'follow' && notif.type !== 'follow_request' && notif.type === 'reaction') notif.type = 'favourite';
|
if (converted.type === 'reaction') {
|
||||||
return notif;
|
converted.type = 'favourite';
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
}
|
||||||
|
return converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rmNotification() {
|
public async rmNotification() {
|
||||||
try {
|
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||||
const data = await this.client.dismissNotification( (this.request.params as any).id );
|
const data = await this.client.dismissNotification(this.request.params.id);
|
||||||
return data.data;
|
return data.data;
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rmNotifications() {
|
public async rmNotifications() {
|
||||||
try {
|
|
||||||
const data = await this.client.dismissNotifications();
|
const data = await this.client.dismissNotifications();
|
||||||
return data.data;
|
return data.data;
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,55 +3,52 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { MastoConverters } from '../converters.js';
|
import { MastoConverters } from '../converters.js';
|
||||||
import { limitToInt } from './timeline.js';
|
import { parseTimelineArgs, TimelineArgs } from '../timelineArgs.js';
|
||||||
|
import Account = Entity.Account;
|
||||||
|
import Status = Entity.Status;
|
||||||
import type { MegalodonInterface } from 'megalodon';
|
import type { MegalodonInterface } from 'megalodon';
|
||||||
import type { FastifyRequest } from 'fastify';
|
import type { FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
export class ApiSearchMastodon {
|
export interface ApiSearchMastodonRoute {
|
||||||
private request: FastifyRequest;
|
Querystring: TimelineArgs & {
|
||||||
private client: MegalodonInterface;
|
type?: 'accounts' | 'hashtags' | 'statuses';
|
||||||
private BASE_URL: string;
|
q?: string;
|
||||||
|
|
||||||
constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoConverter: MastoConverters) {
|
|
||||||
this.request = request;
|
|
||||||
this.client = client;
|
|
||||||
this.BASE_URL = BASE_URL;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiSearchMastodon {
|
||||||
|
constructor(
|
||||||
|
private readonly request: FastifyRequest<ApiSearchMastodonRoute>,
|
||||||
|
private readonly client: MegalodonInterface,
|
||||||
|
private readonly me: MiLocalUser | null,
|
||||||
|
private readonly BASE_URL: string,
|
||||||
|
private readonly mastoConverters: MastoConverters,
|
||||||
|
) {}
|
||||||
|
|
||||||
public async SearchV1() {
|
public async SearchV1() {
|
||||||
try {
|
if (!this.request.query.q) throw new Error('Missing required property "q"');
|
||||||
const query: any = limitToInt(this.request.query as any);
|
const query = parseTimelineArgs(this.request.query);
|
||||||
const type = query.type || '';
|
const data = await this.client.search(this.request.query.q, { type: this.request.query.type, ...query });
|
||||||
const data = await this.client.search(query.q, { type: type, ...query });
|
|
||||||
return data.data;
|
return data.data;
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async SearchV2() {
|
public async SearchV2() {
|
||||||
try {
|
if (!this.request.query.q) throw new Error('Missing required property "q"');
|
||||||
const query: any = limitToInt(this.request.query as any);
|
const query = parseTimelineArgs(this.request.query);
|
||||||
const type = query.type;
|
const type = this.request.query.type;
|
||||||
const acct = !type || type === 'accounts' ? await this.client.search(query.q, { type: 'accounts', ...query }) : null;
|
const acct = !type || type === 'accounts' ? await this.client.search(this.request.query.q, { type: 'accounts', ...query }) : null;
|
||||||
const stat = !type || type === 'statuses' ? await this.client.search(query.q, { type: 'statuses', ...query }) : null;
|
const stat = !type || type === 'statuses' ? await this.client.search(this.request.query.q, { type: 'statuses', ...query }) : null;
|
||||||
const tags = !type || type === 'hashtags' ? await this.client.search(query.q, { type: 'hashtags', ...query }) : null;
|
const tags = !type || type === 'hashtags' ? await this.client.search(this.request.query.q, { type: 'hashtags', ...query }) : null;
|
||||||
const data = {
|
return {
|
||||||
accounts: await Promise.all(acct?.data.accounts.map(async (account: any) => await this.mastoConverter.convertAccount(account)) ?? []),
|
accounts: await Promise.all(acct?.data.accounts.map(async (account: Account) => await this.mastoConverters.convertAccount(account)) ?? []),
|
||||||
statuses: await Promise.all(stat?.data.statuses.map(async (status: any) => await this.mastoConverter.convertStatus(status)) ?? []),
|
statuses: await Promise.all(stat?.data.statuses.map(async (status: Status) => await this.mastoConverters.convertStatus(status, this.me)) ?? []),
|
||||||
hashtags: tags?.data.hashtags ?? [],
|
hashtags: tags?.data.hashtags ?? [],
|
||||||
};
|
};
|
||||||
return data;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return e.response.data;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getStatusTrends() {
|
public async getStatusTrends() {
|
||||||
try {
|
|
||||||
const data = await fetch(`${this.BASE_URL}/api/notes/featured`,
|
const data = await fetch(`${this.BASE_URL}/api/notes/featured`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -59,19 +56,16 @@ export class ApiSearchMastodon {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({}),
|
body: JSON.stringify({
|
||||||
|
i: this.request.headers.authorization?.replace('Bearer ', ''),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json() as Promise<Status[]>)
|
||||||
.then(data => data.map((status: any) => this.mastoConverter.convertStatus(status)));
|
.then(data => data.map(status => this.mastoConverters.convertStatus(status, this.me)));
|
||||||
return data;
|
return Promise.all(data);
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSuggestions() {
|
public async getSuggestions() {
|
||||||
try {
|
|
||||||
const data = await fetch(`${this.BASE_URL}/api/users`,
|
const data = await fetch(`${this.BASE_URL}/api/users`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -79,12 +73,22 @@ export class ApiSearchMastodon {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ i: this.request.headers.authorization?.replace('Bearer ', ''), limit: parseInt((this.request.query as any).limit) || 20, origin: 'local', sort: '+follower', state: 'alive' }),
|
body: JSON.stringify({
|
||||||
}).then((res) => res.json()).then(data => data.map(((entry: any) => { return { source: 'global', account: entry }; })));
|
i: this.request.headers.authorization?.replace('Bearer ', ''),
|
||||||
return Promise.all(data.map(async (suggestion: any) => { suggestion.account = await this.mastoConverter.convertAccount(suggestion.account); return suggestion; }));
|
limit: parseTimelineArgs(this.request.query).limit ?? 20,
|
||||||
} catch (e: any) {
|
origin: 'local',
|
||||||
console.error(e);
|
sort: '+follower',
|
||||||
return [];
|
state: 'alive',
|
||||||
}
|
}),
|
||||||
|
})
|
||||||
|
.then(res => res.json() as Promise<Account[]>)
|
||||||
|
.then(data => data.map((entry => ({
|
||||||
|
source: 'global',
|
||||||
|
account: entry,
|
||||||
|
}))));
|
||||||
|
return Promise.all(data.map(async suggestion => {
|
||||||
|
suggestion.account = await this.mastoConverters.convertAccount(suggestion.account);
|
||||||
|
return suggestion;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,181 +3,212 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import querystring from 'querystring';
|
import querystring, { ParsedUrlQueryInput } from 'querystring';
|
||||||
import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js';
|
import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js';
|
||||||
import { convertAttachment, convertPoll, convertStatusSource, MastoConverters } from '../converters.js';
|
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||||
import { getClient } from '../MastodonApiServerService.js';
|
import { parseTimelineArgs, TimelineArgs, toBoolean, toInt } from '@/server/api/mastodon/timelineArgs.js';
|
||||||
import { limitToInt } from './timeline.js';
|
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
|
||||||
|
import { convertAttachment, convertPoll, MastoConverters } from '../converters.js';
|
||||||
|
import { getAccessToken, getClient, MastodonApiServerService } from '../MastodonApiServerService.js';
|
||||||
import type { Entity } from 'megalodon';
|
import type { Entity } from 'megalodon';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
import type { Config } from '@/config.js';
|
|
||||||
import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
|
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
|
|
||||||
function normalizeQuery(data: any) {
|
function normalizeQuery(data: Record<string, unknown>) {
|
||||||
const str = querystring.stringify(data);
|
const str = querystring.stringify(data as ParsedUrlQueryInput);
|
||||||
return querystring.parse(str);
|
return querystring.parse(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiStatusMastodon {
|
export class ApiStatusMastodon {
|
||||||
private fastify: FastifyInstance;
|
constructor(
|
||||||
private mastoconverter: MastoConverters;
|
private readonly fastify: FastifyInstance,
|
||||||
|
private readonly mastoConverters: MastoConverters,
|
||||||
|
private readonly logger: MastodonLogger,
|
||||||
|
private readonly authenticateService: AuthenticateService,
|
||||||
|
private readonly mastodon: MastodonApiServerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
constructor(fastify: FastifyInstance, mastoconverter: MastoConverters) {
|
public getStatus() {
|
||||||
this.fastify = fastify;
|
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||||
this.mastoconverter = mastoconverter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getStatus() {
|
|
||||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
const data = await client.getStatus(_request.params.id);
|
const data = await client.getStatus(_request.params.id);
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
|
this.logger.error(`GET /v1/statuses/${_request.params.id}`, data);
|
||||||
|
reply.code(_request.is404 ? 404 : 401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getStatusSource() {
|
public getStatusSource() {
|
||||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
|
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
const data = await client.getStatusSource(_request.params.id);
|
const data = await client.getStatusSource(_request.params.id);
|
||||||
reply.send(data.data);
|
reply.send(data.data);
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
|
this.logger.error(`GET /v1/statuses/${_request.params.id}/source`, data);
|
||||||
|
reply.code(_request.is404 ? 404 : 401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getContext() {
|
public getContext() {
|
||||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/context', async (_request, reply) => {
|
this.fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/statuses/:id/context', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
const query: any = _request.query;
|
|
||||||
try {
|
try {
|
||||||
const data = await client.getStatusContext(_request.params.id, limitToInt(query));
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
data.data.ancestors = await Promise.all(data.data.ancestors.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
data.data.descendants = await Promise.all(data.data.descendants.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
|
const { data } = await client.getStatusContext(_request.params.id, parseTimelineArgs(_request.query));
|
||||||
reply.send(data.data);
|
const ancestors = await Promise.all(data.ancestors.map(async status => await this.mastoConverters.convertStatus(status, me)));
|
||||||
} catch (e: any) {
|
const descendants = await Promise.all(data.descendants.map(async status => await this.mastoConverters.convertStatus(status, me)));
|
||||||
console.error(e);
|
reply.send({ ancestors, descendants });
|
||||||
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
|
} catch (e) {
|
||||||
|
const data = getErrorData(e);
|
||||||
|
this.logger.error(`GET /v1/statuses/${_request.params.id}/context`, data);
|
||||||
|
reply.code(_request.is404 ? 404 : 401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHistory() {
|
public getHistory() {
|
||||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
|
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
|
||||||
try {
|
try {
|
||||||
const edits = await this.mastoconverter.getEdits(_request.params.id);
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
const [user] = await this.authenticateService.authenticate(getAccessToken(_request.headers.authorization));
|
||||||
|
const edits = await this.mastoConverters.getEdits(_request.params.id, user);
|
||||||
reply.send(edits);
|
reply.send(edits);
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`GET /v1/statuses/${_request.params.id}/history`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReblogged() {
|
public getReblogged() {
|
||||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
|
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
const data = await client.getStatusRebloggedBy(_request.params.id);
|
const data = await client.getStatusRebloggedBy(_request.params.id);
|
||||||
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account))));
|
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`GET /v1/statuses/${_request.params.id}/reblogged_by`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFavourites() {
|
public getFavourites() {
|
||||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
|
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
const data = await client.getStatusFavouritedBy(_request.params.id);
|
const data = await client.getStatusFavouritedBy(_request.params.id);
|
||||||
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account))));
|
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`GET /v1/statuses/${_request.params.id}/favourited_by`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMedia() {
|
public getMedia() {
|
||||||
this.fastify.get<{ Params: { id: string } }>('/v1/media/:id', async (_request, reply) => {
|
this.fastify.get<{ Params: { id?: string } }>('/v1/media/:id', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
const data = await client.getMedia(_request.params.id);
|
const data = await client.getMedia(_request.params.id);
|
||||||
reply.send(convertAttachment(data.data));
|
reply.send(convertAttachment(data.data));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`GET /v1/media/${_request.params.id}`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPoll() {
|
public getPoll() {
|
||||||
this.fastify.get<{ Params: { id: string } }>('/v1/polls/:id', async (_request, reply) => {
|
this.fastify.get<{ Params: { id?: string } }>('/v1/polls/:id', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
const data = await client.getPoll(_request.params.id);
|
const data = await client.getPoll(_request.params.id);
|
||||||
reply.send(convertPoll(data.data));
|
reply.send(convertPoll(data.data));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`GET /v1/polls/${_request.params.id}`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async votePoll() {
|
public votePoll() {
|
||||||
this.fastify.post<{ Params: { id: string } }>('/v1/polls/:id/votes', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string }, Body: { choices?: number[] } }>('/v1/polls/:id/votes', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const body: any = _request.body;
|
|
||||||
try {
|
try {
|
||||||
const data = await client.votePoll(_request.params.id, body.choices);
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
if (!_request.body.choices) return reply.code(400).send({ error: 'Missing required payload "choices"' });
|
||||||
|
const data = await client.votePoll(_request.params.id, _request.body.choices);
|
||||||
reply.send(convertPoll(data.data));
|
reply.send(convertPoll(data.data));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`GET /v1/polls/${_request.params.id}/votes`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async postStatus() {
|
public postStatus() {
|
||||||
this.fastify.post('/v1/statuses', async (_request, reply) => {
|
this.fastify.post<{
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
Body: {
|
||||||
const accessTokens = _request.headers.authorization;
|
media_ids?: string[],
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
poll?: {
|
||||||
let body: any = _request.body;
|
options?: string[],
|
||||||
|
expires_in?: string,
|
||||||
|
multiple?: string,
|
||||||
|
hide_totals?: string,
|
||||||
|
},
|
||||||
|
in_reply_to_id?: string,
|
||||||
|
sensitive?: string,
|
||||||
|
spoiler_text?: string,
|
||||||
|
visibility?: 'public' | 'unlisted' | 'private' | 'direct',
|
||||||
|
scheduled_at?: string,
|
||||||
|
language?: string,
|
||||||
|
quote_id?: string,
|
||||||
|
status?: string,
|
||||||
|
|
||||||
|
// Broken clients
|
||||||
|
'poll[options][]'?: string[],
|
||||||
|
'media_ids[]'?: string[],
|
||||||
|
}
|
||||||
|
}>('/v1/statuses', async (_request, reply) => {
|
||||||
|
let body = _request.body;
|
||||||
try {
|
try {
|
||||||
if (
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
(!body.poll && body['poll[options][]']) ||
|
if ((!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]'])
|
||||||
(!body.media_ids && body['media_ids[]'])
|
|
||||||
) {
|
) {
|
||||||
body = normalizeQuery(body);
|
body = normalizeQuery(body);
|
||||||
}
|
}
|
||||||
const text = body.status ? body.status : ' ';
|
const text = body.status ??= ' ';
|
||||||
const removed = text.replace(/@\S+/g, '').replace(/\s|/g, '');
|
const removed = text.replace(/@\S+/g, '').replace(/\s|/g, '');
|
||||||
const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
|
const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
|
||||||
const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
|
const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
|
||||||
|
@ -189,226 +220,253 @@ export class ApiStatusMastodon {
|
||||||
reply.send(a.data);
|
reply.send(a.data);
|
||||||
}
|
}
|
||||||
if (body.in_reply_to_id && removed === '/unreact') {
|
if (body.in_reply_to_id && removed === '/unreact') {
|
||||||
try {
|
|
||||||
const id = body.in_reply_to_id;
|
const id = body.in_reply_to_id;
|
||||||
const post = await client.getStatus(id);
|
const post = await client.getStatus(id);
|
||||||
const react = post.data.emoji_reactions.filter((e: any) => e.me)[0].name;
|
const react = post.data.emoji_reactions.filter(e => e.me)[0].name;
|
||||||
const data = await client.deleteEmojiReaction(id, react);
|
const data = await client.deleteEmojiReaction(id, react);
|
||||||
reply.send(data.data);
|
reply.send(data.data);
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
reply.code(401).send(e.response.data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!body.media_ids) body.media_ids = undefined;
|
if (!body.media_ids) body.media_ids = undefined;
|
||||||
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
|
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
|
||||||
|
|
||||||
const { sensitive } = body;
|
if (body.poll && !body.poll.options) {
|
||||||
body.sensitive = typeof sensitive === 'string' ? sensitive === 'true' : sensitive;
|
return reply.code(400).send({ error: 'Missing required payload "poll.options"' });
|
||||||
|
}
|
||||||
if (body.poll) {
|
if (body.poll && !body.poll.expires_in) {
|
||||||
if (
|
return reply.code(400).send({ error: 'Missing required payload "poll.expires_in"' });
|
||||||
body.poll.expires_in != null &&
|
|
||||||
typeof body.poll.expires_in === 'string'
|
|
||||||
) body.poll.expires_in = parseInt(body.poll.expires_in);
|
|
||||||
if (
|
|
||||||
body.poll.multiple != null &&
|
|
||||||
typeof body.poll.multiple === 'string'
|
|
||||||
) body.poll.multiple = body.poll.multiple === 'true';
|
|
||||||
if (
|
|
||||||
body.poll.hide_totals != null &&
|
|
||||||
typeof body.poll.hide_totals === 'string'
|
|
||||||
) body.poll.hide_totals = body.poll.hide_totals === 'true';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await client.postStatus(text, body);
|
const options = {
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data as Entity.Status));
|
...body,
|
||||||
} catch (e: any) {
|
sensitive: toBoolean(body.sensitive),
|
||||||
console.error(e);
|
poll: body.poll ? {
|
||||||
reply.code(401).send(e.response.data);
|
options: body.poll.options!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||||
|
expires_in: toInt(body.poll.expires_in)!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||||
|
multiple: toBoolean(body.poll.multiple),
|
||||||
|
hide_totals: toBoolean(body.poll.hide_totals),
|
||||||
|
} : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await client.postStatus(text, options);
|
||||||
|
reply.send(await this.mastoConverters.convertStatus(data.data as Entity.Status, me));
|
||||||
|
} catch (e) {
|
||||||
|
const data = getErrorData(e);
|
||||||
|
this.logger.error('POST /v1/statuses', data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateStatus() {
|
public updateStatus() {
|
||||||
this.fastify.put<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
this.fastify.put<{
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
Params: { id: string },
|
||||||
const accessTokens = _request.headers.authorization;
|
Body: {
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
status?: string,
|
||||||
const body: any = _request.body;
|
spoiler_text?: string,
|
||||||
|
sensitive?: string,
|
||||||
|
media_ids?: string[],
|
||||||
|
poll?: {
|
||||||
|
options?: string[],
|
||||||
|
expires_in?: string,
|
||||||
|
multiple?: string,
|
||||||
|
hide_totals?: string,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}>('/v1/statuses/:id', async (_request, reply) => {
|
||||||
try {
|
try {
|
||||||
if (!body.media_ids) body.media_ids = undefined;
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
|
const body = _request.body;
|
||||||
const data = await client.editStatus(_request.params.id, body);
|
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
if (!body.media_ids || !body.media_ids.length) {
|
||||||
} catch (e: any) {
|
body.media_ids = undefined;
|
||||||
console.error(e);
|
}
|
||||||
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
|
|
||||||
|
const options = {
|
||||||
|
...body,
|
||||||
|
sensitive: toBoolean(body.sensitive),
|
||||||
|
poll: body.poll ? {
|
||||||
|
options: body.poll.options,
|
||||||
|
expires_in: toInt(body.poll.expires_in),
|
||||||
|
multiple: toBoolean(body.poll.multiple),
|
||||||
|
hide_totals: toBoolean(body.poll.hide_totals),
|
||||||
|
} : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = await client.editStatus(_request.params.id, options);
|
||||||
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
|
} catch (e) {
|
||||||
|
const data = getErrorData(e);
|
||||||
|
this.logger.error(`POST /v1/statuses/${_request.params.id}`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addFavourite() {
|
public addFavourite() {
|
||||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
const data = (await client.createEmojiReaction(_request.params.id, '❤')) as any;
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
} catch (e: any) {
|
const data = await client.createEmojiReaction(_request.params.id, '❤');
|
||||||
console.error(e);
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
reply.code(401).send(e.response.data);
|
} catch (e) {
|
||||||
|
const data = getErrorData(e);
|
||||||
|
this.logger.error(`POST /v1/statuses/${_request.params.id}/favorite`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rmFavourite() {
|
public rmFavourite() {
|
||||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
const data = await client.deleteEmojiReaction(_request.params.id, '❤');
|
const data = await client.deleteEmojiReaction(_request.params.id, '❤');
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`GET /v1/statuses/${_request.params.id}/unfavorite`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reblogStatus() {
|
public reblogStatus() {
|
||||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.reblogStatus(_request.params.id);
|
const data = await client.reblogStatus(_request.params.id);
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`POST /v1/statuses/${_request.params.id}/reblog`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unreblogStatus() {
|
public unreblogStatus() {
|
||||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.unreblogStatus(_request.params.id);
|
const data = await client.unreblogStatus(_request.params.id);
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`POST /v1/statuses/${_request.params.id}/unreblog`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bookmarkStatus() {
|
public bookmarkStatus() {
|
||||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.bookmarkStatus(_request.params.id);
|
const data = await client.bookmarkStatus(_request.params.id);
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`POST /v1/statuses/${_request.params.id}/bookmark`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unbookmarkStatus() {
|
public unbookmarkStatus() {
|
||||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.unbookmarkStatus(_request.params.id);
|
const data = await client.unbookmarkStatus(_request.params.id);
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`POST /v1/statuses/${_request.params.id}/unbookmark`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pinStatus() {
|
public pinStatus() {
|
||||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.pinStatus(_request.params.id);
|
const data = await client.pinStatus(_request.params.id);
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`POST /v1/statuses/${_request.params.id}/pin`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unpinStatus() {
|
public unpinStatus() {
|
||||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.unpinStatus(_request.params.id);
|
const data = await client.unpinStatus(_request.params.id);
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`POST /v1/statuses/${_request.params.id}/unpin`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reactStatus() {
|
public reactStatus() {
|
||||||
this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
|
||||||
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.createEmojiReaction(_request.params.id, _request.params.name);
|
const data = await client.createEmojiReaction(_request.params.id, _request.params.name);
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`POST /v1/statuses/${_request.params.id}/react/${_request.params.name}`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unreactStatus() {
|
public unreactStatus() {
|
||||||
this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
|
||||||
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name);
|
const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name);
|
||||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`POST /v1/statuses/${_request.params.id}/unreact/${_request.params.name}`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteStatus() {
|
public deleteStatus() {
|
||||||
this.fastify.delete<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
this.fastify.delete<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
try {
|
try {
|
||||||
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
const data = await client.deleteStatus(_request.params.id);
|
const data = await client.deleteStatus(_request.params.id);
|
||||||
reply.send(data.data);
|
reply.send(data.data);
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error(`DELETE /v1/statuses/${_request.params.id}`, data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,270 +3,231 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ParsedUrlQuery } from 'querystring';
|
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||||
import { convertConversation, convertList, MastoConverters } from '../converters.js';
|
import { convertList, MastoConverters } from '../converters.js';
|
||||||
import { getClient } from '../MastodonApiServerService.js';
|
import { getClient, MastodonApiServerService } from '../MastodonApiServerService.js';
|
||||||
|
import { parseTimelineArgs, TimelineArgs, toBoolean } from '../timelineArgs.js';
|
||||||
import type { Entity } from 'megalodon';
|
import type { Entity } from 'megalodon';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
import type { Config } from '@/config.js';
|
|
||||||
import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
|
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
|
|
||||||
export function limitToInt(q: ParsedUrlQuery) {
|
|
||||||
const 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);
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function argsToBools(q: ParsedUrlQuery) {
|
|
||||||
// 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
|
|
||||||
const object: any = q;
|
|
||||||
if (q.only_media) if (typeof q.only_media === 'string') object.only_media = toBoolean(q.only_media);
|
|
||||||
if (q.exclude_replies) if (typeof q.exclude_replies === 'string') object.exclude_replies = toBoolean(q.exclude_replies);
|
|
||||||
if (q.exclude_reblogs) if (typeof q.exclude_reblogs === 'string') object.exclude_reblogs = toBoolean(q.exclude_reblogs);
|
|
||||||
if (q.pinned) if (typeof q.pinned === 'string') object.pinned = toBoolean(q.pinned);
|
|
||||||
if (q.local) if (typeof q.local === 'string') object.local = toBoolean(q.local);
|
|
||||||
return q;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ApiTimelineMastodon {
|
export class ApiTimelineMastodon {
|
||||||
private fastify: FastifyInstance;
|
constructor(
|
||||||
|
private readonly fastify: FastifyInstance,
|
||||||
|
private readonly mastoConverters: MastoConverters,
|
||||||
|
private readonly logger: MastodonLogger,
|
||||||
|
private readonly mastodon: MastodonApiServerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
constructor(fastify: FastifyInstance, config: Config, private mastoconverter: MastoConverters) {
|
public getTL() {
|
||||||
this.fastify = fastify;
|
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/public', async (_request, reply) => {
|
||||||
}
|
|
||||||
|
|
||||||
public async getTL() {
|
|
||||||
this.fastify.get('/v1/timelines/public', async (_request, reply) => {
|
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
const query: any = _request.query;
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = query.local === 'true'
|
const data = toBoolean(_request.query.local)
|
||||||
? await client.getLocalTimeline(argsToBools(limitToInt(query)))
|
? await client.getLocalTimeline(parseTimelineArgs(_request.query))
|
||||||
: await client.getPublicTimeline(argsToBools(limitToInt(query)));
|
: await client.getPublicTimeline(parseTimelineArgs(_request.query));
|
||||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
|
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
console.error(e.response.data);
|
this.logger.error('GET /v1/timelines/public', data);
|
||||||
reply.code(401).send(e.response.data);
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHomeTl() {
|
public getHomeTl() {
|
||||||
this.fastify.get('/v1/timelines/home', async (_request, reply) => {
|
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/home', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
const query: any = _request.query;
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.getHomeTimeline(limitToInt(query));
|
const data = await client.getHomeTimeline(parseTimelineArgs(_request.query));
|
||||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
|
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
console.error(e.response.data);
|
this.logger.error('GET /v1/timelines/home', data);
|
||||||
reply.code(401).send(e.response.data);
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTagTl() {
|
public getTagTl() {
|
||||||
this.fastify.get<{ Params: { hashtag: string } }>('/v1/timelines/tag/:hashtag', async (_request, reply) => {
|
this.fastify.get<{ Params: { hashtag?: string }, Querystring: TimelineArgs }>('/v1/timelines/tag/:hashtag', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
const query: any = _request.query;
|
if (!_request.params.hashtag) return reply.code(400).send({ error: 'Missing required parameter "hashtag"' });
|
||||||
const params: any = _request.params;
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.getTagTimeline(params.hashtag, limitToInt(query));
|
const data = await client.getTagTimeline(_request.params.hashtag, parseTimelineArgs(_request.query));
|
||||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
|
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
console.error(e.response.data);
|
this.logger.error(`GET /v1/timelines/tag/${_request.params.hashtag}`, data);
|
||||||
reply.code(401).send(e.response.data);
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getListTL() {
|
public getListTL() {
|
||||||
this.fastify.get<{ Params: { id: string } }>('/v1/timelines/list/:id', async (_request, reply) => {
|
this.fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/timelines/list/:id', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
const query: any = _request.query;
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
const params: any = _request.params;
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.getListTimeline(params.id, limitToInt(query));
|
const data = await client.getListTimeline(_request.params.id, parseTimelineArgs(_request.query));
|
||||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
|
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
console.error(e.response.data);
|
this.logger.error(`GET /v1/timelines/list/${_request.params.id}`, data);
|
||||||
reply.code(401).send(e.response.data);
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getConversations() {
|
public getConversations() {
|
||||||
this.fastify.get('/v1/conversations', async (_request, reply) => {
|
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/conversations', async (_request, reply) => {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
|
||||||
const accessTokens = _request.headers.authorization;
|
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
|
||||||
try {
|
try {
|
||||||
const query: any = _request.query;
|
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||||
const data = await client.getConversationTimeline(limitToInt(query));
|
const data = await client.getConversationTimeline(parseTimelineArgs(_request.query));
|
||||||
reply.send(data.data.map((conversation: Entity.Conversation) => convertConversation(conversation)));
|
const conversations = await Promise.all(data.data.map(async (conversation: Entity.Conversation) => await this.mastoConverters.convertConversation(conversation, me)));
|
||||||
} catch (e: any) {
|
reply.send(conversations);
|
||||||
console.error(e);
|
} catch (e) {
|
||||||
console.error(e.response.data);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(e.response.data);
|
this.logger.error('GET /v1/conversations', data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getList() {
|
public getList() {
|
||||||
this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
|
this.fastify.get<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||||
try {
|
try {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const params: any = _request.params;
|
const data = await client.getList(_request.params.id);
|
||||||
const data = await client.getList(params.id);
|
|
||||||
reply.send(convertList(data.data));
|
reply.send(convertList(data.data));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
console.error(e.response.data);
|
this.logger.error(`GET /v1/lists/${_request.params.id}`, data);
|
||||||
reply.code(401).send(e.response.data);
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLists() {
|
public getLists() {
|
||||||
this.fastify.get('/v1/lists', async (_request, reply) => {
|
this.fastify.get('/v1/lists', async (_request, reply) => {
|
||||||
try {
|
try {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const data = await client.getLists();
|
const data = await client.getLists();
|
||||||
reply.send(data.data.map((list: Entity.List) => convertList(list)));
|
reply.send(data.data.map((list: Entity.List) => convertList(list)));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
return e.response.data;
|
this.logger.error('GET /v1/lists', data);
|
||||||
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getListAccounts() {
|
public getListAccounts() {
|
||||||
this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
this.fastify.get<{ Params: { id?: string }, Querystring: { limit?: number, max_id?: string, since_id?: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||||
try {
|
try {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const params: any = _request.params;
|
const data = await client.getAccountsInList(_request.params.id, _request.query);
|
||||||
const query: any = _request.query;
|
const accounts = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
|
||||||
const data = await client.getAccountsInList(params.id, query);
|
reply.send(accounts);
|
||||||
reply.send(data.data.map((account: Entity.Account) => this.mastoconverter.convertAccount(account)));
|
} catch (e) {
|
||||||
} catch (e: any) {
|
const data = getErrorData(e);
|
||||||
console.error(e);
|
this.logger.error(`GET /v1/lists/${_request.params.id}/accounts`, data);
|
||||||
console.error(e.response.data);
|
reply.code(401).send(data);
|
||||||
reply.code(401).send(e.response.data);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addListAccount() {
|
public addListAccount() {
|
||||||
this.fastify.post<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
this.fastify.post<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||||
try {
|
try {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
|
||||||
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const params: any = _request.params;
|
const data = await client.addAccountsToList(_request.params.id, _request.query.accounts_id);
|
||||||
const query: any = _request.query;
|
|
||||||
const data = await client.addAccountsToList(params.id, query.accounts_id);
|
|
||||||
reply.send(data.data);
|
reply.send(data.data);
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
console.error(e.response.data);
|
this.logger.error(`POST /v1/lists/${_request.params.id}/accounts`, data);
|
||||||
reply.code(401).send(e.response.data);
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rmListAccount() {
|
public rmListAccount() {
|
||||||
this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
this.fastify.delete<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||||
try {
|
try {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
|
||||||
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const params: any = _request.params;
|
const data = await client.deleteAccountsFromList(_request.params.id, _request.query.accounts_id);
|
||||||
const query: any = _request.query;
|
|
||||||
const data = await client.deleteAccountsFromList(params.id, query.accounts_id);
|
|
||||||
reply.send(data.data);
|
reply.send(data.data);
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
console.error(e.response.data);
|
this.logger.error(`DELETE /v1/lists/${_request.params.id}/accounts`, data);
|
||||||
reply.code(401).send(e.response.data);
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createList() {
|
public createList() {
|
||||||
this.fastify.post('/v1/lists', async (_request, reply) => {
|
this.fastify.post<{ Body: { title?: string } }>('/v1/lists', async (_request, reply) => {
|
||||||
try {
|
try {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
|
||||||
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const body: any = _request.body;
|
const data = await client.createList(_request.body.title);
|
||||||
const data = await client.createList(body.title);
|
|
||||||
reply.send(convertList(data.data));
|
reply.send(convertList(data.data));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
console.error(e.response.data);
|
this.logger.error('POST /v1/lists', data);
|
||||||
reply.code(401).send(e.response.data);
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateList() {
|
public updateList() {
|
||||||
this.fastify.put<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
|
this.fastify.put<{ Params: { id?: string }, Body: { title?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||||
try {
|
try {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
|
||||||
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const body: any = _request.body;
|
const data = await client.updateList(_request.params.id, _request.body.title);
|
||||||
const params: any = _request.params;
|
|
||||||
const data = await client.updateList(params.id, body.title);
|
|
||||||
reply.send(convertList(data.data));
|
reply.send(convertList(data.data));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
console.error(e.response.data);
|
this.logger.error(`PUT /v1/lists/${_request.params.id}`, data);
|
||||||
reply.code(401).send(e.response.data);
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteList() {
|
public deleteList() {
|
||||||
this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
|
this.fastify.delete<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||||
try {
|
try {
|
||||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||||
|
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = getClient(BASE_URL, accessTokens);
|
const client = getClient(BASE_URL, accessTokens);
|
||||||
const params: any = _request.params;
|
await client.deleteList(_request.params.id);
|
||||||
const data = await client.deleteList(params.id);
|
|
||||||
reply.send({});
|
reply.send({});
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
console.error(e);
|
const data = getErrorData(e);
|
||||||
console.error(e.response.data);
|
this.logger.error(`DELETE /v1/lists/${_request.params.id}`, data);
|
||||||
reply.code(401).send(e.response.data);
|
reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
47
packages/backend/src/server/api/mastodon/timelineArgs.ts
Normal file
47
packages/backend/src/server/api/mastodon/timelineArgs.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Keys taken from:
|
||||||
|
// - https://docs.joinmastodon.org/methods/accounts/#statuses
|
||||||
|
// - https://docs.joinmastodon.org/methods/timelines/#public
|
||||||
|
// - https://docs.joinmastodon.org/methods/timelines/#tag
|
||||||
|
export interface TimelineArgs {
|
||||||
|
max_id?: string;
|
||||||
|
min_id?: string;
|
||||||
|
since_id?: string;
|
||||||
|
limit?: string;
|
||||||
|
offset?: string;
|
||||||
|
local?: string;
|
||||||
|
pinned?: string;
|
||||||
|
exclude_reblogs?: string;
|
||||||
|
exclude_replies?: string;
|
||||||
|
only_media?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
|
||||||
|
export function toBoolean(value: string | undefined): boolean | undefined {
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
return !['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toInt(value: string | undefined): number | undefined {
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
return parseInt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseTimelineArgs(q: TimelineArgs) {
|
||||||
|
return {
|
||||||
|
max_id: q.max_id,
|
||||||
|
min_id: q.min_id,
|
||||||
|
since_id: q.since_id,
|
||||||
|
limit: typeof(q.limit) === 'string' ? parseInt(q.limit, 10) : undefined,
|
||||||
|
offset: typeof(q.offset) === 'string' ? parseInt(q.offset, 10) : undefined,
|
||||||
|
local: typeof(q.local) === 'string' ? toBoolean(q.local) : undefined,
|
||||||
|
pinned: typeof(q.pinned) === 'string' ? toBoolean(q.pinned) : undefined,
|
||||||
|
exclude_reblogs: typeof(q.exclude_reblogs) === 'string' ? toBoolean(q.exclude_reblogs) : undefined,
|
||||||
|
exclude_replies: typeof(q.exclude_replies) === 'string' ? toBoolean(q.exclude_replies) : undefined,
|
||||||
|
only_media: typeof(q.only_media) === 'string' ? toBoolean(q.only_media) : undefined,
|
||||||
|
};
|
||||||
|
}
|
|
@ -102,7 +102,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="true" :class="$style.urlPreview" @click.stop/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="true" :skipNoteIds="[appearNote.renote?.id]" :class="$style.urlPreview" @click.stop/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
||||||
|
|
|
@ -117,7 +117,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="true" style="margin-top: 6px;"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="true" :skipNoteIds="[appearNote.renote?.id]" style="margin-top: 6px;"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -210,6 +210,8 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const userDetailed: Ref<UserDetailed | null> = ref(null);
|
const userDetailed: Ref<UserDetailed | null> = ref(null);
|
||||||
|
|
||||||
|
const followRequestDone = ref(true);
|
||||||
|
|
||||||
// watch() is required because computed() doesn't support async.
|
// watch() is required because computed() doesn't support async.
|
||||||
watch(props, async () => {
|
watch(props, async () => {
|
||||||
const type = props.notification.type;
|
const type = props.notification.type;
|
||||||
|
@ -242,8 +244,6 @@ const exportEntityName = {
|
||||||
userList: i18n.ts.lists,
|
userList: i18n.ts.lists,
|
||||||
} as const satisfies Record<ExportCompletedNotification['exportedEntity'], string>;
|
} as const satisfies Record<ExportCompletedNotification['exportedEntity'], string>;
|
||||||
|
|
||||||
const followRequestDone = ref(true);
|
|
||||||
|
|
||||||
const acceptFollowRequest = () => {
|
const acceptFollowRequest = () => {
|
||||||
if (!('user' in props.notification)) return;
|
if (!('user' in props.notification)) return;
|
||||||
followRequestDone.value = true;
|
followRequestDone.value = true;
|
||||||
|
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else-if="theNote" :class="[$style.link, { [$style.compact]: compact }]"><XNoteSimple :note="theNote" :class="$style.body"/></div>
|
<div v-else-if="theNote" :class="[$style.link, { [$style.compact]: compact }]"><XNoteSimple :note="theNote" :class="$style.body"/></div>
|
||||||
<div v-else>
|
<div v-else-if="!hidePreview">
|
||||||
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
||||||
<div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`">
|
<div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`">
|
||||||
</div>
|
</div>
|
||||||
|
@ -111,6 +111,7 @@ const props = withDefaults(defineProps<{
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
showAsQuote?: boolean;
|
showAsQuote?: boolean;
|
||||||
showActions?: boolean;
|
showActions?: boolean;
|
||||||
|
skipNoteIds?: string[];
|
||||||
}>(), {
|
}>(), {
|
||||||
detail: false,
|
detail: false,
|
||||||
compact: false,
|
compact: false,
|
||||||
|
@ -121,6 +122,7 @@ const props = withDefaults(defineProps<{
|
||||||
const MOBILE_THRESHOLD = 500;
|
const MOBILE_THRESHOLD = 500;
|
||||||
const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
|
const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
|
||||||
|
|
||||||
|
const hidePreview = ref<boolean>(false);
|
||||||
const self = props.url.startsWith(local);
|
const self = props.url.startsWith(local);
|
||||||
const attr = self ? 'to' : 'href';
|
const attr = self ? 'to' : 'href';
|
||||||
const target = self ? null : '_blank';
|
const target = self ? null : '_blank';
|
||||||
|
@ -155,6 +157,11 @@ watch(activityPub, async (uri) => {
|
||||||
try {
|
try {
|
||||||
const response = await misskeyApi('ap/show', { uri });
|
const response = await misskeyApi('ap/show', { uri });
|
||||||
if (response.type !== 'Note') return;
|
if (response.type !== 'Note') return;
|
||||||
|
const theNoteId = response['object'].id;
|
||||||
|
if (theNoteId && props.skipNoteIds && props.skipNoteIds.includes(theNoteId)) {
|
||||||
|
hidePreview.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
theNote.value = response['object'];
|
theNote.value = response['object'];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
|
|
|
@ -104,7 +104,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll" @click.stop/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="true" :class="$style.urlPreview" @click.stop/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :showAsQuote="true" :skipNoteIds="[appearNote.renote?.id]" :class="$style.urlPreview" @click.stop/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
|
||||||
|
|
|
@ -122,7 +122,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :local="!appearNote.user.host" :class="$style.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis"/>
|
||||||
<div v-if="isEnabledUrlPreview">
|
<div v-if="isEnabledUrlPreview">
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="true" style="margin-top: 6px;"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" :showAsQuote="true" :skipNoteIds="[appearNote.renote?.id]" style="margin-top: 6px;"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"test": "cross-env NODE_ENV=test jest -u --maxWorkers=3"
|
"test": "cross-env NODE_ENV=test jest -u --maxWorkers=3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=15.0.0"
|
"node": "^22.0.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -8,6 +8,7 @@ namespace Entity {
|
||||||
muting: boolean
|
muting: boolean
|
||||||
muting_notifications: boolean
|
muting_notifications: boolean
|
||||||
requested: boolean
|
requested: boolean
|
||||||
|
requested_by?: boolean
|
||||||
domain_blocking: boolean
|
domain_blocking: boolean
|
||||||
showing_reblogs: boolean
|
showing_reblogs: boolean
|
||||||
endorsed: boolean
|
endorsed: boolean
|
||||||
|
|
|
@ -27,7 +27,10 @@ export {
|
||||||
Pleroma,
|
Pleroma,
|
||||||
Misskey,
|
Misskey,
|
||||||
Entity,
|
Entity,
|
||||||
Converter
|
Converter,
|
||||||
|
generator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const megalodon = generator;
|
||||||
|
|
||||||
export default generator
|
export default generator
|
||||||
|
|
|
@ -8,6 +8,7 @@ namespace MastodonEntity {
|
||||||
muting: boolean
|
muting: boolean
|
||||||
muting_notifications: boolean
|
muting_notifications: boolean
|
||||||
requested: boolean
|
requested: boolean
|
||||||
|
requested_by: boolean
|
||||||
domain_blocking: boolean
|
domain_blocking: boolean
|
||||||
showing_reblogs: boolean
|
showing_reblogs: boolean
|
||||||
endorsed: boolean
|
endorsed: boolean
|
||||||
|
|
|
@ -604,7 +604,7 @@ export default class Misskey implements MegalodonInterface {
|
||||||
/**
|
/**
|
||||||
* POST /api/users/relation
|
* POST /api/users/relation
|
||||||
*
|
*
|
||||||
* @param id Array of account ID, for example `['1sdfag', 'ds12aa']`.
|
* @param ids Array of account ID, for example `['1sdfag', 'ds12aa']`.
|
||||||
*/
|
*/
|
||||||
public async getRelationships(ids: Array<string>): Promise<Response<Array<Entity.Relationship>>> {
|
public async getRelationships(ids: Array<string>): Promise<Response<Array<Entity.Relationship>>> {
|
||||||
return Promise.all(ids.map(id => this.getRelationship(id))).then(results => ({
|
return Promise.all(ids.map(id => this.getRelationship(id))).then(results => ({
|
||||||
|
|
|
@ -227,13 +227,14 @@ namespace MisskeyAPI {
|
||||||
blocking: r.isBlocking,
|
blocking: r.isBlocking,
|
||||||
blocked_by: r.isBlocked,
|
blocked_by: r.isBlocked,
|
||||||
muting: r.isMuted,
|
muting: r.isMuted,
|
||||||
muting_notifications: false,
|
muting_notifications: r.isMuted,
|
||||||
requested: r.hasPendingFollowRequestFromYou,
|
requested: r.hasPendingFollowRequestFromYou,
|
||||||
domain_blocking: false,
|
requested_by: r.hasPendingFollowRequestToYou,
|
||||||
showing_reblogs: true,
|
domain_blocking: r.isInstanceMuted ?? false,
|
||||||
|
showing_reblogs: !r.isRenoteMuted,
|
||||||
endorsed: false,
|
endorsed: false,
|
||||||
notifying: false,
|
notifying: !r.isMuted,
|
||||||
note: null
|
note: r.memo ?? '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,5 +8,8 @@ namespace MisskeyEntity {
|
||||||
isBlocking: boolean
|
isBlocking: boolean
|
||||||
isBlocked: boolean
|
isBlocked: boolean
|
||||||
isMuted: boolean
|
isMuted: boolean
|
||||||
|
isRenoteMuted: boolean
|
||||||
|
isInstanceMuted?: boolean
|
||||||
|
memo?: string | null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
|
"target": "ES2022", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||||
"lib": ["es2021", "dom"], /* Specify library files to be included in the compilation. */
|
"lib": ["ES2022", "dom"], /* Specify library files to be included in the compilation. */
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
|
|
|
@ -30820,7 +30820,9 @@ export type operations = {
|
||||||
isBlocked: boolean;
|
isBlocked: boolean;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
isRenoteMuted: boolean;
|
isRenoteMuted: boolean;
|
||||||
}, {
|
isInstanceMuted?: boolean;
|
||||||
|
memo?: string | null;
|
||||||
|
}, ({
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
id: string;
|
id: string;
|
||||||
isFollowing: boolean;
|
isFollowing: boolean;
|
||||||
|
@ -30831,7 +30833,9 @@ export type operations = {
|
||||||
isBlocked: boolean;
|
isBlocked: boolean;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
isRenoteMuted: boolean;
|
isRenoteMuted: boolean;
|
||||||
}[]]>;
|
isInstanceMuted?: boolean;
|
||||||
|
memo?: string | null;
|
||||||
|
})[]]>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Client error */
|
/** @description Client error */
|
||||||
|
|
|
@ -432,6 +432,12 @@ postOn: "Post on"
|
||||||
scheduledNotes: "Scheduled Notes"
|
scheduledNotes: "Scheduled Notes"
|
||||||
|
|
||||||
_permissions:
|
_permissions:
|
||||||
|
"write:admin:approve-user": "Approve new users"
|
||||||
|
"write:admin:decline-user": "Decline new users"
|
||||||
|
"write:admin:nsfw-user": "Mark users as NSFW"
|
||||||
|
"write:admin:unnsfw-user": "Mark users an not NSFW"
|
||||||
|
"write:admin:silence-user": "Silence users"
|
||||||
|
"write:admin:unsilence-user": "Un-silence users"
|
||||||
"read:notes-schedule": "View your list of scheduled notes"
|
"read:notes-schedule": "View your list of scheduled notes"
|
||||||
"write:notes-schedule": "Compose or delete scheduled notes"
|
"write:notes-schedule": "Compose or delete scheduled notes"
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue