diff --git a/.config/ci.yml b/.config/ci.yml index d20ede8d35..8730ccab3a 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -229,3 +229,8 @@ checkActivityPubGetSignature: false # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index 8848e837b7..c8c4a36d8f 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -235,3 +235,8 @@ allowedPrivateNetworks: [ # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 5fac3dc41e..ce2daf3aec 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -312,3 +312,8 @@ checkActivityPubGetSignature: false # Upload or download file size limits (bytes) #maxFileSize: 262144000 + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/.config/example.yml b/.config/example.yml index 10aeb6bccc..ba8e818b5d 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -348,3 +348,8 @@ checkActivityPubGetSignature: false # PID File of master process #pidFile: /tmp/misskey.pid + +# CHMod-style permission bits to apply to uploaded files. +# Permission bits are specified as a base-8 string representing User/Group/Other permissions. +# This setting is only useful for custom deployments, such as using a reverse proxy to serve media. +#filePermissionBits: '644' diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md index 6914647570..a909067269 100644 --- a/.gitlab/issue_templates/bug.md +++ b/.gitlab/issue_templates/bug.md @@ -3,27 +3,33 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md) 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) --> -**What happened?** _(Please give us a brief description of what happened.)_ +# **What happened?** + -**What did you expect to happen?** _(Please give us a brief description of what you expected to happen.)_ +# **What did you expect to happen?** + -**Version** _(What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information.)_ +# **Version** + -**Instance** _(What instance of Sharkey are you using?)_ +# **Instance** + -**What type of issue is this?** _(If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side.)_ +# **What type of issue is this?** + -**What browser are you using? (Client-side issues only)** +# **What browser are you using? (Client-side issues only)** -**What operating system are you using? (Client-side issues only)** +# **What operating system are you using? (Client-side issues only)** -**How do you deploy Sharkey on your server? (Server-side issues only)** +# **How do you deploy Sharkey on your server? (Server-side issues only)** -**What operating system are you using? (Server-side issues only)** +# **What operating system are you using? (Server-side issues only)** -**Relevant log output** _(Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.)_ +# **Relevant log output** + -**Contribution Guidelines** +# **Contribution Guidelines** By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines - [ ] I have searched the issue tracker for similar issues, and this is not a duplicate. diff --git a/.gitlab/issue_templates/feature.md b/.gitlab/issue_templates/feature.md index d4235eb5a3..a77f9335fe 100644 --- a/.gitlab/issue_templates/feature.md +++ b/.gitlab/issue_templates/feature.md @@ -3,15 +3,19 @@ 🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md) 🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) --> -**What feature would you like implemented?** _(Please give us a brief description of what you'd like.)_ +# **What feature would you like implemented?** + -**Why should we add this feature?** _(Please give us a brief description of why your feature is important.)_ +# **Why should we add this feature?** + -**Version** _(What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information.)_ +# **Version** + -**Instance** _(What instance of Sharkey are you using?)_ +# **Instance** + -**Contribution Guidelines** +# **Contribution Guidelines** By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines - [ ] I have searched the issue tracker for similar requests, and this is not a duplicate. diff --git a/.gitlab/merge_request_templates/default.md b/.gitlab/merge_request_templates/default.md index 18bffa5419..e6977def70 100644 --- a/.gitlab/merge_request_templates/default.md +++ b/.gitlab/merge_request_templates/default.md @@ -1,11 +1,12 @@ -**What does this PR do?** _(Please give us a brief description of what this PR does.)_ +# **What does this MR do?** + -**Contribution Guidelines** +# **Contribution Guidelines** By submitting this merge request, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) - [ ] I agree to follow this project's Contribution Guidelines -- [ ] I have made sure to test this pull request +- [ ] I have made sure to test this merge request diff --git a/packages/backend/package.json b/packages/backend/package.json index 58f5ef8e05..702b788061 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -95,6 +95,7 @@ "@swc/core": "1.9.2", "@transfem-org/sfm-js": "0.24.5", "@twemoji/parser": "15.1.1", + "@types/psl": "^1.1.3", "accepts": "1.3.8", "ajv": "8.17.1", "archiver": "7.0.1", @@ -137,6 +138,7 @@ "json5": "2.2.3", "jsonld": "8.3.2", "jsrsasign": "11.1.0", + "juice": "11.0.0", "megalodon": "workspace:*", "meilisearch": "0.45.0", "juice": "11.0.0", @@ -160,6 +162,7 @@ "probe-image-size": "7.2.3", "promise-limit": "2.7.0", "proxy-addr": "^2.0.7", + "psl": "^1.13.0", "pug": "3.0.3", "punycode": "2.3.1", "qrcode": "1.5.4", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 8f8ff5e0bd..4af1140f36 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -117,6 +117,7 @@ type Source = { }; pidFile: string; + filePermissionBits?: string; }; export type Config = { @@ -215,6 +216,7 @@ export type Config = { } | undefined; pidFile: string; + filePermissionBits?: string; }; const _filename = fileURLToPath(import.meta.url); @@ -351,6 +353,7 @@ export function loadConfig(): Config { deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), import: config.import, pidFile: config.pidFile, + filePermissionBits: config.filePermissionBits, }; } @@ -456,7 +459,10 @@ function applyEnvOverrides(config: Source) { } } - const alwaysStrings = { 'chmodSocket': true } as { [key: string]: boolean }; + const alwaysStrings: { [key in string]?: boolean } = { + 'chmodSocket': true, + 'filePermissionBits': true, + }; function _assign(path: (string | number)[], lastStep: string | number, value: string) { let thisConfig = config as any; @@ -494,7 +500,7 @@ function applyEnvOverrides(config: Source) { _apply_top(['sentryForBackend', 'enableNodeProfiling']); _apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]); _apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]); - _apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile']]); + _apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile', 'filePermissionBits']]); _apply_top(['import', ['downloadTimeout', 'maxFileSize']]); _apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature', 'setupPassword']]); } diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index f7371f8e79..b00c5796d2 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -4,7 +4,7 @@ */ import * as fs from 'node:fs'; -import { copyFile, mkdir, unlink, writeFile } from 'node:fs/promises'; +import { copyFile, unlink, writeFile, chmod } from 'node:fs/promises'; import * as Path from 'node:path'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; @@ -41,12 +41,20 @@ export class InternalStorageService { @bindThis public async saveFromPath(key: string, srcPath: string): Promise { await copyFile(srcPath, this.resolvePath(key)); - return `${this.config.url}/files/${key}`; + return await this.finalizeSavedFile(key); } @bindThis public async saveFromBuffer(key: string, data: Buffer): Promise { await writeFile(this.resolvePath(key), data); + return await this.finalizeSavedFile(key); + } + + private async finalizeSavedFile(key: string): Promise { + if (this.config.filePermissionBits) { + const path = this.resolvePath(key); + await chmod(path, this.config.filePermissionBits); + } return `${this.config.url}/files/${key}`; } diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 906adcfd27..f905914022 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -4,9 +4,10 @@ */ import { URL } from 'node:url'; -import { toASCII } from 'punycode'; +import punycode from 'punycode/punycode.js'; import { Inject, Injectable } from '@nestjs/common'; import RE2 from 're2'; +import psl from 'psl'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; @@ -106,13 +107,13 @@ export class UtilityService { @bindThis public toPuny(host: string): string { - return toASCII(host.toLowerCase()); + return punycode.toASCII(host.toLowerCase()); } @bindThis public toPunyNullable(host: string | null | undefined): string | null { if (host == null) return null; - return toASCII(host.toLowerCase()); + return punycode.toASCII(host.toLowerCase()); } @bindThis @@ -122,6 +123,27 @@ export class UtilityService { return host; } + @bindThis + private specialSuffix(hostname: string): string | null { + // masto.host provides domain names for its clients, we have to + // treat it as if it were a public suffix + const mastoHost = hostname.match(/\.?([a-zA-Z0-9-]+\.masto\.host)$/i); + if (mastoHost) { + return mastoHost[1]; + } + + return null; + } + + @bindThis + public punyHostPSLDomain(url: string): string { + const urlObj = new URL(url); + const hostname = urlObj.hostname; + const domain = this.specialSuffix(hostname) ?? psl.get(hostname) ?? hostname; + const host = `${this.toPuny(domain)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`; + return host; + } + @bindThis public isFederationAllowedHost(host: string): boolean { if (this.meta.federation === 'none') return false; diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index 77561bcda5..278c97f907 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -32,7 +32,7 @@ import { AbuseReportService } from '@/core/AbuseReportService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { fromTuple } from '@/misc/from-tuple.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isDislike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -41,7 +41,7 @@ import { ApAudienceService } from './ApAudienceService.js'; import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.js'; import type { Resolver } from './ApResolverService.js'; -import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; +import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IDislike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; @Injectable() export class ApInboxService { @@ -167,6 +167,8 @@ export class ApInboxService { return await this.announce(actor, activity, resolver); } else if (isLike(activity)) { return await this.like(actor, activity, resolver); + } else if (isDislike(activity)) { + return await this.dislike(actor, activity); } else if (isUndo(activity)) { return await this.undo(actor, activity, resolver); } else if (isBlock(activity)) { @@ -221,6 +223,11 @@ export class ApInboxService { } } + @bindThis + private async dislike(actor: MiRemoteUser, dislike: IDislike): Promise { + return await this.undoLike(actor, dislike); + } + @bindThis private async accept(actor: MiRemoteUser, activity: IAccept, resolver?: Resolver): Promise { const uri = activity.id ?? activity; @@ -783,7 +790,7 @@ export class ApInboxService { } @bindThis - private async undoLike(actor: MiRemoteUser, activity: ILike): Promise { + private async undoLike(actor: MiRemoteUser, activity: ILike | IDislike): Promise { const targetUri = getApId(activity.object); const note = await this.apNoteService.fetchNote(targetUri); diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 8fb4981ed4..8036c9638f 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -242,8 +242,10 @@ export class ApRequestService { const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]'); if (alternate) { const href = alternate.getAttribute('href'); - if (href && this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) { - return await this.signedGet(href, user, false); + if (href) { + if (this.utilityService.punyHostPSLDomain(url) === this.utilityService.punyHostPSLDomain(href)) { + return await this.signedGet(href, user, false); + } } } } catch (e) { diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index d4964d544d..e56b9ebc99 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -131,7 +131,7 @@ export class Resolver { throw new UnrecoverableError(`invalid AP object ${value}: missing id`); } - if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) { + if (this.utilityService.punyHostPSLDomain(object.id) !== this.utilityService.punyHostPSLDomain(value)) { throw new UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`); } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index dc4396da0d..e4c4fe54b5 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -192,8 +192,8 @@ export class ApNoteService { throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${entryUri}`); } - if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { - throw new Error(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`); + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) { + throw new UnrecoverableError(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`); } } @@ -444,7 +444,7 @@ export class ApNoteService { throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`); } - if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) { + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) { throw new UnrecoverableError(`note url <> id host mismatch: ${url} <> ${note.id} in ${noteUri}`); } } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 3908b71111..d32811c7e5 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -138,7 +138,7 @@ export class ApPersonService implements OnModuleInit { */ @bindThis private validateActor(x: IObject, uri: string): IActor { - const expectHost = this.utilityService.punyHost(uri); + const expectHost = this.utilityService.punyHostPSLDomain(uri); if (!isActor(x)) { throw new UnrecoverableError(`invalid Actor type '${x.type}' in ${uri}`); @@ -152,7 +152,7 @@ export class ApPersonService implements OnModuleInit { throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox type`); } - const inboxHost = this.utilityService.punyHost(x.inbox); + const inboxHost = this.utilityService.punyHostPSLDomain(x.inbox); if (inboxHost !== expectHost) { throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox ${inboxHost}`); } @@ -160,7 +160,7 @@ export class ApPersonService implements OnModuleInit { const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined); if (sharedInboxObject != null) { const sharedInbox = getApId(sharedInboxObject); - if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHost(sharedInbox) === expectHost)) { + if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHostPSLDomain(sharedInbox) === expectHost)) { throw new UnrecoverableError(`invalid Actor ${uri} - wrong shared inbox ${sharedInbox}`); } } @@ -170,7 +170,7 @@ export class ApPersonService implements OnModuleInit { if (xCollection != null) { const collectionUri = getApId(xCollection); if (typeof collectionUri === 'string' && collectionUri.length > 0) { - if (this.utilityService.punyHost(collectionUri) !== expectHost) { + if (this.utilityService.punyHostPSLDomain(collectionUri) !== expectHost) { throw new UnrecoverableError(`invalid Actor ${uri} - wrong ${collection} ${collectionUri}`); } } else if (collectionUri != null) { @@ -202,7 +202,7 @@ export class ApPersonService implements OnModuleInit { x.summary = truncate(x.summary, summaryLength); } - const idHost = this.utilityService.punyHost(x.id); + const idHost = this.utilityService.punyHostPSLDomain(x.id); if (idHost !== expectHost) { throw new UnrecoverableError(`invalid Actor ${uri} - wrong id ${x.id}`); } @@ -212,7 +212,7 @@ export class ApPersonService implements OnModuleInit { throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id type`); } - const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id); + const publicKeyIdHost = this.utilityService.punyHostPSLDomain(x.publicKey.id); if (publicKeyIdHost !== expectHost) { throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id ${x.publicKey.id}`); } @@ -357,7 +357,7 @@ export class ApPersonService implements OnModuleInit { throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`); } - if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) { throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`); } } @@ -574,7 +574,7 @@ export class ApPersonService implements OnModuleInit { throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`); } - if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) { + if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) { throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`); } } diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 0c689efa96..e61d89f9db 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -339,6 +339,10 @@ export interface ILike extends IActivity { _misskey_reaction?: string; } +export interface IDislike extends IActivity { + type: 'Dislike'; +} + export interface IAnnounce extends IActivity { type: 'Announce'; } @@ -371,6 +375,7 @@ export const isLike = (object: IObject): object is ILike => { const type = getApType(object); return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type); }; +export const isDislike = (object: IObject): object is IDislike => getApType(object) === 'Dislike'; export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce'; export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; diff --git a/packages/backend/test/unit/UtilityService.ts b/packages/backend/test/unit/UtilityService.ts new file mode 100644 index 0000000000..d86e794f2f --- /dev/null +++ b/packages/backend/test/unit/UtilityService.ts @@ -0,0 +1,43 @@ +import * as assert from 'assert'; +import { Test } from '@nestjs/testing'; + +import { CoreModule } from '@/core/CoreModule.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { GlobalModule } from '@/GlobalModule.js'; + +describe('UtilityService', () => { + let utilityService: UtilityService; + + beforeAll(async () => { + const app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + }).compile(); + utilityService = app.get(UtilityService); + }); + + describe('punyHost', () => { + test('simple', () => { + assert.equal(utilityService.punyHost('http://www.foo.com'), 'www.foo.com'); + }); + test('japanese', () => { + assert.equal(utilityService.punyHost('http://www.新聞.com'), 'www.xn--efvv70d.com'); + }); + }); + + describe('punyHostPSLDomain', () => { + test('simple', () => { + assert.equal(utilityService.punyHostPSLDomain('http://www.foo.com'), 'foo.com'); + }); + test('japanese', () => { + assert.equal(utilityService.punyHostPSLDomain('http://www.新聞.com'), 'xn--efvv70d.com'); + }); + test('lower', () => { + assert.equal(utilityService.punyHostPSLDomain('http://foo.github.io'), 'foo.github.io'); + assert.equal(utilityService.punyHostPSLDomain('http://foo.bar.github.io'), 'bar.github.io'); + }); + test('special', () => { + assert.equal(utilityService.punyHostPSLDomain('http://foo.masto.host'), 'foo.masto.host'); + assert.equal(utilityService.punyHostPSLDomain('http://foo.bar.masto.host'), 'bar.masto.host'); + }); + }); +}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 26de19eaf1..cf97473d14 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -612,8 +612,8 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) { username: config.db.user, password: config.db.pass, database: config.db.db, - synchronize: true && !justBorrow, - dropSchema: true && !justBorrow, + synchronize: !justBorrow, + dropSchema: !justBorrow, entities: initEntities ?? entities, }); diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index d15279d633..090cffe203 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -103,8 +103,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter async function getConfirmed(text: string): Promise { const confirm = await os.confirm({ - type: 'warning', - title: 'confirm', + type: 'question', text, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c77005128b..d8c06996a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,6 +170,9 @@ importers: '@twemoji/parser': specifier: 15.1.1 version: 15.1.1 + '@types/psl': + specifier: ^1.1.3 + version: 1.1.3 accepts: specifier: 1.3.8 version: 1.3.8 @@ -365,6 +368,9 @@ importers: proxy-addr: specifier: ^2.0.7 version: 2.0.7 + psl: + specifier: ^1.13.0 + version: 1.13.0 pug: specifier: 3.0.3 version: 3.0.3 @@ -4563,6 +4569,9 @@ packages: '@types/proxy-addr@2.0.3': resolution: {integrity: sha512-TgAHHO4tNG3HgLTUhB+hM4iwW6JUNeQHCLnF1DjaDA9c69PN+IasoFu2MYDhubFc+ZIw5c5t9DMtjvrD6R3Egg==} + '@types/psl@1.1.3': + resolution: {integrity: sha512-Iu174JHfLd7i/XkXY6VDrqSlPvTDQOtQI7wNAXKKOAADJ9TduRLkNdMgjGiMxSttUIZnomv81JAbAbC0DhggxA==} + '@types/pug@2.0.10': resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} @@ -9344,8 +9353,8 @@ packages: pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + psl@1.13.0: + resolution: {integrity: sha512-BFwmFXiJoFqlUpZ5Qssolv15DMyc84gTBds1BjsV1BfXEo1UyyD7GsmN67n7J77uRhoSNW1AXtXKPLcBFQn9Aw==} pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -15067,6 +15076,8 @@ snapshots: dependencies: '@types/node': 22.9.0 + '@types/psl@1.1.3': {} + '@types/pug@2.0.10': {} '@types/punycode@2.1.4': {} @@ -21108,7 +21119,9 @@ snapshots: pseudomap@1.0.2: {} - psl@1.9.0: {} + psl@1.13.0: + dependencies: + punycode: 2.3.1 pstree.remy@1.1.8: {} @@ -22305,7 +22318,7 @@ snapshots: tough-cookie@4.1.4: dependencies: - psl: 1.9.0 + psl: 1.13.0 punycode: 2.3.1 universalify: 0.2.0 url-parse: 1.5.10