Compare commits
25 commits
develop
...
cf/multipl
Author | SHA1 | Date | |
---|---|---|---|
|
89c38a54f3 | ||
|
0c2e4f139b | ||
|
28b04a84aa | ||
|
51c55c2431 | ||
|
af9d5cea33 | ||
|
f45cbb56bb | ||
|
230556c17a | ||
|
fad9a54912 | ||
|
cd4caffc83 | ||
|
5cf9753edd | ||
|
74e3a52cc5 | ||
|
234282c804 | ||
|
d1e7af596b | ||
|
4850a2450e | ||
|
62b42426dd | ||
|
fcfa36c4f1 | ||
|
4e73fdecbf | ||
|
84062c4aa3 | ||
|
bb84c3fb80 | ||
|
5fc3b8d120 | ||
|
4365a5391b | ||
|
7ae5275fdf | ||
|
a642936d43 | ||
|
ee26579083 | ||
|
c2f479c39d |
37 changed files with 463 additions and 215 deletions
|
@ -79,6 +79,14 @@
|
||||||
# Final accessible URL seen by a user.
|
# Final accessible URL seen by a user.
|
||||||
url: http://misskey.local
|
url: http://misskey.local
|
||||||
|
|
||||||
|
# Other public URLs that are served by this instance.
|
||||||
|
# If you've migrated to a new domain using an existing database, then add your previous URL here to continue serving those posts and profiles via federation.
|
||||||
|
# Requires reverse-proxy configuration to forward requests with HOST header preserved.
|
||||||
|
# For advanced usage only!
|
||||||
|
#altUrls:
|
||||||
|
# - https://example.com/
|
||||||
|
# - https://legacy.tld/
|
||||||
|
|
||||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
# URL SETTINGS AFTER THAT!
|
# URL SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,14 @@ setupPassword: example_password_please_change_this_or_you_will_get_hacked
|
||||||
# Final accessible URL seen by a user.
|
# Final accessible URL seen by a user.
|
||||||
url: 'http://misskey.local'
|
url: 'http://misskey.local'
|
||||||
|
|
||||||
|
# Other public URLs that are served by this instance.
|
||||||
|
# If you've migrated to a new domain using an existing database, then add your previous URL here to continue serving those posts and profiles via federation.
|
||||||
|
# Requires reverse-proxy configuration to forward requests with HOST header preserved.
|
||||||
|
# For advanced usage only!
|
||||||
|
#altUrls:
|
||||||
|
# - https://example.com/
|
||||||
|
# - https://legacy.tld/
|
||||||
|
|
||||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
# URL SETTINGS AFTER THAT!
|
# URL SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,14 @@
|
||||||
# You can set url from an environment variable instead.
|
# You can set url from an environment variable instead.
|
||||||
url: https://example.tld/
|
url: https://example.tld/
|
||||||
|
|
||||||
|
# Other public URLs that are served by this instance.
|
||||||
|
# If you've migrated to a new domain using an existing database, then add your previous URL here to continue serving those posts and profiles via federation.
|
||||||
|
# Requires reverse-proxy configuration to forward requests with HOST header preserved.
|
||||||
|
# For advanced usage only!
|
||||||
|
#altUrls:
|
||||||
|
# - https://example.com/
|
||||||
|
# - https://legacy.tld/
|
||||||
|
|
||||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
# URL SETTINGS AFTER THAT!
|
# URL SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,14 @@
|
||||||
# Final accessible URL seen by a user.
|
# Final accessible URL seen by a user.
|
||||||
url: https://example.tld/
|
url: https://example.tld/
|
||||||
|
|
||||||
|
# Other public URLs that are served by this instance.
|
||||||
|
# If you've migrated to a new domain using an existing database, then add your previous URL here to continue serving those posts and profiles via federation.
|
||||||
|
# Requires reverse-proxy configuration to forward requests with HOST header preserved.
|
||||||
|
# For advanced usage only!
|
||||||
|
#altUrls:
|
||||||
|
# - https://example.com/
|
||||||
|
# - https://legacy.tld/
|
||||||
|
|
||||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||||
# URL SETTINGS AFTER THAT!
|
# URL SETTINGS AFTER THAT!
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ type RedisOptionsSource = Partial<RedisOptions> & {
|
||||||
*/
|
*/
|
||||||
type Source = {
|
type Source = {
|
||||||
url?: string;
|
url?: string;
|
||||||
|
altUrls?: string[];
|
||||||
port?: number;
|
port?: number;
|
||||||
address?: string;
|
address?: string;
|
||||||
socket?: string;
|
socket?: string;
|
||||||
|
@ -151,7 +152,7 @@ type Source = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Config = {
|
export type Config = Tenant & {
|
||||||
url: string;
|
url: string;
|
||||||
port: number;
|
port: number;
|
||||||
address: string;
|
address: string;
|
||||||
|
@ -273,6 +274,8 @@ export type Config = {
|
||||||
maxAge: number;
|
maxAge: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
alts: Tenant[];
|
||||||
|
|
||||||
websocketCompression?: boolean;
|
websocketCompression?: boolean;
|
||||||
|
|
||||||
customHtml: {
|
customHtml: {
|
||||||
|
@ -280,6 +283,18 @@ export type Config = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Tenant = {
|
||||||
|
url: string;
|
||||||
|
host: string;
|
||||||
|
hostname: string;
|
||||||
|
scheme: string;
|
||||||
|
wsScheme: string;
|
||||||
|
apiUrl: string;
|
||||||
|
wsUrl: string;
|
||||||
|
authUrl: string;
|
||||||
|
driveUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch' | 'sqlTsvector';
|
export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch' | 'sqlTsvector';
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
|
@ -345,6 +360,24 @@ export function loadConfig(): Config {
|
||||||
const internalMediaProxy = `${scheme}://${host}/proxy`;
|
const internalMediaProxy = `${scheme}://${host}/proxy`;
|
||||||
const redis = convertRedisOptions(config.redis, host);
|
const redis = convertRedisOptions(config.redis, host);
|
||||||
|
|
||||||
|
const alts: Tenant[] = config.altUrls?.map(alt => {
|
||||||
|
const altUrl = tryCreateUrl(alt);
|
||||||
|
const altHost = altUrl.host;
|
||||||
|
const altScheme = altUrl.protocol.replace(/:$/, '');
|
||||||
|
const altWsScheme = altScheme.replace('http', 'ws');
|
||||||
|
return {
|
||||||
|
url: alt,
|
||||||
|
host: altHost,
|
||||||
|
hostname: altUrl.hostname,
|
||||||
|
scheme: altScheme,
|
||||||
|
wsScheme: altWsScheme,
|
||||||
|
wsUrl: `${altWsScheme}://${altHost}`,
|
||||||
|
apiUrl: `${altWsScheme}://${altHost}/api`,
|
||||||
|
authUrl: `${altWsScheme}://${altHost}/auth`,
|
||||||
|
driveUrl: `${altWsScheme}://${altHost}/files`,
|
||||||
|
};
|
||||||
|
}) ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version,
|
version,
|
||||||
publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
|
publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
|
||||||
|
@ -427,6 +460,7 @@ export function loadConfig(): Config {
|
||||||
preSave: config.activityLogging?.preSave ?? false,
|
preSave: config.activityLogging?.preSave ?? false,
|
||||||
maxAge: config.activityLogging?.maxAge ?? (1000 * 60 * 60 * 24 * 30),
|
maxAge: config.activityLogging?.maxAge ?? (1000 * 60 * 60 * 24 * 30),
|
||||||
},
|
},
|
||||||
|
alts,
|
||||||
websocketCompression: config.websocketCompression ?? false,
|
websocketCompression: config.websocketCompression ?? false,
|
||||||
customHtml: {
|
customHtml: {
|
||||||
head: config.customHtml?.head ?? '',
|
head: config.customHtml?.head ?? '',
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||||
import { isSystemAccount } from '@/misc/is-system-account.js';
|
import { isSystemAccount } from '@/misc/is-system-account.js';
|
||||||
|
import type { MiPartialUser } from '@/models/User.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeleteAccountService {
|
export class DeleteAccountService {
|
||||||
|
@ -38,10 +39,7 @@ export class DeleteAccountService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deleteAccount(user: {
|
public async deleteAccount(user: MiPartialUser, moderator?: MiUser): Promise<void> {
|
||||||
id: string;
|
|
||||||
host: string | null;
|
|
||||||
}, moderator?: MiUser): Promise<void> {
|
|
||||||
if (this.meta.rootUserId === user.id) throw new Error('cannot delete a root account');
|
if (this.meta.rootUserId === user.id) throw new Error('cannot delete a root account');
|
||||||
|
|
||||||
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
|
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
|
||||||
|
|
|
@ -71,7 +71,9 @@ type AddFileArgs = {
|
||||||
ext?: string | null;
|
ext?: string | null;
|
||||||
|
|
||||||
requestIp?: string | null;
|
requestIp?: string | null;
|
||||||
requestHeaders?: Record<string, string> | null;
|
requestHeaders?: {
|
||||||
|
[Name in string]?: string | string[]
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UploadFromUrlArgs = {
|
type UploadFromUrlArgs = {
|
||||||
|
@ -84,7 +86,9 @@ type UploadFromUrlArgs = {
|
||||||
isLink?: boolean;
|
isLink?: boolean;
|
||||||
comment?: string | null;
|
comment?: string | null;
|
||||||
requestIp?: string | null;
|
requestIp?: string | null;
|
||||||
requestHeaders?: Record<string, string> | null;
|
requestHeaders?: {
|
||||||
|
[Name in string]?: string | string[]
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -583,6 +587,17 @@ export class DriveService {
|
||||||
|
|
||||||
const folder = await fetchFolder();
|
const folder = await fetchFolder();
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (requestHeaders) {
|
||||||
|
for (const [name, value] of Object.entries(requestHeaders)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
headers[name] = value.join();
|
||||||
|
} else if (value) {
|
||||||
|
headers[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let file = new MiDriveFile();
|
let file = new MiDriveFile();
|
||||||
file.id = this.idService.gen();
|
file.id = this.idService.gen();
|
||||||
file.userId = user ? user.id : null;
|
file.userId = user ? user.id : null;
|
||||||
|
@ -593,7 +608,7 @@ export class DriveService {
|
||||||
file.blurhash = info.blurhash ?? null;
|
file.blurhash = info.blurhash ?? null;
|
||||||
file.isLink = isLink;
|
file.isLink = isLink;
|
||||||
file.requestIp = requestIp;
|
file.requestIp = requestIp;
|
||||||
file.requestHeaders = requestHeaders;
|
file.requestHeaders = headers;
|
||||||
file.maybeSensitive = info.sensitive;
|
file.maybeSensitive = info.sensitive;
|
||||||
file.maybePorn = info.porn;
|
file.maybePorn = info.porn;
|
||||||
file.isSensitive = user
|
file.isSensitive = user
|
||||||
|
|
|
@ -875,8 +875,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (data.localOnly) return null;
|
if (data.localOnly) return null;
|
||||||
|
|
||||||
const content = this.isRenote(data) && !this.isQuote(data)
|
const content = this.isRenote(data) && !this.isQuote(data)
|
||||||
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
|
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note, user)
|
||||||
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, user, false), note);
|
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, user, false), note, user);
|
||||||
|
|
||||||
return this.apRendererService.addContext(content);
|
return this.apRendererService.addContext(content);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { Brackets, In, IsNull, Not } from 'typeorm';
|
import { Brackets, In, IsNull, Not } from 'typeorm';
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiUser, MiLocalUser, MiRemoteUser, MiPartialUser } from '@/models/User.js';
|
||||||
import { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
|
import { MiNote, IMentionedRemoteUsers } from '@/models/Note.js';
|
||||||
import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js';
|
import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import { RelayService } from '@/core/RelayService.js';
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
|
@ -71,7 +71,7 @@ export class NoteDeleteService {
|
||||||
* @param user 投稿者
|
* @param user 投稿者
|
||||||
* @param note 投稿
|
* @param note 投稿
|
||||||
*/
|
*/
|
||||||
async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) {
|
async delete(user: MiPartialUser & { isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) {
|
||||||
const deletedAt = new Date();
|
const deletedAt = new Date();
|
||||||
const cascadingNotes = await this.findCascadingNotes(note);
|
const cascadingNotes = await this.findCascadingNotes(note);
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ export class NoteDeleteService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = this.apRendererService.addContext(renote
|
const content = this.apRendererService.addContext(renote
|
||||||
? this.apRendererService.renderUndo(this.apRendererService.renderAnnounce(renote.uri ?? `${this.config.url}/notes/${renote.id}`, note), user)
|
? this.apRendererService.renderUndo(this.apRendererService.renderAnnounce(renote.uri ?? `${this.config.url}/notes/${renote.id}`, note, user), user)
|
||||||
: this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${note.id}`), user));
|
: this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${note.id}`), user));
|
||||||
|
|
||||||
this.deliverToConcerned(user, note, content);
|
this.deliverToConcerned(user, note, content);
|
||||||
|
|
|
@ -770,7 +770,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
if (data.localOnly) return null;
|
if (data.localOnly) return null;
|
||||||
|
|
||||||
const content = this.isRenote(data) && !this.isQuote(data)
|
const content = this.isRenote(data) && !this.isQuote(data)
|
||||||
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
|
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note, user)
|
||||||
: this.apRendererService.renderUpdate(await this.apRendererService.renderUpNote(note, user, false), user);
|
: this.apRendererService.renderUpdate(await this.apRendererService.renderUpNote(note, user, false), user);
|
||||||
|
|
||||||
return this.apRendererService.addContext(content);
|
return this.apRendererService.addContext(content);
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, NoteThreadMutingsRepository, MiMeta } from '@/models/_.js';
|
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, NoteThreadMutingsRepository, MiMeta } from '@/models/_.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiPartialUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { MiNoteReaction } from '@/models/NoteReaction.js';
|
import type { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||||
|
@ -106,7 +106,7 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
|
public async create(user: MiPartialUser & { isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
|
||||||
// Check blocking
|
// Check blocking
|
||||||
if (note.userId !== user.id) {
|
if (note.userId !== user.id) {
|
||||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||||
|
@ -276,7 +276,7 @@ export class ReactionService {
|
||||||
|
|
||||||
//#region 配信
|
//#region 配信
|
||||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||||
const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note));
|
const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note, user));
|
||||||
const dm = this.apDeliverManagerService.createDeliverManager(user, content);
|
const dm = this.apDeliverManagerService.createDeliverManager(user, content);
|
||||||
if (note.userHost !== null) {
|
if (note.userHost !== null) {
|
||||||
const reactee = await this.usersRepository.findOneBy({ id: note.userId });
|
const reactee = await this.usersRepository.findOneBy({ id: note.userId });
|
||||||
|
@ -337,7 +337,7 @@ export class ReactionService {
|
||||||
|
|
||||||
//#region 配信
|
//#region 配信
|
||||||
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user));
|
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note, user), user));
|
||||||
const dm = this.apDeliverManagerService.createDeliverManager(user, content);
|
const dm = this.apDeliverManagerService.createDeliverManager(user, content);
|
||||||
if (note.userHost !== null) {
|
if (note.userHost !== null) {
|
||||||
const reactee = await this.usersRepository.findOneBy({ id: note.userId });
|
const reactee = await this.usersRepository.findOneBy({ id: note.userId });
|
||||||
|
|
|
@ -40,12 +40,14 @@ export class RemoteUserResolveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolveUser(username: string, host: string | null): Promise<MiLocalUser | MiRemoteUser> {
|
public async resolveUser(username: string, host: string | null, requestHostname?: string): Promise<MiLocalUser | MiRemoteUser> {
|
||||||
const usernameLower = username.toLowerCase();
|
const usernameLower = username.toLowerCase();
|
||||||
|
const selfHost = requestHostname ? this.utilityService.getSelfQueryHost(requestHostname) : IsNull();
|
||||||
|
const tenant = requestHostname ? this.utilityService.getSelfTenant(requestHostname) : this.config;
|
||||||
|
|
||||||
if (host == null) {
|
if (host == null) {
|
||||||
this.logger.info(`return local user: ${usernameLower}`);
|
this.logger.info(`return local user: ${usernameLower}`);
|
||||||
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
return await this.usersRepository.findOneBy({ usernameLower, host: selfHost }).then(u => {
|
||||||
if (u == null) {
|
if (u == null) {
|
||||||
throw new Error('user not found');
|
throw new Error('user not found');
|
||||||
} else {
|
} else {
|
||||||
|
@ -56,9 +58,9 @@ export class RemoteUserResolveService {
|
||||||
|
|
||||||
host = this.utilityService.toPuny(host);
|
host = this.utilityService.toPuny(host);
|
||||||
|
|
||||||
if (host === this.utilityService.toPuny(this.config.host)) {
|
if (host === this.utilityService.toPuny(tenant.host)) {
|
||||||
this.logger.info(`return local user: ${usernameLower}`);
|
this.logger.info(`return local user: ${usernameLower}`);
|
||||||
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
return await this.usersRepository.findOneBy({ usernameLower, host: selfHost }).then(u => {
|
||||||
if (u == null) {
|
if (u == null) {
|
||||||
throw new Error('user not found');
|
throw new Error('user not found');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -73,7 +73,7 @@ export class UserBlockingService implements OnModuleInit {
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
blockee,
|
blockee,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
} as MiBlocking;
|
} satisfies MiBlocking;
|
||||||
|
|
||||||
await this.blockingsRepository.insert(blocking);
|
await this.blockingsRepository.insert(blocking);
|
||||||
|
|
||||||
|
@ -178,7 +178,7 @@ export class UserBlockingService implements OnModuleInit {
|
||||||
|
|
||||||
// deliver if remote bloking
|
// deliver if remote bloking
|
||||||
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
|
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking as MiBlocking & { blocker: MiUser, blockee: MiUser }), blocker));
|
||||||
this.queueService.deliver(blocker, content, blockee.inbox, false);
|
this.queueService.deliver(blocker, content, blockee.inbox, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,9 @@ import { URL, domainToASCII } from 'node:url';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import RE2 from 're2';
|
import RE2 from 're2';
|
||||||
import psl from 'psl';
|
import psl from 'psl';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config, Tenant } from '@/config.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MiMeta } from '@/models/Meta.js';
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
|
|
||||||
|
@ -176,4 +177,23 @@ export class UtilityService {
|
||||||
const host = this.extractDbHost(uri);
|
const host = this.extractDbHost(uri);
|
||||||
return this.isFederationAllowedHost(host);
|
return this.isFederationAllowedHost(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getSelfQueryHost(requestHostname: string) {
|
||||||
|
return this.getSelfDbHost(requestHostname) ?? IsNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getSelfDbHost(requestHostname: string): string | null {
|
||||||
|
if (this.config.alts.some(alt => alt.hostname === requestHostname)) {
|
||||||
|
return this.punyHost(requestHostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getSelfTenant(requestHostname: string): Tenant {
|
||||||
|
return this.config.alts.find(alt => alt.hostname === requestHostname) ?? this.config;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
return { local: false, uri: apId };
|
return { local: false, uri: apId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO support alt-local
|
||||||
const [, type, id, ...rest] = uri.pathname.split(separator);
|
const [, type, id, ...rest] = uri.pathname.split(separator);
|
||||||
return {
|
return {
|
||||||
local: true,
|
local: true,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as mfm from '@transfem-org/sfm-js';
|
||||||
import { UnrecoverableError } from 'bullmq';
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser, MiPartialUser } from '@/models/User.js';
|
||||||
import type { IMentionedRemoteUsers, MiNote } from '@/models/Note.js';
|
import type { IMentionedRemoteUsers, MiNote } from '@/models/Note.js';
|
||||||
import type { MiBlocking } from '@/models/Blocking.js';
|
import type { MiBlocking } from '@/models/Blocking.js';
|
||||||
import type { MiRelay } from '@/models/Relay.js';
|
import type { MiRelay } from '@/models/Relay.js';
|
||||||
|
@ -34,7 +34,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { JsonLdService } from './JsonLdService.js';
|
import { JsonLdService } from './JsonLdService.js';
|
||||||
import { ApMfmService } from './ApMfmService.js';
|
import { ApMfmService } from './ApMfmService.js';
|
||||||
import { CONTEXT } from './misc/contexts.js';
|
import { CONTEXT } from './misc/contexts.js';
|
||||||
import { getApId, IOrderedCollection, IOrderedCollectionPage } from './type.js';
|
import { getApId, getNullableApId, IOrderedCollection, IOrderedCollectionPage } from './type.js';
|
||||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -78,27 +78,30 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAccept(object: string | IObject, user: { id: MiUser['id']; host: null }): IAccept {
|
public renderAccept(object: string | IObject, user: MiPartialUser): IAccept {
|
||||||
return {
|
return {
|
||||||
type: 'Accept',
|
type: 'Accept',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.getUserUri(user),
|
||||||
object,
|
object,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAdd(user: MiLocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
|
public renderAdd(user: MiPartialUser, target: string | IObject | undefined, object: string | IObject): IAdd {
|
||||||
return {
|
return {
|
||||||
type: 'Add',
|
type: 'Add',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.getUserUri(user),
|
||||||
target,
|
target,
|
||||||
object,
|
object,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public renderAnnounce(object: string | IObject, note: MiNote & { user: MiUser }): IAnnounce;
|
||||||
|
public renderAnnounce(object: string | IObject, note: MiNote, author: MiPartialUser): IAnnounce;
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderAnnounce(object: string | IObject, note: MiNote): IAnnounce {
|
public renderAnnounce(object: string | IObject, note: MiNote, author?: MiPartialUser): IAnnounce {
|
||||||
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
const user = author ?? note.user as MiPartialUser;
|
||||||
|
const attributedTo = this.userEntityService.getUserUri(user);
|
||||||
|
|
||||||
let to: string[] = [];
|
let to: string[] = [];
|
||||||
let cc: string[] = [];
|
let cc: string[] = [];
|
||||||
|
@ -116,9 +119,13 @@ export class ApRendererService {
|
||||||
throw new UnrecoverableError(`renderAnnounce: cannot render non-public note: ${getApId(object)}`);
|
throw new UnrecoverableError(`renderAnnounce: cannot render non-public note: ${getApId(object)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const id = note.uri
|
||||||
|
? `${note.uri}/activity`
|
||||||
|
: `${this.config.url}/notes/${note.id}/activity`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
id,
|
||||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
actor: attributedTo,
|
||||||
type: 'Announce',
|
type: 'Announce',
|
||||||
published: this.idService.parse(note.id).date.toISOString(),
|
published: this.idService.parse(note.id).date.toISOString(),
|
||||||
to,
|
to,
|
||||||
|
@ -130,27 +137,35 @@ export class ApRendererService {
|
||||||
/**
|
/**
|
||||||
* Renders a block into its ActivityPub representation.
|
* Renders a block into its ActivityPub representation.
|
||||||
*
|
*
|
||||||
* @param block The block to be rendered. The blockee relation must be loaded.
|
* @param block The block to be rendered. The blockee and blocker relation must be loaded.
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderBlock(block: MiBlocking): IBlock {
|
public renderBlock(block: MiBlocking & { blockee: MiUser, blocker: MiUser }): IBlock {
|
||||||
if (block.blockee?.uri == null) {
|
const id = block.blocker.host
|
||||||
throw new Error('renderBlock: missing blockee uri');
|
? `https://${block.blocker.host}/blocks/${block.id}`
|
||||||
}
|
: `${this.config.url}/blocks/${block.id}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'Block',
|
type: 'Block',
|
||||||
id: `${this.config.url}/blocks/${block.id}`,
|
id,
|
||||||
actor: this.userEntityService.genLocalUserUri(block.blockerId),
|
actor: this.userEntityService.getUserUri(block.blocker),
|
||||||
object: block.blockee.uri,
|
object: this.userEntityService.getUserUri(block.blockee),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public renderCreate(object: IObject, note: MiNote & { user: MiUser }): ICreate;
|
||||||
|
public renderCreate(object: IObject, note: MiNote, author: MiPartialUser): ICreate;
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderCreate(object: IObject, note: MiNote): ICreate {
|
public renderCreate(object: IObject, note: MiNote, author?: MiPartialUser): ICreate {
|
||||||
|
const id = note.uri
|
||||||
|
? `${note.uri}/activity`
|
||||||
|
: `${this.config.url}/notes/${note.id}/activity`;
|
||||||
|
|
||||||
|
const user = author ?? note.user as MiPartialUser;
|
||||||
|
|
||||||
const activity: ICreate = {
|
const activity: ICreate = {
|
||||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
id,
|
||||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
actor: this.userEntityService.getUserUri(user),
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
published: this.idService.parse(note.id).date.toISOString(),
|
published: this.idService.parse(note.id).date.toISOString(),
|
||||||
object,
|
object,
|
||||||
|
@ -163,10 +178,10 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderDelete(object: IObject | string, user: { id: MiUser['id']; host: null }): IDelete {
|
public renderDelete(object: IObject | string, user: MiPartialUser): IDelete {
|
||||||
return {
|
return {
|
||||||
type: 'Delete',
|
type: 'Delete',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.getUserUri(user),
|
||||||
object,
|
object,
|
||||||
published: new Date().toISOString(),
|
published: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
@ -187,7 +202,7 @@ export class ApRendererService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderEmoji(emoji: MiEmoji): IApEmoji {
|
public renderEmoji(emoji: MiEmoji): IApEmoji {
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/emojis/${emoji.name}`,
|
id: emoji.uri ?? `${this.config.url}/emojis/${emoji.name}`,
|
||||||
type: 'Emoji',
|
type: 'Emoji',
|
||||||
name: `:${emoji.name}:`,
|
name: `:${emoji.name}:`,
|
||||||
updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString(),
|
updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString(),
|
||||||
|
@ -205,10 +220,10 @@ export class ApRendererService {
|
||||||
|
|
||||||
// to anonymise reporters, the reporting actor must be a system user
|
// to anonymise reporters, the reporting actor must be a system user
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderFlag(user: MiLocalUser, object: IObject | string, content: string): IFlag {
|
public renderFlag(user: MiUser, object: IObject | string, content: string): IFlag {
|
||||||
return {
|
return {
|
||||||
type: 'Flag',
|
type: 'Flag',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.getUserUri(user),
|
||||||
content,
|
content,
|
||||||
// This MUST be an array for Pleroma compatibility: https://activitypub.software/TransFem-org/Sharkey/-/issues/641#note_7301
|
// This MUST be an array for Pleroma compatibility: https://activitypub.software/TransFem-org/Sharkey/-/issues/641#note_7301
|
||||||
object: [object],
|
object: [object],
|
||||||
|
@ -216,11 +231,15 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderFollowRelay(relay: MiRelay, relayActor: MiLocalUser): IFollow {
|
public renderFollowRelay(relay: MiRelay, relayActor: MiUser): IFollow {
|
||||||
|
const id = relayActor.host
|
||||||
|
? `https://${relayActor.host}/activities/follow-relay/${relay.id}`
|
||||||
|
: `${this.config.url}/activities/follow-relay/${relay.id}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/activities/follow-relay/${relay.id}`,
|
id,
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
actor: this.userEntityService.genLocalUserUri(relayActor.id),
|
actor: this.userEntityService.getUserUri(relayActor),
|
||||||
object: 'https://www.w3.org/ns/activitystreams#Public',
|
object: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -269,7 +288,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderIdenticon(user: MiLocalUser): IApImage {
|
public renderIdenticon(user: MiUser): IApImage {
|
||||||
return {
|
return {
|
||||||
type: 'Image',
|
type: 'Image',
|
||||||
url: this.userEntityService.getIdenticonUrl(user),
|
url: this.userEntityService.getIdenticonUrl(user),
|
||||||
|
@ -279,7 +298,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderSystemAvatar(user: MiLocalUser): IApImage {
|
public renderSystemAvatar(user: MiUser): IApImage {
|
||||||
if (this.meta.iconUrl == null) return this.renderIdenticon(user);
|
if (this.meta.iconUrl == null) return this.renderIdenticon(user);
|
||||||
return {
|
return {
|
||||||
type: 'Image',
|
type: 'Image',
|
||||||
|
@ -312,11 +331,15 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey {
|
public renderKey(user: MiUser, key: MiUserKeypair, postfix?: string): IKey {
|
||||||
|
const id = user.uri
|
||||||
|
? `${user.uri}${postfix ?? '/publickey'}`
|
||||||
|
: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`,
|
id,
|
||||||
type: 'Key',
|
type: 'Key',
|
||||||
owner: this.userEntityService.genLocalUserUri(user.id),
|
owner: this.userEntityService.getUserUri(user),
|
||||||
publicKeyPem: createPublicKey(key.publicKey).export({
|
publicKeyPem: createPublicKey(key.publicKey).export({
|
||||||
type: 'spki',
|
type: 'spki',
|
||||||
format: 'pem',
|
format: 'pem',
|
||||||
|
@ -324,11 +347,15 @@ export class ApRendererService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async renderLike(noteReaction: MiNoteReaction & { user: MiUser }, note: { uri: string | null }): Promise<ILike>;
|
||||||
|
public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }, reactUser: MiPartialUser): Promise<ILike>;
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }): Promise<ILike> {
|
public async renderLike(noteReaction: MiNoteReaction, note: { uri: string | null }, reactUser?: MiPartialUser): Promise<ILike> {
|
||||||
const reaction = noteReaction.reaction;
|
const reaction = noteReaction.reaction;
|
||||||
let isMastodon = false;
|
let isMastodon = false;
|
||||||
|
|
||||||
|
const user = reactUser ?? noteReaction.user as MiPartialUser;
|
||||||
|
|
||||||
if (this.meta.defaultLike && reaction.replaceAll(':', '') === this.meta.defaultLike.replaceAll(':', '')) {
|
if (this.meta.defaultLike && reaction.replaceAll(':', '') === this.meta.defaultLike.replaceAll(':', '')) {
|
||||||
const note = await this.notesRepository.findOneBy({ id: noteReaction.noteId });
|
const note = await this.notesRepository.findOneBy({ id: noteReaction.noteId });
|
||||||
|
|
||||||
|
@ -342,10 +369,14 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const id = user.host
|
||||||
|
? `https://${user.host}/likes/${noteReaction.id}`
|
||||||
|
: `${this.config.url}/likes/${noteReaction.id}`;
|
||||||
|
|
||||||
const object: ILike = {
|
const object: ILike = {
|
||||||
type: 'Like',
|
type: 'Like',
|
||||||
id: `${this.config.url}/likes/${noteReaction.id}`,
|
id,
|
||||||
actor: `${this.config.url}/users/${noteReaction.userId}`,
|
actor: user.uri ?? `${this.config.url}/users/${noteReaction.userId}`,
|
||||||
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
|
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
|
||||||
content: isMastodon ? undefined : reaction,
|
content: isMastodon ? undefined : reaction,
|
||||||
_misskey_reaction: isMastodon ? undefined : reaction,
|
_misskey_reaction: isMastodon ? undefined : reaction,
|
||||||
|
@ -365,7 +396,7 @@ export class ApRendererService {
|
||||||
public renderMention(mention: MiPartialLocalUser | MiPartialRemoteUser): IApMention {
|
public renderMention(mention: MiPartialLocalUser | MiPartialRemoteUser): IApMention {
|
||||||
return {
|
return {
|
||||||
type: 'Mention',
|
type: 'Mention',
|
||||||
href: this.userEntityService.getUserUri(mention),
|
href: mention.uri ?? this.userEntityService.getUserUri(mention),
|
||||||
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as MiLocalUser).username}`,
|
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as MiLocalUser).username}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -377,8 +408,13 @@ export class ApRendererService {
|
||||||
): IMove {
|
): IMove {
|
||||||
const actor = this.userEntityService.getUserUri(src);
|
const actor = this.userEntityService.getUserUri(src);
|
||||||
const target = this.userEntityService.getUserUri(dst);
|
const target = this.userEntityService.getUserUri(dst);
|
||||||
|
|
||||||
|
const id = src.host
|
||||||
|
? `https://${src.host}/moves/${src.id}/${dst.id}`
|
||||||
|
: `${this.config.url}/moves/${src.id}/${dst.id}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
id,
|
||||||
actor,
|
actor,
|
||||||
type: 'Move',
|
type: 'Move',
|
||||||
object: actor,
|
object: actor,
|
||||||
|
@ -429,7 +465,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
const attributedTo = this.userEntityService.getUserUri(author);
|
||||||
|
|
||||||
const mentions = note.mentionedRemoteUsers ? (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri) : [];
|
const mentions = note.mentionedRemoteUsers ? (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri) : [];
|
||||||
|
|
||||||
|
@ -524,7 +560,7 @@ export class ApRendererService {
|
||||||
const replies = isPublic ? await this.renderRepliesCollection(note.id) : undefined;
|
const replies = isPublic ? await this.renderRepliesCollection(note.id) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/notes/${note.id}`,
|
id: note.uri ?? `${this.config.url}/notes/${note.id}`,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
attributedTo,
|
attributedTo,
|
||||||
summary: summary ?? undefined,
|
summary: summary ?? undefined,
|
||||||
|
@ -551,8 +587,8 @@ export class ApRendererService {
|
||||||
|
|
||||||
// if you change this, also change `server/api/endpoints/i/update.ts`
|
// if you change this, also change `server/api/endpoints/i/update.ts`
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderPerson(user: MiLocalUser) {
|
public async renderPerson(user: MiUser) {
|
||||||
const id = this.userEntityService.genLocalUserUri(user.id);
|
const id = this.userEntityService.getUserUri(user);
|
||||||
const isSystem = user.username.includes('.');
|
const isSystem = user.username.includes('.');
|
||||||
|
|
||||||
const [avatar, banner, background, profile] = await Promise.all([
|
const [avatar, banner, background, profile] = await Promise.all([
|
||||||
|
@ -583,14 +619,14 @@ export class ApRendererService {
|
||||||
const person: any = {
|
const person: any = {
|
||||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||||
id,
|
id,
|
||||||
inbox: `${id}/inbox`,
|
inbox: user.inbox ?? user.uri ? `${user.uri}/inbox` : `${id}/inbox`,
|
||||||
outbox: `${id}/outbox`,
|
outbox: user.uri ? `${user.uri}/followers` : `${id}/outbox`,
|
||||||
followers: `${id}/followers`,
|
followers: user.followersUri ?? user.uri ? `${user.uri}/followers` : `${id}/followers`,
|
||||||
following: `${id}/following`,
|
following: user.uri ? `${user.uri}/following` : `${id}/following`,
|
||||||
featured: `${id}/collections/featured`,
|
featured: user.featured ?? user.uri ? `${user.uri}/features` : `${id}/collections/featured`,
|
||||||
sharedInbox: `${this.config.url}/inbox`,
|
sharedInbox: user.sharedInbox ?? user.host ? `${user.host}/inbox` : `${this.config.url}/inbox`,
|
||||||
endpoints: { sharedInbox: `${this.config.url}/inbox` },
|
endpoints: { sharedInbox: user.sharedInbox ?? user.host ? `${user.host}/inbox` : `${this.config.url}/inbox` },
|
||||||
url: `${this.config.url}/@${user.username}`,
|
url: profile.url ?? user.uri ?? `${this.config.url}/@${user.username}`,
|
||||||
preferredUsername: user.username,
|
preferredUsername: user.username,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
||||||
|
@ -639,21 +675,22 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderPersonRedacted(user: MiLocalUser) {
|
public async renderPersonRedacted(user: MiUser) {
|
||||||
const id = this.userEntityService.genLocalUserUri(user.id);
|
const id = this.userEntityService.getUserUri(user);
|
||||||
const isSystem = user.username.includes('.');
|
const isSystem = user.username.includes('.');
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Basic federation metadata
|
// Basic federation metadata
|
||||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||||
id,
|
id,
|
||||||
inbox: `${id}/inbox`,
|
inbox: user.inbox ?? user.uri ? `${user.uri}/inbox` : `${id}/inbox`,
|
||||||
outbox: `${id}/outbox`,
|
outbox: user.uri ? `${user.uri}/followers` : `${id}/outbox`,
|
||||||
sharedInbox: `${this.config.url}/inbox`,
|
sharedInbox: user.sharedInbox ?? user.host ? `${user.host}/inbox` : `${this.config.url}/inbox`,
|
||||||
endpoints: { sharedInbox: `${this.config.url}/inbox` },
|
endpoints: { sharedInbox: user.sharedInbox ?? user.host ? `${user.host}/inbox` : `${this.config.url}/inbox` },
|
||||||
url: `${this.config.url}/@${user.username}`,
|
url: profile.url ?? user.uri ?? `${this.config.url}/@${user.username}`,
|
||||||
preferredUsername: user.username,
|
preferredUsername: user.username,
|
||||||
publicKey: this.renderKey(user, keypair, '#main-key'),
|
publicKey: this.renderKey(user, keypair, '#main-key'),
|
||||||
|
|
||||||
|
@ -671,11 +708,11 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderQuestion(user: { id: MiUser['id'] }, note: MiNote, poll: MiPoll): IQuestion {
|
public renderQuestion(user: MiPartialUser, note: MiNote, poll: MiPoll): IQuestion {
|
||||||
return {
|
return {
|
||||||
type: 'Question',
|
type: 'Question',
|
||||||
id: `${this.config.url}/questions/${note.id}`,
|
id: note.uri ?? `${this.config.url}/questions/${note.id}`,
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.getUserUri(user),
|
||||||
content: note.text ?? '',
|
content: note.text ?? '',
|
||||||
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
||||||
name: text,
|
name: text,
|
||||||
|
@ -689,19 +726,19 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderReject(object: string | IObject, user: { id: MiUser['id'] }): IReject {
|
public renderReject(object: string | IObject, user: MiPartialUser): IReject {
|
||||||
return {
|
return {
|
||||||
type: 'Reject',
|
type: 'Reject',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.getUserUri(user),
|
||||||
object,
|
object,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderRemove(user: { id: MiUser['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
|
public renderRemove(user: MiPartialUser, target: string | IObject | undefined, object: string | IObject): IRemove {
|
||||||
return {
|
return {
|
||||||
type: 'Remove',
|
type: 'Remove',
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.getUserUri(user),
|
||||||
target,
|
target,
|
||||||
object,
|
object,
|
||||||
};
|
};
|
||||||
|
@ -716,23 +753,30 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderUndo(object: string | IObject, user: { id: MiUser['id'] }): IUndo {
|
public renderUndo(object: string | IObject, user: MiPartialUser): IUndo {
|
||||||
const id = typeof object !== 'string' && typeof object.id === 'string' && this.utilityService.isUriLocal(object.id) ? `${object.id}/undo` : undefined;
|
const objectId = getNullableApId(object);
|
||||||
|
const id = objectId
|
||||||
|
? `${objectId}/undo`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'Undo',
|
type: 'Undo',
|
||||||
...(id ? { id } : {}),
|
id,
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.getUserUri(user),
|
||||||
object,
|
object,
|
||||||
published: new Date().toISOString(),
|
published: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderUpdate(object: string | IObject, user: { id: MiUser['id'] }): IUpdate {
|
public renderUpdate(object: string | IObject, user: MiPartialUser): IUpdate {
|
||||||
|
const id = user.uri
|
||||||
|
? `${user.uri}#updates/${new Date().getTime()}`
|
||||||
|
: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
id,
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.getUserUri(user),
|
||||||
type: 'Update',
|
type: 'Update',
|
||||||
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
object,
|
object,
|
||||||
|
@ -783,7 +827,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
const attributedTo = this.userEntityService.getUserUri(author);
|
||||||
|
|
||||||
const mentions = note.mentionedRemoteUsers ? (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri) : [];
|
const mentions = note.mentionedRemoteUsers ? (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri) : [];
|
||||||
|
|
||||||
|
@ -871,7 +915,7 @@ export class ApRendererService {
|
||||||
} as const : {};
|
} as const : {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/notes/${note.id}`,
|
id: note.uri ?? `${this.config.url}/notes/${note.id}`,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
attributedTo,
|
attributedTo,
|
||||||
summary: summary ?? undefined,
|
summary: summary ?? undefined,
|
||||||
|
@ -897,17 +941,24 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate {
|
public renderVote(user: MiPartialUser, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate {
|
||||||
|
const voteId = user.uri
|
||||||
|
? `${user.uri}#votes/${vote.id}`
|
||||||
|
: `${this.config.url}/users/${user.id}#votes/${vote.id}`;
|
||||||
|
|
||||||
|
const activityId = `${voteId}/activity`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`,
|
id: activityId,
|
||||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
actor: this.userEntityService.getUserUri(user),
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
to: [pollOwner.uri],
|
to: [pollOwner.uri],
|
||||||
published: new Date().toISOString(),
|
published: new Date().toISOString(),
|
||||||
object: {
|
object: {
|
||||||
id: `${this.config.url}/users/${user.id}#votes/${vote.id}`,
|
id: voteId,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
attributedTo: this.userEntityService.genLocalUserUri(user.id),
|
attributedTo: this.userEntityService.getUserUri(user),
|
||||||
|
// TODO what about local poll?
|
||||||
to: [pollOwner.uri],
|
to: [pollOwner.uri],
|
||||||
inReplyTo: note.uri,
|
inReplyTo: note.uri,
|
||||||
name: poll.choices[vote.choice],
|
name: poll.choices[vote.choice],
|
||||||
|
@ -925,7 +976,7 @@ export class ApRendererService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> {
|
public async attachLdSignature(activity: any, user: MiPartialUser): Promise<IActivity> {
|
||||||
// Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity.
|
// Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity.
|
||||||
// When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances.
|
// When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances.
|
||||||
// This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests.
|
// This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests.
|
||||||
|
@ -933,11 +984,15 @@ export class ApRendererService {
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keyId = user.uri
|
||||||
|
? `${user.uri}#main-key`
|
||||||
|
: `${this.config.url}/users/${user.id}#main-key`;
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const jsonLd = this.jsonLdService.use();
|
const jsonLd = this.jsonLdService.use();
|
||||||
jsonLd.debug = false;
|
jsonLd.debug = false;
|
||||||
activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
|
activity = await jsonLd.signRsaSignature2017(activity, keypair.privateKey, keyId);
|
||||||
|
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull, Not } from 'typeorm';
|
import { IsNull, Not } from 'typeorm';
|
||||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiLocalUser, MiPartialUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta, SkApFetchLog } from '@/models/_.js';
|
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta, SkApFetchLog, MiNoteReaction } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -238,7 +238,7 @@ export class Resolver {
|
||||||
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
|
||||||
if (parsed.rest === 'activity') {
|
if (parsed.rest === 'activity') {
|
||||||
// this refers to the create activity and not the note itself
|
// this refers to the create activity and not the note itself
|
||||||
return this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author), note));
|
return this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author), note, author));
|
||||||
} else {
|
} else {
|
||||||
return this.apRendererService.renderNote(note, author);
|
return this.apRendererService.renderNote(note, author);
|
||||||
}
|
}
|
||||||
|
@ -249,13 +249,13 @@ export class Resolver {
|
||||||
case 'questions':
|
case 'questions':
|
||||||
// Polls are indexed by the note they are attached to.
|
// Polls are indexed by the note they are attached to.
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this.notesRepository.findOneByOrFail({ id: parsed.id }),
|
this.notesRepository.findOneOrFail({ where: { id: parsed.id }, relations: ['user'] }),
|
||||||
this.pollsRepository.findOneByOrFail({ noteId: parsed.id }),
|
this.pollsRepository.findOneByOrFail({ noteId: parsed.id }),
|
||||||
])
|
])
|
||||||
.then(([note, poll]) => this.apRendererService.renderQuestion({ id: note.userId }, note, poll)) as Promise<IObjectWithId>;
|
.then(([note, poll]) => this.apRendererService.renderQuestion(note.user as MiPartialUser, note, poll)) as Promise<IObjectWithId>;
|
||||||
case 'likes':
|
case 'likes':
|
||||||
return this.noteReactionsRepository.findOneByOrFail({ id: parsed.id }).then(async reaction =>
|
return this.noteReactionsRepository.findOneOrFail({ where: { id: parsed.id }, relations: ['user'] }).then(async reaction =>
|
||||||
this.apRendererService.addContext(await this.apRendererService.renderLike(reaction, { uri: null })));
|
this.apRendererService.addContext(await this.apRendererService.renderLike(reaction as MiNoteReaction & { user: MiUser }, { uri: null })));
|
||||||
case 'follows':
|
case 'follows':
|
||||||
return this.followRequestsRepository.findOneBy({ id: parsed.id })
|
return this.followRequestsRepository.findOneBy({ id: parsed.id })
|
||||||
.then(async followRequest => {
|
.then(async followRequest => {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||||
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MetaEntityService {
|
export class MetaEntityService {
|
||||||
|
@ -28,10 +29,11 @@ export class MetaEntityService {
|
||||||
private adsRepository: AdsRepository,
|
private adsRepository: AdsRepository,
|
||||||
|
|
||||||
private systemAccountService: SystemAccountService,
|
private systemAccountService: SystemAccountService,
|
||||||
|
private readonly utilityService: UtilityService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async pack(meta?: MiMeta): Promise<Packed<'MetaLite'>> {
|
public async pack(meta?: MiMeta, requestHostname?: string): Promise<Packed<'MetaLite'>> {
|
||||||
let instance = meta;
|
let instance = meta;
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
|
@ -64,6 +66,8 @@ export class MetaEntityService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tenant = requestHostname ? this.utilityService.getSelfTenant(requestHostname) : this.config;
|
||||||
|
|
||||||
const packed: Packed<'MetaLite'> = {
|
const packed: Packed<'MetaLite'> = {
|
||||||
maintainerName: instance.maintainerName,
|
maintainerName: instance.maintainerName,
|
||||||
maintainerEmail: instance.maintainerEmail,
|
maintainerEmail: instance.maintainerEmail,
|
||||||
|
@ -73,7 +77,7 @@ export class MetaEntityService {
|
||||||
|
|
||||||
name: instance.name,
|
name: instance.name,
|
||||||
shortName: instance.shortName,
|
shortName: instance.shortName,
|
||||||
uri: this.config.url,
|
uri: tenant.url,
|
||||||
description: instance.description,
|
description: instance.description,
|
||||||
langs: instance.langs,
|
langs: instance.langs,
|
||||||
tosUrl: instance.termsOfServiceUrl,
|
tosUrl: instance.termsOfServiceUrl,
|
||||||
|
@ -151,14 +155,14 @@ export class MetaEntityService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async packDetailed(meta?: MiMeta): Promise<Packed<'MetaDetailed'>> {
|
public async packDetailed(meta?: MiMeta, requestHostname?: string): Promise<Packed<'MetaDetailed'>> {
|
||||||
let instance = meta;
|
let instance = meta;
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
instance = this.meta;
|
instance = this.meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packed = await this.pack(instance);
|
const packed = await this.pack(instance, requestHostname);
|
||||||
|
|
||||||
const proxyAccount = await this.systemAccountService.fetch('proxy');
|
const proxyAccount = await this.systemAccountService.fetch('proxy');
|
||||||
|
|
||||||
|
@ -181,6 +185,10 @@ export class MetaEntityService {
|
||||||
miauth: true,
|
miauth: true,
|
||||||
},
|
},
|
||||||
allowUnsignedFetch: instance.allowUnsignedFetch,
|
allowUnsignedFetch: instance.allowUnsignedFetch,
|
||||||
|
localHosts: [
|
||||||
|
null,
|
||||||
|
...this.config.alts.map(alt => this.utilityService.toPuny(alt.host)),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return packDetailed;
|
return packDetailed;
|
||||||
|
|
|
@ -14,7 +14,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { Promiseable } from '@/misc/prelude/await-all.js';
|
import type { Promiseable } from '@/misc/prelude/await-all.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||||
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiPartialUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import {
|
import {
|
||||||
birthdaySchema,
|
birthdaySchema,
|
||||||
descriptionSchema,
|
descriptionSchema,
|
||||||
|
@ -460,9 +460,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getUserUri(user: MiLocalUser | MiPartialLocalUser | MiRemoteUser | MiPartialRemoteUser): string {
|
public getUserUri(user: MiLocalUser | MiPartialLocalUser | MiRemoteUser | MiPartialRemoteUser | MiPartialUser | MiUser): string {
|
||||||
return this.isRemoteUser(user)
|
return user.uri ?? this.genLocalUserUri(user.id);
|
||||||
? user.uri : this.genLocalUserUri(user.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -407,6 +407,12 @@ export type MiPartialRemoteUser = Partial<MiUser> & {
|
||||||
uri: string;
|
uri: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface MiPartialUser extends Partial<MiUser> {
|
||||||
|
id: MiUser['id'];
|
||||||
|
host: MiUser['host'];
|
||||||
|
uri?: MiUser['uri'];
|
||||||
|
}
|
||||||
|
|
||||||
export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
|
export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
|
||||||
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||||
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||||
|
|
|
@ -436,6 +436,14 @@ export const packedMetaDetailedOnlySchema = {
|
||||||
enum: instanceUnsignedFetchOptions,
|
enum: instanceUnsignedFetchOptions,
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
localHosts: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import accepts from 'accepts';
|
||||||
import vary from 'vary';
|
import vary from 'vary';
|
||||||
import secureJson from 'secure-json-parse';
|
import secureJson from 'secure-json-parse';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
|
import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository, MiMeta, MiNoteReaction } from '@/models/_.js';
|
||||||
import * as url from '@/misc/prelude/url.js';
|
import * as url from '@/misc/prelude/url.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
|
@ -114,10 +114,10 @@ export class ActivityPubServerService {
|
||||||
private async packActivity(note: MiNote, author: MiUser): Promise<ICreate | IAnnounce> {
|
private async packActivity(note: MiNote, author: MiUser): Promise<ICreate | IAnnounce> {
|
||||||
if (isRenote(note) && !isQuote(note)) {
|
if (isRenote(note) && !isQuote(note)) {
|
||||||
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
||||||
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
|
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note, author);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author, false), note);
|
return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author, false), note, author);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,7 +140,7 @@ export class ActivityPubServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth fetch disabled => accept
|
// Auth fetch disabled => accept
|
||||||
const allowUnsignedFetch = await this.getUnsignedFetchAllowance(userId);
|
const allowUnsignedFetch = await this.getUnsignedFetchAllowance(userId, request.hostname);
|
||||||
if (allowUnsignedFetch === 'always') {
|
if (allowUnsignedFetch === 'always') {
|
||||||
return { reject: false, redact: false };
|
return { reject: false, redact: false };
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@ export class ActivityPubServerService {
|
||||||
headers: ['(request-target)', 'host', 'date'],
|
headers: ['(request-target)', 'host', 'date'],
|
||||||
authorizationHeaderName: 'signature',
|
authorizationHeaderName: 'signature',
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch {
|
||||||
// not signed, or malformed signature: refuse
|
// not signed, or malformed signature: refuse
|
||||||
return `${request.id} ${request.url} not signed, or malformed signature: refuse`;
|
return `${request.id} ${request.url} not signed, or malformed signature: refuse`;
|
||||||
}
|
}
|
||||||
|
@ -196,9 +196,12 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const logPrefix = `${request.id} ${request.url} (by ${request.headers['user-agent']}) claims to be from ${keyHost}:`;
|
const logPrefix = `${request.id} ${request.url} (by ${request.headers['user-agent']}) claims to be from ${keyHost}:`;
|
||||||
|
|
||||||
if (signature.params.headers.indexOf('host') === -1 || request.headers.host !== this.config.host) {
|
if (signature.params.headers.indexOf('host') === -1) {
|
||||||
// no destination host, or not us: refuse
|
return `${logPrefix} no destination host: refuse`;
|
||||||
return `${logPrefix} no destination host, or not us: refuse`;
|
}
|
||||||
|
|
||||||
|
if (request.hostname !== this.config.host && !this.config.alts.some(alt => alt.hostname === request.hostname)) {
|
||||||
|
return `${logPrefix} destination not us: refuse`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.utilityService.isFederationAllowedHost(keyHost)) {
|
if (!this.utilityService.isFederationAllowedHost(keyHost)) {
|
||||||
|
@ -291,9 +294,14 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signature.params.headers.indexOf('host') === -1
|
if (signature.params.headers.indexOf('host') === -1) {
|
||||||
|| request.headers.host !== this.config.host) {
|
// no destination host: refuse
|
||||||
// Host not specified or not match.
|
reply.code(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.hostname !== this.config.host && !this.config.alts.some(alt => alt.hostname === request.hostname)) {
|
||||||
|
// destination not us: refuse
|
||||||
reply.code(401);
|
reply.code(401);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -373,7 +381,7 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
id: userId,
|
id: userId,
|
||||||
host: IsNull(),
|
host: this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
@ -470,7 +478,7 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
id: userId,
|
id: userId,
|
||||||
host: IsNull(),
|
host: this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
@ -556,7 +564,7 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
id: userId,
|
id: userId,
|
||||||
host: IsNull(),
|
host: this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
@ -626,7 +634,7 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
id: userId,
|
id: userId,
|
||||||
host: IsNull(),
|
host: this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
@ -723,7 +731,7 @@ export class ActivityPubServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// リモートだったらリダイレクト
|
// リモートだったらリダイレクト
|
||||||
if (user.host != null) {
|
if (user.host !== this.utilityService.getSelfDbHost(request.hostname)) {
|
||||||
if (user.uri == null || this.utilityService.isSelfHost(user.host)) {
|
if (user.uri == null || this.utilityService.isSelfHost(user.host)) {
|
||||||
reply.code(500);
|
reply.code(500);
|
||||||
return;
|
return;
|
||||||
|
@ -735,7 +743,7 @@ export class ActivityPubServerService {
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
|
|
||||||
const person = redact
|
const person = redact
|
||||||
? await this.apRendererService.renderPersonRedacted(user as MiLocalUser)
|
? await this.apRendererService.renderPersonRedacted(user)
|
||||||
: await this.apRendererService.renderPerson(user as MiLocalUser);
|
: await this.apRendererService.renderPerson(user as MiLocalUser);
|
||||||
return this.apRendererService.addContext(person);
|
return this.apRendererService.addContext(person);
|
||||||
}
|
}
|
||||||
|
@ -829,7 +837,7 @@ export class ActivityPubServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// リモートだったらリダイレクト
|
// リモートだったらリダイレクト
|
||||||
if (note.userHost != null) {
|
if (note.userHost !== this.utilityService.getSelfDbHost(request.hostname)) {
|
||||||
if (note.uri == null || this.utilityService.isSelfHost(note.userHost)) {
|
if (note.uri == null || this.utilityService.isSelfHost(note.userHost)) {
|
||||||
reply.code(500);
|
reply.code(500);
|
||||||
return;
|
return;
|
||||||
|
@ -855,7 +863,7 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const note = await this.notesRepository.findOneBy({
|
const note = await this.notesRepository.findOneBy({
|
||||||
id: request.params.note,
|
id: request.params.note,
|
||||||
userHost: IsNull(),
|
userHost: this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
visibility: In(['public', 'home']),
|
visibility: In(['public', 'home']),
|
||||||
localOnly: false,
|
localOnly: false,
|
||||||
});
|
});
|
||||||
|
@ -887,7 +895,7 @@ export class ActivityPubServerService {
|
||||||
.createQueryBuilder('note')
|
.createQueryBuilder('note')
|
||||||
.andWhere({
|
.andWhere({
|
||||||
id: request.params.note,
|
id: request.params.note,
|
||||||
userHost: IsNull(),
|
userHost: this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
visibility: In(['public', 'home']),
|
visibility: In(['public', 'home']),
|
||||||
localOnly: false,
|
localOnly: false,
|
||||||
})
|
})
|
||||||
|
@ -955,7 +963,7 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
id: userId,
|
id: userId,
|
||||||
host: IsNull(),
|
host: this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
@ -1007,7 +1015,7 @@ export class ActivityPubServerService {
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
usernameLower: acct.username.toLowerCase(),
|
usernameLower: acct.username.toLowerCase(),
|
||||||
host: acct.host ?? IsNull(),
|
host: acct.host ?? this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1029,7 +1037,7 @@ export class ActivityPubServerService {
|
||||||
if (reject) return;
|
if (reject) return;
|
||||||
|
|
||||||
const emoji = await this.emojisRepository.findOneBy({
|
const emoji = await this.emojisRepository.findOneBy({
|
||||||
host: IsNull(),
|
host: this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
name: request.params.emoji,
|
name: request.params.emoji,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1039,7 +1047,7 @@ export class ActivityPubServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setResponseType(request, reply);
|
this.setResponseType(request, reply);
|
||||||
return (this.apRendererService.addContext(await this.apRendererService.renderEmoji(emoji)));
|
return (this.apRendererService.addContext(this.apRendererService.renderEmoji(emoji)));
|
||||||
});
|
});
|
||||||
|
|
||||||
// like
|
// like
|
||||||
|
@ -1049,7 +1057,7 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reaction = await this.noteReactionsRepository.findOneBy({ id: request.params.like });
|
const reaction = await this.noteReactionsRepository.findOne({ where: { id: request.params.like }, relations: ['user'] }) as MiNoteReaction & { user: MiUser } | null;
|
||||||
|
|
||||||
const { reject } = await this.checkAuthorizedFetch(request, reply, reaction?.userId);
|
const { reject } = await this.checkAuthorizedFetch(request, reply, reaction?.userId);
|
||||||
if (reject) return;
|
if (reject) return;
|
||||||
|
@ -1059,6 +1067,11 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reaction.user.host !== this.utilityService.getSelfDbHost(request.hostname)) {
|
||||||
|
reply.code(404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const note = await this.notesRepository.findOneBy({ id: reaction.noteId });
|
const note = await this.notesRepository.findOneBy({ id: reaction.noteId });
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
|
@ -1083,14 +1096,15 @@ export class ActivityPubServerService {
|
||||||
// This may be used before the follow is completed, so we do not
|
// This may be used before the follow is completed, so we do not
|
||||||
// check if the following exists.
|
// check if the following exists.
|
||||||
|
|
||||||
|
const queryHost = this.utilityService.getSelfQueryHost(request.hostname);
|
||||||
const [follower, followee] = await Promise.all([
|
const [follower, followee] = await Promise.all([
|
||||||
this.usersRepository.findOneBy({
|
this.usersRepository.findOneBy({
|
||||||
id: request.params.follower,
|
id: request.params.follower,
|
||||||
host: IsNull(),
|
host: queryHost,
|
||||||
}),
|
}),
|
||||||
this.usersRepository.findOneBy({
|
this.usersRepository.findOneBy({
|
||||||
id: request.params.followee,
|
id: request.params.followee,
|
||||||
host: Not(IsNull()),
|
host: Not(queryHost),
|
||||||
}),
|
}),
|
||||||
]) as [MiLocalUser | MiRemoteUser | null, MiLocalUser | MiRemoteUser | null];
|
]) as [MiLocalUser | MiRemoteUser | null, MiLocalUser | MiRemoteUser | null];
|
||||||
|
|
||||||
|
@ -1125,14 +1139,15 @@ export class ActivityPubServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queryHost = this.utilityService.getSelfQueryHost(request.hostname);
|
||||||
const [follower, followee] = await Promise.all([
|
const [follower, followee] = await Promise.all([
|
||||||
this.usersRepository.findOneBy({
|
this.usersRepository.findOneBy({
|
||||||
id: followRequest.followerId,
|
id: followRequest.followerId,
|
||||||
host: IsNull(),
|
host: queryHost,
|
||||||
}),
|
}),
|
||||||
this.usersRepository.findOneBy({
|
this.usersRepository.findOneBy({
|
||||||
id: followRequest.followeeId,
|
id: followRequest.followeeId,
|
||||||
host: Not(IsNull()),
|
host: Not(queryHost),
|
||||||
}),
|
}),
|
||||||
]) as [MiLocalUser | MiRemoteUser | null, MiLocalUser | MiRemoteUser | null];
|
]) as [MiLocalUser | MiRemoteUser | null, MiLocalUser | MiRemoteUser | null];
|
||||||
|
|
||||||
|
@ -1148,11 +1163,15 @@ export class ActivityPubServerService {
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUnsignedFetchAllowance(userId: string | undefined) {
|
private async getUnsignedFetchAllowance(userId: string | undefined, hostname: string) {
|
||||||
const user = userId ? await this.cacheService.findLocalUserById(userId) : null;
|
let user = userId ? await this.cacheService.findUserById(userId) : null;
|
||||||
|
|
||||||
// User system value if there is no user, or if user has deferred the choice.
|
if (user && user.host !== this.utilityService.getSelfDbHost(hostname)) {
|
||||||
if (!user?.allowUnsignedFetch || user.allowUnsignedFetch === 'staff') {
|
user = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use system value if there is no user, or if user has deferred the choice.
|
||||||
|
if (!user || user.allowUnsignedFetch === 'staff') {
|
||||||
return this.meta.allowUnsignedFetch;
|
return this.meta.allowUnsignedFetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import type { MiUser } from '@/models/User.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
import { NodeinfoServerService } from './NodeinfoServerService.js';
|
||||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||||
import type { FindOptionsWhere } from 'typeorm';
|
import type { FindOptionsWhere } from 'typeorm';
|
||||||
|
@ -35,6 +36,7 @@ export class WellKnownServerService {
|
||||||
private nodeinfoServerService: NodeinfoServerService,
|
private nodeinfoServerService: NodeinfoServerService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private oauth2ProviderService: OAuth2ProviderService,
|
private oauth2ProviderService: OAuth2ProviderService,
|
||||||
|
private readonly utilityService: UtilityService,
|
||||||
) {
|
) {
|
||||||
//this.createServer = this.createServer.bind(this);
|
//this.createServer = this.createServer.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -74,11 +76,13 @@ export class WellKnownServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tenant = this.utilityService.getSelfTenant(request.hostname);
|
||||||
|
|
||||||
reply.header('Content-Type', xrd);
|
reply.header('Content-Type', xrd);
|
||||||
return XRD({ element: 'Link', attributes: {
|
return XRD({ element: 'Link', attributes: {
|
||||||
rel: 'lrdd',
|
rel: 'lrdd',
|
||||||
type: xrd,
|
type: xrd,
|
||||||
template: `${this.config.url}${webFingerPath}?resource={uri}`,
|
template: `${tenant.url}${webFingerPath}?resource={uri}`,
|
||||||
} });
|
} });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -88,12 +92,14 @@ export class WellKnownServerService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tenant = this.utilityService.getSelfTenant(request.hostname);
|
||||||
|
|
||||||
reply.header('Content-Type', 'application/json');
|
reply.header('Content-Type', 'application/json');
|
||||||
return {
|
return {
|
||||||
links: [{
|
links: [{
|
||||||
rel: 'lrdd',
|
rel: 'lrdd',
|
||||||
type: jrd,
|
type: jrd,
|
||||||
template: `${this.config.url}${webFingerPath}?resource={uri}`,
|
template: `${tenant.url}${webFingerPath}?resource={uri}`,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -122,24 +128,27 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tenant = this.utilityService.getSelfTenant(request.hostname);
|
||||||
|
const queryHost = this.utilityService.getSelfQueryHost(request.hostname);
|
||||||
|
|
||||||
const fromId = (id: MiUser['id']): FindOptionsWhere<MiUser> => ({
|
const fromId = (id: MiUser['id']): FindOptionsWhere<MiUser> => ({
|
||||||
id,
|
id,
|
||||||
host: IsNull(),
|
host: queryHost,
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateQuery = (resource: string): FindOptionsWhere<MiUser> | number =>
|
const generateQuery = (resource: string): FindOptionsWhere<MiUser> | number =>
|
||||||
resource.startsWith(`${this.config.url.toLowerCase()}/users/`) ?
|
resource.startsWith(`${tenant.url.toLowerCase()}/users/`) ?
|
||||||
fromId(resource.split('/').pop()!) :
|
fromId(resource.split('/').pop()!) :
|
||||||
fromAcct(Acct.parse(
|
fromAcct(Acct.parse(
|
||||||
resource.startsWith(`${this.config.url.toLowerCase()}/@`) ? resource.split('/').pop()! :
|
resource.startsWith(`${tenant.url.toLowerCase()}/@`) ? resource.split('/').pop()! :
|
||||||
resource.startsWith('acct:') ? resource.slice('acct:'.length) :
|
resource.startsWith('acct:') ? resource.slice('acct:'.length) :
|
||||||
resource));
|
resource));
|
||||||
|
|
||||||
const fromAcct = (acct: Acct.Acct): FindOptionsWhere<MiUser> | number =>
|
const fromAcct = (acct: Acct.Acct): FindOptionsWhere<MiUser> | number =>
|
||||||
!acct.host || acct.host === this.config.host.toLowerCase() ? {
|
!acct.host || acct.host === tenant.host.toLowerCase() ? {
|
||||||
usernameLower: acct.username.toLowerCase(),
|
usernameLower: acct.username.toLowerCase(),
|
||||||
host: IsNull(),
|
host: queryHost,
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
} : 422;
|
} : 422;
|
||||||
|
|
||||||
|
@ -162,12 +171,12 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subject = `acct:${user.username}@${this.config.host}`;
|
const subject = `acct:${user.username}@${tenant.host}`;
|
||||||
const profileLink = `${this.config.url}/@${user.username}`;
|
const profileLink = `${tenant.url}/@${user.username}`;
|
||||||
const self = {
|
const self = {
|
||||||
rel: 'self',
|
rel: 'self',
|
||||||
type: 'application/activity+json',
|
type: 'application/activity+json',
|
||||||
href: this.userEntityService.genLocalUserUri(user.id),
|
href: this.userEntityService.getUserUri(user),
|
||||||
};
|
};
|
||||||
const profilePage = {
|
const profilePage = {
|
||||||
rel: 'http://webfinger.net/rel/profile-page',
|
rel: 'http://webfinger.net/rel/profile-page',
|
||||||
|
@ -176,7 +185,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
|
||||||
};
|
};
|
||||||
const subscribe = {
|
const subscribe = {
|
||||||
rel: 'http://ostatus.org/schema/1.0/subscribe',
|
rel: 'http://ostatus.org/schema/1.0/subscribe',
|
||||||
template: `${this.config.url}/authorize-follow?acct={uri}`,
|
template: `${tenant.url}/authorize-follow?acct={uri}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
vary(reply.raw, 'Accept');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
|
@ -23,6 +23,7 @@ import { type RolePolicies, RoleService } from '@/core/RoleService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
|
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
|
||||||
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
|
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
|
||||||
|
import type { PublicExecutor } from '@/server/api/endpoint-base.js';
|
||||||
import { ApiError } from './error.js';
|
import { ApiError } from './error.js';
|
||||||
import { ApiLoggerService } from './ApiLoggerService.js';
|
import { ApiLoggerService } from './ApiLoggerService.js';
|
||||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||||
|
@ -145,7 +146,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public handleRequest(
|
public handleRequest(
|
||||||
endpoint: IEndpoint & { exec: any },
|
endpoint: IEndpoint & { exec: PublicExecutor },
|
||||||
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
): void {
|
): void {
|
||||||
|
@ -181,7 +182,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async handleMultipartRequest(
|
public async handleMultipartRequest(
|
||||||
endpoint: IEndpoint & { exec: any },
|
endpoint: IEndpoint & { exec: PublicExecutor },
|
||||||
request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
|
request: FastifyRequest<{ Body: Record<string, unknown>, Querystring: Record<string, unknown> }>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -274,9 +275,9 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async call(
|
private async call(
|
||||||
ep: IEndpoint & { exec: any },
|
ep: IEndpoint & { exec: PublicExecutor },
|
||||||
user: MiLocalUser | null | undefined,
|
user: MiLocalUser | null,
|
||||||
token: MiAccessToken | null | undefined,
|
token: MiAccessToken | null,
|
||||||
data: any,
|
data: any,
|
||||||
multipartFile: MultipartFile | null,
|
multipartFile: MultipartFile | null,
|
||||||
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
|
||||||
|
@ -430,17 +431,23 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
cleanup = result.cleanup;
|
cleanup = result.cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
ip: request.ip,
|
||||||
|
headers: request.headers,
|
||||||
|
hostname: request.hostname,
|
||||||
|
};
|
||||||
|
|
||||||
// API invoking
|
// API invoking
|
||||||
if (this.config.sentryForBackend) {
|
if (this.config.sentryForBackend) {
|
||||||
return await Sentry.startSpan({
|
return await Sentry.startSpan({
|
||||||
name: 'API: ' + ep.name,
|
name: 'API: ' + ep.name,
|
||||||
}, () => {
|
}, () => {
|
||||||
return ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
|
return ep.exec(data, user, token, attachmentFile, requestData)
|
||||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
|
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
|
||||||
.finally(() => cleanup());
|
.finally(() => cleanup());
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return await ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
|
return await ep.exec(data, user, token, attachmentFile, requestData)
|
||||||
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
|
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
|
||||||
.finally(() => cleanup());
|
.finally(() => cleanup());
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { SignupApiService } from './SignupApiService.js';
|
||||||
import { SigninApiService } from './SigninApiService.js';
|
import { SigninApiService } from './SigninApiService.js';
|
||||||
import { SigninWithPasskeyApiService } from './SigninWithPasskeyApiService.js';
|
import { SigninWithPasskeyApiService } from './SigninWithPasskeyApiService.js';
|
||||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||||
|
import type { PublicExecutor } from './endpoint-base.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApiServerService {
|
export class ApiServerService {
|
||||||
|
@ -67,7 +68,7 @@ export class ApiServerService {
|
||||||
name: endpoint.name,
|
name: endpoint.name,
|
||||||
meta: endpoint.meta,
|
meta: endpoint.meta,
|
||||||
params: endpoint.params,
|
params: endpoint.params,
|
||||||
exec: this.moduleRef.get('ep:' + endpoint.name, { strict: false }).exec,
|
exec: this.moduleRef.get('ep:' + endpoint.name, { strict: false }).exec as PublicExecutor,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (endpoint.meta.requireFile) {
|
if (endpoint.meta.requireFile) {
|
||||||
|
|
|
@ -26,18 +26,40 @@ export type AttachmentFile = {
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Request {
|
||||||
|
ip: string | null;
|
||||||
|
headers: {
|
||||||
|
[Name in string]?: string | string[]
|
||||||
|
};
|
||||||
|
hostname: string;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: paramsの型をT['params']のスキーマ定義から推論する
|
// TODO: paramsの型をT['params']のスキーマ定義から推論する
|
||||||
type Executor<T extends IEndpointMeta, Ps extends Schema> =
|
type Executor<T extends IEndpointMeta, Ps extends Schema> =
|
||||||
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
|
(
|
||||||
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
params: SchemaType<Ps>,
|
||||||
|
user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null,
|
||||||
|
token: MiAccessToken | null,
|
||||||
|
file: AttachmentFile | null,
|
||||||
|
cleanup: (() => void) | undefined,
|
||||||
|
request: Request,
|
||||||
|
) => Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
|
||||||
|
|
||||||
|
export type PublicExecutor<T extends IEndpointMeta = IEndpointMeta> = (
|
||||||
|
params: any,
|
||||||
|
user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null,
|
||||||
|
token: MiAccessToken | null,
|
||||||
|
file: AttachmentFile | null,
|
||||||
|
request: Request,
|
||||||
|
) => Promise<any>;
|
||||||
|
|
||||||
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
|
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
|
||||||
public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
|
public exec: PublicExecutor<T>;
|
||||||
|
|
||||||
constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
|
constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
|
||||||
const validate = ajv.compile(paramDef);
|
const validate = ajv.compile(paramDef);
|
||||||
|
|
||||||
this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => {
|
this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, request: Request) => {
|
||||||
let cleanup: undefined | (() => void) = undefined;
|
let cleanup: undefined | (() => void) = undefined;
|
||||||
|
|
||||||
if (meta.requireFile) {
|
if (meta.requireFile) {
|
||||||
|
@ -54,21 +76,21 @@ export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
|
||||||
|
|
||||||
const valid = validate(params);
|
const valid = validate(params);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
if (file) cleanup!();
|
if (file && cleanup) cleanup();
|
||||||
|
|
||||||
const errors = validate.errors!;
|
const errors = validate.errors;
|
||||||
const err = new ApiError({
|
const err = new ApiError({
|
||||||
message: 'Invalid param.',
|
message: 'Invalid param.',
|
||||||
code: 'INVALID_PARAM',
|
code: 'INVALID_PARAM',
|
||||||
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
||||||
}, {
|
}, errors?.length ? {
|
||||||
param: errors[0].schemaPath,
|
param: errors[0].schemaPath,
|
||||||
reason: errors[0].message,
|
reason: errors[0].message,
|
||||||
});
|
} : undefined);
|
||||||
return Promise.reject(err);
|
return Promise.reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers);
|
return cb(params as SchemaType<Ps>, user, token, file, cleanup, request);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => {
|
super(meta, paramDef, async (ps, me, _, file, cleanup, { ip, headers }) => {
|
||||||
// Get 'name' parameter
|
// Get 'name' parameter
|
||||||
let name = ps.name ?? file!.name ?? null;
|
let name = ps.name ?? file!.name ?? null;
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
|
|
|
@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private driveService: DriveService,
|
private driveService: DriveService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => {
|
super(meta, paramDef, async (ps, user, _1, _2, _3, { ip, headers }) => {
|
||||||
if (ps.comment && ps.comment.length > this.config.maxAltTextLength) {
|
if (ps.comment && ps.comment.length > this.config.maxAltTextLength) {
|
||||||
throw new ApiError(meta.errors.commentTooLong);
|
throw new ApiError(meta.errors.commentTooLong);
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me, _at, _file, _cleanup, { hostname }) => {
|
||||||
// check parameter
|
// check parameter
|
||||||
if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser);
|
if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchUser);
|
||||||
// abort if user is the root
|
// abort if user is the root
|
||||||
|
@ -104,7 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
// parse user's input into the destination account
|
// parse user's input into the destination account
|
||||||
const { username, host } = Acct.parse(ps.moveToAccount);
|
const { username, host } = Acct.parse(ps.moveToAccount);
|
||||||
// retrieve the destination account
|
// retrieve the destination account
|
||||||
let moveTo = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => {
|
let moveTo = await this.remoteUserResolveService.resolveUser(username, host, hostname).catch((e) => {
|
||||||
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`);
|
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${e}`);
|
||||||
throw new ApiError(meta.errors.noSuchUser);
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
});
|
});
|
||||||
|
|
|
@ -302,7 +302,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private avatarDecorationService: AvatarDecorationService,
|
private avatarDecorationService: AvatarDecorationService,
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, _user, token) => {
|
super(meta, paramDef, async (ps, _user, token, _file, _cleanup, { hostname }) => {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
|
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
|
||||||
const isSecure = token == null;
|
const isSecure = token == null;
|
||||||
|
|
||||||
|
@ -501,7 +501,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
const { username, host } = Acct.parse(line);
|
const { username, host } = Acct.parse(line);
|
||||||
|
|
||||||
// Retrieve the old account
|
// Retrieve the old account
|
||||||
const knownAs = await this.remoteUserResolveService.resolveUser(username, host).catch((e) => {
|
const knownAs = await this.remoteUserResolveService.resolveUser(username, host, hostname).catch((e) => {
|
||||||
this.apiLoggerService.logger.warn(`failed to resolve dstination user: ${e}`);
|
this.apiLoggerService.logger.warn(`failed to resolve dstination user: ${e}`);
|
||||||
throw new ApiError(meta.errors.noSuchUser);
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,8 +40,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
constructor(
|
constructor(
|
||||||
private metaEntityService: MetaEntityService,
|
private metaEntityService: MetaEntityService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, _me, _at, _file, _cleanup, { hostname }) => {
|
||||||
return ps.detail ? await this.metaEntityService.packDetailed() : await this.metaEntityService.pack();
|
return ps.detail
|
||||||
|
? await this.metaEntityService.packDetailed(undefined, hostname)
|
||||||
|
: await this.metaEntityService.pack(undefined, hostname);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private perUserPvChart: PerUserPvChart,
|
private perUserPvChart: PerUserPvChart,
|
||||||
private apiLoggerService: ApiLoggerService,
|
private apiLoggerService: ApiLoggerService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => {
|
super(meta, paramDef, async (ps, me, _1, _2, _3, { ip, hostname }) => {
|
||||||
let user;
|
let user;
|
||||||
|
|
||||||
const isModerator = await this.roleService.isModerator(me);
|
const isModerator = await this.roleService.isModerator(me);
|
||||||
|
@ -130,7 +130,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
} else {
|
} else {
|
||||||
// Lookup user
|
// Lookup user
|
||||||
if (typeof ps.host === 'string' && typeof ps.username === 'string') {
|
if (typeof ps.host === 'string' && typeof ps.username === 'string') {
|
||||||
user = await this.remoteUserResolveService.resolveUser(ps.username, ps.host).catch(err => {
|
user = await this.remoteUserResolveService.resolveUser(ps.username, ps.host, hostname).catch(err => {
|
||||||
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${err}`);
|
this.apiLoggerService.logger.warn(`failed to resolve remote user: ${err}`);
|
||||||
throw new ApiError(meta.errors.failedToResolveRemoteUser);
|
throw new ApiError(meta.errors.failedToResolveRemoteUser);
|
||||||
});
|
});
|
||||||
|
|
|
@ -59,6 +59,7 @@ import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||||
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
|
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
|
||||||
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { FeedService } from './FeedService.js';
|
import { FeedService } from './FeedService.js';
|
||||||
import { UrlPreviewService } from './UrlPreviewService.js';
|
import { UrlPreviewService } from './UrlPreviewService.js';
|
||||||
import { ClientLoggerService } from './ClientLoggerService.js';
|
import { ClientLoggerService } from './ClientLoggerService.js';
|
||||||
|
@ -130,6 +131,7 @@ export class ClientServerService {
|
||||||
private feedService: FeedService,
|
private feedService: FeedService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private clientLoggerService: ClientLoggerService,
|
private clientLoggerService: ClientLoggerService,
|
||||||
|
private readonly utilityService: UtilityService,
|
||||||
|
|
||||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||||
|
@ -146,14 +148,14 @@ export class ClientServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async manifestHandler(reply: FastifyReply) {
|
private async manifestHandler(reply: FastifyReply, hostname: string) {
|
||||||
let manifest = {
|
let manifest = {
|
||||||
// 空文字列の場合右辺を使いたいため
|
// 空文字列の場合右辺を使いたいため
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
'short_name': this.meta.shortName || this.meta.name || this.config.host,
|
'short_name': this.meta.shortName || this.meta.name || hostname,
|
||||||
// 空文字列の場合右辺を使いたいため
|
// 空文字列の場合右辺を使いたいため
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
'name': this.meta.name || this.config.host,
|
'name': this.meta.name || hostname,
|
||||||
'start_url': '/',
|
'start_url': '/',
|
||||||
'display': 'standalone',
|
'display': 'standalone',
|
||||||
'background_color': '#313a42',
|
'background_color': '#313a42',
|
||||||
|
@ -415,7 +417,7 @@ export class ClientServerService {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Manifest
|
// Manifest
|
||||||
fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply));
|
fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply, request.hostname));
|
||||||
|
|
||||||
// Embed Javascript
|
// Embed Javascript
|
||||||
fastify.get('/embed.js', async (request, reply) => {
|
fastify.get('/embed.js', async (request, reply) => {
|
||||||
|
@ -467,11 +469,11 @@ export class ClientServerService {
|
||||||
// URL preview endpoint
|
// URL preview endpoint
|
||||||
fastify.get<{ Querystring: { url: string; lang: string; } }>('/url', (request, reply) => this.urlPreviewService.handle(request, reply));
|
fastify.get<{ Querystring: { url: string; lang: string; } }>('/url', (request, reply) => this.urlPreviewService.handle(request, reply));
|
||||||
|
|
||||||
const getFeed = async (acct: string) => {
|
const getFeed = async (acct: string, hostname: string) => {
|
||||||
const { username, host } = Acct.parse(acct);
|
const { username, host } = Acct.parse(acct);
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: host ?? IsNull(),
|
host: host ?? this.utilityService.getSelfQueryHost(hostname),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
enableRss: true,
|
enableRss: true,
|
||||||
requireSigninToViewContents: false,
|
requireSigninToViewContents: false,
|
||||||
|
@ -484,7 +486,7 @@ export class ClientServerService {
|
||||||
fastify.get<{ Params: { user?: string; } }>('/@:user.atom', async (request, reply) => {
|
fastify.get<{ Params: { user?: string; } }>('/@:user.atom', async (request, reply) => {
|
||||||
if (request.params.user == null) return await renderBase(reply);
|
if (request.params.user == null) return await renderBase(reply);
|
||||||
|
|
||||||
const feed = await getFeed(request.params.user);
|
const feed = await getFeed(request.params.user, request.hostname);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
reply.header('Content-Type', 'application/atom+xml; charset=utf-8');
|
reply.header('Content-Type', 'application/atom+xml; charset=utf-8');
|
||||||
|
@ -499,7 +501,7 @@ export class ClientServerService {
|
||||||
fastify.get<{ Params: { user?: string; } }>('/@:user.rss', async (request, reply) => {
|
fastify.get<{ Params: { user?: string; } }>('/@:user.rss', async (request, reply) => {
|
||||||
if (request.params.user == null) return await renderBase(reply);
|
if (request.params.user == null) return await renderBase(reply);
|
||||||
|
|
||||||
const feed = await getFeed(request.params.user);
|
const feed = await getFeed(request.params.user, request.hostname);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
reply.header('Content-Type', 'application/rss+xml; charset=utf-8');
|
reply.header('Content-Type', 'application/rss+xml; charset=utf-8');
|
||||||
|
@ -514,7 +516,7 @@ export class ClientServerService {
|
||||||
fastify.get<{ Params: { user?: string; } }>('/@:user.json', async (request, reply) => {
|
fastify.get<{ Params: { user?: string; } }>('/@:user.json', async (request, reply) => {
|
||||||
if (request.params.user == null) return await renderBase(reply);
|
if (request.params.user == null) return await renderBase(reply);
|
||||||
|
|
||||||
const feed = await getFeed(request.params.user);
|
const feed = await getFeed(request.params.user, request.hostname);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
reply.header('Content-Type', 'application/json; charset=utf-8');
|
reply.header('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
@ -531,7 +533,7 @@ export class ClientServerService {
|
||||||
const { username, host } = Acct.parse(request.params.user);
|
const { username, host } = Acct.parse(request.params.user);
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: host ?? IsNull(),
|
host: host ?? this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -575,7 +577,7 @@ export class ClientServerService {
|
||||||
fastify.get<{ Params: { user: string; } }>('/users/:user', async (request, reply) => {
|
fastify.get<{ Params: { user: string; } }>('/users/:user', async (request, reply) => {
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
id: request.params.user,
|
id: request.params.user,
|
||||||
host: IsNull(),
|
host: this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -586,7 +588,8 @@ export class ClientServerService {
|
||||||
|
|
||||||
vary(reply.raw, 'Accept');
|
vary(reply.raw, 'Accept');
|
||||||
|
|
||||||
reply.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`);
|
const dbHost = this.utilityService.getSelfDbHost(request.hostname);
|
||||||
|
reply.redirect(`/@${user.username}${ user.host === dbHost ? '' : '@' + user.host}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Note
|
// Note
|
||||||
|
@ -630,7 +633,7 @@ export class ClientServerService {
|
||||||
const { username, host } = Acct.parse(request.params.user);
|
const { username, host } = Acct.parse(request.params.user);
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: host ?? IsNull(),
|
host: host ?? this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
|
@ -813,10 +816,10 @@ export class ClientServerService {
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
id: request.params.user,
|
id: request.params.user,
|
||||||
|
host: this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user == null) return;
|
if (user == null) return;
|
||||||
if (user.host != null) return;
|
|
||||||
|
|
||||||
const _user = await this.userEntityService.pack(user);
|
const _user = await this.userEntityService.pack(user);
|
||||||
|
|
||||||
|
@ -835,11 +838,11 @@ export class ClientServerService {
|
||||||
|
|
||||||
const note = await this.notesRepository.findOneBy({
|
const note = await this.notesRepository.findOneBy({
|
||||||
id: request.params.note,
|
id: request.params.note,
|
||||||
|
userHost: this.utilityService.getSelfQueryHost(request.hostname),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (note == null) return;
|
if (note == null) return;
|
||||||
if (['specified', 'followers'].includes(note.visibility)) return;
|
if (['specified', 'followers'].includes(note.visibility)) return;
|
||||||
if (note.userHost != null) return;
|
|
||||||
|
|
||||||
const _note = await this.noteEntityService.pack(note, null, { detail: true });
|
const _note = await this.noteEntityService.pack(note, null, { detail: true });
|
||||||
|
|
||||||
|
@ -887,12 +890,13 @@ export class ClientServerService {
|
||||||
fastify.get('/_info_card_', async (request, reply) => {
|
fastify.get('/_info_card_', async (request, reply) => {
|
||||||
reply.removeHeader('X-Frame-Options');
|
reply.removeHeader('X-Frame-Options');
|
||||||
|
|
||||||
|
const queryHost = this.utilityService.getSelfQueryHost(request.hostname);
|
||||||
return await reply.view('info-card', {
|
return await reply.view('info-card', {
|
||||||
version: this.config.version,
|
version: this.config.version,
|
||||||
host: this.config.host,
|
host: request.hostname,
|
||||||
meta: this.meta,
|
meta: this.meta,
|
||||||
originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }),
|
originalUsersCount: await this.usersRepository.countBy({ host: queryHost }),
|
||||||
originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
|
originalNotesCount: await this.notesRepository.countBy({ userHost: queryHost }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { Test } from '@nestjs/testing';
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiLocalUser, MiPartialUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
|
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
|
||||||
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
|
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
|
||||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||||
|
@ -276,7 +276,11 @@ describe('ActivityPub', () => {
|
||||||
rendererService.renderAnnounce('https://example.com/notes/00example', {
|
rendererService.renderAnnounce('https://example.com/notes/00example', {
|
||||||
id: genAidx(Date.now()),
|
id: genAidx(Date.now()),
|
||||||
visibility: 'followers',
|
visibility: 'followers',
|
||||||
} as MiNote);
|
} as MiNote, {
|
||||||
|
id: genAidx(Date.now()),
|
||||||
|
host: null,
|
||||||
|
uri: null,
|
||||||
|
} as MiPartialUser);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ti ti-chevron-up"></i> <i class="ti ti-user"></i></MkButton>
|
<MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ti ti-chevron-up"></i> <i class="ti ti-user"></i></MkButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="_margin _gaps_s">
|
<div class="_margin _gaps_s">
|
||||||
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
|
<MkRemoteCaution v-if="!instance.localHosts.includes(note.user.host)" :href="note.url ?? note.uri"/>
|
||||||
<SkErrorList :errors="note.processErrors"/>
|
<SkErrorList :errors="note.processErrors"/>
|
||||||
<DynamicNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/>
|
<DynamicNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -67,6 +67,7 @@ import { pleaseLogin } from '@/utility/please-login.js';
|
||||||
import { getAppearNote } from '@/utility/get-appear-note.js';
|
import { getAppearNote } from '@/utility/get-appear-note.js';
|
||||||
import { serverContext, assertServerContext } from '@/server-context.js';
|
import { serverContext, assertServerContext } from '@/server-context.js';
|
||||||
import { $i } from '@/i.js';
|
import { $i } from '@/i.js';
|
||||||
|
import { instance } from '@/instance.js';
|
||||||
|
|
||||||
// contextは非ログイン状態の情報しかないためログイン時は利用できない
|
// contextは非ログイン状態の情報しかないためログイン時は利用できない
|
||||||
const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null;
|
const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null;
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<div class="profile _gaps">
|
<div class="profile _gaps">
|
||||||
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
|
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
|
||||||
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!"/>
|
<MkRemoteCaution v-if="!instance.localHosts.includes(user.host)" :href="user.url ?? user.uri!"/>
|
||||||
<MkInfo v-if="user.host == null && user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
|
<MkInfo v-if="user.host == null && user.username.includes('.')">{{ i18n.ts.isSystemAccount }}</MkInfo>
|
||||||
|
|
||||||
<div :key="user.id" class="main _panel">
|
<div :key="user.id" class="main _panel">
|
||||||
|
@ -220,7 +220,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
|
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
|
||||||
import { useRouter } from '@/router.js';
|
import { useRouter } from '@/router.js';
|
||||||
import { getStaticImageUrl } from '@/utility/media-proxy.js';
|
import { getStaticImageUrl } from '@/utility/media-proxy.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl, instance } from '@/instance.js';
|
||||||
import MkSparkle from '@/components/MkSparkle.vue';
|
import MkSparkle from '@/components/MkSparkle.vue';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import DynamicNote from '@/components/DynamicNote.vue';
|
import DynamicNote from '@/components/DynamicNote.vue';
|
||||||
|
|
|
@ -5709,6 +5709,7 @@ export type components = {
|
||||||
cacheRemoteSensitiveFiles: boolean;
|
cacheRemoteSensitiveFiles: boolean;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
allowUnsignedFetch: 'never' | 'always' | 'essential';
|
allowUnsignedFetch: 'never' | 'always' | 'essential';
|
||||||
|
localHosts: (string | null)[];
|
||||||
};
|
};
|
||||||
MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly'];
|
MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly'];
|
||||||
SystemWebhook: {
|
SystemWebhook: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue