Compare commits

..

5 commits

Author SHA1 Message Date
fbb7be529c fix?
Some checks failed
Lint / pnpm_install (pull_request) Successful in 32s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m9s
Test (frontend) / vitest (22.x) (pull_request) Successful in 1m24s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m34s
Lint / lint (backend) (pull_request) Successful in 1m11s
Lint / lint (misskey-js) (pull_request) Successful in 36s
Lint / lint (sw) (pull_request) Successful in 33s
Test (backend) / unit (22.x) (pull_request) Successful in 2m32s
Lint / lint (frontend) (pull_request) Successful in 7m20s
Lint / typecheck (misskey-js) (pull_request) Successful in 32s
Lint / typecheck (backend) (pull_request) Successful in 1m25s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m30s
2025-02-08 14:15:34 +01:00
b597eb4c04 re-add ignoreAuthor
Some checks failed
Lint / pnpm_install (pull_request) Successful in 32s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m15s
Test (frontend) / vitest (22.x) (pull_request) Successful in 1m25s
Lint / lint (backend) (pull_request) Successful in 1m5s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m41s
Lint / lint (misskey-js) (pull_request) Successful in 34s
Lint / lint (sw) (pull_request) Successful in 33s
Test (backend) / unit (22.x) (pull_request) Successful in 2m33s
Lint / typecheck (misskey-js) (pull_request) Successful in 33s
Lint / typecheck (backend) (pull_request) Successful in 1m26s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m22s
Lint / lint (frontend) (pull_request) Successful in 7m28s
2025-02-08 13:01:15 +01:00
80d1cc174c fix double space
Some checks failed
Lint / pnpm_install (pull_request) Successful in 35s
Test (frontend) / vitest (22.x) (pull_request) Successful in 1m18s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m4s
Lint / lint (backend) (pull_request) Successful in 1m3s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m26s
Lint / lint (misskey-js) (pull_request) Successful in 35s
Lint / lint (sw) (pull_request) Successful in 31s
Lint / typecheck (misskey-js) (pull_request) Successful in 34s
Lint / lint (frontend) (pull_request) Successful in 7m51s
Lint / typecheck (backend) (pull_request) Failing after 47s
Test (backend) / unit (22.x) (pull_request) Successful in 2m26s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m10s
2025-02-05 17:25:46 +01:00
9d6773c55d Merge branch 'main' into mute-fix
Some checks failed
API report (misskey.js) / report (push) Successful in 45s
Lint / pnpm_install (pull_request) Successful in 29s
Test (frontend) / vitest (22.x) (pull_request) Successful in 1m19s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m4s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m27s
Test (backend) / unit (22.x) (pull_request) Successful in 2m30s
Lint / lint (misskey-js) (pull_request) Successful in 38s
Lint / lint (backend) (pull_request) Failing after 59s
Lint / lint (sw) (pull_request) Successful in 32s
Lint / typecheck (backend) (pull_request) Failing after 48s
Lint / typecheck (misskey-js) (pull_request) Successful in 34s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m25s
Lint / lint (frontend) (pull_request) Successful in 7m26s
2025-02-05 12:26:17 +00:00
b60c86ae6e cherrypick improve mutes and blocks from foundkey
Some checks failed
Check Misskey JS version / Check version (pull_request) Successful in 10s
API report (misskey.js) / report (pull_request) Successful in 49s
Lint / pnpm_install (pull_request) Successful in 47s
Test (misskey.js) / test (22.x) (pull_request) Successful in 53s
Test (frontend) / vitest (22.x) (pull_request) Successful in 1m23s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m3s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m42s
Test (backend) / unit (22.x) (pull_request) Successful in 2m32s
Lint / lint (backend) (pull_request) Failing after 58s
Lint / lint (misskey-js) (pull_request) Successful in 34s
Lint / lint (sw) (pull_request) Successful in 33s
Lint / typecheck (misskey-js) (pull_request) Successful in 34s
Lint / typecheck (backend) (pull_request) Failing after 49s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m35s
Lint / lint (frontend) (pull_request) Successful in 7m42s
2025-02-05 13:25:22 +01:00
17 changed files with 88 additions and 366 deletions

View file

@ -2243,7 +2243,6 @@ _permissions:
"read:clip-favorite": "View favorited clips" "read:clip-favorite": "View favorited clips"
"read:federation": "Get federation data" "read:federation": "Get federation data"
"write:report-abuse": "Report violation" "write:report-abuse": "Report violation"
"write:push-notification": "Receive push notifications"
_auth: _auth:
shareAccessTitle: "Granting application permissions" shareAccessTitle: "Granting application permissions"
shareAccess: "Would you like to authorize \"{name}\" to access this account?" shareAccess: "Would you like to authorize \"{name}\" to access this account?"

View file

@ -72,7 +72,7 @@ export function build() {
.reduce((a, [k, v]) => (a[k] = (() => { .reduce((a, [k, v]) => (a[k] = (() => {
const [lang] = k.split('-'); const [lang] = k.split('-');
switch (k) { switch (k) {
case 'ja-JP': return merge(locales['en-US'], v); case 'ja-JP': return v;
case 'ja-KS': case 'ja-KS':
case 'en-US': return merge(locales['ja-JP'], v); case 'en-US': return merge(locales['ja-JP'], v);
default: return merge( default: return merge(

View file

@ -1,18 +0,0 @@
export class MastodonOauth1736599563231 {
name = 'MastodonOauth1736599563231'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "app" ADD "website" character varying(128)`);
await queryRunner.query(`COMMENT ON COLUMN "app"."website" IS 'Application website.'`);
await queryRunner.query(`ALTER TABLE "app" ADD "mastodonScopes" character varying(64) array`);
await queryRunner.query(`COMMENT ON COLUMN "app"."mastodonScopes" IS 'Mastodon app scopes, only set for apps created with Mastodon API.'`);
await queryRunner.query(`ALTER TABLE "app" ADD "redirectUris" character varying(512) array DEFAULT '{}' NOT NULL`);
await queryRunner.query(`COMMENT ON COLUMN "app"."redirectUris" IS 'Redirect URIs.'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "website"`);
await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "mastodonScopes"`);
await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "redirectUris"`);
}
}

View file

@ -1,11 +0,0 @@
export class NoteUserIdIdIndex1736888704471 {
name = 'NoteUserIdIdIndex1736888704471'
async up(queryRunner) {
await queryRunner.query(`CREATE INDEX "IDX_note_userId_id" ON "note" ("userId", "id") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_note_userId_id"`);
}
}

View file

@ -3,18 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean { //Cherrypicked mute fix from foundkey: https://akkoma.dev/FoundKeyGang/FoundKey/pulls/32
if (userIds.has(note.userId) && !ignoreAuthor) { export function isUserRelated(note: any, ids: Set<string>, ignoreAuthor = false): boolean {
return true; if (ignoreAuthor) return false;
}
if (note.reply != null && note.reply.userId !== note.userId && userIds.has(note.reply.userId)) { if (ids.has(note.userId)) return true; // note author is muted
return true; if (note.mentions && note.mentions.some((user: string) => ids.has(user))) return true; // any of mentioned users are muted
} if (note.reply && isUserRelated(note.reply, ids)) return true; // also check reply target
if (note.renote && isUserRelated(note.renote, ids)) return true; // also check renote target
if (note.renote != null && note.renote.userId !== note.userId && userIds.has(note.renote.userId)) {
return true;
}
return false; return false;
} }

View file

@ -1,42 +0,0 @@
import { permissions } from 'misskey-js';
const mastodonToMisskeyScopes: Map<string, (typeof permissions)[number][]> = new Map([
['profile', ['read:account']],
['follow', ['read:following', 'write:following', 'read:blocks', 'write:blocks', 'read:mutes', 'write:mutes']],
['push', ['write:push-notification']],
['read:accounts', ['read:account']],
['read:blocks', ['read:blocks']],
['read:bookmarks', ['read:favorites']],
['read:favourites', ['read:reactions']],
['read:filters', ['read:account']],
['read:follows', ['read:following']],
['read:lists', ['read:account']],
['read:mutes', ['read:mutes']],
['read:notifications', ['read:notifications']],
['read:search', []],
['read:statuses', []],
['write:accounts', ['write:account']],
['write:blocks', ['write:blocks']],
['write:bookmarks', ['write:favorites']],
['write:conversations', ['write:notes']],
['write:favourites', ['write:reactions']],
['write:filters', ['write:account']],
['write:follows', ['write:following']],
['write:lists', ['write:account']],
['write:media', ['read:drive', 'write:drive']],
['write:mutes', ['write:mutes']],
['write:notifications', ['write:notifications']],
['write:reports', ['write:report-abuse']],
['write:statuses', ['write:notes']],
]);
function setHighLevelScope(scopeName: string) {
const granularScopes = Array.from(mastodonToMisskeyScopes)
.flatMap(([key, value]) => key.startsWith(scopeName + ':') ? value : []);
mastodonToMisskeyScopes.set(scopeName, Array.from(new Set(granularScopes)));
}
setHighLevelScope('read');
setHighLevelScope('write');
export { mastodonToMisskeyScopes };

View file

@ -63,22 +63,4 @@ export class MiApp {
comment: 'The callbackUrl of the App.', comment: 'The callbackUrl of the App.',
}) })
public callbackUrl: string | null; public callbackUrl: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'Application website.',
})
public website: string;
@Column('varchar', {
length: 64, array: true, nullable: true,
comment: 'Mastodon app scopes, only set for apps created with Mastodon API.',
})
public mastodonScopes: string[];
@Column('varchar', {
length: 512, array: true, nullable: true,
comment: 'Redirect URIs.',
})
public redirectUris: string[];
} }

View file

@ -15,7 +15,6 @@ import type { MiDriveFile } from './DriveFile.js';
@Index('IDX_NOTE_MENTIONS', { synchronize: false }) @Index('IDX_NOTE_MENTIONS', { synchronize: false })
@Index('IDX_NOTE_FILE_IDS', { synchronize: false }) @Index('IDX_NOTE_FILE_IDS', { synchronize: false })
@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false }) @Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false })
@Index('IDX_note_userId_id', ['userId', 'id'])
export class MiNote { export class MiNote {
@PrimaryColumn(id()) @PrimaryColumn(id())
public id: string; public id: string;

View file

@ -7,7 +7,6 @@ import { Inject, Injectable } from '@nestjs/common';
import cors from '@fastify/cors'; import cors from '@fastify/cors';
import multipart from '@fastify/multipart'; import multipart from '@fastify/multipart';
import fastifyCookie from '@fastify/cookie'; import fastifyCookie from '@fastify/cookie';
import fastifyFormbody from '@fastify/formbody';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js'; import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js';
@ -64,13 +63,6 @@ export class ApiServerService {
done(); done();
}); });
fastify.register(this.createMisskeyServer);
fastify.register(this.createMastodonServer);
done();
}
@bindThis
private createMisskeyServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
for (const endpoint of endpoints) { for (const endpoint of endpoints) {
const ep = { const ep = {
name: endpoint.name, name: endpoint.name,
@ -114,6 +106,32 @@ export class ApiServerService {
} }
} }
const createEndpoint = (endpoint: IMastodonEndpoint): IMastodonEndpoint & { exec: any } => ({
name: endpoint.name,
method: endpoint.method,
meta: endpoint.meta,
params: endpoint.params,
exec: this.moduleRef.get(`mep:${endpoint.method}:${endpoint.name}`, { strict: false }).exec,
});
const groupedMastodonEndpoints = Array.from(Map.groupBy(mastodonEndpoints.map(createEndpoint), endpoint => endpoint.name))
.map(([name, endpoints]) => ({ name, endpoints: new Map(endpoints.map(endpoint => [endpoint.method, endpoint])) }));
for (const { name, endpoints } of groupedMastodonEndpoints) {
fastify.all<{
Params: { endpoint: string; },
Body: Record<string, unknown>,
Querystring: Record<string, unknown>,
}>('/' + name, async (request, reply) => {
const ep = endpoints.get(request.method);
if (!ep) {
reply.code(405);
reply.send();
return;
}
await this.apiCallService.handleMastodonRequest(ep, request, reply);
return reply;
});
}
fastify.post<{ fastify.post<{
Body: { Body: {
username: string; username: string;
@ -194,39 +212,4 @@ export class ApiServerService {
done(); done();
} }
@bindThis
private createMastodonServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.register(fastifyFormbody);
const createEndpoint = (endpoint: IMastodonEndpoint): IMastodonEndpoint & { exec: any } => ({
name: endpoint.name,
method: endpoint.method,
meta: endpoint.meta,
params: endpoint.params,
exec: this.moduleRef.get(`mep:${endpoint.method}:${endpoint.name}`, { strict: false }).exec,
});
const groupedMastodonEndpoints = Array.from(Map.groupBy(mastodonEndpoints.map(createEndpoint), endpoint => endpoint.name))
.map(([name, endpoints]) => ({ name, endpoints: new Map(endpoints.map(endpoint => [endpoint.method, endpoint])) }));
for (const { name, endpoints } of groupedMastodonEndpoints) {
fastify.all<{
Params: { endpoint: string; },
Body: Record<string, unknown>,
Querystring: Record<string, unknown>,
}>('/' + name, async (request, reply) => {
const ep = endpoints.get(request.method);
if (!ep) {
reply.code(405);
reply.send();
return;
}
await this.apiCallService.handleMastodonRequest(ep, request, reply);
return reply;
});
}
done();
}
} }

View file

@ -2,10 +2,8 @@ import { Module, Provider } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js'; import { CoreModule } from '@/core/CoreModule.js';
import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js'; import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js';
import * as mep___apps_v1_post from './mastodon/apps/v1/post.js';
const $accounts_lookup_v1_get: Provider = { provide: 'mep:GET:v1/accounts/lookup', useClass: mep___accounts_lookup_v1_get.default }; const $accounts_lookup_v1_get: Provider = { provide: 'mep:GET:v1/accounts/lookup', useClass: mep___accounts_lookup_v1_get.default };
const $apps_v1_post: Provider = { provide: 'mep:POST:v1/apps', useClass: mep___apps_v1_post.default };
@Module({ @Module({
imports: [ imports: [
@ -13,11 +11,9 @@ const $apps_v1_post: Provider = { provide: 'mep:POST:v1/apps', useClass: mep___a
], ],
providers: [ providers: [
$accounts_lookup_v1_get, $accounts_lookup_v1_get,
$apps_v1_post,
], ],
exports: [ exports: [
$accounts_lookup_v1_get, $accounts_lookup_v1_get,
$apps_v1_post,
], ],
}) })
export class MastodonEndpointsModule {} export class MastodonEndpointsModule {}

View file

@ -1,10 +1,8 @@
import { Schema } from '@/misc/json-schema.js'; import { Schema } from '@/misc/json-schema.js';
import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js'; import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js';
import * as mep___apps_v1_post from './mastodon/apps/v1/post.js';
const eps = [ const eps = [
['GET', 'v1/accounts/lookup', mep___accounts_lookup_v1_get], ['GET', 'v1/accounts/lookup', mep___accounts_lookup_v1_get],
['POST', 'v1/apps', mep___apps_v1_post],
]; ];
export interface IMastodonEndpointMeta { export interface IMastodonEndpointMeta {

View file

@ -1,80 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { MastodonEndpoint } from '@/server/api/mastodon-endpoint-base.js';
import { MastodonApiError } from '@/server/api/error.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import type { AppsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { mastodonToMisskeyScopes } from '@/misc/mastodon/mastodon-to-misskey-scopes.js';
export const meta = {
requireCredential: false,
} as const;
export const paramDef = {
type: 'object',
properties: {
client_name: { type: 'string', minLength: 1 },
redirect_uri: { type: 'string', minLength: 1 },
redirect_uris: {
anyOf: [
{ type: 'array', minItems: 1, items: { type: 'string', minLength: 1 } },
{ type: 'string', minLength: 1 },
],
},
scopes: { type: 'string', minLength: 1 },
website: { type: 'string' },
},
anyOf: [
{ required: ['redirect_uri'] },
{ required: ['redirect_uris'] },
],
required: ['client_name'],
} as const;
@Injectable()
export default class extends MastodonEndpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.appsRepository)
private appsRepository: AppsRepository,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const redirectUrlsRawValue = ps.redirect_uris ?? ps.redirect_uri ?? [];
const redirectUris = typeof redirectUrlsRawValue === 'string' ? redirectUrlsRawValue.split('\n') : redirectUrlsRawValue;
const secret = secureRndstr(32);
const mastodonScopes = (ps.scopes ?? 'read').split(' ');
const scopes = mastodonScopes.flatMap(scope => {
const misskeyScopes = mastodonToMisskeyScopes.get(scope);
if (!misskeyScopes) {
throw new MastodonApiError('Scopes doesn\'t match configured on the server.', 400);
}
return misskeyScopes;
});
const clientId = this.idService.gen();
await this.appsRepository.insert({
id: clientId,
userId: me ? me.id : null,
name: ps.client_name,
description: ps.website ?? '',
permission: scopes,
callbackUrl: null,
secret: secret,
website: ps.website,
mastodonScopes: mastodonScopes,
redirectUris: redirectUris,
});
return {
id: clientId,
name: ps.client_name,
website: ps.website,
scopes: mastodonScopes,
redirect_uri: redirectUris.join('\n'),
redirect_uris: redirectUris,
client_id: `mastodon:${clientId}`,
client_secret: secret,
};
});
}
}

View file

@ -34,7 +34,6 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { import type {
AccessTokensRepository, AccessTokensRepository,
AppsRepository,
IndieAuthClientsRepository, IndieAuthClientsRepository,
UserProfilesRepository, UserProfilesRepository,
UsersRepository, UsersRepository,
@ -46,15 +45,9 @@ import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js'; import Logger from '@/logger.js';
import { StatusError } from '@/misc/status-error.js'; import { StatusError } from '@/misc/status-error.js';
import { normalizeEmailAddress } from '@/misc/normalize-email-address.js'; import { normalizeEmailAddress } from '@/misc/normalize-email-address.js';
import { mastodonToMisskeyScopes } from '@/misc/mastodon/mastodon-to-misskey-scopes.js';
import type { ServerResponse } from 'node:http'; import type { ServerResponse } from 'node:http';
import type { FastifyInstance } from 'fastify'; import type { FastifyInstance } from 'fastify';
function extractMastodonAppId(clientId: string): string | null {
const MASTODON_CLIENT_ID_PREFIX = 'mastodon:';
return clientId.startsWith(MASTODON_CLIENT_ID_PREFIX) ? clientId.substring(MASTODON_CLIENT_ID_PREFIX.length) : null;
}
// TODO: Consider migrating to @node-oauth/oauth2-server once // TODO: Consider migrating to @node-oauth/oauth2-server once
// https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out. // https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out.
// Upstream the various validations and RFC9207 implementation in that case. // Upstream the various validations and RFC9207 implementation in that case.
@ -265,8 +258,6 @@ export class OAuth2ProviderService {
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository) @Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository, private userProfilesRepository: UserProfilesRepository,
@Inject(DI.appsRepository)
private appsRepository: AppsRepository,
private idService: IdService, private idService: IdService,
private cacheService: CacheService, private cacheService: CacheService,
@ -337,25 +328,13 @@ export class OAuth2ProviderService {
if (body.client_id !== granted.clientId) return; if (body.client_id !== granted.clientId) return;
if (redirectUri !== granted.redirectUri) return; if (redirectUri !== granted.redirectUri) return;
const mastodonAppId = extractMastodonAppId(granted.clientId);
// https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.6 // https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.6
// For Mastodon API, code verifier isn't necessary (but if code challenge was provided, then it should be verified) if (!body.code_verifier) return;
if (!mastodonAppId || granted.codeChallenge || body.code_verifier) { if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return;
if (!body.code_verifier) return;
if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return;
}
const accessToken = secureRndstr(128); const accessToken = secureRndstr(128);
const now = new Date(); const now = new Date();
let scopes = granted.scopes;
let name = granted.clientId;
if (mastodonAppId) {
scopes = [...new Set(granted.scopes.flatMap((scope: string) => mastodonToMisskeyScopes.get(scope)))];
name = (await this.appsRepository.findOneBy({ id: mastodonAppId }))?.name ?? name;
}
// NOTE: we don't have a setup for automatic token expiration // NOTE: we don't have a setup for automatic token expiration
await accessTokensRepository.insert({ await accessTokensRepository.insert({
id: idService.gen(now.getTime()), id: idService.gen(now.getTime()),
@ -363,9 +342,8 @@ export class OAuth2ProviderService {
userId: granted.userId, userId: granted.userId,
token: accessToken, token: accessToken,
hash: accessToken, hash: accessToken,
name: name, name: granted.clientId,
permission: scopes, permission: granted.scopes,
appId: mastodonAppId,
}); });
grantedState = await this.redisClient.get(`oauth2:authorization:${code}:state`); grantedState = await this.redisClient.get(`oauth2:authorization:${code}:state`);
@ -376,7 +354,7 @@ export class OAuth2ProviderService {
} }
granted.grantedToken = accessToken; granted.grantedToken = accessToken;
this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${scopes}]`); this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${granted.scopes}]`);
await this.redisClient.set(`oauth2:authorization:${code}`, JSON.stringify(granted), 'EX', 60 * 5); await this.redisClient.set(`oauth2:authorization:${code}`, JSON.stringify(granted), 'EX', 60 * 5);
return [accessToken, undefined, { scope: granted.scopes.join(' ') }]; return [accessToken, undefined, { scope: granted.scopes.join(' ') }];
@ -440,75 +418,50 @@ export class OAuth2ProviderService {
this.#logger.info(`Validating authorization parameters, with client_id: ${clientID}, redirect_uri: ${redirectURI}, scope: ${scope}`); this.#logger.info(`Validating authorization parameters, with client_id: ${clientID}, redirect_uri: ${redirectURI}, scope: ${scope}`);
let clientInfo: ClientInformation; const clientUrl = validateClientId(clientID);
const mastodonAppId = extractMastodonAppId(clientID);
if (mastodonAppId) {
const app = await this.appsRepository.findOneBy({ id: mastodonAppId });
if (!app) {
throw new AuthorizationError('unrecognized client id', 'invalid_request');
}
clientInfo = {
id: clientID,
name: app.name,
redirectUris: app.redirectUris,
};
if (codeChallengeMethod && codeChallengeMethod !== 'S256') {
throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request');
}
try {
const scopes = [...new Set(scope)].filter(s => mastodonToMisskeyScopes.has(s));
if (!scopes.length) {
throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope');
}
areq.scope = scopes;
} catch (err) {
return [err as Error, clientInfo, redirectURI];
}
} else {
const clientUrl = validateClientId(clientID);
// https://indieauth.spec.indieweb.org/#client-information-discovery // https://indieauth.spec.indieweb.org/#client-information-discovery
// "the server may want to resolve the domain name first and avoid fetching the document // "the server may want to resolve the domain name first and avoid fetching the document
// if the IP address is within the loopback range defined by [RFC5735] // if the IP address is within the loopback range defined by [RFC5735]
// or any other implementation-specific internal IP address." // or any other implementation-specific internal IP address."
if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_CHECK_IP_RANGE === '1') { if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_CHECK_IP_RANGE === '1') {
const lookup = await dns.lookup(clientUrl.hostname); const lookup = await dns.lookup(clientUrl.hostname);
if (ipaddr.parse(lookup.address).range() !== 'unicast') { if (ipaddr.parse(lookup.address).range() !== 'unicast') {
throw new AuthorizationError('client_id resolves to disallowed IP range.', 'invalid_request'); throw new AuthorizationError('client_id resolves to disallowed IP range.', 'invalid_request');
}
}
// Find client information from the database.
const registeredClientInfo = await this.indieAuthClientsRepository.findOneBy({ id: clientUrl.href }) as ClientInformation | null;
// Find client information from the remote.
clientInfo = registeredClientInfo ?? await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
try {
const scopes = [...new Set(scope)].filter(s => (<readonly string[]>kinds).includes(s));
if (!scopes.length) {
throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope');
}
areq.scope = scopes;
// Require PKCE parameters. This requirement is skipped for Mastodon clients, as Mastodon API doesn't require it.
// Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack:
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack
if (typeof codeChallenge !== 'string') {
throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request');
}
if (codeChallengeMethod !== 'S256') {
throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request');
}
} catch (err) {
return [err as Error, clientInfo, redirectURI];
} }
} }
// Find client information from the database.
const registeredClientInfo = await this.indieAuthClientsRepository.findOneBy({ id: clientUrl.href }) as ClientInformation | null;
// Find client information from the remote.
const clientInfo = registeredClientInfo ?? await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
// Require the redirect URI to be included in an explicit list, per // Require the redirect URI to be included in an explicit list, per
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3 // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3
if (!clientInfo.redirectUris.includes(redirectURI)) { if (!clientInfo.redirectUris.includes(redirectURI)) {
throw new AuthorizationError('Invalid redirect_uri', 'invalid_request'); throw new AuthorizationError('Invalid redirect_uri', 'invalid_request');
} }
try {
const scopes = [...new Set(scope)].filter(s => (<readonly string[]>kinds).includes(s));
if (!scopes.length) {
throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope');
}
areq.scope = scopes;
// Require PKCE parameters.
// Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack:
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack
if (typeof codeChallenge !== 'string') {
throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request');
}
if (codeChallengeMethod !== 'S256') {
throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request');
}
} catch (err) {
return [err as Error, clientInfo, redirectURI];
}
return [null, clientInfo, redirectURI]; return [null, clientInfo, redirectURI];
})().then(args => done(...args), err => done(err)); })().then(args => done(...args), err => done(err));
}) as ValidateFunctionArity2)); }) as ValidateFunctionArity2));

View file

@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue'; import { nextTick, onMounted, onUnmounted, shallowRef } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { calcPopupPosition } from '@/scripts/popup-position.js'; import { calcPopupPosition } from '@/scripts/popup-position.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@ -70,7 +70,7 @@ function setPosition() {
el.value.style.top = data.top + 'px'; el.value.style.top = data.top + 'px';
} }
let loopHandler: number | undefined; let loopHandler;
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
@ -81,23 +81,12 @@ onMounted(() => {
loopHandler = window.requestAnimationFrame(loop); loopHandler = window.requestAnimationFrame(loop);
}; };
watch(() => props.showing, show => { loop();
if (show) {
if (!loopHandler) {
loop();
}
} else if (loopHandler) {
window.cancelAnimationFrame(loopHandler);
loopHandler = undefined;
}
});
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
if (loopHandler) { window.cancelAnimationFrame(loopHandler);
window.cancelAnimationFrame(loopHandler);
}
}); });
</script> </script>

View file

@ -225,7 +225,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkOmit from '@/components/MkOmit.vue'; import MkOmit from '@/components/MkOmit.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { getScrollContainer } from '@/scripts/scroll.js'; import { getScrollPosition } from '@/scripts/scroll.js';
import { getUserMenu } from '@/scripts/get-user-menu.js'; import { getUserMenu } from '@/scripts/get-user-menu.js';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
@ -282,7 +282,6 @@ const isEditingMemo = ref(false);
const moderationNote = ref(props.user.moderationNote); const moderationNote = ref(props.user.moderationNote);
const editModerationNote = ref(false); const editModerationNote = ref(false);
const movedFromLog = ref<null | {movedFromId:string;}[]>(null); const movedFromLog = ref<null | {movedFromId:string;}[]>(null);
let scrollEl: null | HTMLElement = null;
watch(moderationNote, async () => { watch(moderationNote, async () => {
await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
@ -314,19 +313,15 @@ async function fetchMovedFromLog() {
} }
function parallaxLoop() { function parallaxLoop() {
requestNextParallaxFrame();
parallax();
}
function requestNextParallaxFrame() {
parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop); parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop);
parallax();
} }
function parallax() { function parallax() {
const banner = bannerEl.value as any; const banner = bannerEl.value as any;
if (banner == null) return; if (banner == null) return;
const top = scrollEl?.scrollTop ?? scrollY; const top = getScrollPosition(rootEl.value);
if (top < 0) return; if (top < 0) return;
@ -335,23 +330,6 @@ function parallax() {
banner.style.backgroundPosition = `center calc(50% - ${pos}px)`; banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
} }
function startParallax() {
(scrollEl ?? window).removeEventListener('scroll', startParallax);
requestNextParallaxFrame();
}
function addScrollEvent() {
(scrollEl ?? window).addEventListener('scroll', startParallax);
}
function cancelParallax() {
if (parallaxAnimationId.value) {
window.cancelAnimationFrame(parallaxAnimationId.value);
parallaxAnimationId.value = null;
addScrollEvent();
}
}
function showMemoTextarea() { function showMemoTextarea() {
isEditingMemo.value = true; isEditingMemo.value = true;
nextTick(() => { nextTick(() => {
@ -416,10 +394,7 @@ watch([props.user], () => {
}); });
onMounted(() => { onMounted(() => {
scrollEl = getScrollContainer(rootEl.value); window.requestAnimationFrame(parallaxLoop);
addScrollEvent();
(scrollEl ?? window).addEventListener('scrollend', cancelParallax);
parallax();
narrow.value = rootEl.value!.clientWidth < 1000; narrow.value = rootEl.value!.clientWidth < 1000;
if (props.user.birthday) { if (props.user.birthday) {
@ -442,7 +417,11 @@ onMounted(() => {
}); });
}); });
onUnmounted(cancelParallax); onUnmounted(() => {
if (parallaxAnimationId.value) {
window.cancelAnimationFrame(parallaxAnimationId.value);
}
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -2820,7 +2820,7 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content']
function parse(acct: string): Acct; function parse(acct: string): Acct;
// @public (undocumented) // @public (undocumented)
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:regenerate-user-token", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-name", "write:admin:user-note", "write:admin:user-avatar", "write:admin:user-banner", "write:admin:user-mutual-link", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:push-notification"]; export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:regenerate-user-token", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-name", "write:admin:user-note", "write:admin:user-avatar", "write:admin:user-banner", "write:admin:user-mutual-link", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
// @public (undocumented) // @public (undocumented)
type PingResponse = operations['ping']['responses']['200']['content']['application/json']; type PingResponse = operations['ping']['responses']['200']['content']['application/json'];

View file

@ -103,7 +103,6 @@ export const permissions = [
'read:clip-favorite', 'read:clip-favorite',
'read:federation', 'read:federation',
'write:report-abuse', 'write:report-abuse',
'write:push-notification', // Mastodon permission
] as const; ] as const;
export const moderationLogTypes = [ export const moderationLogTypes = [