Skip to content

Commit

Permalink
相互リンク機能の追加 (#319)
Browse files Browse the repository at this point in the history
  • Loading branch information
kozakura913 authored Aug 15, 2024
2 parents 950d232 + 44be14b commit 1695e17
Show file tree
Hide file tree
Showing 35 changed files with 809 additions and 12 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG_YOJO.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
### Release Date

### General
-
- Feat: 相互リンク機能の追加 [#319](https://github.com/yojo-art/cherrypick/pull/319)
- (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/675)
- (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/684)
- (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/690)
- (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/696)

### Client
-
Expand Down
10 changes: 10 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2541,6 +2541,16 @@ _profile:
changeBanner: "Change banner"
verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field."
avatarDecorationMax: "You can add up to {max} decorations."
mutualLinksEdit: "Edit mutual links"
mutualLinksBanner: "Banner of mutual links"
mutualLinksDescriptionEdit: "Description"
mutualLinksUrl: "URL of the link"
mutualLinksDescription: "Mutual links are displayed as banners on your profile."
addMutualLink: "Add mutual link"
addMutualLinkSection: "Add section"
sectionName: "Section name"
sectionNameNoneDescription: "Do not display the section name"
sectionNameNone: "Section without name"
_exportOrImport:
allNotes: "All notes"
favoritedNotes: "Favorite notes"
Expand Down
76 changes: 76 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2826,6 +2826,14 @@ export interface Locale extends ILocale {
* バナーを解除しますか?
*/
"unsetUserBannerConfirm": string;
/**
* 相互リンクを削除
*/
"unsetUserMutualLink": string;
/**
* 相互リンクを削除しますか?
*/
"unsetUserMutualLinkConfirm": string;
/**
* すべてのファイルを削除
*/
Expand Down Expand Up @@ -5683,6 +5691,18 @@ export interface Locale extends ILocale {
"autoSuspendedForNotResponding": string;
};
};
/**
* 相互リンク
*/
"mutualLink": string;
/**
* このユーザーのバナー
*/
"mutualBannerThisUser": string;
/**
* 最大
*/
"maximum": string;
"_bubbleGame": {
/**
* 遊び方
Expand Down Expand Up @@ -7732,6 +7752,14 @@ export interface Locale extends ILocale {
* アイコンデコレーションの最大取付個数
*/
"avatarDecorationLimit": string;
/**
* 相互リンクのセクションの最大数
*/
"mutualLinkSectionLimit": string;
/**
* セクション内の相互リンクの最大数
*/
"mutualLinkLimit": string;
};
"_condition": {
/**
Expand Down Expand Up @@ -9415,6 +9443,10 @@ export interface Locale extends ILocale {
* ユーザーのバーナーを削除する
*/
"write:admin:unset-user-banner": string;
/**
* ユーザーの相互リンクを削除する
*/
"write:admin:unset-user-mutual-link": string;
/**
* ユーザーの凍結を解除する
*/
Expand Down Expand Up @@ -10017,6 +10049,50 @@ export interface Locale extends ILocale {
* 最大{max}つまでデコレーションを付けられます。
*/
"avatarDecorationMax": ParameterizedString<"max">;
/**
* 相互リンクを編集
*/
"mutualLinksEdit": string;
/**
* 相互リンクのバナー
*/
"mutualLinksBanner": string;
/**
* 説明
*/
"mutualLinksDescriptionEdit": string;
/**
* リンク先のURL
*/
"mutualLinksUrl": string;
/**
* このセクションをプロフィールにピン留め
*/
"mutualLinkPining": string;
/**
* 相互リンクを設定すると、あなたのプロフィールにバナーが表示されます。
*/
"mutualLinksDescription": string;
/**
* 相互リンクを追加
*/
"addMutualLink": string;
/**
* セクションを追加
*/
"addMutualLinkSection": string;
/**
* セクション名
*/
"sectionName": string;
/**
* セクション名を表示しないようにする
*/
"sectionNameNoneDescription": string;
/**
* 名前が表示されないセクション
*/
"sectionNameNone": string;
};
"_exportOrImport": {
/**
Expand Down
19 changes: 19 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,8 @@ unsetUserAvatar: "アイコンを解除"
unsetUserAvatarConfirm: "アイコンを解除しますか?"
unsetUserBanner: "バナーを解除"
unsetUserBannerConfirm: "バナーを解除しますか?"
unsetUserMutualLink: "相互リンクを削除"
unsetUserMutualLinkConfirm: "相互リンクを削除しますか?"
deleteAllFiles: "すべてのファイルを削除"
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
removeAllFollowing: "フォローを全解除"
Expand Down Expand Up @@ -1425,6 +1427,9 @@ _delivery:
manuallySuspended: "手動停止中"
goneSuspended: "サーバー削除のため停止中"
autoSuspendedForNotResponding: "サーバー応答なしのため停止中"
mutualLink: "相互リンク"
mutualBannerThisUser: "このユーザーのバナー"
maximum: "最大"

_bubbleGame:
howToPlay: "遊び方"
Expand Down Expand Up @@ -2007,6 +2012,8 @@ _role:
canAdvancedSearchNotes: "高度な検索の利用"
canUseTranslator: "翻訳機能の利用"
avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
mutualLinkSectionLimit: "相互リンクのセクションの最大数"
mutualLinkLimit: "セクション内の相互リンクの最大数"
_condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー"
Expand Down Expand Up @@ -2476,6 +2483,7 @@ _permissions:
"write:admin:suspend-user": "ユーザーを凍結する"
"write:admin:unset-user-avatar": "ユーザーのアバターを削除する"
"write:admin:unset-user-banner": "ユーザーのバーナーを削除する"
"write:admin:unset-user-mutual-link": "ユーザーの相互リンクを削除する"
"write:admin:unsuspend-user": "ユーザーの凍結を解除する"
"write:admin:meta": "インスタンスのメタデータを操作する"
"write:admin:user-note": "モデレーションノートを操作する"
Expand Down Expand Up @@ -2641,6 +2649,17 @@ _profile:
changeBanner: "バナー画像を変更"
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
mutualLinksEdit: "相互リンクを編集"
mutualLinksBanner: "相互リンクのバナー"
mutualLinksDescriptionEdit: "説明"
mutualLinksUrl: "リンク先のURL"
mutualLinkPining: "このセクションをプロフィールにピン留め"
mutualLinksDescription: "相互リンクを設定すると、あなたのプロフィールにバナーが表示されます。"
addMutualLink: "相互リンクを追加"
addMutualLinkSection: "セクションを追加"
sectionName: "セクション名"
sectionNameNoneDescription: "セクション名を表示しないようにする"
sectionNameNone: "名前が表示されないセクション"

_exportOrImport:
allNotes: "全てのノート"
Expand Down
10 changes: 10 additions & 0 deletions locales/ko-KR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2522,6 +2522,16 @@ _profile:
changeBanner: "배너 이미지 변경"
verifiedLinkDescription: "내용에 자신의 프로필로 향하는 링크가 포함된 페이지의 URL을 삽입하면 소유자 인증 마크가 표시돼요."
avatarDecorationMax: "최대 {max}개까지 장식을 달 수 있어요."
mutualLinksEdit: "서로링크 편집"
mutualLinksBanner: "서로링크 배너"
mutualLinksDescriptionEdit: "설명"
mutualLinksUrl: "링크 URL"
mutualLinksDescription: "서로링크를 설정하면 프로필에 배너가 표시됩니다."
addMutualLink: "서로링크 추가"
addMutualLinkSection: "섹션 추가"
sectionName: "섹션 이름"
sectionNameNoneDescription: "섹션 이름이 표시되지 않도록 합니다."
sectionNameNone: "이름이 표시되지 않는 섹션"
_exportOrImport:
allNotes: "모든 노트"
favoritedNotes: "즐겨찾기한 노트"
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/migration/1723311628855-mutuallinks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project MisskeyIO
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class Mutuallinks1723311628855 {
name = 'Mutuallinks1723311628855'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutualLinkSections" jsonb NOT NULL DEFAULT '[]'`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutualLinkSections"`);
}
}
6 changes: 6 additions & 0 deletions packages/backend/src/core/RoleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export type RolePolicies = {
rateLimitFactor: number;
avatarDecorationLimit: number;
fileSizeLimit: number;
mutualLinkSectionLimit: number;
mutualLinkLimit: number;
};

export const DEFAULT_POLICIES: RolePolicies = {
Expand Down Expand Up @@ -93,6 +95,8 @@ export const DEFAULT_POLICIES: RolePolicies = {
rateLimitFactor: 1,
avatarDecorationLimit: 1,
fileSizeLimit: 50,
mutualLinkSectionLimit: 1,
mutualLinkLimit: 15,
};

@Injectable()
Expand Down Expand Up @@ -398,6 +402,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
fileSizeLimit: calc('fileSizeLimit', vs => Math.max(...vs)),
mutualLinkSectionLimit: calc('mutualLinkSectionLimit', vs => Math.max(...vs)),
mutualLinkLimit: calc('mutualLinkLimit', vs => Math.max(...vs)),
};
}

Expand Down
19 changes: 19 additions & 0 deletions packages/backend/src/core/activitypub/ApRendererService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,25 @@ export class ApRendererService {
person['vcard:Address'] = profile.location;
}

if (profile.mutualLinkSections.length > 0) {
const ApMutualLinkSections = await Promise.all(profile.mutualLinkSections.map(async section => {
return {
sectionName: section.name ? this.mfmService.toHtml(mfm.parse(section.name)) : null,
_misskey_sectionName: section.name,
entrys: await Promise.all(section.mutualLinks.map(async entry => {
const img = await this.driveFilesRepository.findOneBy({ id: entry.fileId });
return {
description: entry.description ? this.mfmService.toHtml(mfm.parse(entry.description)) : null,
_misskey_description: entry.description,
image: img ? this.renderImage(img) : null,
url: entry.url,
};
})),
};
}));
person.banner = ApMutualLinkSections;
}

return person;
}

Expand Down
48 changes: 46 additions & 2 deletions packages/backend/src/core/activitypub/models/ApPersonService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import promiseLimit from 'promise-limit';
import { DataSource } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
import type { FollowingsRepository, InstancesRepository, MiDriveFile, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { MiUser } from '@/models/User.js';
Expand Down Expand Up @@ -47,7 +47,7 @@ import type { ApNoteService } from './ApNoteService.js';
import type { ApMfmService } from '../ApMfmService.js';
import type { ApResolverService, Resolver } from '../ApResolverService.js';
import type { ApLoggerService } from '../ApLoggerService.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports

import type { ApImageService } from './ApImageService.js';
import type { IActor, IObject } from '../type.js';

Expand Down Expand Up @@ -448,6 +448,7 @@ export class ApPersonService implements OnModuleInit {
birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null,
userHost: host,
mutualLinkSections: await this.mutualLinkSections(person, user),
}));

if (person.publicKey) {
Expand Down Expand Up @@ -696,6 +697,7 @@ export class ApPersonService implements OnModuleInit {
description: _description,
birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null,
mutualLinkSections: await this.mutualLinkSections(person, exist),
});

this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: exist.id });
Expand Down Expand Up @@ -736,6 +738,48 @@ export class ApPersonService implements OnModuleInit {

return 'skip';
}
async mutualLinkSections(person: IActor, actor: MiRemoteUser) : Promise<[] | {
name: string | null;
mutualLinks: {
fileId: MiDriveFile['id'];
description: string | null;
imgSrc: string;
url: string;
}[];
}[]> {
const apMutualLinkSections = person.banner;

if (apMutualLinkSections === undefined) return [];

return await Promise.all(apMutualLinkSections.map(async ap => {
let name = null;
if (ap._misskey_sectionName) {
name = truncate(ap._misskey_sectionName, summaryLength);
} else if (ap.sectionName) {
name = this.apMfmService.htmlToMfm(truncate(ap.sectionName, summaryLength), person.tag);
}
return {
name,
mutualLinks: (await Promise.all(ap.entrys.map(async entry => {
if (entry.url === null) return null;
const image = entry.image ? await this.apImageService.resolveImage(actor, entry.image).catch(() => null) : null;
if (image === null) return null;
let description = null;
if (entry._misskey_description) {
description = truncate(entry._misskey_description, summaryLength);
} else if (entry.description) {
description = this.apMfmService.htmlToMfm(truncate(entry.description, summaryLength), person.tag);
}
return {
fileId: image.id,
imgSrc: image.url,
url: entry.url,
description,
};
}))).filter(e => e !== null),
};
}));
}

/**
* Personを解決します。
Expand Down
10 changes: 10 additions & 0 deletions packages/backend/src/core/activitypub/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,16 @@ export interface IActor extends IObject {
};
'vcard:bday'?: string;
'vcard:Address'?: string;
banner?: {
sectionName?: string | null;
_misskey_sectionName?: string | null;
entrys: {
description?: string | null;
_misskey_description: string | null;
image: string | IObject | null;//ap image
url: string | null;//link to
}[] | [];
}[];
}

export const isCollection = (object: IObject): object is ICollection =>
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { PageEntityService } from './PageEntityService.js';

const Ajv = _Ajv.default;
Expand Down Expand Up @@ -573,6 +572,7 @@ export class UserEntityService implements OnModuleInit {
lang: profile!.lang,
fields: profile!.fields,
verifiedLinks: profile!.verifiedLinks,
mutualLinkSections: profile!.mutualLinkSections,
followersCount: followersCount ?? '?',
followingCount: followingCount ?? '?',
notesCount: user.notesCount,
Expand Down
Loading

0 comments on commit 1695e17

Please sign in to comment.