Compare commits

...

53 commits

Author SHA1 Message Date
Marie 6da5fa4c10 merge: Add Stripe Identity for Age Verification (!611)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/611

Closes #618

Approved-by: fEmber <acomputerdog@gmail.com>
2024-09-23 21:08:34 +00:00
Julia c224ed031f merge: Merge stable into develop (!636)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/636

Approved-by: dakkar <dakkar@thenautilus.net>
2024-09-23 20:10:52 +00:00
Julia Johannesen 8eb8d72889
Re-bump develop version 2024-09-23 15:58:14 -04:00
Julia 674fd13807 merge: Bump version (!635)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/635
2024-09-23 19:33:41 +00:00
Julia Johannesen 0672ed921e
Bump version 2024-09-23 15:03:34 -04:00
dakkar 69efba9366 merge: Fetch sponsors from OC (!624)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/624

Closes #653

Approved-by: Julia <julia@insertdomain.name>
Approved-by: Tess K <me@thvxl.se>
2024-09-23 17:57:39 +00:00
Julia c94f6994dd merge: Only accept HTML <link rel="alternate"> on successful HTTP statuses (!633)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/633

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Tess K <me@thvxl.se>
Approved-by: fEmber <acomputerdog@gmail.com>
Approved-by: Marie <github@yuugi.dev>
2024-09-23 17:51:26 +00:00
Julia 30c1c7c24d merge: simpler RateLimitService, might help with the leaks (!627)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/627

Approved-by: Julia <julia@insertdomain.name>
Approved-by: Marie <github@yuugi.dev>
Approved-by: Tess K <me@thvxl.se>
Approved-by: fEmber <acomputerdog@gmail.com>
2024-09-23 16:51:39 +00:00
Julia 7c157408af merge: Add DetachedWindowAPI.close calls to MfmService (!634)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/634

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <github@yuugi.dev>
2024-09-23 16:39:13 +00:00
Julia Johannesen ceaec33249
Add DetachedWindowAPI.close calls to MfmService 2024-09-22 19:41:12 -04:00
Julia Johannesen e4cbd58821
Remove superfluous DetachedWindowAPI.close call 2024-09-22 18:51:29 -04:00
Julia Johannesen 5b282924ea
Add DetachedWindowAPI.close calls 2024-09-22 18:36:46 -04:00
Julia Johannesen b667a68bd4
Use res.ok instead of 200-299 2024-09-22 18:35:29 -04:00
Julia Johannesen 6ea48be84a
Only accept HTML <link rel="alternate"> on success 2024-09-22 17:13:24 -04:00
dakkar 8a982c61c0 move rate-limit-exceeded error reporting, earlier
a rate-limit-exceeded error has `kind:'client'`, so the branch that
adds the `Retry-After` would never get taken
2024-09-20 09:16:44 +01:00
dakkar 3f6beb97d2 copy RateLimiterService from MisskeyIO
This implementation allocates fewer Promises, might help with the
memory leaks
2024-09-20 08:35:45 +01:00
dakkar 7439230401 bump happy-dom
just because MisskeyIO uses this version
2024-09-20 08:30:24 +01:00
dakkar e9e51fdc01 bump glob
latest version no longer uses `inflight`; other dependencies still use
an older `glob`, though…
2024-09-20 08:29:36 +01:00
Marie 62a81bed9b
upd: change sorting of supporters 2024-09-16 19:02:06 +02:00
Marie 2e18359dad chore: lint 2024-09-15 18:17:35 +00:00
Marie b7815df134
upd: fetch sponsors from OC 2024-09-15 20:04:29 +02:00
Marie 3a86a8a3b6
chore: lint 2024-09-14 22:33:30 +02:00
Marie 28ec785c50
upd: add temporary block to certain api endpoints
This will be only temporary till a better solution is available
2024-09-14 22:32:36 +02:00
Marie 2c241f8d3b
upd: switch isIdConfirmed back to isIdNotConfirmed 2024-09-14 17:52:11 +02:00
Marie 2726e7e546
chore: lint 2024-09-14 14:40:00 +02:00
Marie 52919d1799
chore: change return to throw 2024-09-14 14:13:49 +02:00
Marie 5cd11cacdc
upd: apply suggestions 2024-09-14 14:11:12 +02:00
Marie 8b4c6e00b4
chore: vue lint error 2024-09-14 01:32:36 +02:00
Marie 76f85a2a95
chore: lint 2024-09-14 01:11:19 +02:00
Marie 43ee1c52b8
upd: use metadata to fetch idRequired value
This should speed up the loading as it doesn't need to load all the instance info on initial load then
2024-09-14 01:07:33 +02:00
Marie fe32d3a4d9
lint 2024-09-14 00:25:29 +02:00
Marie 46f9bcc8dc
chore: make features in meta not optional on detailed
Why did Misskey set the optional boolean on features between 2024.6 and 2024.8 considering it will always have values when called via detailed
2024-09-14 00:18:16 +02:00
Marie 01466279e8
chore: fix indentation on misskey-js md file 2024-09-14 00:11:10 +02:00
Marie 737b919c33
upd: handle failed id verifications 2024-09-14 00:04:59 +02:00
Marie 63368deab9
upd: let mod/admin prompt for id verification 2024-09-13 23:36:31 +02:00
Marie c33f474620
Merge branch 'develop' into add/id 2024-09-13 22:54:32 +02:00
Marie c38afd2a85
upd: set idCheckRequired boolean on signup 2024-09-12 09:18:55 +02:00
Marie 53d17b21f3
upd: add required boolean, hide timelines on required ID
Blocks guest timeline on sidebar and on welcome page if ID Verification is required
Blocks guests from going to timeline view, channels, antennas and lists
2024-09-12 00:41:56 +02:00
Marie c3e2ca9314
upd: remove additional check and comment 2024-09-11 22:05:00 +02:00
Marie 74e720c294
upd: move the prompting of the id verification from page into router 2024-09-11 22:04:36 +02:00
Marie 2eaff3388f
upd: make sure system users get the values properly set 2024-09-08 12:55:12 +02:00
Marie ed95e8168c
chore: apply suggestions 2024-09-07 12:40:02 +02:00
Marie 58c8c00376
upd: create verification dialog 2024-08-29 22:16:10 +02:00
Marie 1caf3636d1
upd: add typing for misskey-js 2024-08-29 21:52:18 +02:00
Marie b60dd15568
chore: make stripeAgeCheck not type undefined 2024-08-29 18:41:32 +02:00
Marie b3b5872e3e
upd(config): change the way stripeAgeCheck is handled in config file 2024-08-23 20:09:15 +02:00
Marie dbded3b68d
upd: remove idSession since Stripe tends to return NULL 2024-08-23 20:00:26 +02:00
Marie 0627f84e30
upd: move stripe webhook into its own service 2024-08-23 14:11:39 +02:00
Marie 03039d110a
upd: import config properly 2024-08-23 13:21:19 +02:00
Marie da4f0c75fc
add: webhook 2024-08-23 13:19:34 +02:00
Marie a56066ce53
add(migration): add two new columns to user for ID Check 2024-08-19 07:19:25 +02:00
Marie 531e4a43ad
upd(configs): add new StripeVerify boolean 2024-08-19 07:08:57 +02:00
Marie e7a08244d4
add(packages): stripe 2024-08-19 07:02:33 +02:00
58 changed files with 1076 additions and 112 deletions

View file

@ -217,5 +217,17 @@ checkActivityPubGetSignature: false
#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks']
# timeout and maximum size for imports (e.g. note imports)
#import:
# downloadTimeout: 30
# maxFileSize: 262144000
# Stripe identity for ID verification
stripeAgeCheck:
enabled: false
required: false
key: sk_
hookKey: whsec_
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View file

@ -292,5 +292,17 @@ checkActivityPubGetSignature: false
#customMOTD: ['Hello World', 'The sharks rule all', 'Shonks']
# timeout and maximum size for imports (e.g. note imports)
#import:
# downloadTimeout: 30
# maxFileSize: 262144000
# Stripe identity for ID verification
stripeAgeCheck:
enabled: false
required: false
key: sk_
hookKey: whsec_
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View file

@ -312,5 +312,12 @@ checkActivityPubGetSignature: false
# downloadTimeout: 30
# maxFileSize: 262144000
# Stripe identity for ID verification
stripeAgeCheck:
enabled: false
required: false
key: sk_
hookKey: whsec_
# PID File of master process
#pidFile: /tmp/misskey.pid

View file

@ -471,6 +471,7 @@ unregister: "Unregister"
passwordLessLogin: "Password-less login"
passwordLessLoginDescription: "Allows password-less login using a security- or passkey only"
resetPassword: "Reset password"
promptIdCheck: "Prompt for ID Verification"
newPasswordIs: "The new password is \"{password}\""
reduceUiAnimation: "Reduce UI animations"
share: "Share"
@ -1114,6 +1115,7 @@ nonSensitiveOnly: "Non-sensitive only"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "Non-sensitive only (Only likes from remote)"
rolesAssignedToMe: "Roles assigned to me"
resetPasswordConfirm: "Really reset your password?"
promptIdCheckConfirm: "Are you sure you want to prompt id verification on this user?"
sensitiveWords: "Sensitive words"
sensitiveWordsDescription: "The visibility of all notes containing any of the configured words will be set to \"Home\" automatically. You can list multiple by separating them via line breaks."
sensitiveWordsDescription2: "Using spaces will create AND expressions and surrounding keywords with slashes will turn them into a regular expression."
@ -2819,3 +2821,12 @@ _contextMenu:
app: "Application"
appWithShift: "Application with shift key"
native: "Native"
_stripeAgeCheck:
startText: "We require you to confirm your identity, Please press the start button to begin the process"
beginProcess: "Press the open Stripe in new tab button to begin Identification through Stripe"
endProcess: "If you have completed the Stripe verification process, wait a few seconds and then click Confirm completion."
_buttons:
start: "Start Verification"
openInNewTab: "Open Stripe in new tab"
confirmFinish: "Confirm completion"

36
locales/index.d.ts vendored
View file

@ -1900,6 +1900,10 @@ export interface Locale extends ILocale {
*
*/
"resetPassword": string;
/**
*
*/
"promptIdCheck": string;
/**
* {password}
*/
@ -4473,6 +4477,10 @@ export interface Locale extends ILocale {
*
*/
"resetPasswordConfirm": string;
/**
* ID確認を促しますか
*/
"promptIdCheckConfirm": string;
/**
*
*/
@ -10881,6 +10889,34 @@ export interface Locale extends ILocale {
*/
"native": string;
};
"_stripeAgeCheck": {
/**
*
*/
"startText": string;
/**
* Stripeを開くボタンを押すとStripeでの本人確認が開始されます
*/
"beginProcess": string;
/**
* Stripeでの本人確認手続きが完了した場合は
*/
"endProcess": string;
"_buttons": {
/**
*
*/
"start": string;
/**
* Stripeを開く
*/
"openInNewTab": string;
/**
*
*/
"confirmFinish": string;
};
};
}
declare const locales: {
[lang: string]: Locale;

View file

@ -471,6 +471,7 @@ unregister: "登録を解除"
passwordLessLogin: "パスワードレスログイン"
passwordLessLoginDescription: "パスワードを使用せず、セキュリティキーやパスキーなどのみでログインします"
resetPassword: "パスワードをリセット"
promptIdCheck: "身分証の確認を求める"
newPasswordIs: "新しいパスワードは「{password}」です"
reduceUiAnimation: "UIのアニメーションを減らす"
share: "共有"
@ -1114,6 +1115,7 @@ nonSensitiveOnly: "非センシティブのみ"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "非センシティブのみ (リモートはいいねのみ)"
rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワードリセットしますか?"
promptIdCheckConfirm: "本当にこのユーザーのID確認を促しますか"
sensitiveWords: "センシティブワード"
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
@ -2894,3 +2896,12 @@ _contextMenu:
app: "アプリケーション"
appWithShift: "Shiftキーでアプリケーション"
native: "ブラウザのUI"
_stripeAgeCheck:
startText: "ご本人様確認をさせていただきますので、スタートボタンを押して手続きを開始してください。"
beginProcess: "新しいタブでStripeを開くボタンを押すと、Stripeでの本人確認が開始されます。"
endProcess: "Stripeでの本人確認手続きが完了した場合は、数秒待ってから完了をクリックしてください。"
_buttons:
start: "検証を開始する"
openInNewTab: "新しいタブでStripeを開く"
confirmFinish: "完成を確認する"

View file

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: marie and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class idCheck1724044488000 {
name = 'idCheck1724044488000';
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "idCheckRequired" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "user" ADD "idVerified" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`UPDATE "user" SET "idVerified" = true WHERE "usernameLower" = 'instance.actor' AND "idVerified" = false`);
await queryRunner.query(`UPDATE "user" SET "idVerified" = true WHERE "usernameLower" = 'relay.actor' AND "idVerified" = false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "idCheckRequired"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "idVerified"`);
}
}

View file

@ -120,9 +120,9 @@
"file-type": "19.3.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0",
"glob": "10.3.10",
"glob": "11.0.0",
"got": "14.4.2",
"happy-dom": "15.6.1",
"happy-dom": "15.7.4",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
"http-link-header": "1.1.3",
@ -174,6 +174,7 @@
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"stripe": "^16.8.0",
"systeminformation": "5.22.11",
"tinycolor2": "1.6.0",
"tmp": "0.2.3",

View file

@ -108,6 +108,13 @@ type Source = {
maxFileSize: number;
};
stripeAgeCheck: {
enabled: boolean;
required: boolean;
key: string;
hookKey: string;
};
pidFile: string;
};
@ -197,6 +204,13 @@ export type Config = {
maxFileSize: number;
} | undefined;
stripeAgeCheck: {
enabled: boolean | undefined;
required: boolean | undefined;
key: string;
hookKey: string;
};
pidFile: string;
};
@ -319,6 +333,7 @@ export function loadConfig(): Config {
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
import: config.import,
stripeAgeCheck: config.stripeAgeCheck,
pidFile: config.pidFile,
};
}
@ -465,5 +480,6 @@ function applyEnvOverrides(config: Source) {
_apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]);
_apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]);
_apply_top(['import', ['downloadTimeout', 'maxFileSize']]);
_apply_top(['stripeAgeCheck', ['enabled', 'required', 'key', 'hookKey']]);
_apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature']]);
}

View file

@ -63,6 +63,8 @@ export class CreateSystemUserService {
isExplorable: false,
approved: true,
isBot: true,
idCheckRequired: false,
idVerified: true,
}).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0]));
await transactionalEntityManager.insert(MiUserKeypair, {

View file

@ -166,6 +166,10 @@ export interface AdminEventTypes {
reporterId: MiUser['id'],
comment: string;
};
failedIdCheck: {
userId: MiUser['id'],
comment: string;
};
}
export interface ReversiEventTypes {

View file

@ -245,7 +245,7 @@ export class MfmService {
return null;
}
const { window } = new Window();
const { happyDOM, window } = new Window();
const doc = window.document;
@ -463,7 +463,11 @@ export class MfmService {
appendChildren(nodes, body);
return new XMLSerializer().serializeToString(body);
const serialized = new XMLSerializer().serializeToString(body);
happyDOM.close().catch(e => {});
return serialized;
}
// the toMastoApiHtml function was taken from Iceshrimp and written by zotan and modified by marie to work with the current MK version
@ -474,7 +478,7 @@ export class MfmService {
return null;
}
const { window } = new Window();
const { happyDOM, window } = new Window();
const doc = window.document;
@ -681,6 +685,8 @@ export class MfmService {
result = result.replace(/^<p>/, '').replace(/<\/p>$/, '');
}
happyDOM.close().catch(e => {});
return result;
}
}

View file

@ -23,6 +23,7 @@ import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MetaService } from '@/core/MetaService.js';
import { UserService } from '@/core/UserService.js';
import type { Config } from '@/config.js';
@Injectable()
export class SignupService {
@ -36,6 +37,9 @@ export class SignupService {
@Inject(DI.usedUsernamesRepository)
private usedUsernamesRepository: UsedUsernamesRepository,
@Inject(DI.config)
private config: Config,
private utilityService: UtilityService,
private userService: UserService,
private userEntityService: UserEntityService,
@ -136,6 +140,7 @@ export class SignupService {
token: secret,
isRoot: isTheFirstUser,
approved: defaultApproval,
idCheckRequired: this.config.stripeAgeCheck.required && this.config.stripeAgeCheck.enabled ? true : false,
signupReason: reason,
}));

View file

@ -207,7 +207,11 @@ export class ApRequestService {
//#region リクエスト先がhtmlかつactivity+jsonへのalternate linkタグがあるとき
const contentType = res.headers.get('content-type');
if ((contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html' && _followAlternate === true) {
if (
res.ok
&& (contentType ?? '').split(';')[0].trimEnd().toLowerCase() === 'text/html'
&& _followAlternate === true
) {
const html = await res.text();
const window = new Window({
settings: {
@ -242,6 +246,8 @@ export class ApRequestService {
}
} catch (e) {
// something went wrong parsing the HTML, ignore the whole thing
} finally {
await window.happyDOM.close();
}
}
//#endregion

View file

@ -165,6 +165,7 @@ export class MetaEntityService {
turnstile: instance.enableTurnstile,
objectStorage: instance.useObjectStorage,
serviceWorker: instance.enableServiceWorker,
idRequired: this.config.stripeAgeCheck.enabled && this.config.stripeAgeCheck.required ? true : false,
miauth: true,
},
};

View file

@ -605,6 +605,8 @@ export class UserEntityService implements OnModuleInit {
}))),
memo: memo,
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
idCheckRequired: user.idCheckRequired,
idVerified: user.idVerified,
} : {}),
...(isDetailed && isMe ? {

View file

@ -305,6 +305,16 @@ export class MiUser {
})
public signupReason: string | null;
@Column('boolean', {
default: false,
})
public idCheckRequired: boolean;
@Column('boolean', {
default: false,
})
public idVerified: boolean;
constructor(data: Partial<MiUser>) {
if (data == null) return;

View file

@ -316,6 +316,10 @@ export const packedMetaDetailedOnlySchema = {
type: 'boolean',
optional: false, nullable: false,
},
idRequired: {
type: 'boolean',
optional: false, nullable: false,
},
miauth: {
type: 'boolean',
optional: true, nullable: false,

View file

@ -453,6 +453,14 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean',
nullable: false, optional: true,
},
idCheckRequired: {
type: 'boolean',
nullable: false, optional: false
},
idVerified: {
type: 'boolean',
nullable: false, optional: false
},
//#endregion
},
} as const;
@ -693,7 +701,7 @@ export const packedMeDetailedOnlySchema = {
},
},
},
},
}
//#endregion
},
} as const;

View file

@ -49,6 +49,7 @@ import { MastodonApiServerService } from './api/mastodon/MastodonApiServerServic
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
import { ReversiChannelService } from './api/stream/channels/reversi.js';
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
import { StripeHookServerService } from './StripeHookServerService.js';
@Module({
imports: [
@ -98,6 +99,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
MastodonApiServerService,
OAuth2ProviderService,
MastoConverters,
StripeHookServerService,
],
exports: [
ServerService,

View file

@ -32,6 +32,7 @@ import { HealthServerService } from './HealthServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { MastodonApiServerService } from './api/mastodon/MastodonApiServerService.js';
import { StripeHookServerService } from './StripeHookServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
const _dirname = fileURLToPath(new URL('.', import.meta.url));
@ -66,6 +67,7 @@ export class ServerService implements OnApplicationShutdown {
private fileServerService: FileServerService,
private healthServerService: HealthServerService,
private clientServerService: ClientServerService,
private stripeHookServerService: StripeHookServerService,
private globalEventService: GlobalEventService,
private loggerService: LoggerService,
private oauth2ProviderService: OAuth2ProviderService,
@ -109,6 +111,8 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.mastodonApiServerService.createServer, { prefix: '/api' });
fastify.register(this.fileServerService.createServer);
fastify.register(this.activityPubServerService.createServer);
// only enable stripe webhook if verification is enabled
if (this.config.stripeAgeCheck.enabled) fastify.register(this.stripeHookServerService.createServer, { prefix: '/stripe' });
fastify.register(this.nodeinfoServerService.createServer);
fastify.register(this.wellKnownServerService.createServer);
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });

View file

@ -0,0 +1,201 @@
/*
* SPDX-FileCopyrightText: marie and sharkey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type {
UserProfilesRepository,
UsersRepository,
} from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiLocalUser } from '@/models/User.js';
import { bindThis } from '@/decorators.js';
import cors from '@fastify/cors';
import secureJson from 'secure-json-parse';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import Stripe from 'stripe';
import type Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import { RoleService } from '@/core/RoleService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
@Injectable()
export class StripeHookServerService {
private logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private loggerService: LoggerService,
private globalEventService: GlobalEventService,
private roleService: RoleService,
) {
this.logger = this.loggerService.getLogger('stripe', 'gray');
}
@bindThis
private async stripehook(
request: FastifyRequest,
reply: FastifyReply,
) {
if (!this.config.stripeAgeCheck.enabled) return reply.code(400);
if (request.rawBody == null) {
// Bad request
reply.code(400);
return;
}
function error(status: number, error: { id: string }) {
reply.code(status);
return { error };
}
const stripe = new Stripe(this.config.stripeAgeCheck.key);
const body = request.rawBody;
const headers = request.headers;
let event;
// Verify the event came from Stripe
try {
const sig = headers['stripe-signature']!;
event = stripe.webhooks.constructEvent(body, sig, this.config.stripeAgeCheck.hookKey);
} catch (err: any) {
// On error, log and return the error message
this.logger.error(`❌ Stripe Error`, err);
return reply.code(400).send(`Webhook Error: ${err.message}`);
}
// Successfully constructed event
switch (event.type) {
case 'identity.verification_session.verified': {
// All the verification checks passed
const verificationSession = event.data.object;
const user = await this.usersRepository.findOneBy({
id: verificationSession.metadata.user_id,
host: IsNull(),
}) as MiLocalUser;
if (user == null) {
return error(404, {
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
});
}
if (user.isSuspended) {
return error(403, {
id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
});
}
this.logger.succ(`${user.username} has succesfully approved their ID via Session ${verificationSession.id}`);
await this.usersRepository.update(user.id, { idCheckRequired: false, idVerified: true });
break;
}
case 'identity.verification_session.requires_input': {
// Verification failed for some reason
const verificationSession = event.data.object;
const user = await this.usersRepository.findOneBy({
id: verificationSession.metadata.user_id,
host: IsNull(),
}) as MiLocalUser;
if (user == null) {
return error(404, {
id: '6cc579cc-885d-43d8-95c2-b8c7fc963280',
});
}
if (user.isSuspended) {
return error(403, {
id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
});
}
this.logger.succ(`${user.username} has failed ID Verification via Session ${verificationSession.id}`);
// If general instance then unset idCheckRequired as to prevent locking the user out forever admins/mods can see the mod note in case of the failure
if (!this.config.stripeAgeCheck.required) await this.usersRepository.update(user.id, { idCheckRequired: false });
await this.userProfilesRepository.update(user.id, { moderationNote: 'ADM/IDFAIL: Possibly underage' });
const moderatorIds = await this.roleService.getModeratorIds(true, true);
for (const moderatorId of moderatorIds) {
this.globalEventService.publishAdminStream(
moderatorId,
'failedIdCheck',
{
userId: user.id,
comment: 'ID Check Failed',
},
);
}
break;
}
}
reply.code(200);
return { received: true };
// never get here
}
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
const almostDefaultJsonParser: FastifyBodyParser<Buffer> = function (request, rawBody, done) {
if (rawBody.length === 0) {
const err = new Error('Body cannot be empty!') as any;
err.statusCode = 400;
return done(err);
}
try {
const json = secureJson.parse(rawBody.toString('utf8'), null, {
protoAction: 'ignore',
constructorAction: 'ignore',
});
done(null, json);
} catch (err: any) {
err.statusCode = 400;
return done(err);
}
};
fastify.register(cors, {
origin: '*',
});
fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, almostDefaultJsonParser);
fastify.addHook('onRequest', (request, reply, done) => {
reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
done();
});
fastify.post<{
Body: any,
Headers: any,
}>('/hook', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, async (request, reply) => await this.stripehook(request, reply));
done();
}
}

View file

@ -64,15 +64,6 @@ export class ApiCallService implements OnApplicationShutdown {
let statusCode = err.httpStatusCode;
if (err.httpStatusCode === 401) {
reply.header('WWW-Authenticate', 'Bearer realm="Misskey"');
} else if (err.kind === 'client') {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`);
statusCode = statusCode ?? 400;
} else if (err.kind === 'permission') {
// (ROLE_PERMISSION_DENIEDは関係ない)
if (err.code === 'PERMISSION_DENIED') {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
}
statusCode = statusCode ?? 403;
} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
const info: unknown = err.info;
const unixEpochInSeconds = Date.now();
@ -83,6 +74,15 @@ export class ApiCallService implements OnApplicationShutdown {
} else {
this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
}
} else if (err.kind === 'client') {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`);
statusCode = statusCode ?? 400;
} else if (err.kind === 'permission') {
// (ROLE_PERMISSION_DENIEDは関係ない)
if (err.code === 'PERMISSION_DENIED') {
reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
}
statusCode = statusCode ?? 403;
} else if (!statusCode) {
statusCode = 500;
}

View file

@ -97,6 +97,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
import * as ep___admin_prompt_id_check from './endpoints/admin/prompt-id-check.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
@ -398,6 +399,7 @@ import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
import * as ep___reversi_verify from './endpoints/reversi/verify.js';
import * as ep___stripe_createVerifySession from './endpoints/stripe/create-verify-session.js';
import { GetterService } from './GetterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
import type { Provider } from '@nestjs/common';
@ -493,6 +495,7 @@ const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhoo
const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
const $admin_prompt_id_check: Provider = { provide: 'ep:admin/prompt-id-check', useClass: ep___admin_prompt_id_check.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
@ -794,6 +797,7 @@ const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useC
const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default };
const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default };
const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep___reversi_verify.default };
const $stripe_createVerifySession: Provider = { provide: 'ep:stripe/create-verify-session', useClass: ep___stripe_createVerifySession.default };
@Module({
imports: [
@ -893,6 +897,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
$admin_prompt_id_check,
$announcements,
$announcements_show,
$antennas_create,
@ -1194,6 +1199,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$reversi_showGame,
$reversi_surrender,
$reversi_verify,
$stripe_createVerifySession,
],
exports: [
$admin_meta,
@ -1287,6 +1293,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
$admin_prompt_id_check,
$announcements,
$announcements_show,
$antennas_create,
@ -1586,6 +1593,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$reversi_showGame,
$reversi_surrender,
$reversi_verify,
$stripe_createVerifySession,
],
})
export class EndpointsModule {}

View file

@ -32,18 +32,11 @@ export class RateLimiterService {
@bindThis
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
{
if (this.disabled) {
return Promise.resolve();
}
// those lines with the "wrong" brace style / indentation are
// done that way so that the *other* lines stay identical to
// Misskey, simplifying merges
return new Promise<void>((ok, reject) => {
if (this.disabled) ok();
// Short-term limit
// eslint-disable-next-line brace-style
const minP = () => { return new Promise<void>((ok, reject) => {
const minP = (): void => {
const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval! * factor,
@ -62,18 +55,16 @@ export class RateLimiterService {
return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
} else {
if (hasLongTermLimit) {
return maxP().then(ok, reject);
return maxP();
} else {
return ok();
}
}
});
// eslint-disable-next-line brace-style
}); };
};
// Long term limit
// eslint-disable-next-line brace-style
const maxP = () => { return new Promise<void>((ok, reject) => {
const maxP = (): void => {
const limiter = new Limiter({
id: `${actor}:${limitation.key}`,
duration: limitation.duration! * factor,
@ -94,8 +85,7 @@ export class RateLimiterService {
return ok();
}
});
// eslint-disable-next-line brace-style
}); };
};
const hasShortTermLimit = typeof limitation.minInterval === 'number';
@ -104,12 +94,12 @@ export class RateLimiterService {
typeof limitation.max === 'number';
if (hasShortTermLimit) {
return minP();
minP();
} else if (hasLongTermLimit) {
return maxP();
maxP();
} else {
return Promise.resolve();
ok();
}
}
});
}
}

View file

@ -103,6 +103,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
import * as ep___admin_prompt_id_check from './endpoints/admin/prompt-id-check.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
@ -404,6 +405,7 @@ import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
import * as ep___reversi_verify from './endpoints/reversi/verify.js';
import * as ep___stripe_createVerifySession from './endpoints/stripe/create-verify-session.js';
const eps = [
['admin/meta', ep___admin_meta],
@ -483,6 +485,7 @@ const eps = [
['admin/update-meta', ep___admin_updateMeta],
['admin/delete-account', ep___admin_deleteAccount],
['admin/update-user-note', ep___admin_updateUserNote],
['admin/prompt-id-check', ep___admin_prompt_id_check],
['admin/roles/create', ep___admin_roles_create],
['admin/roles/delete', ep___admin_roles_delete],
['admin/roles/list', ep___admin_roles_list],
@ -798,6 +801,7 @@ const eps = [
['reversi/show-game', ep___reversi_showGame],
['reversi/surrender', ep___reversi_surrender],
['reversi/verify', ep___reversi_verify],
['stripe/create-verify-session', ep___stripe_createVerifySession],
];
interface IEndpointMetaBase {

View file

@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: marie and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:prompt-id-check-user',
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
if (await this.roleService.isModerator(user)) {
throw new Error('cannot prompt on moderator account');
}
await this.usersRepository.update(user.id, {
idCheckRequired: true,
});
});
}
}

View file

@ -172,6 +172,14 @@ export const meta = {
},
},
},
idVerified: {
type: 'boolean',
optional: false, nullable: false,
},
idCheckRequired: {
type: 'boolean',
optional: false, nullable: false,
},
},
},
} as const;
@ -253,6 +261,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: a.expiresAt ? a.expiresAt.toISOString() : null,
roleId: a.roleId,
})),
idVerified: user.idVerified,
idCheckRequired: user.idCheckRequired,
};
});
}

View file

@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
export const meta = {
tags: ['notes'],
@ -45,10 +46,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.config)
private config: Config,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
if (this.config.stripeAgeCheck.enabled && me && me.idCheckRequired || this.config.stripeAgeCheck.required && me && !me.idVerified || this.config.stripeAgeCheck.required && !me) {
// return no notes until we can figure out a way to simulate notes
return [];
}
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere('note.visibility = \'public\'')
.andWhere('note.localOnly = FALSE')

View file

@ -10,6 +10,7 @@ import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
import { CacheService } from '@/core/CacheService.js';
import { MetaService } from '@/core/MetaService.js';
import type { Config } from '@/config.js';
export const meta = {
tags: ['notes'],
@ -54,6 +55,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.config)
private config: Config,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private roleService: RoleService,
@ -68,6 +72,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.btlDisabled);
}
if (this.config.stripeAgeCheck.enabled && me && me.idCheckRequired || this.config.stripeAgeCheck.required && me && !me.idVerified || this.config.stripeAgeCheck.required && !me) {
// return no notes until we can figure out a way to simulate notes
return [];
}
const [
followings,
] = me ? await Promise.all([

View file

@ -139,6 +139,12 @@ export const meta = {
code: 'CONTAINS_TOO_MANY_MENTIONS',
id: '4de0363a-3046-481b-9b0f-feff3e211025',
},
userIdNotVerified: {
message: 'Cannot post because your account needs to go through ID Verification.',
code: 'USER_ID_NOT_VERIFIED',
id: '4de0363a-3046-481b-9b0f-feff3e211029',
},
},
} as const;
@ -256,6 +262,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.maxLength);
}
if (this.config.stripeAgeCheck.enabled && me && me.idCheckRequired || this.config.stripeAgeCheck.required && me && !me.idVerified) {
throw new ApiError(meta.errors.userIdNotVerified);
}
let visibleUsers: MiUser[] = [];
if (ps.visibleUserIds) {
visibleUsers = await this.usersRepository.findBy({

View file

@ -14,6 +14,7 @@ import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
export const meta = {
tags: ['notes'],
@ -58,6 +59,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.config)
private config: Config,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private roleService: RoleService,
@ -70,6 +74,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.gtlDisabled);
}
if (this.config.stripeAgeCheck.enabled && me && me.idCheckRequired || this.config.stripeAgeCheck.required && me && !me.idVerified || this.config.stripeAgeCheck.required && !me) {
// return no notes until we can figure out a way to simulate notes
return [];
}
const [
followings,
] = me ? await Promise.all([

View file

@ -20,6 +20,7 @@ import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
import type { Config } from '@/config.js';
export const meta = {
tags: ['notes'],
@ -81,6 +82,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.config)
private config: Config,
private noteEntityService: NoteEntityService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
@ -100,6 +104,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.stlDisabled);
}
if (this.config.stripeAgeCheck.enabled && me && me.idCheckRequired || this.config.stripeAgeCheck.required && me && !me.idVerified || this.config.stripeAgeCheck.required && !me) {
// return no notes until we can figure out a way to simulate notes
return [];
}
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
const serverSettings = await this.metaService.fetch();

View file

@ -18,6 +18,7 @@ import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
import type { Config } from '@/config.js';
export const meta = {
tags: ['notes'],
@ -70,6 +71,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.config)
private config: Config,
private noteEntityService: NoteEntityService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
@ -88,6 +92,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.ltlDisabled);
}
if (this.config.stripeAgeCheck.enabled && me && me.idCheckRequired || this.config.stripeAgeCheck.required && me && !me.idVerified || this.config.stripeAgeCheck.required && !me) {
// return no notes until we can figure out a way to simulate notes
return [];
}
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
const serverSettings = await this.metaService.fetch();

View file

@ -17,6 +17,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MiLocalUser } from '@/models/User.js';
import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import type { Config } from '@/config.js';
export const meta = {
tags: ['notes'],
@ -63,6 +64,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.config)
private config: Config,
private noteEntityService: NoteEntityService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
@ -78,6 +82,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const serverSettings = await this.metaService.fetch();
if (this.config.stripeAgeCheck.enabled && me && me.idCheckRequired || this.config.stripeAgeCheck.required && me && !me.idVerified) {
// return no notes until we can figure out a way to simulate notes
return [];
}
if (!serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,

View file

@ -10,7 +10,7 @@ import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['meta'],
description: 'Get Sharkey GH Sponsors',
description: 'Get Sharkey Sponsors',
requireCredential: false,
requireCredentialPrivateMode: false,
@ -30,29 +30,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.redis) private redisClient: Redis.Redis,
) {
super(meta, paramDef, async (ps, me) => {
let sponsors;
const cachedSponsors = await this.redisClient.get('sponsors');
let totalSponsors;
const cachedSponsors = await this.redisClient.get('sponsors');
if (!ps.forceUpdate && cachedSponsors) {
sponsors = JSON.parse(cachedSponsors);
totalSponsors = JSON.parse(cachedSponsors);
} else {
AbortSignal.timeout ??= function timeout(ms) {
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), ms);
return ctrl.signal;
};
try {
sponsors = await fetch('https://kaifa.ch/transfem-sponsors.json', { signal: AbortSignal.timeout(2000) })
.then((response) => response.json());
const backers = await fetch('https://opencollective.com/sharkey/tiers/backer/all.json').then((response) => response.json());
const sponsorsOC = await fetch('https://opencollective.com/sharkey/tiers/sponsor/all.json').then((response) => response.json());
await this.redisClient.set('sponsors', JSON.stringify(sponsors), 'EX', 3600);
// Merge both together into one array and make sure it only has Active subscriptions
const allSponsors = [...sponsorsOC, ...backers].filter(sponsor => sponsor.isActive === true);
// Remove possible duplicates
totalSponsors = [...new Map(allSponsors.map(v => [v.profile, v])).values()];
await this.redisClient.set('sponsors', JSON.stringify(totalSponsors), 'EX', 3600);
} catch (error) {
sponsors = {
sponsors: [],
};
totalSponsors = [];
}
}
return { sponsor_data: sponsors['sponsors'] };
return { sponsor_data: totalSponsors };
});
}
}

View file

@ -0,0 +1,86 @@
/*
* SPDX-FileCopyrightText: marie and sharkey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '../../error.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import Stripe from 'stripe';
import type { Config } from '@/config.js';
import ms from 'ms';
export const meta = {
tags: ['account'],
requireCredential: true,
kind: "read:account",
res: {
type: 'object',
optional: false, nullable: false,
},
limit: {
duration: ms('1hour'),
max: 5,
},
errors: {
userIsDeleted: {
message: 'User is deleted.',
code: 'USER_IS_DELETED',
id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a',
kind: 'permission',
},
stripeIsDisabled: {
message: 'Stripe is disabled.',
code: 'STRIPE_IS_DISABLED',
id: 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1b',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.config)
private config: Config,
) {
super(meta, paramDef, async (ps, me) => {
if (!this.config.stripeAgeCheck.enabled) throw new ApiError(meta.errors.stripeIsDisabled);
const userProfile = await this.usersRepository.findOne({
where: {
id: me.id,
}
});
if (userProfile == null) {
throw new ApiError(meta.errors.userIsDeleted);
}
const stripe = new Stripe(this.config.stripeAgeCheck.key);
const verificationSession = await stripe.identity.verificationSessions.create({
type: 'document',
metadata: {
user_id: me.id,
},
});
return { 'url': verificationSession.url };
});
}
}

View file

@ -198,6 +198,7 @@ export class ClientServerService {
notFoundImageUrl: meta.notFoundImageUrl ?? 'https://launcher.moe/missingpage.webp',
instanceUrl: this.config.url,
randomMOTD: this.config.customMOTD ? this.config.customMOTD[Math.floor(Math.random() * this.config.customMOTD.length)] : undefined,
idRequired: this.config.stripeAgeCheck.enabled && this.config.stripeAgeCheck.required ? "true" : "false",
metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)),
now: Date.now(),
};

View file

@ -31,6 +31,7 @@ html
meta(name='theme-color-orig' content= themeColor || '#86b300')
meta(property='og:site_name' content= instanceName || 'Sharkey')
meta(property='instance_url' content= instanceUrl)
meta(property='idRequired' content= idRequired)
meta(name='viewport' content='width=device-width, initial-scale=1')
meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no')
link(rel='icon' href= icon || '/favicon.ico')

View file

@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.statsItemCount"><MkNumber :value="stats.originalNotesCount"/></div>
</div>
</div>
<div v-if="instance.policies.ltlAvailable" :class="[$style.tl, $style.panel]">
<div v-if="instance.policies.ltlAvailable && !instance.features?.idRequired" :class="[$style.tl, $style.panel]">
<div :class="$style.tlHeader">{{ i18n.ts.letsLookAtTimeline }}</div>
<div :class="$style.tlBody">
<MkTimeline src="local"/>

View file

@ -0,0 +1,129 @@
<!--
SPDX-FileCopyrightText: marie and other Sharkey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow ref="dialog" :width="500" :height="550" :withCloseButton="false" @closed="emit('closed')">
<template #header>
<i class="ph-warning-circle ph-bold ph-lg" style="margin-right: 0.5em;"></i>
<b>Confirm your Identity</b>
</template>
<Transition
mode="out-in"
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
:leaveToClass="$style.transition_x_leaveTo"
>
<template v-if="page === 0">
<div :class="$style.centerPage">
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
<div style="font-size: 110%;">{{ i18n.ts._stripeAgeCheck.startText }}</div>
<div class="_buttonsCenter">
<MkButton rounded primary @click="startCheck()">{{ i18n.ts._stripeAgeCheck._buttons.start }}</MkButton>
</div>
</div>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 1">
<div :class="$style.centerPage">
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
<div style="font-size: 110%;">{{ i18n.ts._stripeAgeCheck.beginProcess }}</div>
<div class="_buttonsCenter">
<MkButton rounded primary @click="openCheck()">{{ i18n.ts._stripeAgeCheck._buttons.openInNewTab }}</MkButton>
</div>
</div>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 2">
<div :class="$style.centerPage">
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
<div style="font-size: 110%;">{{ i18n.ts._stripeAgeCheck.endProcess }}</div>
<div class="_buttonsCenter">
<MkButton rounded primary @click="confirmFinish()">{{ i18n.ts._stripeAgeCheck._buttons.confirmFinish }}</MkButton>
</div>
</div>
</MkSpacer>
</div>
</template>
</Transition>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { shallowRef, ref } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { $i, updateAccount } from '@/account.js';
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const url = ref('');
const page = ref(0);
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
async function startCheck() {
await misskeyApi('stripe/create-verify-session').then(res => {
url.value = res.url;
page.value = page.value + 1;
});
}
function openCheck() {
window.open(url.value, '_blank', 'noopener');
page.value = page.value + 1;
}
async function confirmFinish() {
await misskeyApi('i').then(res => {
if (!res.idCheckRequired && $i) {
updateAccount({ idCheckRequired: res.idCheckRequired, idVerified: res.idVerified });
dialog.value?.close();
}
});
}
</script>
<style lang="scss" module>
.transition_x_enterActive,
.transition_x_leaveActive {
transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
}
.transition_x_enterFrom {
opacity: 0;
transform: translateX(50px);
}
.transition_x_leaveTo {
opacity: 0;
transform: translateX(-50px);
}
.centerPage {
display: flex;
justify-content: center;
align-items: center;
height: 100cqh;
padding-bottom: 30px;
box-sizing: border-box;
}
.pageRoot {
display: flex;
flex-direction: column;
min-height: 100%;
}
.pageMain {
flex-grow: 1;
}
</style>

View file

@ -21,6 +21,7 @@ export const version = _VERSION_;
export const instanceName = siteName === 'Sharkey' || siteName == null ? host : siteName;
export const ui = miLocalStorage.getItem('ui');
export const debug = miLocalStorage.getItem('debug') === 'true';
export const idRequired = document.querySelector<HTMLMetaElement>('meta[property="idRequired"]')?.content === 'true' ? true : false;
export function updateLocale(newLocale): void {
locale = newLocale;

View file

@ -13,6 +13,7 @@ interface RouteDefBase {
path: string;
query?: Record<string, string>;
loginRequired?: boolean;
idRequired?: boolean;
name?: string;
hash?: string;
globalCacheKey?: string;
@ -186,13 +187,14 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
public currentRoute: ShallowRef<RouteDef>;
private currentPath: string;
private isLoggedIn: boolean;
private isIdNotConfirmed: boolean;
private notFoundPageComponent: Component;
private currentKey = Date.now().toString();
private redirectCount = 0;
public navHook: ((path: string, flag?: any) => boolean) | null = null;
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, isIdNotConfirmed: boolean, notFoundPageComponent: Component) {
super();
this.routes = routes;
@ -201,6 +203,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
this.currentRoute = shallowRef(this.current.route);
this.currentPath = currentPath;
this.isLoggedIn = isLoggedIn;
this.isIdNotConfirmed = isIdNotConfirmed;
this.notFoundPageComponent = notFoundPageComponent;
}
@ -366,6 +369,11 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
res.props.set('showLoginPopup', true);
}
if (res.route.idRequired && this.isIdNotConfirmed) {
res.route.component = this.notFoundPageComponent;
res.props.set('showIdConfirm', true);
}
const isSamePath = beforePath === path;
if (isSamePath && key == null) key = this.currentKey;
this.current = res;

View file

@ -170,9 +170,9 @@ SPDX-License-Identifier: AGPL-3.0-only
:key="sponsor"
style="margin-bottom: 0.5rem;"
>
<a :href="sponsor.profile" target="_blank" :class="$style.contributor">
<img :src="sponsor.avatar" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">{{ sponsor.details.name }}</span>
<a :href="sponsor.website || sponsor.profile" target="_blank" :class="$style.contributor">
<img :src="sponsor.image || `https://ui-avatars.com/api/?background=0D8ABC&color=fff&name=${sponsor.name}`" :class="$style.contributorAvatar">
<span :class="$style.contributorUsername">{{ sponsor.name }}</span>
</a>
</span>
</div>
@ -209,7 +209,7 @@ const easterEggEngine = ref(null);
const sponsors = ref([]);
const containerEl = shallowRef<HTMLElement>();
await misskeyApi('sponsors', { forceUpdate: true }).then((res) => sponsors.value.push(res.sponsor_data));
await misskeyApi('sponsors', { forceUpdate: false }).then((res) => sponsors.value.push(res.sponsor_data));
function iconLoaded() {
const emojis = defaultStore.state.reactions;

View file

@ -84,6 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
<MkButton v-if="user.host == null && user.idVerified == false && user.idCheckRequired == false" inline style="margin-right: 8px;" @click="promptIdCheck"><i class="ti ti-shield"></i> {{ i18n.ts.promptIdCheck }}</MkButton>
</div>
<MkFolder>
@ -304,6 +305,21 @@ async function resetPassword() {
}
}
async function promptIdCheck() {
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.promptIdCheckConfirm,
});
if (confirm.canceled) {
return;
} else {
await misskeyApi('admin/prompt-id-check', {
userId: user.value.id,
});
await refreshUser();
}
}
async function toggleNSFW(v) {
const confirm = await os.confirm({
type: 'warning',

View file

@ -17,16 +17,22 @@ import { computed } from 'vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { confirmId } from '@/scripts/confirm-id.js';
import { notFoundImageUrl } from '@/instance.js';
const props = defineProps<{
showLoginPopup?: boolean;
showIdConfirm?: boolean;
}>();
if (props.showLoginPopup) {
pleaseLogin('/');
}
if (props.showIdConfirm) {
confirmId('/');
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div v-if="meta" class="rsqzvsbo">
<MkFeaturedPhotos class="bg"/>
<XTimeline class="tl"/>
<XTimeline v-if="!meta.features?.idRequired" class="tl"/>
<div class="shape1"></div>
<div class="shape2"></div>
<div class="logo-wrapper">

View file

@ -7,6 +7,7 @@ import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue';
import type { RouteDef } from '@/nirax.js';
import { IRouter, Router } from '@/nirax.js';
import { $i, iAmModerator } from '@/account.js';
import { idRequired } from '@/config.js';
import MkLoading from '@/pages/_loading_.vue';
import MkError from '@/pages/_error_.vue';
import { setMainRouter } from '@/router/main.js';
@ -17,6 +18,10 @@ const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
errorComponent: MkError,
});
const userNeedsChecking = $i && $i.idCheckRequired ? true : false;
const noUserButCheckRequired = !$i && idRequired ? true : false;
const userNotVerifiedYet = $i && !$i.idVerified && idRequired ? true : false;
const routes: RouteDef[] = [{
path: '/@:initUser/pages/:initPageName/view-source',
component: page(() => import('@/pages/page-editor/page-editor.vue')),
@ -52,6 +57,7 @@ const routes: RouteDef[] = [{
path: '/settings',
component: page(() => import('@/pages/settings/index.vue')),
loginRequired: true,
idRequired: userNeedsChecking,
children: [{
path: '/profile',
name: 'profile',
@ -335,9 +341,11 @@ const routes: RouteDef[] = [{
}, {
path: '/channels/:channelId',
component: page(() => import('@/pages/channel.vue')),
idRequired: userNeedsChecking || noUserButCheckRequired,
}, {
path: '/channels',
component: page(() => import('@/pages/channels.vue')),
idRequired: userNeedsChecking || noUserButCheckRequired,
}, {
path: '/custom-emojis-manager',
component: page(() => import('@/pages/custom-emojis-manager.vue')),
@ -559,10 +567,12 @@ const routes: RouteDef[] = [{
path: '/timeline/list/:listId',
component: page(() => import('@/pages/user-list-timeline.vue')),
loginRequired: true,
idRequired: userNeedsChecking || noUserButCheckRequired,
}, {
path: '/timeline/antenna/:antennaId',
component: page(() => import('@/pages/antenna-timeline.vue')),
loginRequired: true,
idRequired: userNeedsChecking || noUserButCheckRequired,
}, {
path: '/clicker',
component: page(() => import('@/pages/clicker.vue')),
@ -586,11 +596,13 @@ const routes: RouteDef[] = [{
}, {
path: '/timeline',
component: page(() => import('@/pages/timeline.vue')),
idRequired: userNeedsChecking || noUserButCheckRequired,
}, {
name: 'index',
path: '/',
component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
globalCacheKey: 'index',
idRequired: userNeedsChecking || userNotVerifiedYet,
}, {
// テスト用リダイレクト設定。ログイン中ユーザのプロフィールにリダイレクトする
path: '/redirect-test',
@ -601,8 +613,10 @@ const routes: RouteDef[] = [{
component: page(() => import('@/pages/not-found.vue')),
}];
const isIdNotConfirmedCheck = userNeedsChecking || noUserButCheckRequired || userNotVerifiedYet;
function createRouterImpl(path: string): IRouter {
return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue')));
return new Router(routes, path, !!$i, isIdNotConfirmedCheck, page(() => import('@/pages/not-found.vue')));
}
/**

View file

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: marie and sharkey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineAsyncComponent } from 'vue';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { popup } from '@/os.js';
export function confirmId(path?: string) {
if (!$i) {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
autoSet: true,
message: i18n.ts.signinRequired,
}, {
cancelled: () => {
if (path) {
window.location.href = path;
}
},
closed: () => dispose(),
});
throw new Error('User Account required for id verification');
}
if ($i && $i.idVerified) return;
const { dispose } = popup(defineAsyncComponent(() => import('@/components/SkStripeIdDialog.vue')), {
}, {
closed: () => {
dispose();
if (path) {
window.location.href = path;
}
},
});
throw new Error('id confirmation required');
}

View file

@ -63,6 +63,7 @@ const devConfig: UserConfig = {
'/bios': httpUrl,
'/cli': httpUrl,
'/inbox': httpUrl,
'/stripe': httpUrl,
'/emoji/': httpUrl,
'/notes': {
target: httpUrl,

View file

@ -247,6 +247,9 @@ type AdminNsfwUserRequest = operations['admin___nsfw-user']['requestBody']['cont
// @public (undocumented)
type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminPromptIdCheckRequest = operations['admin___prompt-id-check']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json'];
@ -780,6 +783,10 @@ export type Channels = {
reporterId: string;
comment: string;
};
failedIdCheck: {
userId: string;
comment: string;
};
};
receives: null;
};
@ -1323,6 +1330,7 @@ declare namespace entities {
AdminUpdateMetaRequest,
AdminDeleteAccountRequest,
AdminUpdateUserNoteRequest,
AdminPromptIdCheckRequest,
AdminRolesCreateRequest,
AdminRolesCreateResponse,
AdminRolesDeleteRequest,
@ -1808,6 +1816,7 @@ declare namespace entities {
ReversiSurrenderRequest,
ReversiVerifyRequest,
ReversiVerifyResponse,
StripeCreateVerifySessionResponse,
Error_2 as Error,
UserLite,
UserDetailedNotMeOnly,
@ -2841,7 +2850,7 @@ type PartialRolePolicyOverride = Partial<{
}>;
// @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", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "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: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:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
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", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "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: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:ad", "read:admin:ad", "write:admin:prompt-id-check-user", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
// @public (undocumented)
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];
@ -3115,6 +3124,9 @@ export class Stream extends EventEmitter<StreamEvents> {
useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnection<Channels[C]>;
}
// @public (undocumented)
type StripeCreateVerifySessionResponse = operations['stripe___create-verify-session']['responses']['200']['content']['application/json'];
// Warning: (ae-forgotten-export) The symbol "SwitchCase" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "IsCaseMatched" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "GetCaseResult" needs to be exported by the entry point index.d.ts
@ -3344,8 +3356,8 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['
// Warnings were encountered during analysis:
//
// src/entities.ts:49:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:236:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:246:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:240:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:250:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)

View file

@ -856,6 +856,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:prompt-id-check-user*
*/
request<E extends 'admin/prompt-id-check', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
@ -4366,5 +4377,16 @@ declare module '../api.js' {
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
request<E extends 'stripe/create-verify-session', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
}
}

View file

@ -101,6 +101,7 @@ import type {
AdminUpdateMetaRequest,
AdminDeleteAccountRequest,
AdminUpdateUserNoteRequest,
AdminPromptIdCheckRequest,
AdminRolesCreateRequest,
AdminRolesCreateResponse,
AdminRolesDeleteRequest,
@ -586,6 +587,7 @@ import type {
ReversiSurrenderRequest,
ReversiVerifyRequest,
ReversiVerifyResponse,
StripeCreateVerifySessionResponse,
} from './entities.js';
export type Endpoints = {
@ -666,6 +668,7 @@ export type Endpoints = {
'admin/update-meta': { req: AdminUpdateMetaRequest; res: EmptyResponse };
'admin/delete-account': { req: AdminDeleteAccountRequest; res: EmptyResponse };
'admin/update-user-note': { req: AdminUpdateUserNoteRequest; res: EmptyResponse };
'admin/prompt-id-check': { req: AdminPromptIdCheckRequest; res: EmptyResponse };
'admin/roles/create': { req: AdminRolesCreateRequest; res: AdminRolesCreateResponse };
'admin/roles/delete': { req: AdminRolesDeleteRequest; res: EmptyResponse };
'admin/roles/list': { req: EmptyRequest; res: AdminRolesListResponse };
@ -981,6 +984,7 @@ export type Endpoints = {
'reversi/show-game': { req: ReversiShowGameRequest; res: ReversiShowGameResponse };
'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse };
'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse };
'stripe/create-verify-session': { req: EmptyRequest; res: StripeCreateVerifySessionResponse };
}
export const endpointReqTypes: Record<keyof Endpoints, 'application/json' | 'multipart/form-data'> = {
@ -1061,6 +1065,7 @@ export const endpointReqTypes: Record<keyof Endpoints, 'application/json' | 'mul
'admin/update-meta': 'application/json',
'admin/delete-account': 'application/json',
'admin/update-user-note': 'application/json',
'admin/prompt-id-check': 'application/json',
'admin/roles/create': 'application/json',
'admin/roles/delete': 'application/json',
'admin/roles/list': 'application/json',
@ -1376,4 +1381,5 @@ export const endpointReqTypes: Record<keyof Endpoints, 'application/json' | 'mul
'reversi/show-game': 'application/json',
'reversi/surrender': 'application/json',
'reversi/verify': 'application/json',
'stripe/create-verify-session': 'application/json',
};

View file

@ -104,6 +104,7 @@ export type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['re
export type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
export type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json'];
export type AdminUpdateUserNoteRequest = operations['admin___update-user-note']['requestBody']['content']['application/json'];
export type AdminPromptIdCheckRequest = operations['admin___prompt-id-check']['requestBody']['content']['application/json'];
export type AdminRolesCreateRequest = operations['admin___roles___create']['requestBody']['content']['application/json'];
export type AdminRolesCreateResponse = operations['admin___roles___create']['responses']['200']['content']['application/json'];
export type AdminRolesDeleteRequest = operations['admin___roles___delete']['requestBody']['content']['application/json'];
@ -589,3 +590,4 @@ export type ReversiShowGameResponse = operations['reversi___show-game']['respons
export type ReversiSurrenderRequest = operations['reversi___surrender']['requestBody']['content']['application/json'];
export type ReversiVerifyRequest = operations['reversi___verify']['requestBody']['content']['application/json'];
export type ReversiVerifyResponse = operations['reversi___verify']['responses']['200']['content']['application/json'];
export type StripeCreateVerifySessionResponse = operations['stripe___create-verify-session']['responses']['200']['content']['application/json'];

View file

@ -711,6 +711,15 @@ export type paths = {
*/
post: operations['admin___update-user-note'];
};
'/admin/prompt-id-check': {
/**
* admin/prompt-id-check
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:prompt-id-check-user*
*/
post: operations['admin___prompt-id-check'];
};
'/admin/roles/create': {
/**
* admin/roles/create
@ -3767,6 +3776,15 @@ export type paths = {
*/
post: operations['reversi___verify'];
};
'/stripe/create-verify-session': {
/**
* stripe/create-verify-session
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
post: operations['stripe___create-verify-session'];
};
};
export type webhooks = Record<string, never>;
@ -3912,6 +3930,8 @@ export type components = {
/** @enum {string} */
notify?: 'normal' | 'none';
withReplies?: boolean;
idCheckRequired: boolean;
idVerified: boolean;
};
MeDetailedOnly: {
/** Format: id */
@ -5108,6 +5128,7 @@ export type components = {
recaptcha: boolean;
objectStorage: boolean;
serviceWorker: boolean;
idRequired: boolean;
/** @default true */
miauth?: boolean;
};
@ -9207,6 +9228,8 @@ export type operations = {
expiresAt: string | null;
roleId: string;
})[];
idVerified: boolean;
idCheckRequired: boolean;
};
};
};
@ -9956,6 +9979,58 @@ export type operations = {
};
};
};
/**
* admin/prompt-id-check
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:prompt-id-check-user*
*/
'admin___prompt-id-check': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
userId: string;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* admin/roles/create
* @description No description provided.
@ -28541,5 +28616,57 @@ export type operations = {
};
};
};
/**
* stripe/create-verify-session
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:account*
*/
'stripe___create-verify-session': {
responses: {
/** @description OK (with results) */
200: {
content: {
'application/json': Record<string, never>;
};
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description To many requests */
429: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
};

View file

@ -109,6 +109,7 @@ export const permissions = [
'read:admin:drive',
'write:admin:ad',
'read:admin:ad',
'write:admin:prompt-id-check-user',
'write:invite-codes',
'read:invite-codes',
'write:clip-favorite',

View file

@ -220,6 +220,10 @@ export type Channels = {
targetUserId: string;
reporterId: string;
comment: string;
};
failedIdCheck: {
userId: string;
comment: string;
}
};
receives: null;

View file

@ -251,14 +251,14 @@ importers:
specifier: 4.0.0
version: 4.0.0
glob:
specifier: 10.3.10
version: 10.3.10
specifier: 11.0.0
version: 11.0.0
got:
specifier: 14.4.2
version: 14.4.2
happy-dom:
specifier: 15.6.1
version: 15.6.1
specifier: 15.7.4
version: 15.7.4
hpagent:
specifier: 1.2.0
version: 1.2.0
@ -412,6 +412,9 @@ importers:
stringz:
specifier: 2.1.0
version: 2.1.0
stripe:
specifier: ^16.8.0
version: 16.8.0
systeminformation:
specifier: 5.22.11
version: 5.22.11
@ -7580,11 +7583,6 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
glob@10.4.2:
resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==}
engines: {node: '>=16 || 14 >=14.18'}
hasBin: true
glob@11.0.0:
resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==}
engines: {node: 20 || >=22}
@ -7671,8 +7669,8 @@ packages:
happy-dom@10.0.3:
resolution: {integrity: sha512-WkCP+Z5fX6U5PY+yHP3ElV5D9PoxRAHRWPFq3pG9rg/6Hjf5ak7dozAgSCywsTRUq2qfa8vV8OQvUy5pRXy8EQ==}
happy-dom@15.6.1:
resolution: {integrity: sha512-dsMHLsJHZYhXeExP47B2siAfKNVxptlwFss3/bq/9sG3iBt0P2WYFBq68JgMR5vB5gsN2Ev0feTTPD/+rosUNQ==}
happy-dom@15.7.4:
resolution: {integrity: sha512-r1vadDYGMtsHAAsqhDuk4IpPvr6N8MGKy5ntBo7tSdim+pWDxus2PNqOcOt8LuDZ4t3KJHE+gCuzupcx/GKnyQ==}
engines: {node: '>=18.0.0'}
hard-rejection@2.1.0:
@ -7902,6 +7900,7 @@ packages:
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@ -8220,10 +8219,6 @@ packages:
resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
engines: {node: '>=14'}
jackspeak@3.4.0:
resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==}
engines: {node: '>=14'}
jackspeak@4.0.1:
resolution: {integrity: sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==}
engines: {node: 20 || >=22}
@ -9538,10 +9533,6 @@ packages:
resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
engines: {node: '>=16 || 14 >=14.17'}
path-scurry@1.11.1:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
path-scurry@2.0.0:
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
engines: {node: 20 || >=22}
@ -10904,6 +10895,10 @@ packages:
resolution: {integrity: sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
stripe@16.8.0:
resolution: {integrity: sha512-6rOIcGOkxcc29jvhEyOYmpPFilekOBV+7vpemAoIAfbtCRW1yxzdDGM0/0vyekHglLL+wqGpP5ldrhO3dJ2JEQ==}
engines: {node: '>=12.*'}
strnum@1.0.5:
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
@ -11121,7 +11116,6 @@ packages:
ts-case-convert@2.0.2:
resolution: {integrity: sha512-vdKfx1VAdpvEBOBv5OpVu5ZFqRg9HdTI4sYt6qqMeICBeNyXvitrarCnFWNDAki51IKwCyx+ZssY46Q9jH5otA==}
bundledDependencies: []
ts-dedent@2.2.0:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
@ -11633,8 +11627,8 @@ packages:
vue-component-type-helpers@2.0.29:
resolution: {integrity: sha512-58i+ZhUAUpwQ+9h5Hck0D+jr1qbYl4voRt5KffBx8qzELViQ4XdT/Tuo+mzq8u63teAG8K0lLaOiL5ofqW38rg==}
vue-component-type-helpers@2.1.2:
resolution: {integrity: sha512-URuxnrOhO9lUG4LOAapGWBaa/WOLDzzyAbL+uKZqT7RS+PFy0cdXI2mUSh7GaMts6vtHaeVbGk7trd0FPJi65Q==}
vue-component-type-helpers@2.1.6:
resolution: {integrity: sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==}
vue-demi@0.14.7:
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
@ -14082,7 +14076,7 @@ snapshots:
content-disposition: 0.5.4
fastify-plugin: 4.5.0
fastq: 1.17.1
glob: 10.4.2
glob: 10.3.10
'@fastify/view@8.2.0':
dependencies:
@ -16281,7 +16275,7 @@ snapshots:
ts-dedent: 2.2.0
type-fest: 2.19.0
vue: 3.4.37(typescript@5.5.4)
vue-component-type-helpers: 2.1.2
vue-component-type-helpers: 2.1.6
transitivePeerDependencies:
- encoding
- prettier
@ -16300,7 +16294,7 @@ snapshots:
ts-dedent: 2.2.0
type-fest: 2.19.0
vue: 3.4.37(typescript@5.5.4)
vue-component-type-helpers: 2.1.2
vue-component-type-helpers: 2.1.6
'@swc/cli@0.3.12(@swc/core@1.6.6)(chokidar@3.5.3)':
dependencies:
@ -20034,15 +20028,6 @@ snapshots:
minipass: 7.0.4
path-scurry: 1.10.1
glob@10.4.2:
dependencies:
foreground-child: 3.1.1
jackspeak: 3.4.0
minimatch: 9.0.4
minipass: 7.1.2
package-json-from-dist: 1.0.0
path-scurry: 1.11.1
glob@11.0.0:
dependencies:
foreground-child: 3.1.1
@ -20179,7 +20164,7 @@ snapshots:
whatwg-encoding: 2.0.0
whatwg-mimetype: 3.0.0
happy-dom@15.6.1:
happy-dom@15.7.4:
dependencies:
entities: 4.5.0
webidl-conversions: 7.0.0
@ -20694,12 +20679,6 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jackspeak@3.4.0:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jackspeak@4.0.1:
dependencies:
'@isaacs/cliui': 8.0.2
@ -22434,11 +22413,6 @@ snapshots:
lru-cache: 10.2.2
minipass: 7.0.4
path-scurry@1.11.1:
dependencies:
lru-cache: 10.2.2
minipass: 7.1.2
path-scurry@2.0.0:
dependencies:
lru-cache: 11.0.0
@ -23913,6 +23887,11 @@ snapshots:
strip-outer@2.0.0: {}
stripe@16.8.0:
dependencies:
'@types/node': 20.14.12
qs: 6.11.1
strnum@1.0.5: {}
strtok3@7.0.0:
@ -24597,7 +24576,7 @@ snapshots:
vue-component-type-helpers@2.0.29: {}
vue-component-type-helpers@2.1.2: {}
vue-component-type-helpers@2.1.6: {}
vue-demi@0.14.7(vue@3.4.37(typescript@5.5.4)):
dependencies: