Merge branch 'misskey-dev:develop' into error-style
This commit is contained in:
		
						commit
						8a335caff5
					
				
					 170 changed files with 2102 additions and 1299 deletions
				
			
		
							
								
								
									
										10
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -22,21 +22,29 @@ You should also include the user name that made the change. | ||||||
| - Client: Improve control panel @syuilo | - Client: Improve control panel @syuilo | ||||||
| - Client: Show warning in control panel when there is an unresolved abuse report @syuilo | - Client: Show warning in control panel when there is an unresolved abuse report @syuilo | ||||||
| - Client: Add instance-cloud widget @syuilo | - Client: Add instance-cloud widget @syuilo | ||||||
| - Client: Add rss-marquee widget @syuilo | - Client: Add rss-ticker widget @syuilo | ||||||
| - Client: Removing entries from a clip @futchitwo | - Client: Removing entries from a clip @futchitwo | ||||||
| - Client: Poll highlights in explore page @syuilo | - Client: Poll highlights in explore page @syuilo | ||||||
|  | - Client: Improve deck UI @syuilo | ||||||
|  | - Client: Word mute also checks content warnings @Johann150 | ||||||
|  | - ユーザーにモデレーションメモを残せる機能 @syuilo | ||||||
| - Make possible to delete an account by admin @syuilo | - Make possible to delete an account by admin @syuilo | ||||||
| - Improve player detection in URL preview @mei23 | - Improve player detection in URL preview @mei23 | ||||||
| - Add Badge Image to Push Notification #8012 @tamaina | - Add Badge Image to Push Notification #8012 @tamaina | ||||||
| - Server: Improve performance | - Server: Improve performance | ||||||
| - Server: Supports IPv6 on Redis transport. @mei23   | - Server: Supports IPv6 on Redis transport. @mei23   | ||||||
|   IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`. |   IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`. | ||||||
|  | - Server: Add possibility to log IP addresses of users @syuilo | ||||||
| 
 | 
 | ||||||
| ### Bugfixes | ### Bugfixes | ||||||
| - Server: Fix GenerateVideoThumbnail failed @mei23 | - Server: Fix GenerateVideoThumbnail failed @mei23 | ||||||
| - Server: Ensure temp directory cleanup @Johann150 | - Server: Ensure temp directory cleanup @Johann150 | ||||||
| - favicons of federated instances not showing @syuilo | - favicons of federated instances not showing @syuilo | ||||||
| - Admin: The checkbox for blocking an instance works again @Johann150 | - Admin: The checkbox for blocking an instance works again @Johann150 | ||||||
|  | - Client: Prevent access to user pages when not logged in @pixeldesu @Johann150 | ||||||
|  | - Client: Disable some hotkeys (e.g. for creating a post) for not logged in users @pixeldesu | ||||||
|  | - Client: Ask users that are not logged in to log in when trying to vote in a poll @Johann150 | ||||||
|  | - Instance mutes also apply in antennas etc. @Johann150 | ||||||
| 
 | 
 | ||||||
| ## 12.111.1 (2022/06/13) | ## 12.111.1 (2022/06/13) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -381,6 +381,7 @@ administrator: "管理者" | ||||||
| token: "トークン" | token: "トークン" | ||||||
| twoStepAuthentication: "二段階認証" | twoStepAuthentication: "二段階認証" | ||||||
| moderator: "モデレーター" | moderator: "モデレーター" | ||||||
|  | moderation: "モデレーション" | ||||||
| nUsersMentioned: "{n}人が投稿" | nUsersMentioned: "{n}人が投稿" | ||||||
| securityKey: "セキュリティキー" | securityKey: "セキュリティキー" | ||||||
| securityKeyName: "キーの名前" | securityKeyName: "キーの名前" | ||||||
|  | @ -854,6 +855,7 @@ noEmailServerWarning: "メールサーバーの設定がされていません。 | ||||||
| thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。" | thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。" | ||||||
| recommended: "推奨" | recommended: "推奨" | ||||||
| check: "チェック" | check: "チェック" | ||||||
|  | requireAdminForView: "閲覧するには管理者アカウントでログインしている必要があります。" | ||||||
| isSystemAccount: "システムにより自動で作成・管理されているアカウントです。" | isSystemAccount: "システムにより自動で作成・管理されているアカウントです。" | ||||||
| typeToConfirm: "この操作を行うには {x} と入力してください" | typeToConfirm: "この操作を行うには {x} と入力してください" | ||||||
| deleteAccount: "アカウント削除" | deleteAccount: "アカウント削除" | ||||||
|  | @ -861,6 +863,9 @@ document: "ドキュメント" | ||||||
| numberOfPageCache: "ページキャッシュ数" | numberOfPageCache: "ページキャッシュ数" | ||||||
| numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。" | numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。" | ||||||
| logoutConfirm: "ログアウトしますか?" | logoutConfirm: "ログアウトしますか?" | ||||||
|  | lastActiveDate: "最終利用日時" | ||||||
|  | statusbar: "ステータスバー" | ||||||
|  | pleaseSelect: "選択してください" | ||||||
| 
 | 
 | ||||||
| _emailUnavailable: | _emailUnavailable: | ||||||
|   used: "既に使用されています" |   used: "既に使用されています" | ||||||
|  | @ -1246,7 +1251,7 @@ _widgets: | ||||||
|   trends: "トレンド" |   trends: "トレンド" | ||||||
|   clock: "時計" |   clock: "時計" | ||||||
|   rss: "RSSリーダー" |   rss: "RSSリーダー" | ||||||
|   rssMarquee: "RSSリーダー(マーキー)" |   rssTicker: "RSSティッカー" | ||||||
|   activity: "アクティビティ" |   activity: "アクティビティ" | ||||||
|   photos: "フォト" |   photos: "フォト" | ||||||
|   digitalClock: "デジタル時計" |   digitalClock: "デジタル時計" | ||||||
|  | @ -1716,8 +1721,6 @@ _notification: | ||||||
| _deck: | _deck: | ||||||
|   alwaysShowMainColumn: "常にメインカラムを表示" |   alwaysShowMainColumn: "常にメインカラムを表示" | ||||||
|   columnAlign: "カラムの寄せ" |   columnAlign: "カラムの寄せ" | ||||||
|   columnMargin: "カラム間のマージン" |  | ||||||
|   columnHeaderHeight: "カラムのヘッダー幅" |  | ||||||
|   addColumn: "カラムを追加" |   addColumn: "カラムを追加" | ||||||
|   swapLeft: "左に移動" |   swapLeft: "左に移動" | ||||||
|   swapRight: "右に移動" |   swapRight: "右に移動" | ||||||
|  | @ -1726,6 +1729,8 @@ _deck: | ||||||
|   stackLeft: "左に重ねる" |   stackLeft: "左に重ねる" | ||||||
|   popRight: "右に出す" |   popRight: "右に出す" | ||||||
|   profile: "プロファイル" |   profile: "プロファイル" | ||||||
|  |   introduction: "カラムを組み合わせて自分だけのインターフェイスを作りましょう!" | ||||||
|  |   introduction2: "画面の右にある + を押して、いつでもカラムを追加できます。" | ||||||
| 
 | 
 | ||||||
|   _columns: |   _columns: | ||||||
|     main: "メイン" |     main: "メイン" | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
| 	"name": "misskey", | 	"name": "misskey", | ||||||
| 	"version": "12.112.0-beta.11", | 	"version": "12.112.0-beta.16", | ||||||
| 	"codename": "indigo", | 	"codename": "indigo", | ||||||
| 	"repository": { | 	"repository": { | ||||||
| 		"type": "git", | 		"type": "git", | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								packages/backend/migration/1655918165614-user-ip.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								packages/backend/migration/1655918165614-user-ip.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | export class userIp1655918165614 { | ||||||
|  |     name = 'userIp1655918165614' | ||||||
|  | 
 | ||||||
|  |     async up(queryRunner) { | ||||||
|  |         await queryRunner.query(`CREATE TABLE "user_ip" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "ip" character varying(128) NOT NULL, CONSTRAINT "PK_2c44ddfbf7c0464d028dcef325e" PRIMARY KEY ("id"))`); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_7f7f1c66f48e9a8e18a33bc515" ON "user_ip" ("userId") `); | ||||||
|  |         await queryRunner.query(`CREATE UNIQUE INDEX "IDX_361b500e06721013c124b7b6c5" ON "user_ip" ("userId", "ip") `); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_ip" ADD CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async down(queryRunner) { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_ip" DROP CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150"`); | ||||||
|  |         await queryRunner.query(`DROP INDEX "public"."IDX_361b500e06721013c124b7b6c5"`); | ||||||
|  |         await queryRunner.query(`DROP INDEX "public"."IDX_7f7f1c66f48e9a8e18a33bc515"`); | ||||||
|  |         await queryRunner.query(`DROP TABLE "user_ip"`); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								packages/backend/migration/1656122560740-file-ip.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/backend/migration/1656122560740-file-ip.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | export class fileIp1656122560740 { | ||||||
|  |     name = 'fileIp1656122560740' | ||||||
|  | 
 | ||||||
|  |     async up(queryRunner) { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "drive_file" ADD "requestHeaders" jsonb DEFAULT '{}'`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "drive_file" ADD "requestIp" character varying(128)`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async down(queryRunner) { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "requestIp"`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "requestHeaders"`); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								packages/backend/migration/1656328812281-ip-2.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/backend/migration/1656328812281-ip-2.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | ||||||
|  | export class ip21656328812281 { | ||||||
|  |     name = 'ip21656328812281' | ||||||
|  | 
 | ||||||
|  |     async up(queryRunner) { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_ip" DROP CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150"`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "meta" ADD "enableIpLogging" boolean NOT NULL DEFAULT false`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async down(queryRunner) { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableIpLogging"`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_ip" ADD CONSTRAINT "FK_7f7f1c66f48e9a8e18a33bc5150" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,11 @@ | ||||||
|  | export class userModerationNote1656772790599 { | ||||||
|  |     name = 'userModerationNote1656772790599' | ||||||
|  | 
 | ||||||
|  |     async up(queryRunner) { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_profile" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async down(queryRunner) { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "moderationNote"`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -92,6 +92,7 @@ | ||||||
| 		"rename": "1.0.4", | 		"rename": "1.0.4", | ||||||
| 		"require-all": "3.0.0", | 		"require-all": "3.0.0", | ||||||
| 		"rndstr": "1.0.0", | 		"rndstr": "1.0.0", | ||||||
|  | 		"rss-parser": "3.12.0", | ||||||
| 		"s-age": "1.1.2", | 		"s-age": "1.1.2", | ||||||
| 		"sanitize-html": "2.7.0", | 		"sanitize-html": "2.7.0", | ||||||
| 		"semver": "7.3.7", | 		"semver": "7.3.7", | ||||||
|  |  | ||||||
|  | @ -68,9 +68,10 @@ import { RegistryItem } from '@/models/entities/registry-item.js'; | ||||||
| import { Ad } from '@/models/entities/ad.js'; | import { Ad } from '@/models/entities/ad.js'; | ||||||
| import { PasswordResetRequest } from '@/models/entities/password-reset-request.js'; | import { PasswordResetRequest } from '@/models/entities/password-reset-request.js'; | ||||||
| import { UserPending } from '@/models/entities/user-pending.js'; | import { UserPending } from '@/models/entities/user-pending.js'; | ||||||
|  | import { Webhook } from '@/models/entities/webhook.js'; | ||||||
|  | import { UserIp } from '@/models/entities/user-ip.js'; | ||||||
| 
 | 
 | ||||||
| import { entities as charts } from '@/services/chart/entities.js'; | import { entities as charts } from '@/services/chart/entities.js'; | ||||||
| import { Webhook } from '@/models/entities/webhook.js'; |  | ||||||
| import { envOption } from '../env.js'; | import { envOption } from '../env.js'; | ||||||
| import { dbLogger } from './logger.js'; | import { dbLogger } from './logger.js'; | ||||||
| import { redisClient } from './redis.js'; | import { redisClient } from './redis.js'; | ||||||
|  | @ -173,6 +174,7 @@ export const entities = [ | ||||||
| 	PasswordResetRequest, | 	PasswordResetRequest, | ||||||
| 	UserPending, | 	UserPending, | ||||||
| 	Webhook, | 	Webhook, | ||||||
|  | 	UserIp, | ||||||
| 	...charts, | 	...charts, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; | import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; | ||||||
|  | import { id } from '../id.js'; | ||||||
| import { User } from './user.js'; | import { User } from './user.js'; | ||||||
| import { DriveFolder } from './drive-folder.js'; | import { DriveFolder } from './drive-folder.js'; | ||||||
| import { id } from '../id.js'; |  | ||||||
| 
 | 
 | ||||||
| @Entity() | @Entity() | ||||||
| @Index(['userId', 'folderId', 'id']) | @Index(['userId', 'folderId', 'id']) | ||||||
|  | @ -165,4 +165,15 @@ export class DriveFile { | ||||||
| 		comment: 'Whether the DriveFile is direct link to remote server.', | 		comment: 'Whether the DriveFile is direct link to remote server.', | ||||||
| 	}) | 	}) | ||||||
| 	public isLink: boolean; | 	public isLink: boolean; | ||||||
|  | 
 | ||||||
|  | 	@Column('jsonb', { | ||||||
|  | 		default: {}, | ||||||
|  | 		nullable: true, | ||||||
|  | 	}) | ||||||
|  | 	public requestHeaders: Record<string, string> | null; | ||||||
|  | 
 | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 128, nullable: true, | ||||||
|  | 	}) | ||||||
|  | 	public requestIp: string | null; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; | import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm'; | ||||||
| import { User } from './user.js'; |  | ||||||
| import { id } from '../id.js'; | import { id } from '../id.js'; | ||||||
|  | import { User } from './user.js'; | ||||||
| import { Clip } from './clip.js'; | import { Clip } from './clip.js'; | ||||||
| 
 | 
 | ||||||
| @Entity() | @Entity() | ||||||
|  | @ -427,4 +427,9 @@ export class Meta { | ||||||
| 		default: true, | 		default: true, | ||||||
| 	}) | 	}) | ||||||
| 	public objectStorageS3ForcePathStyle: boolean; | 	public objectStorageS3ForcePathStyle: boolean; | ||||||
|  | 
 | ||||||
|  | 	@Column('boolean', { | ||||||
|  | 		default: false, | ||||||
|  | 	}) | ||||||
|  | 	public enableIpLogging: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										24
									
								
								packages/backend/src/models/entities/user-ip.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/backend/src/models/entities/user-ip.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | ||||||
|  | import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; | ||||||
|  | import { id } from '../id.js'; | ||||||
|  | import { Note } from './note.js'; | ||||||
|  | import { User } from './user.js'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | @Index(['userId', 'ip'], { unique: true }) | ||||||
|  | export class UserIp { | ||||||
|  | 	@PrimaryGeneratedColumn() | ||||||
|  | 	public id: string; | ||||||
|  | 
 | ||||||
|  | 	@Column('timestamp with time zone', { | ||||||
|  | 	}) | ||||||
|  | 	public createdAt: Date; | ||||||
|  | 
 | ||||||
|  | 	@Index() | ||||||
|  | 	@Column(id()) | ||||||
|  | 	public userId: User['id']; | ||||||
|  | 
 | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 128, | ||||||
|  | 	}) | ||||||
|  | 	public ip: string; | ||||||
|  | } | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; | import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; | ||||||
|  | import { ffVisibility, notificationTypes } from '@/types.js'; | ||||||
| import { id } from '../id.js'; | import { id } from '../id.js'; | ||||||
| import { User } from './user.js'; | import { User } from './user.js'; | ||||||
| import { Page } from './page.js'; | import { Page } from './page.js'; | ||||||
| import { ffVisibility, notificationTypes } from '@/types.js'; |  | ||||||
| 
 | 
 | ||||||
| // TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
 | // TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
 | ||||||
| //       ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
 | //       ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
 | ||||||
|  | @ -117,6 +117,11 @@ export class UserProfile { | ||||||
| 	}) | 	}) | ||||||
| 	public password: string | null; | 	public password: string | null; | ||||||
| 
 | 
 | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 8192, default: '', | ||||||
|  | 	}) | ||||||
|  | 	public moderationNote: string | null; | ||||||
|  | 
 | ||||||
| 	// TODO: そのうち消す
 | 	// TODO: そのうち消す
 | ||||||
| 	@Column('jsonb', { | 	@Column('jsonb', { | ||||||
| 		default: {}, | 		default: {}, | ||||||
|  |  | ||||||
|  | @ -65,6 +65,7 @@ import { PasswordResetRequest } from './entities/password-reset-request.js'; | ||||||
| import { UserPending } from './entities/user-pending.js'; | import { UserPending } from './entities/user-pending.js'; | ||||||
| import { InstanceRepository } from './repositories/instance.js'; | import { InstanceRepository } from './repositories/instance.js'; | ||||||
| import { Webhook } from './entities/webhook.js'; | import { Webhook } from './entities/webhook.js'; | ||||||
|  | import { UserIp } from './entities/user-ip.js'; | ||||||
| 
 | 
 | ||||||
| export const Announcements = db.getRepository(Announcement); | export const Announcements = db.getRepository(Announcement); | ||||||
| export const AnnouncementReads = db.getRepository(AnnouncementRead); | export const AnnouncementReads = db.getRepository(AnnouncementRead); | ||||||
|  | @ -90,6 +91,7 @@ export const UserGroups = (UserGroupRepository); | ||||||
| export const UserGroupJoinings = db.getRepository(UserGroupJoining); | export const UserGroupJoinings = db.getRepository(UserGroupJoining); | ||||||
| export const UserGroupInvitations = (UserGroupInvitationRepository); | export const UserGroupInvitations = (UserGroupInvitationRepository); | ||||||
| export const UserNotePinings = db.getRepository(UserNotePining); | export const UserNotePinings = db.getRepository(UserNotePining); | ||||||
|  | export const UserIps = db.getRepository(UserIp); | ||||||
| export const UsedUsernames = db.getRepository(UsedUsername); | export const UsedUsernames = db.getRepository(UsedUsername); | ||||||
| export const Followings = (FollowingRepository); | export const Followings = (FollowingRepository); | ||||||
| export const FollowRequests = (FollowRequestRepository); | export const FollowRequests = (FollowRequestRepository); | ||||||
|  |  | ||||||
|  | @ -2,6 +2,9 @@ import httpSignature from '@peertube/http-signature'; | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| 
 | 
 | ||||||
| import config from '@/config/index.js'; | import config from '@/config/index.js'; | ||||||
|  | import { DriveFile } from '@/models/entities/drive-file.js'; | ||||||
|  | import { IActivity } from '@/remote/activitypub/type.js'; | ||||||
|  | import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js'; | ||||||
| import { envOption } from '../env.js'; | import { envOption } from '../env.js'; | ||||||
| 
 | 
 | ||||||
| import processDeliver from './processors/deliver.js'; | import processDeliver from './processors/deliver.js'; | ||||||
|  | @ -12,18 +15,15 @@ import processSystemQueue from './processors/system/index.js'; | ||||||
| import processWebhookDeliver from './processors/webhook-deliver.js'; | import processWebhookDeliver from './processors/webhook-deliver.js'; | ||||||
| import { endedPollNotification } from './processors/ended-poll-notification.js'; | import { endedPollNotification } from './processors/ended-poll-notification.js'; | ||||||
| import { queueLogger } from './logger.js'; | import { queueLogger } from './logger.js'; | ||||||
| import { DriveFile } from '@/models/entities/drive-file.js'; |  | ||||||
| import { getJobInfo } from './get-job-info.js'; | import { getJobInfo } from './get-job-info.js'; | ||||||
| import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js'; | import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js'; | ||||||
| import { ThinUser } from './types.js'; | import { ThinUser } from './types.js'; | ||||||
| import { IActivity } from '@/remote/activitypub/type.js'; |  | ||||||
| import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js'; |  | ||||||
| 
 | 
 | ||||||
| function renderError(e: Error): any { | function renderError(e: Error): any { | ||||||
| 	return { | 	return { | ||||||
| 		stack: e?.stack, | 		stack: e.stack, | ||||||
| 		message: e?.message, | 		message: e.message, | ||||||
| 		name: e?.name, | 		name: e.name, | ||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -314,6 +314,12 @@ export default function() { | ||||||
| 		removeOnComplete: true, | 		removeOnComplete: true, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
|  | 	systemQueue.add('clean', { | ||||||
|  | 	}, { | ||||||
|  | 		repeat: { cron: '0 0 * * *' }, | ||||||
|  | 		removeOnComplete: true, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
| 	systemQueue.add('checkExpiredMutings', { | 	systemQueue.add('checkExpiredMutings', { | ||||||
| 	}, { | 	}, { | ||||||
| 		repeat: { cron: '*/5 * * * *' }, | 		repeat: { cron: '*/5 * * * *' }, | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								packages/backend/src/queue/processors/system/clean.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								packages/backend/src/queue/processors/system/clean.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | import Bull from 'bull'; | ||||||
|  | import { LessThan } from 'typeorm'; | ||||||
|  | import { UserIps } from '@/models/index.js'; | ||||||
|  | 
 | ||||||
|  | import { queueLogger } from '../../logger.js'; | ||||||
|  | 
 | ||||||
|  | const logger = queueLogger.createSubLogger('clean'); | ||||||
|  | 
 | ||||||
|  | export async function clean(job: Bull.Job<Record<string, unknown>>, done: any): Promise<void> { | ||||||
|  | 	logger.info('Cleaning...'); | ||||||
|  | 
 | ||||||
|  | 	UserIps.delete({ | ||||||
|  | 		createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))), | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	logger.succ('Cleaned.'); | ||||||
|  | 	done(); | ||||||
|  | } | ||||||
|  | @ -3,12 +3,14 @@ import { tickCharts } from './tick-charts.js'; | ||||||
| import { resyncCharts } from './resync-charts.js'; | import { resyncCharts } from './resync-charts.js'; | ||||||
| import { cleanCharts } from './clean-charts.js'; | import { cleanCharts } from './clean-charts.js'; | ||||||
| import { checkExpiredMutings } from './check-expired-mutings.js'; | import { checkExpiredMutings } from './check-expired-mutings.js'; | ||||||
|  | import { clean } from './clean.js'; | ||||||
| 
 | 
 | ||||||
| const jobs = { | const jobs = { | ||||||
| 	tickCharts, | 	tickCharts, | ||||||
| 	resyncCharts, | 	resyncCharts, | ||||||
| 	cleanCharts, | 	cleanCharts, | ||||||
| 	checkExpiredMutings, | 	checkExpiredMutings, | ||||||
|  | 	clean, | ||||||
| } as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>; | } as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>> | Bull.ProcessPromiseFunction<Record<string, unknown>>>; | ||||||
| 
 | 
 | ||||||
| export default function(dbQueue: Bull.Queue<Record<string, unknown>>) { | export default function(dbQueue: Bull.Queue<Record<string, unknown>>) { | ||||||
|  |  | ||||||
|  | @ -1,10 +1,19 @@ | ||||||
| import Koa from 'koa'; | import Koa from 'koa'; | ||||||
| 
 | 
 | ||||||
|  | import { User } from '@/models/entities/user.js'; | ||||||
|  | import { UserIps } from '@/models/index.js'; | ||||||
|  | import { fetchMeta } from '@/misc/fetch-meta.js'; | ||||||
| import { IEndpoint } from './endpoints.js'; | import { IEndpoint } from './endpoints.js'; | ||||||
| import authenticate, { AuthenticationError } from './authenticate.js'; | import authenticate, { AuthenticationError } from './authenticate.js'; | ||||||
| import call from './call.js'; | import call from './call.js'; | ||||||
| import { ApiError } from './error.js'; | import { ApiError } from './error.js'; | ||||||
| 
 | 
 | ||||||
|  | const userIpHistories = new Map<User['id'], Set<string>>(); | ||||||
|  | 
 | ||||||
|  | setInterval(() => { | ||||||
|  | 	userIpHistories.clear(); | ||||||
|  | }, 1000 * 60 * 60); | ||||||
|  | 
 | ||||||
| export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => { | export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => { | ||||||
| 	const body = ctx.is('multipart/form-data') | 	const body = ctx.is('multipart/form-data') | ||||||
| 		? (ctx.request as any).body | 		? (ctx.request as any).body | ||||||
|  | @ -44,6 +53,31 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res | ||||||
| 		}).catch((e: ApiError) => { | 		}).catch((e: ApiError) => { | ||||||
| 			reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); | 			reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); | ||||||
| 		}); | 		}); | ||||||
|  | 
 | ||||||
|  | 		// Log IP
 | ||||||
|  | 		if (user) { | ||||||
|  | 			fetchMeta().then(meta => { | ||||||
|  | 				if (!meta.enableIpLogging) return; | ||||||
|  | 				const ip = ctx.ip; | ||||||
|  | 				const ips = userIpHistories.get(user.id); | ||||||
|  | 				if (ips == null || !ips.has(ip)) { | ||||||
|  | 					if (ips == null) { | ||||||
|  | 						userIpHistories.set(user.id, new Set([ip])); | ||||||
|  | 					} else { | ||||||
|  | 						ips.add(ip); | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					try { | ||||||
|  | 						UserIps.insert({ | ||||||
|  | 							createdAt: new Date(), | ||||||
|  | 							userId: user.id, | ||||||
|  | 							ip: ip, | ||||||
|  | 						}); | ||||||
|  | 					} catch { | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
| 	}).catch(e => { | 	}).catch(e => { | ||||||
| 		if (e instanceof AuthenticationError) { | 		if (e instanceof AuthenticationError) { | ||||||
| 			reply(403, new ApiError({ | 			reply(403, new ApiError({ | ||||||
|  |  | ||||||
|  | @ -116,7 +116,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi | ||||||
| 
 | 
 | ||||||
| 	// API invoking
 | 	// API invoking
 | ||||||
| 	const before = performance.now(); | 	const before = performance.now(); | ||||||
| 	return await ep.exec(data, user, token, ctx?.file).catch((e: Error) => { | 	return await ep.exec(data, user, token, ctx?.file, ctx?.ip, ctx?.headers).catch((e: Error) => { | ||||||
| 		if (e instanceof ApiError) { | 		if (e instanceof ApiError) { | ||||||
| 			throw e; | 			throw e; | ||||||
| 		} else { | 		} else { | ||||||
|  |  | ||||||
|  | @ -1,16 +1,16 @@ | ||||||
| import * as fs from 'node:fs'; | import * as fs from 'node:fs'; | ||||||
| import Ajv from 'ajv'; | import Ajv from 'ajv'; | ||||||
| import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js'; | import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js'; | ||||||
| import { IEndpointMeta } from './endpoints.js'; |  | ||||||
| import { ApiError } from './error.js'; |  | ||||||
| import { Schema, SchemaType } from '@/misc/schema.js'; | import { Schema, SchemaType } from '@/misc/schema.js'; | ||||||
| import { AccessToken } from '@/models/entities/access-token.js'; | import { AccessToken } from '@/models/entities/access-token.js'; | ||||||
|  | import { IEndpointMeta } from './endpoints.js'; | ||||||
|  | import { ApiError } from './error.js'; | ||||||
| 
 | 
 | ||||||
| export type Response = Record<string, any> | void; | export type Response = Record<string, any> | void; | ||||||
| 
 | 
 | ||||||
| // TODO: paramsの型をT['params']のスキーマ定義から推論する
 | // TODO: paramsの型をT['params']のスキーマ定義から推論する
 | ||||||
| type executor<T extends IEndpointMeta, Ps extends Schema> = | type executor<T extends IEndpointMeta, Ps extends Schema> = | ||||||
| 	(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) => | 	(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) => | ||||||
| 		Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>; | 		Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>; | ||||||
| 
 | 
 | ||||||
| const ajv = new Ajv({ | const ajv = new Ajv({ | ||||||
|  | @ -20,23 +20,27 @@ const ajv = new Ajv({ | ||||||
| ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); | ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); | ||||||
| 
 | 
 | ||||||
| export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>) | export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>) | ||||||
| 		: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise<any> { | 		: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => Promise<any> { | ||||||
| 	const validate = ajv.compile(paramDef); | 	const validate = ajv.compile(paramDef); | ||||||
| 
 | 
 | ||||||
| 	return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => { | 	return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, ip?: string | null, headers?: Record<string, string> | null) => { | ||||||
| 		function cleanup() { | 		let cleanup: undefined | (() => void) = undefined; | ||||||
| 			fs.unlink(file.path, () => {}); |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		if (meta.requireFile && file == null) return Promise.reject(new ApiError({ | 		if (meta.requireFile) { | ||||||
|  | 			cleanup = () => { | ||||||
|  | 				fs.unlink(file.path, () => {}); | ||||||
|  | 			}; | ||||||
|  | 
 | ||||||
|  | 			if (file == null) return Promise.reject(new ApiError({ | ||||||
| 				message: 'File required.', | 				message: 'File required.', | ||||||
| 				code: 'FILE_REQUIRED', | 				code: 'FILE_REQUIRED', | ||||||
| 				id: '4267801e-70d1-416a-b011-4ee502885d8b', | 				id: '4267801e-70d1-416a-b011-4ee502885d8b', | ||||||
| 			})); | 			})); | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 		const valid = validate(params); | 		const valid = validate(params); | ||||||
| 		if (!valid) { | 		if (!valid) { | ||||||
| 			if (file) cleanup(); | 			if (file) cleanup!(); | ||||||
| 
 | 
 | ||||||
| 			const errors = validate.errors!; | 			const errors = validate.errors!; | ||||||
| 			const err = new ApiError({ | 			const err = new ApiError({ | ||||||
|  | @ -50,6 +54,6 @@ export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, pa | ||||||
| 			return Promise.reject(err); | 			return Promise.reject(err); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return cb(params as SchemaType<Ps>, user, token, file, cleanup); | 		return cb(params as SchemaType<Ps>, user, token, file, cleanup, ip, headers); | ||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -35,6 +35,7 @@ import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/fed | ||||||
| import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js'; | import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js'; | ||||||
| import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; | import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; | ||||||
| import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; | import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; | ||||||
|  | import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js'; | ||||||
| import * as ep___admin_invite from './endpoints/admin/invite.js'; | import * as ep___admin_invite from './endpoints/admin/invite.js'; | ||||||
| import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js'; | import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js'; | ||||||
| import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js'; | import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js'; | ||||||
|  | @ -60,6 +61,7 @@ import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; | ||||||
| import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; | import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; | ||||||
| import * as ep___admin_vacuum from './endpoints/admin/vacuum.js'; | import * as ep___admin_vacuum from './endpoints/admin/vacuum.js'; | ||||||
| import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; | import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; | ||||||
|  | import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js'; | ||||||
| import * as ep___announcements from './endpoints/announcements.js'; | import * as ep___announcements from './endpoints/announcements.js'; | ||||||
| import * as ep___antennas_create from './endpoints/antennas/create.js'; | import * as ep___antennas_create from './endpoints/antennas/create.js'; | ||||||
| import * as ep___antennas_delete from './endpoints/antennas/delete.js'; | import * as ep___antennas_delete from './endpoints/antennas/delete.js'; | ||||||
|  | @ -311,6 +313,7 @@ import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by | ||||||
| import * as ep___users_search from './endpoints/users/search.js'; | import * as ep___users_search from './endpoints/users/search.js'; | ||||||
| import * as ep___users_show from './endpoints/users/show.js'; | import * as ep___users_show from './endpoints/users/show.js'; | ||||||
| import * as ep___users_stats from './endpoints/users/stats.js'; | import * as ep___users_stats from './endpoints/users/stats.js'; | ||||||
|  | import * as ep___fetchRss from './endpoints/fetch-rss.js'; | ||||||
| 
 | 
 | ||||||
| const eps = [ | const eps = [ | ||||||
| 	['admin/meta', ep___admin_meta], | 	['admin/meta', ep___admin_meta], | ||||||
|  | @ -348,6 +351,7 @@ const eps = [ | ||||||
| 	['admin/federation/update-instance', ep___admin_federation_updateInstance], | 	['admin/federation/update-instance', ep___admin_federation_updateInstance], | ||||||
| 	['admin/get-index-stats', ep___admin_getIndexStats], | 	['admin/get-index-stats', ep___admin_getIndexStats], | ||||||
| 	['admin/get-table-stats', ep___admin_getTableStats], | 	['admin/get-table-stats', ep___admin_getTableStats], | ||||||
|  | 	['admin/get-user-ips', ep___admin_getUserIps], | ||||||
| 	['admin/invite', ep___admin_invite], | 	['admin/invite', ep___admin_invite], | ||||||
| 	['admin/moderators/add', ep___admin_moderators_add], | 	['admin/moderators/add', ep___admin_moderators_add], | ||||||
| 	['admin/moderators/remove', ep___admin_moderators_remove], | 	['admin/moderators/remove', ep___admin_moderators_remove], | ||||||
|  | @ -373,6 +377,7 @@ const eps = [ | ||||||
| 	['admin/update-meta', ep___admin_updateMeta], | 	['admin/update-meta', ep___admin_updateMeta], | ||||||
| 	['admin/vacuum', ep___admin_vacuum], | 	['admin/vacuum', ep___admin_vacuum], | ||||||
| 	['admin/delete-account', ep___admin_deleteAccount], | 	['admin/delete-account', ep___admin_deleteAccount], | ||||||
|  | 	['admin/update-user-note', ep___admin_updateUserNote], | ||||||
| 	['announcements', ep___announcements], | 	['announcements', ep___announcements], | ||||||
| 	['antennas/create', ep___antennas_create], | 	['antennas/create', ep___antennas_create], | ||||||
| 	['antennas/delete', ep___antennas_delete], | 	['antennas/delete', ep___antennas_delete], | ||||||
|  | @ -624,6 +629,7 @@ const eps = [ | ||||||
| 	['users/search', ep___users_search], | 	['users/search', ep___users_search], | ||||||
| 	['users/show', ep___users_show], | 	['users/show', ep___users_show], | ||||||
| 	['users/stats', ep___users_stats], | 	['users/stats', ep___users_stats], | ||||||
|  | 	['fetch-rss', ep___fetchRss], | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| export interface IEndpointMeta { | export interface IEndpointMeta { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
|  | import { DriveFiles } from '@/models/index.js'; | ||||||
| import define from '../../../define.js'; | import define from '../../../define.js'; | ||||||
| import { ApiError } from '../../../error.js'; | import { ApiError } from '../../../error.js'; | ||||||
| import { DriveFiles } from '@/models/index.js'; |  | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	tags: ['admin'], | 	tags: ['admin'], | ||||||
|  | @ -184,5 +184,10 @@ export default define(meta, paramDef, async (ps, me) => { | ||||||
| 		throw new ApiError(meta.errors.noSuchFile); | 		throw new ApiError(meta.errors.noSuchFile); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if (!me.isAdmin) { | ||||||
|  | 		delete file.requestIp; | ||||||
|  | 		delete file.requestHeaders; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return file; | 	return file; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | import { UserIps } from '@/models/index.js'; | ||||||
|  | import define from '../../define.js'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['admin'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 	requireAdmin: true, | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 		userId: { type: 'string', format: 'misskey:id' }, | ||||||
|  | 	}, | ||||||
|  | 	required: ['userId'], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | // eslint-disable-next-line import/no-default-export
 | ||||||
|  | export default define(meta, paramDef, async (ps, me) => { | ||||||
|  | 	const ips = await UserIps.find({ | ||||||
|  | 		where: { userId: ps.userId }, | ||||||
|  | 		order: { createdAt: 'DESC' }, | ||||||
|  | 		take: 30, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	return ips.map(x => ({ | ||||||
|  | 		ip: x.ip, | ||||||
|  | 		createdAt: x.createdAt.toISOString(), | ||||||
|  | 	})); | ||||||
|  | }); | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import config from '@/config/index.js'; | import config from '@/config/index.js'; | ||||||
| import define from '../../define.js'; |  | ||||||
| import { fetchMeta } from '@/misc/fetch-meta.js'; | import { fetchMeta } from '@/misc/fetch-meta.js'; | ||||||
| import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; | import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; | ||||||
|  | import define from '../../define.js'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	tags: ['meta'], | 	tags: ['meta'], | ||||||
|  | @ -304,6 +304,10 @@ export const meta = { | ||||||
| 				type: 'boolean', | 				type: 'boolean', | ||||||
| 				optional: true, nullable: false, | 				optional: true, nullable: false, | ||||||
| 			}, | 			}, | ||||||
|  | 			enableIpLogging: { | ||||||
|  | 				type: 'boolean', | ||||||
|  | 				optional: true, nullable: false, | ||||||
|  | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| } as const; | } as const; | ||||||
|  | @ -360,7 +364,6 @@ export default define(meta, paramDef, async (ps, me) => { | ||||||
| 		pinnedPages: instance.pinnedPages, | 		pinnedPages: instance.pinnedPages, | ||||||
| 		pinnedClipId: instance.pinnedClipId, | 		pinnedClipId: instance.pinnedClipId, | ||||||
| 		cacheRemoteFiles: instance.cacheRemoteFiles, | 		cacheRemoteFiles: instance.cacheRemoteFiles, | ||||||
| 
 |  | ||||||
| 		useStarForReactionFallback: instance.useStarForReactionFallback, | 		useStarForReactionFallback: instance.useStarForReactionFallback, | ||||||
| 		pinnedUsers: instance.pinnedUsers, | 		pinnedUsers: instance.pinnedUsers, | ||||||
| 		hiddenTags: instance.hiddenTags, | 		hiddenTags: instance.hiddenTags, | ||||||
|  | @ -397,5 +400,6 @@ export default define(meta, paramDef, async (ps, me) => { | ||||||
| 		objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, | 		objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, | ||||||
| 		deeplAuthKey: instance.deeplAuthKey, | 		deeplAuthKey: instance.deeplAuthKey, | ||||||
| 		deeplIsPro: instance.deeplIsPro, | 		deeplIsPro: instance.deeplIsPro, | ||||||
|  | 		enableIpLogging: instance.enableIpLogging, | ||||||
| 	}; | 	}; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -25,7 +25,7 @@ export const paramDef = { | ||||||
| export default define(meta, paramDef, async (ps, me) => { | export default define(meta, paramDef, async (ps, me) => { | ||||||
| 	const [user, profile] = await Promise.all([ | 	const [user, profile] = await Promise.all([ | ||||||
| 		Users.findOneBy({ id: ps.userId }), | 		Users.findOneBy({ id: ps.userId }), | ||||||
| 		UserProfiles.findOneBy({ userId: ps.userId }) | 		UserProfiles.findOneBy({ userId: ps.userId }), | ||||||
| 	]); | 	]); | ||||||
| 
 | 
 | ||||||
| 	if (user == null || profile == null) { | 	if (user == null || profile == null) { | ||||||
|  | @ -68,6 +68,8 @@ export default define(meta, paramDef, async (ps, me) => { | ||||||
| 		isModerator: user.isModerator, | 		isModerator: user.isModerator, | ||||||
| 		isSilenced: user.isSilenced, | 		isSilenced: user.isSilenced, | ||||||
| 		isSuspended: user.isSuspended, | 		isSuspended: user.isSuspended, | ||||||
|  | 		lastActiveDate: user.lastActiveDate, | ||||||
|  | 		moderationNote: profile.moderationNote, | ||||||
| 		signins, | 		signins, | ||||||
| 	}; | 	}; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,8 +1,8 @@ | ||||||
| import define from '../../define.js'; |  | ||||||
| import { Meta } from '@/models/entities/meta.js'; | import { Meta } from '@/models/entities/meta.js'; | ||||||
| import { insertModerationLog } from '@/services/insert-moderation-log.js'; | import { insertModerationLog } from '@/services/insert-moderation-log.js'; | ||||||
| import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js'; | import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits.js'; | ||||||
| import { db } from '@/db/postgre.js'; | import { db } from '@/db/postgre.js'; | ||||||
|  | import define from '../../define.js'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	tags: ['admin'], | 	tags: ['admin'], | ||||||
|  | @ -96,6 +96,7 @@ export const paramDef = { | ||||||
| 		objectStorageUseProxy: { type: 'boolean' }, | 		objectStorageUseProxy: { type: 'boolean' }, | ||||||
| 		objectStorageSetPublicRead: { type: 'boolean' }, | 		objectStorageSetPublicRead: { type: 'boolean' }, | ||||||
| 		objectStorageS3ForcePathStyle: { type: 'boolean' }, | 		objectStorageS3ForcePathStyle: { type: 'boolean' }, | ||||||
|  | 		enableIpLogging: { type: 'boolean' }, | ||||||
| 	}, | 	}, | ||||||
| 	required: [], | 	required: [], | ||||||
| } as const; | } as const; | ||||||
|  | @ -396,6 +397,10 @@ export default define(meta, paramDef, async (ps, me) => { | ||||||
| 		set.deeplIsPro = ps.deeplIsPro; | 		set.deeplIsPro = ps.deeplIsPro; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if (ps.enableIpLogging !== undefined) { | ||||||
|  | 		set.enableIpLogging = ps.enableIpLogging; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	await db.transaction(async transactionalEntityManager => { | 	await db.transaction(async transactionalEntityManager => { | ||||||
| 		const metas = await transactionalEntityManager.find(Meta, { | 		const metas = await transactionalEntityManager.find(Meta, { | ||||||
| 			order: { | 			order: { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | import { UserProfiles, Users } from '@/models/index.js'; | ||||||
|  | import define from '../../define.js'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['admin'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true, | ||||||
|  | 	requireModerator: true, | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 		userId: { type: 'string', format: 'misskey:id' }, | ||||||
|  | 		text: { type: 'string' }, | ||||||
|  | 	}, | ||||||
|  | 	required: ['userId', 'text'], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | // eslint-disable-next-line import/no-default-export
 | ||||||
|  | export default define(meta, paramDef, async (ps, me) => { | ||||||
|  | 	const user = await Users.findOneBy({ id: ps.userId }); | ||||||
|  | 
 | ||||||
|  | 	if (user == null) { | ||||||
|  | 		throw new Error('user not found'); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	await UserProfiles.update({ userId: user.id }, { | ||||||
|  | 		moderationNote: ps.text, | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | @ -1,10 +1,11 @@ | ||||||
| import ms from 'ms'; | import ms from 'ms'; | ||||||
| import { addFile } from '@/services/drive/add-file.js'; | import { addFile } from '@/services/drive/add-file.js'; | ||||||
|  | import { DriveFiles } from '@/models/index.js'; | ||||||
|  | import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; | ||||||
|  | import { fetchMeta } from '@/misc/fetch-meta.js'; | ||||||
| import define from '../../../define.js'; | import define from '../../../define.js'; | ||||||
| import { apiLogger } from '../../../logger.js'; | import { apiLogger } from '../../../logger.js'; | ||||||
| import { ApiError } from '../../../error.js'; | import { ApiError } from '../../../error.js'; | ||||||
| import { DriveFiles } from '@/models/index.js'; |  | ||||||
| import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; |  | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	tags: ['drive'], | 	tags: ['drive'], | ||||||
|  | @ -50,7 +51,7 @@ export const paramDef = { | ||||||
| } as const; | } as const; | ||||||
| 
 | 
 | ||||||
| // eslint-disable-next-line import/no-default-export
 | // eslint-disable-next-line import/no-default-export
 | ||||||
| export default define(meta, paramDef, async (ps, user, _, file, cleanup) => { | export default define(meta, paramDef, async (ps, user, _, file, cleanup, ip, headers) => { | ||||||
| 	// Get 'name' parameter
 | 	// Get 'name' parameter
 | ||||||
| 	let name = ps.name || file.originalname; | 	let name = ps.name || file.originalname; | ||||||
| 	if (name !== undefined && name !== null) { | 	if (name !== undefined && name !== null) { | ||||||
|  | @ -66,9 +67,21 @@ export default define(meta, paramDef, async (ps, user, _, file, cleanup) => { | ||||||
| 		name = null; | 		name = null; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	const meta = await fetchMeta(); | ||||||
|  | 
 | ||||||
| 	try { | 	try { | ||||||
| 		// Create file
 | 		// Create file
 | ||||||
| 		const driveFile = await addFile({ user, path: file.path, name, comment: ps.comment, folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive }); | 		const driveFile = await addFile({ | ||||||
|  | 			user, | ||||||
|  | 			path: file.path, | ||||||
|  | 			name, | ||||||
|  | 			comment: ps.comment, | ||||||
|  | 			folderId: ps.folderId, | ||||||
|  | 			force: ps.force, | ||||||
|  | 			sensitive: ps.isSensitive, | ||||||
|  | 			requestIp: meta.enableIpLogging ? ip : null, | ||||||
|  | 			requestHeaders: meta.enableIpLogging ? headers : null, | ||||||
|  | 		}); | ||||||
| 		return await DriveFiles.pack(driveFile, { self: true }); | 		return await DriveFiles.pack(driveFile, { self: true }); | ||||||
| 	} catch (e) { | 	} catch (e) { | ||||||
| 		if (e instanceof Error || typeof e === 'string') { | 		if (e instanceof Error || typeof e === 'string') { | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import ms from 'ms'; | import ms from 'ms'; | ||||||
| import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; | import { uploadFromUrl } from '@/services/drive/upload-from-url.js'; | ||||||
| import define from '../../../define.js'; |  | ||||||
| import { DriveFiles } from '@/models/index.js'; | import { DriveFiles } from '@/models/index.js'; | ||||||
| import { publishMainStream } from '@/services/stream.js'; | import { publishMainStream } from '@/services/stream.js'; | ||||||
| import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; | import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js'; | ||||||
|  | import define from '../../../define.js'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	tags: ['drive'], | 	tags: ['drive'], | ||||||
|  | @ -34,8 +34,8 @@ export const paramDef = { | ||||||
| } as const; | } as const; | ||||||
| 
 | 
 | ||||||
| // eslint-disable-next-line import/no-default-export
 | // eslint-disable-next-line import/no-default-export
 | ||||||
| export default define(meta, paramDef, async (ps, user) => { | export default define(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => { | ||||||
| 	uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment }).then(file => { | 	uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => { | ||||||
| 		DriveFiles.pack(file, { self: true }).then(packedFile => { | 		DriveFiles.pack(file, { self: true }).then(packedFile => { | ||||||
| 			publishMainStream(user.id, 'urlUploadFinished', { | 			publishMainStream(user.id, 'urlUploadFinished', { | ||||||
| 				marker: ps.marker, | 				marker: ps.marker, | ||||||
|  |  | ||||||
|  | @ -54,7 +54,7 @@ export default define(meta, paramDef, async (ps) => { | ||||||
| 	]); | 	]); | ||||||
| 
 | 
 | ||||||
| 	const gotSubCount = topSubInstances.map(x => x.followersCount).reduce((a, b) => a + b, 0); | 	const gotSubCount = topSubInstances.map(x => x.followersCount).reduce((a, b) => a + b, 0); | ||||||
| 	const gotPubCount = topSubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0); | 	const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0); | ||||||
| 
 | 
 | ||||||
| 	return await awaitAll({ | 	return await awaitAll({ | ||||||
| 		topSubInstances: Instances.packMany(topSubInstances), | 		topSubInstances: Instances.packMany(topSubInstances), | ||||||
|  |  | ||||||
							
								
								
									
										39
									
								
								packages/backend/src/server/api/endpoints/fetch-rss.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								packages/backend/src/server/api/endpoints/fetch-rss.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | ||||||
|  | import Parser from 'rss-parser'; | ||||||
|  | import { getResponse } from '@/misc/fetch.js'; | ||||||
|  | import config from '@/config/index.js'; | ||||||
|  | import define from '../define.js'; | ||||||
|  | 
 | ||||||
|  | const rssParser = new Parser(); | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['meta'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: false, | ||||||
|  | 	allowGet: true, | ||||||
|  | 	cacheSec: 60 * 3, | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | export const paramDef = { | ||||||
|  | 	type: 'object', | ||||||
|  | 	properties: { | ||||||
|  | 		url: { type: 'string' }, | ||||||
|  | 	}, | ||||||
|  | 	required: ['url'], | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | // eslint-disable-next-line import/no-default-export
 | ||||||
|  | export default define(meta, paramDef, async (ps) => { | ||||||
|  | 	const res = await getResponse({ | ||||||
|  | 		url: ps.url, | ||||||
|  | 		method: 'GET', | ||||||
|  | 		headers: Object.assign({ | ||||||
|  | 			'User-Agent': config.userAgent, | ||||||
|  | 			Accept: 'application/rss+xml, */*', | ||||||
|  | 		}), | ||||||
|  | 		timeout: 5000, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	const text = await res.text(); | ||||||
|  | 
 | ||||||
|  | 	return rssParser.parseString(text); | ||||||
|  | }); | ||||||
|  | @ -60,12 +60,21 @@ export default define(meta, paramDef, async (ps, user) => { | ||||||
| 	query.setParameters(mutingQuery.getParameters()); | 	query.setParameters(mutingQuery.getParameters()); | ||||||
| 	//#endregion
 | 	//#endregion
 | ||||||
| 
 | 
 | ||||||
| 	const polls = await query.take(ps.limit).skip(ps.offset).getMany(); | 	const polls = await query | ||||||
|  | 		.orderBy('poll.noteId', 'DESC') | ||||||
|  | 		.take(ps.limit) | ||||||
|  | 		.skip(ps.offset) | ||||||
|  | 		.getMany(); | ||||||
| 
 | 
 | ||||||
| 	if (polls.length === 0) return []; | 	if (polls.length === 0) return []; | ||||||
| 
 | 
 | ||||||
| 	const notes = await Notes.findBy({ | 	const notes = await Notes.find({ | ||||||
|  | 		where: { | ||||||
| 			id: In(polls.map(poll => poll.noteId)), | 			id: In(polls.map(poll => poll.noteId)), | ||||||
|  | 		}, | ||||||
|  | 		order: { | ||||||
|  | 			createdAt: 'DESC', | ||||||
|  | 		}, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	return await Notes.packMany(notes, user, { | 	return await Notes.packMany(notes, user, { | ||||||
|  |  | ||||||
|  | @ -2,26 +2,26 @@ import * as fs from 'node:fs'; | ||||||
| 
 | 
 | ||||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||||
| 
 | 
 | ||||||
|  | import S3 from 'aws-sdk/clients/s3.js'; | ||||||
|  | import sharp from 'sharp'; | ||||||
|  | import { IsNull } from 'typeorm'; | ||||||
| import { publishMainStream, publishDriveStream } from '@/services/stream.js'; | import { publishMainStream, publishDriveStream } from '@/services/stream.js'; | ||||||
| import { deleteFile } from './delete-file.js'; |  | ||||||
| import { fetchMeta } from '@/misc/fetch-meta.js'; | import { fetchMeta } from '@/misc/fetch-meta.js'; | ||||||
| import { GenerateVideoThumbnail } from './generate-video-thumbnail.js'; |  | ||||||
| import { driveLogger } from './logger.js'; |  | ||||||
| import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js'; |  | ||||||
| import { contentDisposition } from '@/misc/content-disposition.js'; | import { contentDisposition } from '@/misc/content-disposition.js'; | ||||||
| import { getFileInfo } from '@/misc/get-file-info.js'; | import { getFileInfo } from '@/misc/get-file-info.js'; | ||||||
| import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '@/models/index.js'; | import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '@/models/index.js'; | ||||||
| import { InternalStorage } from './internal-storage.js'; |  | ||||||
| import { DriveFile } from '@/models/entities/drive-file.js'; | import { DriveFile } from '@/models/entities/drive-file.js'; | ||||||
| import { IRemoteUser, User } from '@/models/entities/user.js'; | import { IRemoteUser, User } from '@/models/entities/user.js'; | ||||||
| import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index.js'; | import { driveChart, perUserDriveChart, instanceChart } from '@/services/chart/index.js'; | ||||||
| import { genId } from '@/misc/gen-id.js'; | import { genId } from '@/misc/gen-id.js'; | ||||||
| import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; | import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; | ||||||
| import S3 from 'aws-sdk/clients/s3.js'; |  | ||||||
| import { getS3 } from './s3.js'; |  | ||||||
| import sharp from 'sharp'; |  | ||||||
| import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; | import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; | ||||||
| import { IsNull } from 'typeorm'; | import { getS3 } from './s3.js'; | ||||||
|  | import { InternalStorage } from './internal-storage.js'; | ||||||
|  | import { IImage, convertSharpToJpeg, convertSharpToWebp, convertSharpToPng } from './image-processor.js'; | ||||||
|  | import { driveLogger } from './logger.js'; | ||||||
|  | import { GenerateVideoThumbnail } from './generate-video-thumbnail.js'; | ||||||
|  | import { deleteFile } from './delete-file.js'; | ||||||
| 
 | 
 | ||||||
| const logger = driveLogger.createSubLogger('register', 'yellow'); | const logger = driveLogger.createSubLogger('register', 'yellow'); | ||||||
| 
 | 
 | ||||||
|  | @ -171,7 +171,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (!['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'].includes(type)) { | 	if (!['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml'].includes(type)) { | ||||||
| 		logger.debug(`web image and thumbnail not created (not an required file)`); | 		logger.debug('web image and thumbnail not created (not an required file)'); | ||||||
| 		return { | 		return { | ||||||
| 			webpublic: null, | 			webpublic: null, | ||||||
| 			thumbnail: null, | 			thumbnail: null, | ||||||
|  | @ -212,7 +212,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool | ||||||
| 	let webpublic: IImage | null = null; | 	let webpublic: IImage | null = null; | ||||||
| 
 | 
 | ||||||
| 	if (generateWeb && !satisfyWebpublic) { | 	if (generateWeb && !satisfyWebpublic) { | ||||||
| 		logger.info(`creating web image`); | 		logger.info('creating web image'); | ||||||
| 
 | 
 | ||||||
| 		try { | 		try { | ||||||
| 			if (['image/jpeg', 'image/webp'].includes(type)) { | 			if (['image/jpeg', 'image/webp'].includes(type)) { | ||||||
|  | @ -222,14 +222,14 @@ export async function generateAlts(path: string, type: string, generateWeb: bool | ||||||
| 			} else if (['image/svg+xml'].includes(type)) { | 			} else if (['image/svg+xml'].includes(type)) { | ||||||
| 				webpublic = await convertSharpToPng(img, 2048, 2048); | 				webpublic = await convertSharpToPng(img, 2048, 2048); | ||||||
| 			} else { | 			} else { | ||||||
| 				logger.debug(`web image not created (not an required image)`); | 				logger.debug('web image not created (not an required image)'); | ||||||
| 			} | 			} | ||||||
| 		} catch (err) { | 		} catch (err) { | ||||||
| 			logger.warn(`web image not created (an error occured)`, err as Error); | 			logger.warn('web image not created (an error occured)', err as Error); | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		if (satisfyWebpublic) logger.info(`web image not created (original satisfies webpublic)`); | 		if (satisfyWebpublic) logger.info('web image not created (original satisfies webpublic)'); | ||||||
| 		else logger.info(`web image not created (from remote)`); | 		else logger.info('web image not created (from remote)'); | ||||||
| 	} | 	} | ||||||
| 	// #endregion webpublic
 | 	// #endregion webpublic
 | ||||||
| 
 | 
 | ||||||
|  | @ -240,10 +240,10 @@ export async function generateAlts(path: string, type: string, generateWeb: bool | ||||||
| 		if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(type)) { | 		if (['image/jpeg', 'image/webp', 'image/png', 'image/svg+xml'].includes(type)) { | ||||||
| 			thumbnail = await convertSharpToWebp(img, 498, 280); | 			thumbnail = await convertSharpToWebp(img, 498, 280); | ||||||
| 		} else { | 		} else { | ||||||
| 			logger.debug(`thumbnail not created (not an required file)`); | 			logger.debug('thumbnail not created (not an required file)'); | ||||||
| 		} | 		} | ||||||
| 	} catch (err) { | 	} catch (err) { | ||||||
| 		logger.warn(`thumbnail not created (an error occured)`, err as Error); | 		logger.warn('thumbnail not created (an error occured)', err as Error); | ||||||
| 	} | 	} | ||||||
| 	// #endregion thumbnail
 | 	// #endregion thumbnail
 | ||||||
| 
 | 
 | ||||||
|  | @ -276,7 +276,7 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, | ||||||
| 	const s3 = getS3(meta); | 	const s3 = getS3(meta); | ||||||
| 
 | 
 | ||||||
| 	const upload = s3.upload(params, { | 	const upload = s3.upload(params, { | ||||||
| 		partSize: s3.endpoint?.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024, | 		partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	const result = await upload.promise(); | 	const result = await upload.promise(); | ||||||
|  | @ -326,6 +326,9 @@ type AddFileArgs = { | ||||||
| 	uri?: string | null; | 	uri?: string | null; | ||||||
| 	/** Mark file as sensitive */ | 	/** Mark file as sensitive */ | ||||||
| 	sensitive?: boolean | null; | 	sensitive?: boolean | null; | ||||||
|  | 
 | ||||||
|  | 	requestIp?: string | null; | ||||||
|  | 	requestHeaders?: Record<string, string> | null; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -342,7 +345,9 @@ export async function addFile({ | ||||||
| 	isLink = false, | 	isLink = false, | ||||||
| 	url = null, | 	url = null, | ||||||
| 	uri = null, | 	uri = null, | ||||||
| 	sensitive = null | 	sensitive = null, | ||||||
|  | 	requestIp = null, | ||||||
|  | 	requestHeaders = null, | ||||||
| }: AddFileArgs): Promise<DriveFile> { | }: AddFileArgs): Promise<DriveFile> { | ||||||
| 	const info = await getFileInfo(path); | 	const info = await getFileInfo(path); | ||||||
| 	logger.info(`${JSON.stringify(info)}`); | 	logger.info(`${JSON.stringify(info)}`); | ||||||
|  | @ -427,6 +432,8 @@ export async function addFile({ | ||||||
| 	file.properties = properties; | 	file.properties = properties; | ||||||
| 	file.blurhash = info.blurhash || null; | 	file.blurhash = info.blurhash || null; | ||||||
| 	file.isLink = isLink; | 	file.isLink = isLink; | ||||||
|  | 	file.requestIp = requestIp; | ||||||
|  | 	file.requestHeaders = requestHeaders; | ||||||
| 	file.isSensitive = user | 	file.isSensitive = user | ||||||
| 		? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true : | 		? Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true : | ||||||
| 		(sensitive !== null && sensitive !== undefined) | 		(sensitive !== null && sensitive !== undefined) | ||||||
|  |  | ||||||
|  | @ -1,12 +1,12 @@ | ||||||
| import { URL } from 'node:url'; | import { URL } from 'node:url'; | ||||||
| import { addFile } from './add-file.js'; |  | ||||||
| import { User } from '@/models/entities/user.js'; | import { User } from '@/models/entities/user.js'; | ||||||
| import { driveLogger } from './logger.js'; |  | ||||||
| import { createTemp } from '@/misc/create-temp.js'; | import { createTemp } from '@/misc/create-temp.js'; | ||||||
| import { downloadUrl } from '@/misc/download-url.js'; | import { downloadUrl } from '@/misc/download-url.js'; | ||||||
| import { DriveFolder } from '@/models/entities/drive-folder.js'; | import { DriveFolder } from '@/models/entities/drive-folder.js'; | ||||||
| import { DriveFile } from '@/models/entities/drive-file.js'; | import { DriveFile } from '@/models/entities/drive-file.js'; | ||||||
| import { DriveFiles } from '@/models/index.js'; | import { DriveFiles } from '@/models/index.js'; | ||||||
|  | import { driveLogger } from './logger.js'; | ||||||
|  | import { addFile } from './add-file.js'; | ||||||
| 
 | 
 | ||||||
| const logger = driveLogger.createSubLogger('downloader'); | const logger = driveLogger.createSubLogger('downloader'); | ||||||
| 
 | 
 | ||||||
|  | @ -19,6 +19,8 @@ type Args = { | ||||||
| 	force?: boolean; | 	force?: boolean; | ||||||
| 	isLink?: boolean; | 	isLink?: boolean; | ||||||
| 	comment?: string | null; | 	comment?: string | null; | ||||||
|  | 	requestIp?: string | null; | ||||||
|  | 	requestHeaders?: Record<string, string> | null; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export async function uploadFromUrl({ | export async function uploadFromUrl({ | ||||||
|  | @ -30,6 +32,8 @@ export async function uploadFromUrl({ | ||||||
| 	force = false, | 	force = false, | ||||||
| 	isLink = false, | 	isLink = false, | ||||||
| 	comment = null, | 	comment = null, | ||||||
|  | 	requestIp = null, | ||||||
|  | 	requestHeaders = null, | ||||||
| }: Args): Promise<DriveFile> { | }: Args): Promise<DriveFile> { | ||||||
| 	let name = new URL(url).pathname.split('/').pop() || null; | 	let name = new URL(url).pathname.split('/').pop() || null; | ||||||
| 	if (name == null || !DriveFiles.validateFileName(name)) { | 	if (name == null || !DriveFiles.validateFileName(name)) { | ||||||
|  | @ -49,7 +53,7 @@ export async function uploadFromUrl({ | ||||||
| 		// write content at URL to temp file
 | 		// write content at URL to temp file
 | ||||||
| 		await downloadUrl(url, path); | 		await downloadUrl(url, path); | ||||||
| 
 | 
 | ||||||
| 		const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive }); | 		const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive, requestIp, requestHeaders }); | ||||||
| 		logger.succ(`Got: ${driveFile.id}`); | 		logger.succ(`Got: ${driveFile.id}`); | ||||||
| 		return driveFile!; | 		return driveFile!; | ||||||
| 	} catch (e) { | 	} catch (e) { | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -186,7 +186,7 @@ export function connectStream(user: any, channel: string, listener: (message: Re | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean) => { | export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: any) => { | ||||||
| 	return new Promise<boolean>(async (res, rej) => { | 	return new Promise<boolean>(async (res, rej) => { | ||||||
| 		let timer: NodeJS.Timeout; | 		let timer: NodeJS.Timeout; | ||||||
| 
 | 
 | ||||||
|  | @ -198,7 +198,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond | ||||||
| 					if (timer) clearTimeout(timer); | 					if (timer) clearTimeout(timer); | ||||||
| 					res(true); | 					res(true); | ||||||
| 				} | 				} | ||||||
| 			}); | 			}, params); | ||||||
| 		} catch (e) { | 		} catch (e) { | ||||||
| 			rej(e); | 			rej(e); | ||||||
| 		} | 		} | ||||||
|  | @ -208,7 +208,7 @@ export const waitFire = async (user: any, channel: string, trgr: () => any, cond | ||||||
| 		timer = setTimeout(() => { | 		timer = setTimeout(() => { | ||||||
| 			ws.close(); | 			ws.close(); | ||||||
| 			res(false); | 			res(false); | ||||||
| 		}, 5000); | 		}, 3000); | ||||||
| 
 | 
 | ||||||
| 		try { | 		try { | ||||||
| 			await trgr(); | 			await trgr(); | ||||||
|  |  | ||||||
|  | @ -2485,6 +2485,11 @@ entities@^2.0.0: | ||||||
|   resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" |   resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" | ||||||
|   integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== |   integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== | ||||||
| 
 | 
 | ||||||
|  | entities@^2.0.3: | ||||||
|  |   version "2.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" | ||||||
|  |   integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== | ||||||
|  | 
 | ||||||
| entities@^4.3.0: | entities@^4.3.0: | ||||||
|   version "4.3.0" |   version "4.3.0" | ||||||
|   resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.0.tgz#62915f08d67353bb4eb67e3d62641a4059aec656" |   resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.0.tgz#62915f08d67353bb4eb67e3d62641a4059aec656" | ||||||
|  | @ -5985,6 +5990,14 @@ rndstr@1.0.0: | ||||||
|     rangestr "0.0.1" |     rangestr "0.0.1" | ||||||
|     seedrandom "2.4.2" |     seedrandom "2.4.2" | ||||||
| 
 | 
 | ||||||
|  | rss-parser@3.12.0: | ||||||
|  |   version "3.12.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/rss-parser/-/rss-parser-3.12.0.tgz#b8888699ea46304a74363fbd8144671b2997984c" | ||||||
|  |   integrity sha512-aqD3E8iavcCdkhVxNDIdg1nkBI17jgqF+9OqPS1orwNaOgySdpvq6B+DoONLhzjzwV8mWg37sb60e4bmLK117A== | ||||||
|  |   dependencies: | ||||||
|  |     entities "^2.0.3" | ||||||
|  |     xml2js "^0.4.19" | ||||||
|  | 
 | ||||||
| run-parallel@^1.1.9: | run-parallel@^1.1.9: | ||||||
|   version "1.1.9" |   version "1.1.9" | ||||||
|   resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" |   resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" | ||||||
|  | @ -7174,7 +7187,7 @@ xml2js@0.4.19: | ||||||
|     sax ">=0.6.0" |     sax ">=0.6.0" | ||||||
|     xmlbuilder "~9.0.1" |     xmlbuilder "~9.0.1" | ||||||
| 
 | 
 | ||||||
| xml2js@^0.4.23: | xml2js@^0.4.19, xml2js@^0.4.23: | ||||||
|   version "0.4.23" |   version "0.4.23" | ||||||
|   resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" |   resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" | ||||||
|   integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== |   integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== | ||||||
|  |  | ||||||
|  | @ -22,9 +22,8 @@ module.exports = { | ||||||
| 			}, | 			}, | ||||||
| 		], | 		], | ||||||
| 		// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
 | 		// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
 | ||||||
| 		// data の禁止理由: 抽象的すぎるため
 |  | ||||||
| 		// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
 | 		// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
 | ||||||
| 		'id-denylist': ['error', 'window', 'data', 'e'], | 		'id-denylist': ['error', 'window', 'e'], | ||||||
| 		'no-shadow': ['warn'], | 		'no-shadow': ['warn'], | ||||||
| 		'vue/attributes-order': ['error', { | 		'vue/attributes-order': ['error', { | ||||||
| 			'alphabetical': false, | 			'alphabetical': false, | ||||||
|  |  | ||||||
|  | @ -76,7 +76,6 @@ | ||||||
| 		"vanilla-tilt": "1.7.2", | 		"vanilla-tilt": "1.7.2", | ||||||
| 		"vite": "3.0.0-beta.5", | 		"vite": "3.0.0-beta.5", | ||||||
| 		"vue": "3.2.37", | 		"vue": "3.2.37", | ||||||
| 		"vue-marquee-text-component": "2.0.1", |  | ||||||
| 		"vue-prism-editor": "2.0.0-alpha.2", | 		"vue-prism-editor": "2.0.0-alpha.2", | ||||||
| 		"vuedraggable": "4.0.1", | 		"vuedraggable": "4.0.1", | ||||||
| 		"websocket": "1.0.34", | 		"websocket": "1.0.34", | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ const accountData = localStorage.getItem('account'); | ||||||
| export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; | export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; | ||||||
| 
 | 
 | ||||||
| export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); | export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); | ||||||
|  | export const iAmAdmin = $i != null && $i.isAdmin; | ||||||
| 
 | 
 | ||||||
| export async function signout() { | export async function signout() { | ||||||
| 	waiting(); | 	waiting(); | ||||||
|  |  | ||||||
|  | @ -59,7 +59,7 @@ const isThumbnailAvailable = computed(() => { | ||||||
| 	display: flex; | 	display: flex; | ||||||
| 	background: var(--panel); | 	background: var(--panel); | ||||||
| 	border-radius: 8px; | 	border-radius: 8px; | ||||||
| 	overflow: clip; | 	overflow: hidden; overflow: clip; | ||||||
| 
 | 
 | ||||||
| 	> .icon-sub { | 	> .icon-sub { | ||||||
| 		position: absolute; | 		position: absolute; | ||||||
|  |  | ||||||
|  | @ -41,7 +41,7 @@ | ||||||
| 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | ||||||
| 					<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option> | 					<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option> | ||||||
| 				</FormRadios> | 				</FormRadios> | ||||||
| 				<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock"> | 				<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock"> | ||||||
| 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template> | ||||||
| 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template> | 					<template v-if="form[item].description" #caption>{{ form[item].description }}</template> | ||||||
| 				</FormRange> | 				</FormRange> | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
| 			<div class="track"> | 			<div class="track"> | ||||||
| 				<div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div> | 				<div class="highlight" :style="{ width: (steppedRawValue * 100) + '%' }"></div> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div v-if="steps" class="ticks"> | 			<div v-if="steps && showTicks" class="ticks"> | ||||||
| 				<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div> | 				<div v-for="i in (steps + 1)" class="tick" :style="{ left: (((i - 1) / steps) * 100) + '%' }"></div> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div> | 			<div ref="thumbEl" v-tooltip="textConverter(finalValue)" class="thumb" :style="{ left: thumbPosition + 'px' }" @mousedown="onMousedown" @touchstart="onMousedown"></div> | ||||||
|  | @ -27,6 +27,7 @@ const props = withDefaults(defineProps<{ | ||||||
| 	max: number; | 	max: number; | ||||||
| 	step?: number; | 	step?: number; | ||||||
| 	textConverter?: (value: number) => string, | 	textConverter?: (value: number) => string, | ||||||
|  | 	showTicks?: boolean; | ||||||
| }>(), { | }>(), { | ||||||
| 	step: 1, | 	step: 1, | ||||||
| 	textConverter: (v) => v.toString(), | 	textConverter: (v) => v.toString(), | ||||||
|  | @ -188,7 +189,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => { | ||||||
| 				height: 3px; | 				height: 3px; | ||||||
| 				background: rgba(0, 0, 0, 0.1); | 				background: rgba(0, 0, 0, 0.1); | ||||||
| 				border-radius: 999px; | 				border-radius: 999px; | ||||||
| 				overflow: clip; | 				overflow: hidden; overflow: clip; | ||||||
| 
 | 
 | ||||||
| 				> .highlight { | 				> .highlight { | ||||||
| 					position: absolute; | 					position: absolute; | ||||||
|  |  | ||||||
|  | @ -172,11 +172,8 @@ onUnmounted(() => { | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .fdidabkb { | .fdidabkb { | ||||||
| 	--height: 60px; | 	--height: 55px; | ||||||
| 	display: flex; | 	display: flex; | ||||||
| 	position: sticky; |  | ||||||
| 	top: var(--stickyTop, 0); |  | ||||||
| 	z-index: 1000; |  | ||||||
| 	width: 100%; | 	width: 100%; | ||||||
| 	-webkit-backdrop-filter: var(--blur, blur(15px)); | 	-webkit-backdrop-filter: var(--blur, blur(15px)); | ||||||
| 	backdrop-filter: var(--blur, blur(15px)); | 	backdrop-filter: var(--blur, blur(15px)); | ||||||
|  |  | ||||||
|  | @ -1,46 +1,38 @@ | ||||||
| <template> | <template> | ||||||
| <div ref="rootEl"> | <div ref="rootEl"> | ||||||
|  | 	<div ref="headerEl"> | ||||||
| 		<slot name="header"></slot> | 		<slot name="header"></slot> | ||||||
|  | 	</div> | ||||||
| 	<div ref="bodyEl" :data-sticky-container-header-height="headerHeight"> | 	<div ref="bodyEl" :data-sticky-container-header-height="headerHeight"> | ||||||
| 		<slot></slot> | 		<slot></slot> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts"> | ||||||
| import { onMounted, onUnmounted } from 'vue'; | // なんか動かない | ||||||
|  | //const CURRENT_STICKY_TOP = Symbol('CURRENT_STICKY_TOP'); | ||||||
|  | const CURRENT_STICKY_TOP = 'CURRENT_STICKY_TOP'; | ||||||
|  | </script> | ||||||
| 
 | 
 | ||||||
| const props = withDefaults(defineProps<{ | <script lang="ts" setup> | ||||||
| 	autoSticky?: boolean; | import { onMounted, onUnmounted, provide, inject, Ref, ref, watch } from 'vue'; | ||||||
| }>(), { |  | ||||||
| 	autoSticky: false, |  | ||||||
| }); |  | ||||||
| 
 | 
 | ||||||
| const rootEl = $ref<HTMLElement>(); | const rootEl = $ref<HTMLElement>(); | ||||||
|  | const headerEl = $ref<HTMLElement>(); | ||||||
| const bodyEl = $ref<HTMLElement>(); | const bodyEl = $ref<HTMLElement>(); | ||||||
| 
 | 
 | ||||||
| let headerHeight = $ref<string | undefined>(); | let headerHeight = $ref<string | undefined>(); | ||||||
|  | let childStickyTop = $ref(0); | ||||||
|  | const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0)); | ||||||
|  | provide(CURRENT_STICKY_TOP, $$(childStickyTop)); | ||||||
| 
 | 
 | ||||||
| const calc = () => { | const calc = () => { | ||||||
| 	const currentStickyTop = getComputedStyle(rootEl).getPropertyValue('--stickyTop') || '0px'; | 	childStickyTop = parentStickyTop.value + headerEl.offsetHeight; | ||||||
| 
 | 	headerHeight = headerEl.offsetHeight.toString(); | ||||||
| 	const header = rootEl.children[0] as HTMLElement; |  | ||||||
| 	if (header === bodyEl) { |  | ||||||
| 		bodyEl.style.setProperty('--stickyTop', currentStickyTop); |  | ||||||
| 	} else { |  | ||||||
| 		bodyEl.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${header.offsetHeight}px)`); |  | ||||||
| 		headerHeight = header.offsetHeight.toString(); |  | ||||||
| 
 |  | ||||||
| 		if (props.autoSticky) { |  | ||||||
| 			header.style.setProperty('--stickyTop', currentStickyTop); |  | ||||||
| 			header.style.position = 'sticky'; |  | ||||||
| 			header.style.top = 'var(--stickyTop)'; |  | ||||||
| 			header.style.zIndex = '1'; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const observer = new MutationObserver(() => { | const observer = new ResizeObserver(() => { | ||||||
| 	window.setTimeout(() => { | 	window.setTimeout(() => { | ||||||
| 		calc(); | 		calc(); | ||||||
| 	}, 100); | 	}, 100); | ||||||
|  | @ -49,11 +41,19 @@ const observer = new MutationObserver(() => { | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
| 	calc(); | 	calc(); | ||||||
| 
 | 
 | ||||||
| 	observer.observe(rootEl, { | 	watch(parentStickyTop, calc); | ||||||
| 		attributes: false, | 
 | ||||||
| 		childList: true, | 	watch($$(childStickyTop), () => { | ||||||
| 		subtree: false, | 		bodyEl.style.setProperty('--stickyTop', `${childStickyTop}px`); | ||||||
|  | 	}, { | ||||||
|  | 		immediate: true, | ||||||
| 	}); | 	}); | ||||||
|  | 
 | ||||||
|  | 	headerEl.style.position = 'sticky'; | ||||||
|  | 	headerEl.style.top = 'var(--stickyTop, 0)'; | ||||||
|  | 	headerEl.style.zIndex = '1000'; | ||||||
|  | 
 | ||||||
|  | 	observer.observe(headerEl); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| onUnmounted(() => { | onUnmounted(() => { | ||||||
|  |  | ||||||
							
								
								
									
										99
									
								
								packages/client/src/components/marquee.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								packages/client/src/components/marquee.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,99 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | import { h, onMounted, onUnmounted, ref, watch } from 'vue'; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  | 	name: 'MarqueeText', | ||||||
|  | 	props: { | ||||||
|  | 		duration: { | ||||||
|  | 			type: Number, | ||||||
|  | 			default: 15, | ||||||
|  | 		}, | ||||||
|  | 		repeat: { | ||||||
|  | 			type: Number, | ||||||
|  | 			default: 2, | ||||||
|  | 		}, | ||||||
|  | 		paused: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			default: false, | ||||||
|  | 		}, | ||||||
|  | 		reverse: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			default: false, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	setup(props) { | ||||||
|  | 		const contentEl = ref(); | ||||||
|  | 
 | ||||||
|  | 		function calc() { | ||||||
|  | 			const eachLength = contentEl.value.offsetWidth / props.repeat; | ||||||
|  | 			const factor = 3000; | ||||||
|  | 			const duration = props.duration / ((1 / eachLength) * factor); | ||||||
|  | 
 | ||||||
|  | 			contentEl.value.style.animationDuration = `${duration}s`; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		watch(() => props.duration, calc); | ||||||
|  | 
 | ||||||
|  | 		onMounted(() => { | ||||||
|  | 			calc(); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		onUnmounted(() => { | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			contentEl, | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 	render({ | ||||||
|  | 		$slots, $style, $props: { | ||||||
|  | 			duration, repeat, paused, reverse, | ||||||
|  | 		}, | ||||||
|  | 	}) { | ||||||
|  | 		return h('div', { class: [$style.wrap] }, [ | ||||||
|  | 			h('span', { | ||||||
|  | 				ref: 'contentEl', | ||||||
|  | 				class: [ | ||||||
|  | 					paused | ||||||
|  | 						? $style.paused | ||||||
|  | 						: undefined, | ||||||
|  | 					$style.content, | ||||||
|  | 				], | ||||||
|  | 			}, Array(repeat).fill( | ||||||
|  | 				h('span', { | ||||||
|  | 					class: $style.text, | ||||||
|  | 					style: { | ||||||
|  | 						animationDirection: reverse | ||||||
|  | 							? 'reverse' | ||||||
|  | 							: undefined, | ||||||
|  | 					}, | ||||||
|  | 				}, $slots.default()), | ||||||
|  | 			)), | ||||||
|  | 		]); | ||||||
|  | 	}, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" module> | ||||||
|  | .wrap { | ||||||
|  | 	overflow: hidden; overflow: clip; | ||||||
|  | } | ||||||
|  | .content { | ||||||
|  | 	display: inline-block; | ||||||
|  | 	white-space: nowrap; | ||||||
|  | } | ||||||
|  | .text { | ||||||
|  | 	display: inline-block; | ||||||
|  | 	animation-name: marquee; | ||||||
|  | 	animation-timing-function: linear; | ||||||
|  | 	animation-iteration-count: infinite; | ||||||
|  | 	animation-duration: inherit; | ||||||
|  | } | ||||||
|  | .paused .text { | ||||||
|  | 	animation-play-state: paused; | ||||||
|  | } | ||||||
|  | @keyframes marquee { | ||||||
|  | 	0% { transform:translateX(0); } | ||||||
|  | 	100% { transform:translateX(-100%); } | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -27,7 +27,7 @@ const props = defineProps<{ | ||||||
| 	display: flex; | 	display: flex; | ||||||
| 	margin: 0; | 	margin: 0; | ||||||
| 	padding: 0; | 	padding: 0; | ||||||
| 	overflow: clip; | 	overflow: hidden; overflow: clip; | ||||||
| 	font-size: 0.95em; | 	font-size: 0.95em; | ||||||
| 
 | 
 | ||||||
| 	&.min-width_350px { | 	&.min-width_350px { | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ const showContent = $ref(false); | ||||||
| 	display: flex; | 	display: flex; | ||||||
| 	margin: 0; | 	margin: 0; | ||||||
| 	padding: 0; | 	padding: 0; | ||||||
| 	overflow: clip; | 	overflow: hidden; overflow: clip; | ||||||
| 	font-size: 0.95em; | 	font-size: 0.95em; | ||||||
| 
 | 
 | ||||||
| 	&.min-width_350px { | 	&.min-width_350px { | ||||||
|  |  | ||||||
|  | @ -297,7 +297,7 @@ function readPromo() { | ||||||
| 	position: relative; | 	position: relative; | ||||||
| 	transition: box-shadow 0.1s ease; | 	transition: box-shadow 0.1s ease; | ||||||
| 	font-size: 1.05em; | 	font-size: 1.05em; | ||||||
| 	overflow: clip; | 	overflow: hidden; overflow: clip; | ||||||
| 	contain: content; | 	contain: content; | ||||||
| 
 | 
 | ||||||
| 	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、 | 	// これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、 | ||||||
|  |  | ||||||
|  | @ -48,7 +48,10 @@ const router = new Router(routes, props.initialPath); | ||||||
| 
 | 
 | ||||||
| let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); | let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); | ||||||
| let windowEl = $ref<InstanceType<typeof XWindow>>(); | let windowEl = $ref<InstanceType<typeof XWindow>>(); | ||||||
| const history = $ref<string[]>([props.initialPath]); | const history = $ref<{ path: string; key: any; }[]>([{ | ||||||
|  | 	path: router.getCurrentPath(), | ||||||
|  | 	key: router.getCurrentKey(), | ||||||
|  | }]); | ||||||
| const buttonsLeft = $computed(() => { | const buttonsLeft = $computed(() => { | ||||||
| 	const buttons = []; | 	const buttons = []; | ||||||
| 
 | 
 | ||||||
|  | @ -72,7 +75,7 @@ const buttonsRight = $computed(() => { | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| router.addListener('push', ctx => { | router.addListener('push', ctx => { | ||||||
| 	history.push(router.getCurrentPath()); | 	history.push({ path: ctx.path, key: ctx.key }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| provide('router', router); | provide('router', router); | ||||||
|  | @ -111,7 +114,7 @@ function menu(ev) { | ||||||
| 
 | 
 | ||||||
| function back() { | function back() { | ||||||
| 	history.pop(); | 	history.pop(); | ||||||
| 	router.change(history[history.length - 1]); | 	router.change(history[history.length - 1].path, history[history.length - 1].key); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function close() { | function close() { | ||||||
|  | @ -136,5 +139,6 @@ defineExpose({ | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .yrolvcoq { | .yrolvcoq { | ||||||
| 	min-height: 100%; | 	min-height: 100%; | ||||||
|  | 	background: var(--bg); | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <template> | ||||||
| <div ref="rootEl" class="root"> | <div ref="rootEl" class="meijqfqm"> | ||||||
| 	<canvas :id="idForCanvas" ref="canvasEl" class="canvas" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas> | 	<canvas :id="idForCanvas" ref="canvasEl" class="canvas" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas> | ||||||
| 	<div :id="idForTags" ref="tagsEl" class="tags"> | 	<div :id="idForTags" ref="tagsEl" class="tags"> | ||||||
| 		<ul> | 		<ul> | ||||||
|  | @ -71,9 +71,9 @@ defineExpose({ | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .root { | .meijqfqm { | ||||||
| 	position: relative; | 	position: relative; | ||||||
| 	overflow: clip; | 	overflow: hidden; overflow: clip; | ||||||
| 	display: grid; | 	display: grid; | ||||||
| 	place-items: center; | 	place-items: center; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -54,7 +54,7 @@ onMounted(() => { | ||||||
| 		width: min-content; | 		width: min-content; | ||||||
| 		box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); | 		box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); | ||||||
| 		border-radius: 8px; | 		border-radius: 8px; | ||||||
| 		overflow: clip; | 		overflow: hidden; overflow: clip; | ||||||
| 		text-align: center; | 		text-align: center; | ||||||
| 		pointer-events: none; | 		pointer-events: none; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -148,7 +148,7 @@ export default defineComponent({ | ||||||
| 	text-decoration: none; | 	text-decoration: none; | ||||||
| 	background: var(--buttonBg); | 	background: var(--buttonBg); | ||||||
| 	border-radius: 5px; | 	border-radius: 5px; | ||||||
| 	overflow: clip; | 	overflow: hidden; overflow: clip; | ||||||
| 	box-sizing: border-box; | 	box-sizing: border-box; | ||||||
| 	transition: background 0.1s ease; | 	transition: background 0.1s ease; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,7 +10,8 @@ | ||||||
| 			</button> | 			</button> | ||||||
| 		</div> | 		</div> | ||||||
| 	</header> | 	</header> | ||||||
| 	<transition :name="$store.state.animation ? 'container-toggle' : ''" | 	<transition | ||||||
|  | 		:name="$store.state.animation ? 'container-toggle' : ''" | ||||||
| 		@enter="enter" | 		@enter="enter" | ||||||
| 		@after-enter="afterEnter" | 		@after-enter="afterEnter" | ||||||
| 		@leave="leave" | 		@leave="leave" | ||||||
|  | @ -34,37 +35,37 @@ export default defineComponent({ | ||||||
| 		showHeader: { | 		showHeader: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: true | 			default: true, | ||||||
| 		}, | 		}, | ||||||
| 		thin: { | 		thin: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false, | ||||||
| 		}, | 		}, | ||||||
| 		naked: { | 		naked: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false, | ||||||
| 		}, | 		}, | ||||||
| 		foldable: { | 		foldable: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false, | ||||||
| 		}, | 		}, | ||||||
| 		expanded: { | 		expanded: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: true | 			default: true, | ||||||
| 		}, | 		}, | ||||||
| 		scrollable: { | 		scrollable: { | ||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: false | 			default: false, | ||||||
| 		}, | 		}, | ||||||
| 		maxHeight: { | 		maxHeight: { | ||||||
| 			type: Number, | 			type: Number, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: null | 			default: null, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
|  | @ -79,12 +80,12 @@ export default defineComponent({ | ||||||
| 			const headerHeight = this.showHeader ? this.$refs.header.offsetHeight : 0; | 			const headerHeight = this.showHeader ? this.$refs.header.offsetHeight : 0; | ||||||
| 			this.$el.style.minHeight = `${headerHeight}px`; | 			this.$el.style.minHeight = `${headerHeight}px`; | ||||||
| 			if (showBody) { | 			if (showBody) { | ||||||
| 				this.$el.style.flexBasis = `auto`; | 				this.$el.style.flexBasis = 'auto'; | ||||||
| 			} else { | 			} else { | ||||||
| 				this.$el.style.flexBasis = `${headerHeight}px`; | 				this.$el.style.flexBasis = `${headerHeight}px`; | ||||||
| 			} | 			} | ||||||
| 		}, { | 		}, { | ||||||
| 			immediate: true | 			immediate: true, | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px'); | 		this.$el.style.setProperty('--maxHeight', this.maxHeight + 'px'); | ||||||
|  | @ -124,7 +125,7 @@ export default defineComponent({ | ||||||
| 		afterLeave(el) { | 		afterLeave(el) { | ||||||
| 			el.style.height = null; | 			el.style.height = null; | ||||||
| 		}, | 		}, | ||||||
| 	} | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -142,7 +143,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| .ukygtjoj { | .ukygtjoj { | ||||||
| 	position: relative; | 	position: relative; | ||||||
| 	overflow: clip; | 	overflow: hidden; overflow: clip; | ||||||
| 
 | 
 | ||||||
| 	&.naked { | 	&.naked { | ||||||
| 		background: transparent !important; | 		background: transparent !important; | ||||||
|  |  | ||||||
|  | @ -136,11 +136,11 @@ function focusDown() { | ||||||
| 	> .item { | 	> .item { | ||||||
| 		display: block; | 		display: block; | ||||||
| 		position: relative; | 		position: relative; | ||||||
| 		padding: 8px 18px; | 		padding: 6px 18px; | ||||||
| 		width: 100%; | 		width: 100%; | ||||||
| 		box-sizing: border-box; | 		box-sizing: border-box; | ||||||
| 		white-space: nowrap; | 		white-space: nowrap; | ||||||
| 		font-size: 0.9em; | 		font-size: 0.85em; | ||||||
| 		line-height: 20px; | 		line-height: 20px; | ||||||
| 		text-align: left; | 		text-align: left; | ||||||
| 		overflow: hidden; | 		overflow: hidden; | ||||||
|  |  | ||||||
|  | @ -389,7 +389,7 @@ defineExpose({ | ||||||
| 		left: 0; | 		left: 0; | ||||||
| 		width: 100%; | 		width: 100%; | ||||||
| 		height: 100%; | 		height: 100%; | ||||||
| 		overflow: clip; | 		overflow: hidden; overflow: clip; | ||||||
| 
 | 
 | ||||||
| 		> .content { | 		> .content { | ||||||
| 			position: fixed; | 			position: fixed; | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <MkStickyContainer> | ||||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
| 	<div style="overflow: clip;"> | 	<div style="overflow: hidden; overflow: clip;"> | ||||||
| 		<MkSpacer :content-max="600" :margin-min="20"> | 		<MkSpacer :content-max="600" :margin-min="20"> | ||||||
| 			<div class="_formRoot znqjceqz"> | 			<div class="_formRoot znqjceqz"> | ||||||
| 				<div id="debug"></div> | 				<div id="debug"></div> | ||||||
|  | @ -204,7 +204,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.aboutMisskey, | 	title: i18n.ts.aboutMisskey, | ||||||
| 	icon: null, | 	icon: null, | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -131,7 +131,6 @@ const headerTabs = $computed(() => [{ | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
| 	title: i18n.ts.instanceInfo, | 	title: i18n.ts.instanceInfo, | ||||||
| 	icon: 'fas fa-info-circle', | 	icon: 'fas fa-info-circle', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }))); | }))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -139,7 +138,7 @@ definePageMetadata(computed(() => ({ | ||||||
| .fwhjspax { | .fwhjspax { | ||||||
| 	text-align: center; | 	text-align: center; | ||||||
| 	border-radius: 10px; | 	border-radius: 10px; | ||||||
| 	overflow: clip; | 	overflow: hidden; overflow: clip; | ||||||
| 	background-size: cover; | 	background-size: cover; | ||||||
| 	background-position: center center; | 	background-position: center center; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| <template> | <template> | ||||||
| <MkStickyContainer> | <MkStickyContainer> | ||||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
| 	<MkSpacer v-if="file" :content-max="500" :margin-min="16" :margin-max="32"> | 	<MkSpacer v-if="file" :content-max="600" :margin-min="16" :margin-max="32"> | ||||||
| 		<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot"> | 		<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot"> | ||||||
| 			<a class="_formBlock thumbnail" :href="file.url" target="_blank"> | 			<a class="_formBlock thumbnail" :href="file.url" target="_blank"> | ||||||
| 				<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | 				<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||||
|  | @ -39,6 +39,20 @@ | ||||||
| 				<MkButton danger @click="del"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</MkButton> | 				<MkButton danger @click="del"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</MkButton> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
|  | 		<div v-else-if="tab === 'ip' && info" class="_formRoot"> | ||||||
|  | 			<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo> | ||||||
|  | 			<MkKeyValue v-if="info.requestIp" class="_formBlock _monospace" :copy="info.requestIp" oneline> | ||||||
|  | 				<template #key>IP</template> | ||||||
|  | 				<template #value>{{ info.requestIp }}</template> | ||||||
|  | 			</MkKeyValue> | ||||||
|  | 			<FormSection v-if="info.requestHeaders"> | ||||||
|  | 				<template #label>Headers</template> | ||||||
|  | 				<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_formBlock _monospace"> | ||||||
|  | 					<template #key>{{ k }}</template> | ||||||
|  | 					<template #value>{{ v }}</template> | ||||||
|  | 				</MkKeyValue> | ||||||
|  | 			</FormSection> | ||||||
|  | 		</div> | ||||||
| 		<div v-else-if="tab === 'raw'" class="_formRoot"> | 		<div v-else-if="tab === 'raw'" class="_formRoot"> | ||||||
| 			<MkObjectView v-if="info" tall :value="info"> | 			<MkObjectView v-if="info" tall :value="info"> | ||||||
| 			</MkObjectView> | 			</MkObjectView> | ||||||
|  | @ -54,13 +68,15 @@ import MkSwitch from '@/components/form/switch.vue'; | ||||||
| import MkObjectView from '@/components/object-view.vue'; | import MkObjectView from '@/components/object-view.vue'; | ||||||
| import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; | import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; | ||||||
| import MkKeyValue from '@/components/key-value.vue'; | import MkKeyValue from '@/components/key-value.vue'; | ||||||
| import FormLink from '@/components/form/link.vue'; | import FormSection from '@/components/form/section.vue'; | ||||||
| import MkUserCardMini from '@/components/user-card-mini.vue'; | import MkUserCardMini from '@/components/user-card-mini.vue'; | ||||||
|  | import MkInfo from '@/components/ui/info.vue'; | ||||||
| import bytes from '@/filters/bytes'; | import bytes from '@/filters/bytes'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| import { acct } from '@/filters/user'; | import { acct } from '@/filters/user'; | ||||||
|  | import { iAmAdmin, iAmModerator } from '@/account'; | ||||||
| 
 | 
 | ||||||
| let tab = $ref('overview'); | let tab = $ref('overview'); | ||||||
| let file: any = $ref(null); | let file: any = $ref(null); | ||||||
|  | @ -108,7 +124,11 @@ const headerTabs = $computed(() => [{ | ||||||
| 	key: 'overview', | 	key: 'overview', | ||||||
| 	title: i18n.ts.overview, | 	title: i18n.ts.overview, | ||||||
| 	icon: 'fas fa-info-circle', | 	icon: 'fas fa-info-circle', | ||||||
| }, { | }, iAmModerator ? { | ||||||
|  | 	key: 'ip', | ||||||
|  | 	title: 'IP', | ||||||
|  | 	icon: 'fas fa-bars-staggered', | ||||||
|  | } : null, { | ||||||
| 	key: 'raw', | 	key: 'raw', | ||||||
| 	title: 'Raw data', | 	title: 'Raw data', | ||||||
| 	icon: 'fas fa-code', | 	icon: 'fas fa-code', | ||||||
|  | @ -117,7 +137,6 @@ const headerTabs = $computed(() => [{ | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
| 	title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file, | 	title: file ? i18n.ts.file + ': ' + file.name : i18n.ts.file, | ||||||
| 	icon: 'fas fa-file', | 	icon: 'fas fa-file', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }))); | }))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -152,9 +152,6 @@ onUnmounted(() => { | ||||||
| .fdidabkc { | .fdidabkc { | ||||||
| 	--height: 60px; | 	--height: 60px; | ||||||
| 	display: flex; | 	display: flex; | ||||||
| 	position: sticky; |  | ||||||
| 	top: var(--stickyTop, 0); |  | ||||||
| 	z-index: 1000; |  | ||||||
| 	width: 100%; | 	width: 100%; | ||||||
| 	-webkit-backdrop-filter: var(--blur, blur(15px)); | 	-webkit-backdrop-filter: var(--blur, blur(15px)); | ||||||
| 	backdrop-filter: var(--blur, blur(15px)); | 	backdrop-filter: var(--blur, blur(15px)); | ||||||
|  |  | ||||||
|  | @ -87,7 +87,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.abuseReports, | 	title: i18n.ts.abuseReports, | ||||||
| 	icon: 'fas fa-exclamation-circle', | 	icon: 'fas fa-exclamation-circle', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -116,7 +116,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.ads, | 	title: i18n.ts.ads, | ||||||
| 	icon: 'fas fa-audio-description', | 	icon: 'fas fa-audio-description', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -102,7 +102,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.announcements, | 	title: i18n.ts.announcements, | ||||||
| 	icon: 'fas fa-broadcast-tower', | 	icon: 'fas fa-broadcast-tower', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -29,6 +29,5 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.database, | 	title: i18n.ts.database, | ||||||
| 	icon: 'fas fa-database', | 	icon: 'fas fa-database', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -122,6 +122,5 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.emailServer, | 	title: i18n.ts.emailServer, | ||||||
| 	icon: 'fas fa-envelope', | 	icon: 'fas fa-envelope', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -292,7 +292,6 @@ const headerTabs = $computed(() => [{ | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
| 	title: i18n.ts.customEmojis, | 	title: i18n.ts.customEmojis, | ||||||
| 	icon: 'fas fa-laugh', | 	icon: 'fas fa-laugh', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }))); | }))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -110,7 +110,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
| 	title: i18n.ts.files, | 	title: i18n.ts.files, | ||||||
| 	icon: 'fas fa-cloud', | 	icon: 'fas fa-cloud', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }))); | }))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -41,7 +41,6 @@ const router = useRouter(); | ||||||
| const indexInfo = { | const indexInfo = { | ||||||
| 	title: i18n.ts.controlPanel, | 	title: i18n.ts.controlPanel, | ||||||
| 	icon: 'fas fa-cog', | 	icon: 'fas fa-cog', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| 	hideHeader: true, | 	hideHeader: true, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -47,6 +47,5 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.instanceBlocking, | 	title: i18n.ts.instanceBlocking, | ||||||
| 	icon: 'fas fa-ban', | 	icon: 'fas fa-ban', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -53,6 +53,5 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.integration, | 	title: i18n.ts.integration, | ||||||
| 	icon: 'fas fa-share-alt', | 	icon: 'fas fa-share-alt', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -144,6 +144,5 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.objectStorage, | 	title: i18n.ts.objectStorage, | ||||||
| 	icon: 'fas fa-cloud', | 	icon: 'fas fa-cloud', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -40,6 +40,5 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.other, | 	title: i18n.ts.other, | ||||||
| 	icon: 'fas fa-cogs', | 	icon: 'fas fa-cogs', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -468,7 +468,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.dashboard, | 	title: i18n.ts.dashboard, | ||||||
| 	icon: 'fas fa-tachometer-alt', | 	icon: 'fas fa-tachometer-alt', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -562,7 +561,7 @@ definePageMetadata({ | ||||||
| 				> .body { | 				> .body { | ||||||
| 					background: var(--panel); | 					background: var(--panel); | ||||||
| 					border-radius: var(--radius); | 					border-radius: var(--radius); | ||||||
| 					overflow: clip; | 					overflow: hidden; overflow: clip; | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | @ -621,7 +620,7 @@ definePageMetadata({ | ||||||
| 				> .body { | 				> .body { | ||||||
| 					background: var(--panel); | 					background: var(--panel); | ||||||
| 					border-radius: var(--radius); | 					border-radius: var(--radius); | ||||||
| 					overflow: clip; | 					overflow: hidden; overflow: clip; | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -58,6 +58,5 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.proxyAccount, | 	title: i18n.ts.proxyAccount, | ||||||
| 	icon: 'fas fa-ghost', | 	icon: 'fas fa-ghost', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -52,6 +52,5 @@ const headerTabs = $computed(() => [{ | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.jobQueue, | 	title: i18n.ts.jobQueue, | ||||||
| 	icon: 'fas fa-clipboard-list', | 	icon: 'fas fa-clipboard-list', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -78,7 +78,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.relays, | 	title: i18n.ts.relays, | ||||||
| 	icon: 'fas fa-globe', | 	icon: 'fas fa-globe', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,6 +14,18 @@ | ||||||
| 					<XBotProtection/> | 					<XBotProtection/> | ||||||
| 				</FormFolder> | 				</FormFolder> | ||||||
| 
 | 
 | ||||||
|  | 				<FormFolder class="_formBlock"> | ||||||
|  | 					<template #label>Log IP address</template> | ||||||
|  | 					<template v-if="enableIpLogging" #suffix>Enabled</template> | ||||||
|  | 					<template v-else #suffix>Disabled</template> | ||||||
|  | 
 | ||||||
|  | 					<div class="_formRoot"> | ||||||
|  | 						<FormSwitch v-model="enableIpLogging" class="_formBlock" @update:modelValue="save"> | ||||||
|  | 							<template #label>Enable</template> | ||||||
|  | 						</FormSwitch> | ||||||
|  | 					</div> | ||||||
|  | 				</FormFolder> | ||||||
|  | 
 | ||||||
| 				<FormFolder class="_formBlock"> | 				<FormFolder class="_formBlock"> | ||||||
| 					<template #label>Summaly Proxy</template> | 					<template #label>Summaly Proxy</template> | ||||||
| 
 | 
 | ||||||
|  | @ -51,17 +63,20 @@ import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| let summalyProxy: string = $ref(''); | let summalyProxy: string = $ref(''); | ||||||
| let enableHcaptcha: boolean = $ref(false); | let enableHcaptcha: boolean = $ref(false); | ||||||
| let enableRecaptcha: boolean = $ref(false); | let enableRecaptcha: boolean = $ref(false); | ||||||
|  | let enableIpLogging: boolean = $ref(false); | ||||||
| 
 | 
 | ||||||
| async function init() { | async function init() { | ||||||
| 	const meta = await os.api('admin/meta'); | 	const meta = await os.api('admin/meta'); | ||||||
| 	summalyProxy = meta.summalyProxy; | 	summalyProxy = meta.summalyProxy; | ||||||
| 	enableHcaptcha = meta.enableHcaptcha; | 	enableHcaptcha = meta.enableHcaptcha; | ||||||
| 	enableRecaptcha = meta.enableRecaptcha; | 	enableRecaptcha = meta.enableRecaptcha; | ||||||
|  | 	enableIpLogging = meta.enableIpLogging; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function save() { | function save() { | ||||||
| 	os.apiWithDialog('admin/update-meta', { | 	os.apiWithDialog('admin/update-meta', { | ||||||
| 		summalyProxy, | 		summalyProxy, | ||||||
|  | 		enableIpLogging, | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
| 		fetchInstance(); | 		fetchInstance(); | ||||||
| 	}); | 	}); | ||||||
|  | @ -74,6 +89,5 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.security, | 	title: i18n.ts.security, | ||||||
| 	icon: 'fas fa-lock', | 	icon: 'fas fa-lock', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -258,6 +258,5 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.general, | 	title: i18n.ts.general, | ||||||
| 	icon: 'fas fa-cog', | 	icon: 'fas fa-cog', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -135,7 +135,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
| 	title: i18n.ts.users, | 	title: i18n.ts.users, | ||||||
| 	icon: 'fas fa-users', | 	icon: 'fas fa-users', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }))); | }))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -47,7 +47,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.announcements, | 	title: i18n.ts.announcements, | ||||||
| 	icon: 'fas fa-broadcast-tower', | 	icon: 'fas fa-broadcast-tower', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,6 @@ | ||||||
| <template> | <template> | ||||||
|  | <MkStickyContainer> | ||||||
|  | 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||||
| 	<div ref="rootEl" v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks"> | 	<div ref="rootEl" v-hotkey.global="keymap" v-size="{ min: [800] }" class="tqmomfks"> | ||||||
| 		<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> | 		<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> | ||||||
| 		<div class="tl _block"> | 		<div class="tl _block"> | ||||||
|  | @ -12,6 +14,7 @@ | ||||||
| 			/> | 			/> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
|  | </MkStickyContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | @ -21,7 +24,7 @@ import { scroll } from '@/scripts/scroll'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { useRouter } from '@/router'; | import { useRouter } from '@/router'; | ||||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| import i18n from '@/components/global/i18n'; | import { i18n } from '@/i18n'; | ||||||
| 
 | 
 | ||||||
| const router = useRouter(); | const router = useRouter(); | ||||||
| 
 | 
 | ||||||
|  | @ -68,15 +71,7 @@ watch(() => props.antennaId, async () => { | ||||||
| 	}); | 	}); | ||||||
| }, { immediate: true }); | }, { immediate: true }); | ||||||
| 
 | 
 | ||||||
| const headerActions = $computed(() => []); | const headerActions = $computed(() => antenna ? [{ | ||||||
| 
 |  | ||||||
| const headerTabs = $computed(() => []); |  | ||||||
| 
 |  | ||||||
| definePageMetadata(computed(() => antenna ? { |  | ||||||
| 	title: antenna.name, |  | ||||||
| 	icon: 'fas fa-satellite', |  | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| 	actions: [{ |  | ||||||
| 	icon: 'fas fa-calendar-alt', | 	icon: 'fas fa-calendar-alt', | ||||||
| 	text: i18n.ts.jumpToSpecifiedDate, | 	text: i18n.ts.jumpToSpecifiedDate, | ||||||
| 	handler: timetravel, | 	handler: timetravel, | ||||||
|  | @ -84,7 +79,13 @@ definePageMetadata(computed(() => antenna ? { | ||||||
| 	icon: 'fas fa-cog', | 	icon: 'fas fa-cog', | ||||||
| 	text: i18n.ts.settings, | 	text: i18n.ts.settings, | ||||||
| 	handler: settings, | 	handler: settings, | ||||||
| 	}], | }] : []); | ||||||
|  | 
 | ||||||
|  | const headerTabs = $computed(() => []); | ||||||
|  | 
 | ||||||
|  | definePageMetadata(computed(() => antenna ? { | ||||||
|  | 	title: antenna.name, | ||||||
|  | 	icon: 'fas fa-satellite', | ||||||
| } : null)); | } : null)); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -109,7 +110,7 @@ definePageMetadata(computed(() => antenna ? { | ||||||
| 	> .tl { | 	> .tl { | ||||||
| 		background: var(--bg); | 		background: var(--bg); | ||||||
| 		border-radius: var(--radius); | 		border-radius: var(--radius); | ||||||
| 		overflow: clip; | 		overflow: hidden; overflow: clip; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	&.min-width_800px { | 	&.min-width_800px { | ||||||
|  |  | ||||||
|  | @ -111,11 +111,9 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata(computed(() => props.channelId ? { | definePageMetadata(computed(() => props.channelId ? { | ||||||
| 	title: i18n.ts._channel.edit, | 	title: i18n.ts._channel.edit, | ||||||
| 	icon: 'fas fa-satellite-dish', | 	icon: 'fas fa-satellite-dish', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| } : { | } : { | ||||||
| 	title: i18n.ts._channel.create, | 	title: i18n.ts._channel.create, | ||||||
| 	icon: 'fas fa-satellite-dish', | 	icon: 'fas fa-satellite-dish', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| })); | })); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -80,7 +80,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata(computed(() => channel ? { | definePageMetadata(computed(() => channel ? { | ||||||
| 	title: channel.name, | 	title: channel.name, | ||||||
| 	icon: 'fas fa-satellite-dish', | 	icon: 'fas fa-satellite-dish', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| } : null)); | } : null)); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -75,6 +75,5 @@ const headerTabs = $computed(() => [{ | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
| 	title: i18n.ts.channel, | 	title: i18n.ts.channel, | ||||||
| 	icon: 'fas fa-satellite-dish', | 	icon: 'fas fa-satellite-dish', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }))); | }))); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -102,7 +102,6 @@ const headerActions = $computed(() => clip && isOwned ? [{ | ||||||
| definePageMetadata(computed(() => clip ? { | definePageMetadata(computed(() => clip ? { | ||||||
| 	title: clip.name, | 	title: clip.name, | ||||||
| 	icon: 'fas fa-paperclip', | 	icon: 'fas fa-paperclip', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| } : null)); | } : null)); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,7 +20,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
| 	title: folder ? folder.name : i18n.ts.drive, | 	title: folder ? folder.name : i18n.ts.drive, | ||||||
| 	icon: 'fas fa-cloud', | 	icon: 'fas fa-cloud', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| 	hideHeader: true, | 	hideHeader: true, | ||||||
| }))); | }))); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <MkSpacer :content-max="800"> | <MkSpacer :content-max="800"> | ||||||
| 	<MkTab v-model="tab"> | 	<MkTab v-model="tab" style="margin-bottom: var(--margin);"> | ||||||
| 		<option value="notes">{{ i18n.ts.notes }}</option> | 		<option value="notes">{{ i18n.ts.notes }}</option> | ||||||
| 		<option value="polls">{{ i18n.ts.poll }}</option> | 		<option value="polls">{{ i18n.ts.poll }}</option> | ||||||
| 	</MkTab> | 	</MkTab> | ||||||
|  |  | ||||||
|  | @ -87,6 +87,5 @@ const headerTabs = $computed(() => [{ | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
| 	title: i18n.ts.explore, | 	title: i18n.ts.explore, | ||||||
| 	icon: 'fas fa-hashtag', | 	icon: 'fas fa-hashtag', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }))); | }))); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -38,7 +38,6 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>(); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.favorites, | 	title: i18n.ts.favorites, | ||||||
| 	icon: 'fas fa-star', | 	icon: 'fas fa-star', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -65,7 +65,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata(computed(() => ({ | definePageMetadata(computed(() => ({ | ||||||
| 	title: i18n.ts.followRequests, | 	title: i18n.ts.followRequests, | ||||||
| 	icon: 'fas fa-user-clock', | 	icon: 'fas fa-user-clock', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }))); | }))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -116,11 +116,9 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata(computed(() => props.postId ? { | definePageMetadata(computed(() => props.postId ? { | ||||||
| 	title: i18n.ts.edit, | 	title: i18n.ts.edit, | ||||||
| 	icon: 'fas fa-pencil-alt', | 	icon: 'fas fa-pencil-alt', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| } : { | } : { | ||||||
| 	title: i18n.ts.postToGallery, | 	title: i18n.ts.postToGallery, | ||||||
| 	icon: 'fas fa-pencil-alt', | 	icon: 'fas fa-pencil-alt', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| })); | })); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -122,7 +122,6 @@ const headerTabs = $computed(() => [{ | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.gallery, | 	title: i18n.ts.gallery, | ||||||
| 	icon: 'fas fa-icons', | 	icon: 'fas fa-icons', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -149,7 +149,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata(computed(() => post ? { | definePageMetadata(computed(() => post ? { | ||||||
| 	title: post.title, | 	title: post.title, | ||||||
| 	avatar: post.user, | 	avatar: post.user, | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| } : null)); | } : null)); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -215,7 +215,6 @@ const headerTabs = $computed(() => [{ | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: props.host, | 	title: props.host, | ||||||
| 	icon: 'fas fa-server', | 	icon: 'fas fa-server', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -159,7 +159,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.messaging, | 	title: i18n.ts.messaging, | ||||||
| 	icon: 'fas fa-comments', | 	icon: 'fas fa-comments', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -38,7 +38,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.manageAntennas, | 	title: i18n.ts.manageAntennas, | ||||||
| 	icon: 'fas fa-satellite', | 	icon: 'fas fa-satellite', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -34,7 +34,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.manageAntennas, | 	title: i18n.ts.manageAntennas, | ||||||
| 	icon: 'fas fa-satellite', | 	icon: 'fas fa-satellite', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -69,7 +69,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.clip, | 	title: i18n.ts.clip, | ||||||
| 	icon: 'fas fa-paperclip', | 	icon: 'fas fa-paperclip', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| 	action: { | 	action: { | ||||||
| 		icon: 'fas fa-plus', | 		icon: 'fas fa-plus', | ||||||
| 		handler: create, | 		handler: create, | ||||||
|  |  | ||||||
|  | @ -46,7 +46,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata({ | definePageMetadata({ | ||||||
| 	title: i18n.ts.manageLists, | 	title: i18n.ts.manageLists, | ||||||
| 	icon: 'fas fa-list-ul', | 	icon: 'fas fa-list-ul', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| 	action: { | 	action: { | ||||||
| 		icon: 'fas fa-plus', | 		icon: 'fas fa-plus', | ||||||
| 		handler: create, | 		handler: create, | ||||||
|  |  | ||||||
|  | @ -120,7 +120,6 @@ const headerTabs = $computed(() => []); | ||||||
| definePageMetadata(computed(() => list ? { | definePageMetadata(computed(() => list ? { | ||||||
| 	title: list.name, | 	title: list.name, | ||||||
| 	icon: 'fas fa-list-ul', | 	icon: 'fas fa-list-ul', | ||||||
| 	bg: 'var(--bg)', |  | ||||||
| } : null)); | } : null)); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue