Compare commits

...

19 commits

Author SHA1 Message Date
3eeee88973 Merge remote-tracking branch 'upstream/main' into nyaaa 2022-10-16 17:15:50 +02:00
a74c1d9126
update changelog 2022-10-16 16:20:50 +02:00
811d5cd0d7 Merge pull request 'deliver Delete activities to all known instances' (#198) from deliver-delete-everyone into main
Reviewed-on: FoundKeyGang/FoundKey#198
2022-10-16 13:46:23 +00:00
d762143b89 backend: fixup missing deadTime and incorrect import 2022-10-16 09:32:01 -04:00
21c1e5c06c backend: simplify suspended and dead queries
This should also have better latency due to being a single query.
Furthermore, it's no longer a linear scan, since host is indexed.
Would be cool to simplify it further to a single query for blocks also...
Why exactly are blocks not in the db?
2022-10-16 09:22:05 -04:00
91a4f38871 backend: add automatic dead instance detection
It works by having a day-long cache of
"when did we last successfully communicate with this instance?"
Anything over a specified threshold (1 month) will act as though the instance
is suspended - all outgoing jobs are dropped on processing.
The day-long cache is in place because the ordering is necessarily a
linear scan.
Once an instance comes back online, we will detect that is the case as soon as
we receive an activity from them (which will update the "last communicated at")
field.

Potential future TODOs:
* Improve the caching system, it's actually pretty inefficient as it is.
  CacheBox with a call override?
* Think of ways to make it not-a-linear-scan, since the instances table can get
  pretty big. It's around 4500 on toast cafe.

ChangeLog: Added
2022-10-16 12:16:04 +00:00
756ecbb1f7
fix type error 2022-10-16 04:20:11 +02:00
b431471fd1
update SECURITY.md 2022-10-16 00:28:00 +02:00
6edf5928e9 Merge remote-tracking branch 'upstream/secure-mode' into nyaaa 2022-10-15 01:56:38 +02:00
8aee4bb4d8 Remove deprecated URLs 2022-10-14 23:36:11 +00:00
86355f948c Skip rendering private data in privateMode
Co-authored-by: Francis Dinh <normandy@biribiri.dev>
2022-10-14 23:36:11 +00:00
a0525cb8ec Add secure mode settings to Security tab 2022-10-14 23:36:11 +00:00
fdbc72130f In private mode, block access to many public APIs 2022-10-14 23:34:37 +00:00
e465ea8103 Add Secure Mode and Private Mode
- Add instance actor
- Add private mode, which uses an allowlist
- Add Secure Mode, restricts access to blocked instances

Co-authored-by: Francis Dinh <normandy@biribiri.dev>
2022-10-14 21:47:47 +00:00
3f438bcdab Add migration for allowedHosts, secureMode, privateMode 2022-10-14 21:47:47 +00:00
7cd11e7afd
fix function name 2022-10-11 21:26:20 +02:00
0b8fa2665c
use DISTINCT instead of GROUP BY
This should have better performance for large recordsets.

Ref: FoundKeyGang/FoundKey#198 (comment)
2022-10-11 20:15:59 +02:00
421b42d07d
backend: send delete activity to all known instances
closes FoundKeyGang/FoundKey#190

Changelog: Added
2022-10-11 19:32:26 +02:00
8920eeb86a
ActivityPub: allow all known shared inboxes to be addressed
This is oriented on this paragraph from the AP spec:

> Additionally, if an object is addressed to the Public special collection,
> a server MAY deliver that object to all known sharedInbox endpoints
> on the network.
2022-10-11 00:27:43 +02:00
19 changed files with 242 additions and 104 deletions

View file

@ -11,37 +11,84 @@ Unreleased changes should not be listed in this file.
Instead, run `git shortlog --format='%h %s' --group=trailer:changelog <last tag>..` to see unreleased changes; replace `<last tag>` with the tag you wish to compare from.
If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead.
## Unreleased
## 13.0.0-preview2 - 2022-10-16
### Security
- server: Update `multer` dependency to resolve [CVE-2022-24434](https://nvd.nist.gov/vuln/detail/CVE-2022-24434)
- server: Update `file-type`, `got`, and `sharp` dependencies to fix various security issues
### Added
- Client: Show instance info in ticker
- Client: Readded group pages
- Client: add re-collapsing to quoted notes
- allow to mute only renotes of a user
- allow to export only selected custom emoji
- client: improve emoji picker search
- client: Extend Emoji list
- client: show alt text in image viewer
- client: Show instance info in ticker
- client: Readded group pages
- client: add re-collapsing to quoted notes
- server: allow files storage path to be set explicitly
- server: refactor expiring data and expire signins after 60 days
- server: send delete activity to all known instances
- server: add automatic dead instance detection
### Changed
- Client: Use consistent date formatting based on language setting
- Client: Add threshold to reduce occurances of "future" timestamps
- Pages have been considerably simplified, several of the very complex features have been removed.
- foundkey-js: Sync possible endpoints from backend
- foundkey-js: update LiteInstanceMetadata fields
- meta: use parallel and incremental builds
- meta: update WORKDIR to foundkey
- meta: update dependencies
- client: consolidate about & notifications pages
- client: include renote in visibility computation
- client: make emoji amount slider more intuitive
- client: sort emojis by query similarity in fuzzy picker
- client: discard drafts that are just the default state
- client: Use consistent date formatting based on language setting
- client: Add threshold to reduce occurances of "future" timestamps
- server: mute notifications in muted threads
- server: allow for source lang to be overridden in note/translate
- server: allow redis family to be specified as a string
- server: increase image description limit to 2048 characters
- server: Pages have been considerably simplified, several of the very complex features have been removed.
Pages are now MFM only.
**For admins:** There is a migration in place to convert page contents to text, but not everything can be migrated.
You might want to check if you have any more complex pages on your instance and ask users to migrate them by hand.
Or generally advise all users to simplify their pages to only text.
### Removed
- Okteto config and Helm chart
- Client: acrylic styling
- Client: Twitter embeds, the standard URL preview is used instead.
- Promotion entities and endpoints
- Server: The configuration item `signToActivityPubGet` has been removed and will be ignored if set explicitly.
Foundkey will now work as if it was set to `true`.
### Fixed
- Client: Notifications for ended polls can now be turned off
- Client: Emoji picker should load faster now
- Server: Blocking remote accounts
- client: alt text dialog properly handles non-images
- client: Fix style scoping in MkMention
- client: default instance ticker name to instance's domain name
- client: improve error message for empty gallery posts
- client: fix default-selected reply scopes
- client: Make MFM cheatsheet interactive again
- client: Fix reports not showing in control panel
- client: make hard coded strings in emoji admin panel internationalized
- client: Notifications for ended polls can now be turned off
- client: improve emoji picker performance
- server: Blocking remote accounts
- server: fix table name used in toHtml
- server: Fix appendChildren TypeError
- server: ensure only own notifications can be marked as read
- server: render HTML mentions correctly
- server: increase requestId max size for GNU Social
- server: fix HTTP GET parameters in OpenAPI docs
- server: proper error messages for creating accounts
- server: Fix thread muting queries
- docker: add built foundkey-js files to container
- service worker: Remove fetch handler from service worker
### Security
- Server: Update `multer` dependency to resolve [CVE-2022-24434](https://nvd.nist.gov/vuln/detail/CVE-2022-24434)
- Server: Update `file-type`, `got`, and `sharp` dependencies to fix various security issues
### Removed
- remove misskey-assets submodule
- server: remove room data from user
- client: remove ai mode
- client: remove "Disable AiScript on Pages" setting
- client: acrylic styling
- client: Twitter embeds, the standard URL preview is used instead.
- foundkey-js: remove room api endpoints
- server: remove unusable setting to send error reports
- server: ignore detail parameter on meta endpoint
- server: Promotion entities and endpoints
- server: The configuration item `signToActivityPubGet` has been removed and will be ignored if set explicitly.
Foundkey will now work as if it was set to `true`.
## 13.0.0-preview1 - 2022-08-05
### Added

View file

@ -1,9 +1,9 @@
# Reporting Security Issues
If you discover a security issue in Misskey, please report it by sending an
email to [syuilotan@yahoo.co.jp](mailto:syuilotan@yahoo.co.jp).
If you discover a security issue in Foundkey, please report it by sending an
email to [johann@qwertqwefsday.eu](mailto:johann@qwertqwefsday.eu).
This will allow us to assess the risk, and make a fix available before we add a
bug report to the GitHub repository.
bug report to the repository.
Thanks for helping make Misskey safe for everyone.
Thanks for helping make Foundkey safe for everyone.

View file

@ -825,6 +825,13 @@ middle: "Medium"
low: "Low"
emailNotConfiguredWarning: "Email address not set."
ratio: "Ratio"
secureMode: "Secure Mode (Authorized Fetch)"
instanceSecurity: "Instance Security"
secureModeInfo: "Requests from other instances must be signed, otherwise notes won't be returned. signToActivityPubGet must be set to true in the other instance's configuration file."
privateMode: "Private Mode"
privateModeInfo: "When enabled, only authorized instances may fetch notes. Hides all notes from public."
allowedInstances: "Allowed Instances"
allowedInstancesDescription: "Set the hosts of the instances you want to allow, separated by line. Valid in private mode only."
previewNoteText: "Show preview"
customCss: "Custom CSS"
customCssWarn: "This setting should only be used if you know what it does. Entering\

View file

@ -12,19 +12,22 @@ import { Cache } from '@/misc/cache.js';
import { Instance } from '@/models/entities/instance.js';
import { StatusError } from '@/misc/fetch.js';
import { DeliverJobData } from '@/queue/types.js';
import { LessThan } from 'typeorm';
import { DAY } from '@/const.js';
const logger = new Logger('deliver');
let latest: string | null = null;
const suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
const deadThreshold = 30 * DAY;
export default async (job: Bull.Job<DeliverJobData>) => {
const { host } = new URL(job.data.to);
const puny = toPuny(host);
// ブロックしてたら中断
const meta = await fetchMeta();
if (meta.blockedHosts.includes(toPuny(host))) {
if (meta.blockedHosts.includes(puny)) {
return 'skip (blocked)';
}
@ -32,18 +35,13 @@ export default async (job: Bull.Job<DeliverJobData>) => {
return 'skip (not allowed)';
}
// isSuspendedなら中断
let suspendedHosts = suspendedHostsCache.get(null);
if (suspendedHosts == null) {
suspendedHosts = await Instances.find({
where: {
isSuspended: true,
},
});
suspendedHostsCache.set(null, suspendedHosts);
}
if (suspendedHosts.map(x => x.host).includes(toPuny(host))) {
return 'skip (suspended)';
const deadTime = new Date(Date.now() - deadThreshold);
const isSuspendedOrDead = await Instances.countBy([
{ host: puny, isSuspended: true },
{ host: puny, lastCommunicatedAt: LessThan(deadTime) },
]);
if (isSuspendedOrDead) {
return 'skip (suspended or dead)';
}
try {

View file

@ -39,7 +39,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
return `Blocked request: ${host}`;
}
// 非公開モードなら許可なインスタンスのみ
// Only permitted instances if in private mode.
if (meta.privateMode && !meta.allowedHosts.includes(host)) {
return `Blocked request: ${host}`;
}

View file

@ -7,7 +7,6 @@ import { toPuny } from '@/misc/convert-host.js';
import DbResolver from '@/remote/activitypub/db-resolver.js';
import { getApId } from '@/remote/activitypub/type.js';
export default async function checkFetch(req: IncomingMessage): Promise<number> {
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
@ -38,31 +37,28 @@ export default async function checkFetch(req: IncomingMessage): Promise<number>
const dbResolver = new DbResolver();
// HTTP-Signature keyIdを元にDBから取得
// Get user from database based on HTTP-Signature keyId
let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
// keyIdでわからなければ、resolveしてみる
// If keyid is unknown, try resolving it
if (authUser == null) {
try {
keyId.hash = '';
authUser = await dbResolver.getAuthUserFromApId(getApId(keyId.toString()));
} catch (e) {
// できなければ駄目
return 403;
}
}
// publicKey がなくても終了
if (authUser?.key == null) {
return 403;
}
// もう一回チェック
if (authUser.user.host !== host) {
return 403;
}
// HTTP-Signatureの検証
// HTTP-Signature validation
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
if (!httpSignatureValidated) {

View file

@ -8,6 +8,10 @@ interface IRecipe {
type: string;
}
interface IEveryoneRecipe extends IRecipe {
type: 'Everyone';
}
interface IFollowersRecipe extends IRecipe {
type: 'Followers';
}
@ -17,6 +21,9 @@ interface IDirectRecipe extends IRecipe {
to: IRemoteUser;
}
const isEveryone = (recipe: any): recipe is IEveryoneRecipe =>
recipe.type === 'Everyone';
const isFollowers = (recipe: any): recipe is IFollowersRecipe =>
recipe.type === 'Followers';
@ -63,6 +70,13 @@ export default class DeliverManager {
this.addRecipe(recipe);
}
/**
* Add recipe to send this activity to all known sharedInboxes
*/
public addEveryone() {
this.addRecipe({ type: 'Everyone' } as IEveryoneRecipe);
}
/**
* Add recipe
* @param recipe Recipe
@ -82,9 +96,26 @@ export default class DeliverManager {
/*
build inbox list
Process follower recipes first to avoid duplication when processing
direct recipes later.
Processing order matters to avoid duplication.
*/
if (this.recipes.some(r => isEveryone(r))) {
// deliver to all of known network
const sharedInboxes = await Users.createQueryBuilder('users')
.select('users.sharedInbox', 'sharedInbox')
// so we don't have to make our inboxes Set work as hard
.distinct(true)
// can't deliver to unknown shared inbox
.where('users.sharedInbox IS NOT NULL')
// don't deliver to ourselves
.andWhere('users.host IS NOT NULL')
.getRawMany();
for (const inbox of sharedInboxes) {
inboxes.add(inbox.sharedInbox);
}
}
if (this.recipes.some(r => isFollowers(r))) {
// followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう

View file

@ -76,7 +76,7 @@ export default class Resolver {
throw new Error('Instance is not allowed');
}
if (!this.user) {
if (config.signToActivityPubGet && !this.user) {
this.user = await getInstanceActor();
}

View file

@ -70,7 +70,7 @@ router.get('/notes/:note', async (ctx, next) => {
if (!isActivityPubReq(ctx)) return await next();
const verify = await checkFetch(ctx.req);
if (verify != 200) {
if (verify !== 200) {
ctx.status = verify;
return;
}
@ -87,7 +87,7 @@ router.get('/notes/:note', async (ctx, next) => {
}
// リモートだったらリダイレクト
if (note.userHost != null) {
if (note.userHost !== null) {
if (note.uri == null || isSelfHost(note.userHost)) {
ctx.status = 500;
return;
@ -100,7 +100,7 @@ router.get('/notes/:note', async (ctx, next) => {
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
ctx.set('Cache-Control', 'no-store');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
@ -110,7 +110,7 @@ router.get('/notes/:note', async (ctx, next) => {
// note activity
router.get('/notes/:note/activity', async ctx => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
if (verify !== 200) {
ctx.status = verify;
return;
}
@ -130,7 +130,7 @@ router.get('/notes/:note/activity', async ctx => {
ctx.body = renderActivity(await packActivity(note));
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
ctx.set('Cache-Control', 'no-store');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
@ -160,7 +160,7 @@ router.get('/users/:user/publickey', async ctx => {
}
const verify = await checkFetch(ctx.req);
if (verify != 200) {
if (verify !== 200) {
ctx.status = verify;
return;
}
@ -183,7 +183,7 @@ router.get('/users/:user/publickey', async ctx => {
ctx.body = renderActivity(renderKey(user, keypair));
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
ctx.set('Cache-Control', 'no-store');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
@ -203,7 +203,7 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) {
ctx.body = renderActivity(await renderPerson(user as ILocalUser));
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
ctx.set('Cache-Control', 'no-store');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
@ -220,7 +220,7 @@ router.get('/users/:user', async (ctx, next) => {
}
const verify = await checkFetch(ctx.req);
if (verify != 200) {
if (verify !== 200) {
ctx.status = verify;
return;
}
@ -246,7 +246,7 @@ router.get('/@:user', async (ctx, next) => {
}
const verify = await checkFetch(ctx.req);
if (verify != 200) {
if (verify !== 200) {
ctx.status = verify;
return;
}
@ -287,7 +287,7 @@ router.get('/emojis/:emoji', async ctx => {
ctx.body = renderActivity(await renderEmoji(emoji));
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
ctx.set('Cache-Control', 'no-store');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
@ -297,7 +297,7 @@ router.get('/emojis/:emoji', async ctx => {
// like
router.get('/likes/:like', async ctx => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
if (verify !== 200) {
ctx.status = verify;
return;
}
@ -322,7 +322,7 @@ router.get('/likes/:like', async ctx => {
ctx.body = renderActivity(await renderLike(reaction, note));
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
ctx.set('Cache-Control', 'no-store');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}
@ -332,7 +332,7 @@ router.get('/likes/:like', async ctx => {
// follow
router.get('/follows/:follower/:followee', async ctx => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
if (verify !== 200) {
ctx.status = verify;
return;
}
@ -358,7 +358,7 @@ router.get('/follows/:follower/:followee', async ctx => {
ctx.body = renderActivity(renderFollow(follower, followee));
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
ctx.set('Cache-Control', 'no-store');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}

View file

@ -8,10 +8,13 @@ import { Users, Notes, UserNotePinings } from '@/models/index.js';
import checkFetch from '@/remote/activitypub/check-fetch.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { setResponseType } from '../activitypub.js';
import { IsNull } from 'typeorm';
import checkFetch from '@/remote/activitypub/check-fetch.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
if (verify !== 200) {
ctx.status = verify;
return;
}
@ -47,7 +50,7 @@ export default async (ctx: Router.RouterContext) => {
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
ctx.set('Cache-Control', 'no-store');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}

View file

@ -14,7 +14,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
if (verify !== 200) {
ctx.status = verify;
return;
}
@ -22,7 +22,7 @@ export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user;
const cursor = ctx.request.query.cursor;
if (cursor != null && typeof cursor !== 'string') {
if (cursor !== null && typeof cursor !== 'string') {
ctx.status = 400;
return;
}
@ -101,7 +101,7 @@ export default async (ctx: Router.RouterContext) => {
}
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
ctx.set('Cache-Control', 'no-store');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}

View file

@ -14,7 +14,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
if (verify !== 200) {
ctx.status = verify;
return;
}
@ -22,7 +22,7 @@ export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user;
const cursor = ctx.request.query.cursor;
if (cursor != null && typeof cursor !== 'string') {
if (cursor !== null && typeof cursor !== 'string') {
ctx.status = 400;
return;
}
@ -101,7 +101,7 @@ export default async (ctx: Router.RouterContext) => {
}
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
ctx.set('Cache-Control', 'no-store');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}

View file

@ -19,7 +19,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
export default async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify != 200) {
if (verify !== 200) {
ctx.status = verify;
return;
}
@ -27,20 +27,20 @@ export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user;
const sinceId = ctx.request.query.since_id;
if (sinceId != null && typeof sinceId !== 'string') {
if (sinceId !== null && typeof sinceId !== 'string') {
ctx.status = 400;
return;
}
const untilId = ctx.request.query.until_id;
if (untilId != null && typeof untilId !== 'string') {
if (untilId !== null && typeof untilId !== 'string') {
ctx.status = 400;
return;
}
const page = ctx.request.query.page === 'true';
if (countIf(x => x != null, [sinceId, untilId]) > 1) {
if (countIf(x => x !== null, [sinceId, untilId]) > 1) {
ctx.status = 400;
return;
}
@ -103,7 +103,7 @@ export default async (ctx: Router.RouterContext) => {
}
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
ctx.set('Cache-Control', 'no-store');
} else {
ctx.set('Cache-Control', 'public, max-age=180');
}

View file

@ -693,8 +693,8 @@ export interface IEndpointMeta {
readonly secure?: boolean;
/**
*
* false
* If in private mode, whether credentials are required when making a request to this endpoint.
* If omitted, this is interpreted as false.
*/
readonly requireCredentialPrivateMode?: boolean;

View file

@ -139,10 +139,6 @@ export default define(meta, paramDef, async (ps, me) => {
set.secureMode = ps.secureMode;
}
if (ps.mascotImageUrl !== undefined) {
set.mascotImageUrl = ps.mascotImageUrl;
}
if (ps.bannerUrl !== undefined) {
set.bannerUrl = ps.bannerUrl;
}

View file

@ -253,7 +253,7 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
export default define(meta, paramDef, async (ps, me): Promise<Record<string, any>> => {
const instance = await fetchMeta(true);
const emojis = await Emojis.find({
@ -270,7 +270,7 @@ export default define(meta, paramDef, async (ps, me) => {
},
});
return {
const response: Record<string, any> = {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
@ -281,8 +281,10 @@ export default define(meta, paramDef, async (ps, me) => {
description: instance.description,
langs: instance.langs,
tosUrl: instance.ToSUrl,
secureMode: instance.secureMode,
privateMode: instance.privateMode,
disableRegistration: instance.disableRegistration,
disableLocalTimeline: instance.disableLocalTimeline,
disableGlobalTimeline: instance.disableGlobalTimeline,
@ -309,17 +311,22 @@ export default define(meta, paramDef, async (ps, me) => {
translatorAvailable: instance.deeplAuthKey != null,
pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles,
requireSetup: (await Users.countBy({
host: IsNull(),
})) === 0,
if (!instance.privateMode || me) {
proxyAccountName: instance.proxyAccountId ? (await Users.pack(instance.proxyAccountId).catch(() => null))?.username : null,
}
...(ps.detail ? {
pinnedPages: instance.privateMode && !me ? [] : instance.pinnedPages,
pinnedClipId: instance.privateMode && !me ? [] : instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles,
requireSetup: (await Users.countBy({
host: IsNull(),
})) === 0,
} : {}),
};
features: {
if (ps.detail) {
if (!instance.privateMode || me) {
const proxyAccount = instance.proxyAccountId ? await Users.pack(instance.proxyAccountId).catch(() => null) : null;
response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
}
response.features = {
registration: !instance.disableRegistration,
localTimeLine: !instance.disableLocalTimeline,
globalTimeLine: !instance.disableGlobalTimeline,
@ -330,6 +337,8 @@ export default define(meta, paramDef, async (ps, me) => {
objectStorage: instance.useObjectStorage,
serviceWorker: instance.enableServiceWorker,
miauth: true,
},
};
};
}
return response;
});

View file

@ -24,6 +24,7 @@ import { getNoteSummary } from '@/misc/get-note-summary.js';
import { queues } from '@/queue/queues.js';
import { MINUTE, DAY } from '@/const.js';
import { genOpenapiSpec } from '../api/openapi/gen-spec.js';
import meta from '../api/endpoints/meta.js';
import { urlPreviewHandler } from './url-preview.js';
import { manifestHandler } from './manifest.js';
import packFeed from './feed.js';
@ -271,6 +272,12 @@ router.get('/@:user.json', async ctx => {
//#region SSR (for crawlers)
// User
router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
const meta = await fetchMeta();
if (meta.privateMode) {
await next();
return;
}
const { username, host } = Acct.parse(ctx.params.user);
const user = await Users.findOneBy({
usernameLower: username.toLowerCase(),
@ -360,6 +367,12 @@ router.get('/notes/:note', async (ctx, next) => {
// Page
router.get('/@:user/pages/:page', async (ctx, next) => {
const meta = await fetchMeta();
if (meta.privateMode) {
await next();
return;
}
const { username, host } = Acct.parse(ctx.params.user);
const user = await Users.findOneBy({
usernameLower: username.toLowerCase(),
@ -402,6 +415,12 @@ router.get('/@:user/pages/:page', async (ctx, next) => {
// Clip
// TODO: 非publicなclipのハンドリング
router.get('/clips/:clip', async (ctx, next) => {
const meta = await fetchMeta();
if (meta.privateMode) {
await next();
return;
}
const clip = await Clips.findOneBy({
id: ctx.params.clip,
});
@ -430,6 +449,12 @@ router.get('/clips/:clip', async (ctx, next) => {
// Gallery post
router.get('/gallery/:post', async (ctx, next) => {
const meta = await fetchMeta();
if (meta.privateMode) {
await next();
return;
}
const post = await GalleryPosts.findOneBy({ id: ctx.params.post });
if (post) {
@ -456,6 +481,12 @@ router.get('/gallery/:post', async (ctx, next) => {
// Channel
router.get('/channels/:channel', async (ctx, next) => {
const meta = await fetchMeta();
if (meta.privateMode) {
await next();
return;
}
const channel = await Channels.findOneBy({
id: ctx.params.channel,
});
@ -526,6 +557,10 @@ router.get('/streaming', async ctx => {
// Render base html for all requests
router.get('(.*)', async ctx => {
const meta = await fetchMeta();
if (meta.privateMode) {
return;
}
await ctx.render('base', {
img: meta.bannerUrl,
title: meta.name || 'FoundKey',

View file

@ -10,7 +10,7 @@ import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
import { Note } from '@/models/entities/note.js';
import { Notes, Users, Instances } from '@/models/index.js';
import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index.js';
import { deliverToFollowers, deliverToUser } from '@/remote/activitypub/deliver-manager.js';
import DeliverManager from '@/remote/activitypub/deliver-manager.js';
import { countSameRenotes } from '@/misc/count-same-renotes.js';
import { isPureRenote } from '@/misc/renote.js';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
@ -109,7 +109,7 @@ async function getMentionedRemoteUsers(note: Note): Promise<IRemoteUser[]> {
const where = [] as any[];
// mention / reply / dm
if (note.mentions > 0) {
if (note.mentions.length > 0) {
where.push({
id: In(note.mentions),
// only remote users, local users are on the server and do not need to be notified
@ -132,10 +132,22 @@ async function getMentionedRemoteUsers(note: Note): Promise<IRemoteUser[]> {
}
async function deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any) {
deliverToFollowers(user, content);
deliverToRelays(user, content);
const manager = new DeliverManager(user, content);
const remoteUsers = await getMentionedRemoteUsers(note);
for (const remoteUser of remoteUsers) {
deliverToUser(user, content, remoteUser);
manager.addDirectRecipe(remoteUser);
}
if (['public', 'home', 'followers'].includes(note.visibility)) {
manager.addFollowersRecipe();
}
if (['public', 'home'].includes(note.visibility)) {
manager.addEveryone();
}
await manager.execute();
deliverToRelays(user, content);
}

View file

@ -43,7 +43,7 @@
<template #label>{{ i18n.ts.allowedInstances }}</template>
<template #caption>{{ i18n.ts.allowedInstancesDescription }}</template>
</FormTextarea>
<FormButton primary class="_formBlock" @click="saveInstance"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</div>
</FormFolder>
</div>
@ -77,6 +77,7 @@ async function init(): Promise<void> {
summalyProxy = meta.summalyProxy;
enableHcaptcha = meta.enableHcaptcha;
enableRecaptcha = meta.enableRecaptcha;
secureMode = meta.secureMode;
privateMode = meta.privateMode;
allowedHosts = meta.allowedHosts.join('\n');
@ -85,6 +86,9 @@ async function init(): Promise<void> {
function save(): void {
os.apiWithDialog('admin/update-meta', {
summalyProxy,
secureMode,
privateMode,
allowedHosts: allowedHosts.split('\n'),
}).then(() => {
fetchInstance();
});