diff options
| author | piuvas <mail@piuvas.net> | 2025-05-26 22:44:29 -0300 |
|---|---|---|
| committer | piuvas <mail@piuvas.net> | 2025-05-26 22:44:29 -0300 |
| commit | ca32c231d0aaab9dacaa88a0b0564d626827e797 (patch) | |
| tree | 680c63207a1ae13b91cdc586c5f5f216998930d0 /packages/backend/src | |
| parent | merge: Synchronize database entities and code models (!1040) (diff) | |
| download | sharkey-ca32c231d0aaab9dacaa88a0b0564d626827e797.tar.gz sharkey-ca32c231d0aaab9dacaa88a0b0564d626827e797.tar.bz2 sharkey-ca32c231d0aaab9dacaa88a0b0564d626827e797.zip | |
initial link attributions work.
Diffstat (limited to 'packages/backend/src')
9 files changed, 76 insertions, 2 deletions
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index f41eeba39f..46a78687f3 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -613,6 +613,7 @@ export class ApRendererService { enableRss: user.enableRss, speakAsCat: user.speakAsCat, attachment: attachment.length ? attachment : undefined, + attributionDomains: user.attributionDomains, }; if (user.movedToUri) { diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index 5c0b8ffcbb..cedd1d8dd5 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -546,6 +546,10 @@ const extension_context_definition = { featured: 'toot:featured', discoverable: 'toot:discoverable', indexable: 'toot:indexable', + attributionDomains: { + '@id': 'toot:attributionDomains', + '@type': '@id', + }, // schema schema: 'http://schema.org#', PropertyValue: 'schema:PropertyValue', diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 4b685f7e1b..744b339254 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -433,6 +433,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { makeNotesFollowersOnlyBefore: (person as any).makeNotesFollowersOnlyBefore ?? null, makeNotesHiddenBefore: (person as any).makeNotesHiddenBefore ?? null, emojis, + attributionDomains: (person as any).attributionDomains, })) as MiRemoteUser; let _description: string | null = null; @@ -616,6 +617,7 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { // We use "!== false" to handle incorrect types, missing / null values, and "default to true" logic. hideOnlineStatus: person.hideOnlineStatus !== false, isExplorable: person.discoverable !== false, + attributionDomains: person.attributionDomains, ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image, person.backgroundUrl).catch(() => ({}))), } as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'speakAsCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 281733d484..0122697f2a 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -255,6 +255,7 @@ export interface IActor extends IObject { enableRss?: boolean; listenbrainz?: string; backgroundUrl?: string; + attributionDomains?: string[]; } export const isCollection = (object: IObject): object is ICollection => diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 56506a5fa4..f66a36336d 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -603,6 +603,7 @@ export class UserEntityService implements OnModuleInit { enableRss: user.enableRss, mandatoryCW: user.mandatoryCW, rejectQuotes: user.rejectQuotes, + attributionDomains: user.attributionDomains, isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote), speakAsCat: user.speakAsCat ?? false, approved: user.approved, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 46f8e84a94..d5f572a879 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -378,6 +378,12 @@ export class MiUser { }) public allowUnsignedFetch: UserUnsignedFetchOption; + @Column('varchar', { + name: 'attributionDomains', + length: 128, array: true, default: '{}', + }) + public attributionDomains: string[]; + constructor(data: Partial<MiUser>) { if (data == null) return; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 964a179244..9bfb57fc66 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -236,6 +236,13 @@ export const packedUserLiteSchema = { }, }, }, + attributionDomains: { + type: 'object', + nullable: false, optional: false, + additionalProperties: { + type: 'string', + }, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index f35e395841..dad605f151 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -263,6 +263,9 @@ export const paramDef = { enum: userUnsignedFetchOptions, nullable: false, }, + attributionDomains: { type: 'array', items: { + type: 'string', + } }, }, } as const; @@ -373,6 +376,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; + if (ps.attributionDomains !== undefined) updates.attributionDomains = ps.attributionDomains; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; @@ -663,7 +667,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // these two methods need to be kept in sync with // `ApRendererService.renderPerson` private userNeedsPublishing(oldUser: MiLocalUser, newUser: Partial<MiUser>): boolean { - const basicFields: (keyof MiUser)[] = ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs', 'hideOnlineStatus', 'enableRss', 'requireSigninToViewContents', 'makeNotesFollowersOnlyBefore', 'makeNotesHiddenBefore']; + const basicFields: (keyof MiUser)[] = ['avatarId', 'bannerId', 'backgroundId', 'isBot', 'username', 'name', 'isLocked', 'isExplorable', 'isCat', 'noindex', 'speakAsCat', 'movedToUri', 'alsoKnownAs', 'hideOnlineStatus', 'enableRss', 'requireSigninToViewContents', 'makeNotesFollowersOnlyBefore', 'makeNotesHiddenBefore', 'attributionDomains']; for (const field of basicFields) { if ((field in newUser) && oldUser[field] !== newUser[field]) { return true; diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 203bc908a8..d5d62c67c0 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -19,7 +19,8 @@ import { MiMeta } from '@/models/Meta.js'; import { RedisKVCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; -import type { MiAccessToken, NotesRepository } from '@/models/_.js'; +import type { MiAccessToken, NotesRepository, UsersRepository } from '@/models/_.js'; +import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js'; import { ApRequestService } from '@/core/activitypub/ApRequestService.js'; import { SystemAccountService } from '@/core/SystemAccountService.js'; @@ -34,6 +35,12 @@ import type { FastifyRequest, FastifyReply } from 'fastify'; export type LocalSummalyResult = SummalyResult & { haveNoteLocally?: boolean; + linkAttribution?: { + name: string, + username: string, + avatarUrl: string, + avatarBlurhash: string, + } }; // Increment this to invalidate cached previews after a major change. @@ -77,11 +84,15 @@ export class UrlPreviewService { @Inject(DI.notesRepository) private readonly notesRepository: NotesRepository, + @Inject(DI.usersRepository) + private readonly usersRepository: UsersRepository, + private httpRequestService: HttpRequestService, private loggerService: LoggerService, private readonly utilityService: UtilityService, private readonly apUtilityService: ApUtilityService, private readonly apDbResolverService: ApDbResolverService, + private readonly remoteUserResolveService: RemoteUserResolveService, private readonly apRequestService: ApRequestService, private readonly systemAccountService: SystemAccountService, private readonly apNoteService: ApNoteService, @@ -206,6 +217,8 @@ export class UrlPreviewService { } } + await this.validateLinkAttribution(summary); + // Await this to avoid hammering redis when a bunch of URLs are fetched at once await this.previewCache.set(cacheKey, summary); @@ -426,6 +439,41 @@ export class UrlPreviewService { } } + private async validateLinkAttribution(summary: LocalSummalyResult) { + if (!summary.fediverseCreator) return; + + const url = URL.parse(summary.url); + if (!url) return; + let fediverseCreator = summary.fediverseCreator; + // expecting either '@username@host' or 'username@host' + if (fediverseCreator.startsWith('@')) { + fediverseCreator = fediverseCreator.substring(1); + } + + // + const array = fediverseCreator.split('@'); + const username = array[0].toLowerCase(); + let host: string | null = array[1]; + if (host.toLowerCase() === this.config.host) { + host = null; + } + try { + const user = await this.remoteUserResolveService.resolveUser(username, host) + + const attributionDomains = user.attributionDomains; + if (attributionDomains.some(x => `.${url.host.toLowerCase()}`.endsWith(`.${x}`))) { + summary.linkAttribution = { + name: user.name ?? user.username, + username: fediverseCreator, + avatarUrl: user.avatarUrl ?? '', + avatarBlurhash: user.avatarBlurhash ?? '', + } + } + } catch { + console.warn('user not found: ' + fediverseCreator) + } + } + // Adapted from ApiCallService private async checkFetchPermissions(auth: AuthArray, reply: FastifyReply): Promise<boolean> { const [user, app] = auth; |