forked from FoundKeyGang/FoundKey
Compare commits
37 Commits
9a4eef780f
...
b1b6eb2a86
Author | SHA1 | Date |
---|---|---|
vib | b1b6eb2a86 | |
Johann150 | ca257d7d0c | |
Johann150 | 30c26abde7 | |
Johann150 | 17324e1e94 | |
Johann150 | 8b98c9f2f4 | |
Johann150 | be30e70344 | |
Johann150 | 39fb7e5946 | |
Johann150 | 75b14124f2 | |
Johann150 | 7480e27c0c | |
Johann150 | 953de3e4b2 | |
Johann150 | 2d32bc33d7 | |
Chloe Kudryavtsev | bb3ec8bafe | |
Johann150 | 6fd80816fa | |
Johann150 | cc83cbe523 | |
Johann150 | 8abd3ebec7 | |
Johann150 | 36031c083a | |
Johann150 | 05f8172ce9 | |
Johann150 | 151053897d | |
Johann150 | 95a9027a66 | |
Johann150 | 57cf6c7163 | |
Johann150 | 9b76c805ec | |
Johann150 | 21b20920c2 | |
Johann150 | e7644eb757 | |
Johann150 | 66ec875624 | |
Johann150 | 78f5ca3792 | |
Johann150 | c792e4199c | |
Johann150 | afa4094050 | |
Johann150 | c4b5952788 | |
Johann150 | e3fd371f4a | |
Johann150 | 5893a44ff5 | |
Johann150 | 9bdf24d3a5 | |
Johann150 | 2bbb85b472 | |
Johann150 | 70fb1e9a5c | |
Johann150 | 48163872ed | |
Johann150 | b245d39b6e | |
Johann150 | 80f72e21cd | |
Johann150 | 85e985d13f |
6
.mailmap
6
.mailmap
|
@ -1,9 +1,9 @@
|
|||
Andreas Nedbal <git@pixelde.su> <andreas.nedbal@in2code.de>
|
||||
Andreas Nedbal <git@pixelde.su> <github-bf215181b5140522137b3d4f6b73544a@desu.email>
|
||||
Balazs Nadasdi <balazs@weave.works> <yitsushi@gmail.com>
|
||||
Chloe Kudryavtsev <code@code.bunkerlabs.net> <code@toast.bunkerlabs.net>
|
||||
Chloe Kudryavtsev <code@code.bunkerlabs.net> <toast+git@toast.cafe>
|
||||
Chloe Kudryavtsev <code@code.bunkerlabs.net> <toast@toast.cafe>
|
||||
Chloe Kudryavtsev <code@toast.bunkerlabs.net> <code@code.bunkerlabs.net>
|
||||
Chloe Kudryavtsev <code@toast.bunkerlabs.net> <toast+git@toast.cafe>
|
||||
Chloe Kudryavtsev <code@toast.bunkerlabs.net> <toast@toast.cafe>
|
||||
Dr. Gutfuck LLC <40531868+gutfuckllc@users.noreply.github.com>
|
||||
Ehsan Javadynia <31900907+ehsanjavadynia@users.noreply.github.com> <ehsan.javadynia@gmail.com>
|
||||
Francis Dinh <normandy@biribiri.dev>
|
||||
|
|
|
@ -679,7 +679,6 @@ editCode: "Edit code"
|
|||
apply: "Apply"
|
||||
receiveAnnouncementFromInstance: "Receive notifications from this instance"
|
||||
emailNotification: "Email notifications"
|
||||
publish: "Publish"
|
||||
useReactionPickerForContextMenu: "Open reaction picker on right-click"
|
||||
typingUsers: "{users} is/are typing..."
|
||||
jumpToSpecifiedDate: "Jump to specific date"
|
||||
|
@ -720,11 +719,7 @@ switch: "Switch"
|
|||
noMaintainerInformationWarning: "Maintainer information is not configured."
|
||||
noBotProtectionWarning: "Bot protection is not configured."
|
||||
configure: "Configure"
|
||||
postToGallery: "Create new gallery post"
|
||||
attachmentRequired: "At least 1 attachment is required."
|
||||
gallery: "Gallery"
|
||||
recentPosts: "Recent posts"
|
||||
popularPosts: "Popular posts"
|
||||
shareWithNote: "Share with note"
|
||||
emailNotConfiguredWarning: "Email address not set."
|
||||
ratio: "Ratio"
|
||||
|
@ -863,11 +858,6 @@ _forgotPassword:
|
|||
\ instance administrator instead."
|
||||
contactAdmin: "This instance does not support using email addresses, please contact\
|
||||
\ the instance administrator to reset your password instead."
|
||||
_gallery:
|
||||
my: "My Gallery"
|
||||
liked: "Liked Posts"
|
||||
like: "Like"
|
||||
unlike: "Remove like"
|
||||
_email:
|
||||
_follow:
|
||||
title: "You've got a new follower"
|
||||
|
@ -1109,10 +1099,6 @@ _permissions:
|
|||
"write:user-groups": "Create, modify, delete, transfer, join and leave groups. Invite and ban others from groups. Accept and reject group invitations."
|
||||
"read:channels": "List and read followed and joined channels"
|
||||
"write:channels": "Create, modify, follow and unfollow channels"
|
||||
"read:gallery": "List and read gallery posts"
|
||||
"write:gallery": "Create, modify and delete gallery posts"
|
||||
"read:gallery-likes": "List and read gallery post likes"
|
||||
"write:gallery-likes": "Like and unlike gallery posts"
|
||||
_auth:
|
||||
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
|
||||
shareAccessAsk: "Are you sure you want to authorize this application to access your\
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import { genId } from '../built/misc/gen-id.js';
|
||||
|
||||
export class removeGroups1673892262930 {
|
||||
name = 'removeGroups1673892262930';
|
||||
|
||||
async up(queryRunner) {
|
||||
// migrate gallery posts into notes, keeping the ids
|
||||
await queryRunner.query(`
|
||||
INSERT INTO "note" (
|
||||
"id", "createdAt", "text", "cw", "userId", "visibility", "fileIds", "attachedFileTypes", "tags"
|
||||
)
|
||||
WITH "file_types" ("id", "types") AS (
|
||||
SELECT "gallery_post"."id", ARRAY_AGG("drive_file"."type")
|
||||
FROM "gallery_post"
|
||||
JOIN "drive_file" ON "drive_file"."id" = ANY("gallery_post"."fileIds")
|
||||
GROUP BY "gallery_post"."id"
|
||||
)
|
||||
SELECT "gallery_post"."id", "gallery_post"."createdAt",
|
||||
CASE
|
||||
WHEN "gallery_post"."title" IS NULL THEN "gallery_post"."description"
|
||||
ELSE '<b>' || "gallery_post"."title" || E'</b>\\n\\n' || "gallery_post"."description"
|
||||
END,
|
||||
CASE
|
||||
WHEN "gallery_post"."isSensitive" THEN 'NSFW'
|
||||
ELSE NULL
|
||||
END,
|
||||
"gallery_post"."userId", 'home', "gallery_post"."fileIds", "file_types"."types", "gallery_post"."tags"
|
||||
FROM "gallery_post"
|
||||
JOIN "file_types" ON "gallery_post"."id" = "file_types"."id"
|
||||
`);
|
||||
// make a clip for each users gallery
|
||||
await queryRunner.query(`SELECT DISTINCT "userId" FROM "gallery_post"`).then(userIds =>
|
||||
Promise.all(userIds.map(({ userId }) => {
|
||||
const clipId = genId();
|
||||
|
||||
// generate the clip itself
|
||||
return queryRunner.query(`INSERT INTO "clip" ("id", "createdAt", "userId", "name", "isPublic") VALUES ($1, now(), $2, 'Gallery', true)`, [clipId, userId])
|
||||
// and add all the previous gallery posts to it
|
||||
// to not have to use genId for each gallery post, we just prepend a zero, something that could never be generated by genId
|
||||
.then(() => queryRunner.query(`INSERT INTO "clip_note" ("id", "noteId", "clipId") SELECT '0' || "id", "id", $1 FROM "gallery_post" WHERE "userId" = $2`, [clipId, userId]));
|
||||
}))
|
||||
);
|
||||
|
||||
await queryRunner.query(`DROP TABLE "gallery_like"`);
|
||||
await queryRunner.query(`DROP TABLE "gallery_post"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
// can only restore the table structure
|
||||
await queryRunner.query(`CREATE TABLE "gallery_post" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "description" character varying(2048), "userId" character varying(32) NOT NULL, "fileIds" character varying(32) array NOT NULL DEFAULT '{}'::varchar[], "isSensitive" boolean NOT NULL DEFAULT false, "likedCount" integer NOT NULL DEFAULT '0', "tags" character varying(128) array NOT NULL DEFAULT '{}'::varchar[], CONSTRAINT "PK_8e90d7b6015f2c4518881b14753" PRIMARY KEY ("id")); COMMENT ON COLUMN "gallery_post"."createdAt" IS 'The created date of the GalleryPost.'; COMMENT ON COLUMN "gallery_post"."updatedAt" IS 'The updated date of the GalleryPost.'; COMMENT ON COLUMN "gallery_post"."userId" IS 'The ID of author.'; COMMENT ON COLUMN "gallery_post"."isSensitive" IS 'Whether the post is sensitive.'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_8f1a239bd077c8864a20c62c2c" ON "gallery_post" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f631d37835adb04792e361807c" ON "gallery_post" ("updatedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_985b836dddd8615e432d7043dd" ON "gallery_post" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3ca50563facd913c425e7a89ee" ON "gallery_post" ("fileIds") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f2d744d9a14d0dfb8b96cb7fc5" ON "gallery_post" ("isSensitive") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_1a165c68a49d08f11caffbd206" ON "gallery_post" ("likedCount") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_05cca34b985d1b8edc1d1e28df" ON "gallery_post" ("tags") `);
|
||||
await queryRunner.query(`CREATE TABLE "gallery_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "postId" character varying(32) NOT NULL, CONSTRAINT "PK_853ab02be39b8de45cd720cc15f" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_8fd5215095473061855ceb948c" ON "gallery_like" ("userId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_df1b5f4099e99fb0bc5eae53b6" ON "gallery_like" ("userId", "postId") `);
|
||||
await queryRunner.query(`ALTER TABLE "gallery_post" ADD CONSTRAINT "FK_985b836dddd8615e432d7043ddb" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "gallery_like" ADD CONSTRAINT "FK_8fd5215095473061855ceb948cf" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "gallery_like" ADD CONSTRAINT "FK_b1cb568bfe569e47b7051699fc8" FOREIGN KEY ("postId") REFERENCES "gallery_post"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
export class syncOrm1674499888924 {
|
||||
name = 'syncOrm1674499888924'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS 'The native access token of local users, or null.'`);
|
||||
await queryRunner.query(`ALTER TABLE "auth_session" DROP CONSTRAINT "UQ_8e001e5a101c6dca37df1a76d66"`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_71d35fceee0d0fa62b2fa8f3b2" ON "note" ("url") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_d9ecaed8c6dc43f3592c229282" ON "user_group_joining" ("userId", "userGroupId") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_d9ecaed8c6dc43f3592c229282"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_71d35fceee0d0fa62b2fa8f3b2"`);
|
||||
await queryRunner.query(`ALTER TABLE "auth_session" ADD CONSTRAINT "UQ_8e001e5a101c6dca37df1a76d66" UNIQUE ("accessTokenId")`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "user"."token" IS 'The native access token of the User. It will be null if the origin of the user is local.'`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
export class registryRemoveDomain1675375940759 {
|
||||
name = 'registryRemoveDomain1675375940759'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_0a72bdfcdb97c0eca11fe7ecad"`);
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" DROP COLUMN "domain"`);
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "key" TYPE text USING "key"::text`);
|
||||
// delete existing duplicated entries, keeping the latest updated one
|
||||
await queryRunner.query(`DELETE FROM "registry_item" AS "a" WHERE "updatedAt" != (SELECT MAX("updatedAt") OVER (PARTITION BY "userId", "key", "scope") FROM "registry_item" AS "b" WHERE "a"."userId" = "b"."userId" AND "a"."key" = "b"."key" AND "a"."scope" = "b"."scope")`);
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ADD CONSTRAINT "UQ_b8d6509f847331273ab99daccc7" UNIQUE ("userId", "key", "scope")`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" DROP CONSTRAINT "UQ_b8d6509f847331273ab99daccc7"`);
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "key" TYPE character varying(1024) USING "key"::varchar(1024)`);
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ADD "domain" character varying(512)`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad" ON "registry_item" ("domain") `);
|
||||
}
|
||||
}
|
|
@ -50,8 +50,6 @@ import { UserSecurityKey } from '@/models/entities/user-security-key.js';
|
|||
import { AttestationChallenge } from '@/models/entities/attestation-challenge.js';
|
||||
import { Page } from '@/models/entities/page.js';
|
||||
import { PageLike } from '@/models/entities/page-like.js';
|
||||
import { GalleryPost } from '@/models/entities/gallery-post.js';
|
||||
import { GalleryLike } from '@/models/entities/gallery-like.js';
|
||||
import { ModerationLog } from '@/models/entities/moderation-log.js';
|
||||
import { UsedUsername } from '@/models/entities/used-username.js';
|
||||
import { Announcement } from '@/models/entities/announcement.js';
|
||||
|
@ -143,8 +141,6 @@ export const entities = [
|
|||
NoteUnread,
|
||||
Page,
|
||||
PageLike,
|
||||
GalleryPost,
|
||||
GalleryLike,
|
||||
DriveFile,
|
||||
DriveFolder,
|
||||
Poll,
|
||||
|
|
|
@ -27,9 +27,5 @@ export const kinds = [
|
|||
'write:user-groups',
|
||||
'read:channels',
|
||||
'write:channels',
|
||||
'read:gallery',
|
||||
'write:gallery',
|
||||
'read:gallery-likes',
|
||||
'write:gallery-likes',
|
||||
];
|
||||
// IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions).
|
||||
|
|
|
@ -28,7 +28,6 @@ import { packedAntennaSchema } from '@/models/schema/antenna.js';
|
|||
import { packedClipSchema } from '@/models/schema/clip.js';
|
||||
import { packedFederationInstanceSchema } from '@/models/schema/federation-instance.js';
|
||||
import { packedQueueCountSchema } from '@/models/schema/queue.js';
|
||||
import { packedGalleryPostSchema } from '@/models/schema/gallery-post.js';
|
||||
import { packedEmojiSchema } from '@/models/schema/emoji.js';
|
||||
|
||||
export const refs = {
|
||||
|
@ -61,7 +60,6 @@ export const refs = {
|
|||
Antenna: packedAntennaSchema,
|
||||
Clip: packedClipSchema,
|
||||
FederationInstance: packedFederationInstanceSchema,
|
||||
GalleryPost: packedGalleryPostSchema,
|
||||
Emoji: packedEmojiSchema,
|
||||
};
|
||||
|
||||
|
|
|
@ -79,7 +79,6 @@ export class AccessToken {
|
|||
|
||||
@Column('varchar', {
|
||||
length: 64, array: true,
|
||||
default: '{}',
|
||||
})
|
||||
public permission: string[];
|
||||
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from '../id.js';
|
||||
import { User } from './user.js';
|
||||
import { GalleryPost } from './gallery-post.js';
|
||||
|
||||
@Entity()
|
||||
@Index(['userId', 'postId'], { unique: true })
|
||||
export class GalleryLike {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone')
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column(id())
|
||||
public postId: GalleryPost['id'];
|
||||
|
||||
@ManyToOne(() => GalleryPost, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public post: GalleryPost | null;
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { id } from '../id.js';
|
||||
import { User } from './user.js';
|
||||
import { DriveFile } from './drive-file.js';
|
||||
|
||||
@Entity()
|
||||
export class GalleryPost {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the GalleryPost.',
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The updated date of the GalleryPost.',
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public title: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 2048, nullable: true,
|
||||
})
|
||||
public description: string | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The ID of author.',
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
array: true, default: '{}',
|
||||
})
|
||||
public fileIds: DriveFile['id'][];
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether the post is sensitive.',
|
||||
})
|
||||
public isSensitive: boolean;
|
||||
|
||||
@Index()
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
public likedCount: number;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 128, array: true, default: '{}',
|
||||
})
|
||||
public tags: string[];
|
||||
|
||||
constructor(data: Partial<GalleryPost>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -117,6 +117,7 @@ export class Note {
|
|||
})
|
||||
public uri: string | null;
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
comment: 'The human readable url of a note. it will be null when the note is local.',
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, Unique } from 'typeorm';
|
||||
import { id } from '../id.js';
|
||||
import { User } from './user.js';
|
||||
|
||||
// TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい
|
||||
@Entity()
|
||||
@Unique(['userId', 'key', 'scope'])
|
||||
export class RegistryItem {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
@ -31,8 +31,7 @@ export class RegistryItem {
|
|||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
@Column('text', {
|
||||
comment: 'The key of the RegistryItem.',
|
||||
})
|
||||
public key: string;
|
||||
|
@ -48,11 +47,4 @@ export class RegistryItem {
|
|||
length: 1024, array: true, default: '{}',
|
||||
})
|
||||
public scope: string[];
|
||||
|
||||
// サードパーティアプリに開放するときのためのカラム
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
})
|
||||
public domain: string | null;
|
||||
}
|
||||
|
|
|
@ -42,8 +42,6 @@ import { UserSecurityKey } from './entities/user-security-key.js';
|
|||
import { HashtagRepository } from './repositories/hashtag.js';
|
||||
import { PageRepository } from './repositories/page.js';
|
||||
import { PageLikeRepository } from './repositories/page-like.js';
|
||||
import { GalleryPostRepository } from './repositories/gallery-post.js';
|
||||
import { GalleryLikeRepository } from './repositories/gallery-like.js';
|
||||
import { ModerationLogRepository } from './repositories/moderation-logs.js';
|
||||
import { UsedUsername } from './entities/used-username.js';
|
||||
import { ClipRepository } from './repositories/clip.js';
|
||||
|
@ -108,8 +106,6 @@ export const Signins = (SigninRepository);
|
|||
export const MessagingMessages = (MessagingMessageRepository);
|
||||
export const Pages = (PageRepository);
|
||||
export const PageLikes = (PageLikeRepository);
|
||||
export const GalleryPosts = (GalleryPostRepository);
|
||||
export const GalleryLikes = (GalleryLikeRepository);
|
||||
export const ModerationLogs = (ModerationLogRepository);
|
||||
export const Clips = (ClipRepository);
|
||||
export const ClipNotes = db.getRepository(ClipNote);
|
||||
|
|
|
@ -108,9 +108,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
|||
folderId: file.folderId,
|
||||
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
|
||||
detail: true,
|
||||
}) : null,
|
||||
userId: opts.withUser ? file.userId : null,
|
||||
user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null,
|
||||
}) : undefined,
|
||||
userId: file.userId,
|
||||
user: (opts.withUser && file.userId) ? Users.pack(file.userId) : undefined,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import { db } from '@/db/postgre.js';
|
||||
import { GalleryLike } from '@/models/entities/gallery-like.js';
|
||||
import { GalleryPosts } from '../index.js';
|
||||
|
||||
export const GalleryLikeRepository = db.getRepository(GalleryLike).extend({
|
||||
async pack(
|
||||
src: GalleryLike['id'] | GalleryLike,
|
||||
me?: any,
|
||||
) {
|
||||
const like = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: like.id,
|
||||
post: await GalleryPosts.pack(like.post || like.postId, me),
|
||||
};
|
||||
},
|
||||
|
||||
packMany(
|
||||
likes: any[],
|
||||
me: any,
|
||||
) {
|
||||
return Promise.all(likes.map(x => this.pack(x, me)));
|
||||
},
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
import { db } from '@/db/postgre.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { GalleryPost } from '@/models/entities/gallery-post.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
import { Users, DriveFiles, GalleryLikes } from '../index.js';
|
||||
|
||||
export const GalleryPostRepository = db.getRepository(GalleryPost).extend({
|
||||
async pack(
|
||||
src: GalleryPost['id'] | GalleryPost,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
): Promise<Packed<'GalleryPost'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const post = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: post.id,
|
||||
createdAt: post.createdAt.toISOString(),
|
||||
updatedAt: post.updatedAt.toISOString(),
|
||||
userId: post.userId,
|
||||
user: Users.pack(post.user || post.userId, me),
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
fileIds: post.fileIds,
|
||||
files: DriveFiles.packMany(post.fileIds),
|
||||
tags: post.tags.length > 0 ? post.tags : undefined,
|
||||
isSensitive: post.isSensitive,
|
||||
likedCount: post.likedCount,
|
||||
isLiked: meId ? await GalleryLikes.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
|
||||
});
|
||||
},
|
||||
|
||||
packMany(
|
||||
posts: GalleryPost[],
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(posts.map(x => this.pack(x, me)));
|
||||
},
|
||||
});
|
|
@ -1,69 +0,0 @@
|
|||
export const packedGalleryPostSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
fileIds: {
|
||||
type: 'array',
|
||||
optional: true, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
files: {
|
||||
type: 'array',
|
||||
optional: true, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
optional: true, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
isSensitive: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
|
@ -62,9 +62,11 @@ export class DbResolver {
|
|||
id: parsed.id,
|
||||
});
|
||||
} else {
|
||||
return await Notes.findOneBy({
|
||||
return await Notes.findOneBy([{
|
||||
uri: parsed.uri,
|
||||
});
|
||||
}, {
|
||||
url: parsed.uri,
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import { DbResolver } from '../db-resolver.js';
|
|||
import { apLogger } from '../logger.js';
|
||||
import { resolvePerson } from './person.js';
|
||||
import { resolveImage } from './image.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
import { extractApHashtags, extractQuoteUrl } from './tag.js';
|
||||
import { extractPollFromQuestion } from './question.js';
|
||||
import { extractApMentions } from './mention.js';
|
||||
|
||||
|
@ -154,10 +154,10 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
|||
})
|
||||
: null;
|
||||
|
||||
// 引用
|
||||
let quote: Note | undefined | null;
|
||||
const quoteUrl = extractQuoteUrl(note.tag);
|
||||
|
||||
if (note._misskey_quote || note.quoteUri) {
|
||||
if (quoteUrl || note._misskey_quote || note.quoteUri) {
|
||||
const tryResolveNote = async (uri: string): Promise<{
|
||||
status: 'ok';
|
||||
res: Note | null;
|
||||
|
@ -184,10 +184,16 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
|||
}
|
||||
};
|
||||
|
||||
const uris = unique([note._misskey_quote, note.quoteUri].filter((x): x is string => typeof x === 'string'));
|
||||
const results = await Promise.all(uris.map(uri => tryResolveNote(uri)));
|
||||
|
||||
quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x);
|
||||
const uris = unique([quoteUrl, note._misskey_quote, note.quoteUri].filter((x): x is string => typeof x === 'string'));
|
||||
// check the urls sequentially and abort early to not do unnecessary HTTP requests
|
||||
// picks the first one that works
|
||||
for (const uri in uris) {
|
||||
const res = await tryResolveNote(uri);
|
||||
if (res.status === 'ok') {
|
||||
quote = res.res;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!quote) {
|
||||
if (results.some(x => x.status === 'temperror')) {
|
||||
throw new Error('quote resolve failed');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { toArray } from '@/prelude/array.js';
|
||||
import { IObject, isHashtag, IApHashtag } from '../type.js';
|
||||
import { IObject, isHashtag, IApHashtag, isLink, ILink } from '../type.js';
|
||||
|
||||
export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
|
||||
if (tags == null) return [];
|
||||
|
@ -16,3 +16,34 @@ export function extractApHashtagObjects(tags: IObject | IObject[] | null | undef
|
|||
if (tags == null) return [];
|
||||
return toArray(tags).filter(isHashtag);
|
||||
}
|
||||
|
||||
// implements FEP-e232: Object Links (2022-12-23 version)
|
||||
export function extractQuoteUrl(tags: IObject | IObject[] | null | undefined): string | null {
|
||||
if (tags == null) return null;
|
||||
|
||||
// filter out correct links
|
||||
let quotes: ILink[] = toArray(tags)
|
||||
.filter(isLink)
|
||||
.filter(link =>
|
||||
[
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'application/activity+json'
|
||||
].includes(link.mediaType?.toLowerCase())
|
||||
)
|
||||
.filter(link =>
|
||||
toArray(link.rel)
|
||||
.some(rel =>
|
||||
[
|
||||
'https://misskey-hub.net/ns#_misskey_quote',
|
||||
'http://fedibird.com/ns#quoteUri',
|
||||
'https://www.w3.org/ns/activitystreams#quoteUrl',
|
||||
].includes(rel)
|
||||
)
|
||||
)
|
||||
// Deduplicate by href.
|
||||
.filter((x, i, arr) => arr.findIndex(y => x.href === y.href) === i);
|
||||
|
||||
if (quotes.length === 0) return null;
|
||||
// If there is more than one quote, we just pick the first/a random one.
|
||||
else return quotes[0].href;
|
||||
}
|
||||
|
|
|
@ -111,6 +111,16 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
|||
...apemojis,
|
||||
];
|
||||
|
||||
if (quote) {
|
||||
tag.push({
|
||||
type: 'Link',
|
||||
mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
href: quote,
|
||||
name: `RE: ${quote}`,
|
||||
rel: 'https://misskey-hub.net/ns#_misskey_quote',
|
||||
});
|
||||
}
|
||||
|
||||
const asPoll = poll ? {
|
||||
type: 'Question',
|
||||
content: await toHtml(text, note.mentions),
|
||||
|
|
|
@ -30,12 +30,21 @@ export async function renderPerson(user: ILocalUser) {
|
|||
|
||||
if (profile.fields) {
|
||||
for (const field of profile.fields) {
|
||||
let value = field.value;
|
||||
// try to parse it as a url
|
||||
try {
|
||||
if (field.value?.match(/^https?:/)) {
|
||||
const url = new URL(field.value);
|
||||
value = `<a href="${url.href}" rel="me nofollow noopener" target="_blank">${url.href}</a>`;
|
||||
}
|
||||
} catch {
|
||||
// guess it wasn't a url after all...
|
||||
}
|
||||
|
||||
attachment.push({
|
||||
type: 'PropertyValue',
|
||||
name: field.name,
|
||||
value: (field.value != null && field.value.match(/^https?:/))
|
||||
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
|
||||
: field.value,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ export function getOneApId(value: ApObject): string {
|
|||
/**
|
||||
* Get ActivityStreams Object id
|
||||
*/
|
||||
export function getApId(value: string | IObject): string {
|
||||
export function getApId(value: string | Object): string {
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value.id === 'string') return value.id;
|
||||
throw new Error('cannot detemine id');
|
||||
|
@ -54,7 +54,7 @@ export function getApId(value: string | IObject): string {
|
|||
/**
|
||||
* Get ActivityStreams Object type
|
||||
*/
|
||||
export function getApType(value: IObject): string {
|
||||
export function getApType(value: Object): string {
|
||||
if (typeof value.type === 'string') return value.type;
|
||||
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
|
||||
throw new Error('cannot detect type');
|
||||
|
@ -196,24 +196,6 @@ export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
|
|||
typeof object.name === 'string' &&
|
||||
typeof (object as any).value === 'string';
|
||||
|
||||
export interface IApMention extends IObject {
|
||||
type: 'Mention';
|
||||
href: string;
|
||||
}
|
||||
|
||||
export const isMention = (object: IObject): object is IApMention =>
|
||||
getApType(object) === 'Mention' &&
|
||||
typeof object.href === 'string';
|
||||
|
||||
export interface IApHashtag extends IObject {
|
||||
type: 'Hashtag';
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const isHashtag = (object: IObject): object is IApHashtag =>
|
||||
getApType(object) === 'Hashtag' &&
|
||||
typeof object.name === 'string';
|
||||
|
||||
export interface IApEmoji extends IObject {
|
||||
type: 'Emoji';
|
||||
updated: Date;
|
||||
|
@ -293,3 +275,34 @@ export const isLike = (object: IObject): object is ILike => getApType(object) ==
|
|||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||
|
||||
export interface ILink {
|
||||
href: string;
|
||||
rel?: string | string[];
|
||||
mediaType?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface IApMention extends ILink {
|
||||
type: 'Mention';
|
||||
}
|
||||
|
||||
export interface IApHashtag extends ILink {
|
||||
type: 'Hashtag';
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const isLink = (object: Record<string, any>): object is ILink =>
|
||||
typeof object.href === 'string'
|
||||
&& (
|
||||
object.rel == undefined
|
||||
|| typeof object.rel === 'string'
|
||||
|| (Array.isArray(object.rel) && object.rel.every(x => typeof x === 'string'))
|
||||
)
|
||||
&& (object.mediaType == undefined || typeof object.mediaType === 'string');
|
||||
export const isMention = (object: Record<string, any>): object is IApMention =>
|
||||
getApType(object) === 'Mention' && isLink(object);
|
||||
export const isHashtag = (object: Record<string, any>): object is IApHashtag =>
|
||||
getApType(object) === 'Hashtag'
|
||||
&& isLink(object)
|
||||
&& typeof object.name === 'string';
|
||||
|
|
|
@ -35,10 +35,8 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
|||
limit.key = ep.name;
|
||||
}
|
||||
|
||||
// Rate limit
|
||||
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(() => {
|
||||
throw new ApiError('RATE_LIMIT_EXCEEDED');
|
||||
});
|
||||
// Rate limit, may throw an ApiError
|
||||
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor);
|
||||
}
|
||||
|
||||
if (ep.meta.requireCredential && user == null) {
|
||||
|
|
|
@ -138,15 +138,6 @@ import * as ep___following_requests_accept from './endpoints/following/requests/
|
|||
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
|
||||
import * as ep___following_requests_list from './endpoints/following/requests/list.js';
|
||||
import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
|
||||
import * as ep___gallery_featured from './endpoints/gallery/featured.js';
|
||||
import * as ep___gallery_popular from './endpoints/gallery/popular.js';
|
||||
import * as ep___gallery_posts from './endpoints/gallery/posts.js';
|
||||
import * as ep___gallery_posts_create from './endpoints/gallery/posts/create.js';
|
||||
import * as ep___gallery_posts_delete from './endpoints/gallery/posts/delete.js';
|
||||
import * as ep___gallery_posts_like from './endpoints/gallery/posts/like.js';
|
||||
import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js';
|
||||
import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js';
|
||||
import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js';
|
||||
import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js';
|
||||
import * as ep___hashtags_list from './endpoints/hashtags/list.js';
|
||||
import * as ep___hashtags_search from './endpoints/hashtags/search.js';
|
||||
|
@ -171,8 +162,6 @@ import * as ep___i_exportMute from './endpoints/i/export-mute.js';
|
|||
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
|
||||
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
||||
import * as ep___i_favorites from './endpoints/i/favorites.js';
|
||||
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
|
||||
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
|
||||
import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js';
|
||||
import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
|
||||
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
|
||||
|
@ -276,7 +265,6 @@ import * as ep___users from './endpoints/users.js';
|
|||
import * as ep___users_clips from './endpoints/users/clips.js';
|
||||
import * as ep___users_followers from './endpoints/users/followers.js';
|
||||
import * as ep___users_following from './endpoints/users/following.js';
|
||||
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
|
||||
import * as ep___users_groups_create from './endpoints/users/groups/create.js';
|
||||
import * as ep___users_groups_delete from './endpoints/users/groups/delete.js';
|
||||
import * as ep___users_groups_invitations_accept from './endpoints/users/groups/invitations/accept.js';
|
||||
|
@ -446,15 +434,6 @@ const eps = [
|
|||
['following/requests/cancel', ep___following_requests_cancel],
|
||||
['following/requests/list', ep___following_requests_list],
|
||||
['following/requests/reject', ep___following_requests_reject],
|
||||
['gallery/featured', ep___gallery_featured],
|
||||
['gallery/popular', ep___gallery_popular],
|
||||
['gallery/posts', ep___gallery_posts],
|
||||
['gallery/posts/create', ep___gallery_posts_create],
|
||||
['gallery/posts/delete', ep___gallery_posts_delete],
|
||||
['gallery/posts/like', ep___gallery_posts_like],
|
||||
['gallery/posts/show', ep___gallery_posts_show],
|
||||
['gallery/posts/unlike', ep___gallery_posts_unlike],
|
||||
['gallery/posts/update', ep___gallery_posts_update],
|
||||
['get-online-users-count', ep___getOnlineUsersCount],
|
||||
['hashtags/list', ep___hashtags_list],
|
||||
['hashtags/search', ep___hashtags_search],
|
||||
|
@ -479,8 +458,6 @@ const eps = [
|
|||
['i/export-notes', ep___i_exportNotes],
|
||||
['i/export-user-lists', ep___i_exportUserLists],
|
||||
['i/favorites', ep___i_favorites],
|
||||
['i/gallery/likes', ep___i_gallery_likes],
|
||||
['i/gallery/posts', ep___i_gallery_posts],
|
||||
['i/get-word-muted-notes-count', ep___i_getWordMutedNotesCount],
|
||||
['i/import-blocking', ep___i_importBlocking],
|
||||
['i/import-following', ep___i_importFollowing],
|
||||
|
@ -584,7 +561,6 @@ const eps = [
|
|||
['users/clips', ep___users_clips],
|
||||
['users/followers', ep___users_followers],
|
||||
['users/following', ep___users_following],
|
||||
['users/gallery/posts', ep___users_gallery_posts],
|
||||
['users/groups/create', ep___users_groups_create],
|
||||
['users/groups/delete', ep___users_groups_delete],
|
||||
['users/groups/invitations/accept', ep___users_groups_invitations_accept],
|
||||
|
@ -717,6 +693,14 @@ export interface IEndpointMeta {
|
|||
* @example (v0) /api/notes/create -> /api/v2/notes
|
||||
*/
|
||||
readonly alias?: string;
|
||||
|
||||
/**
|
||||
* If any path parameters were used, they have to be listed here.
|
||||
* Otherwise they will show up as query parameters in the documentation.
|
||||
*
|
||||
* Note: Path parameters cannot be optional.
|
||||
*/
|
||||
readonly pathParamers?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
description: 'Tries to fetch the given `uri` from the remote server.',
|
||||
|
||||
limit: {
|
||||
duration: HOUR,
|
||||
max: 30,
|
||||
|
|
|
@ -18,6 +18,8 @@ export const meta = {
|
|||
|
||||
requireCredential: true,
|
||||
|
||||
description: 'Shows the requested object. If necessary, fetches the object from the remote server.',
|
||||
|
||||
limit: {
|
||||
duration: HOUR,
|
||||
max: 30,
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import { DAY } from '@/const.js';
|
||||
import { GalleryPosts } from '@/models/index.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'GalleryPost',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const query = GalleryPosts.createQueryBuilder('post')
|
||||
.andWhere('post.createdAt > :date', { date: new Date(Date.now() - 3 * DAY) })
|
||||
.andWhere('post.likedCount > 0')
|
||||
.orderBy('post.likedCount', 'DESC');
|
||||
|
||||
const posts = await query.take(10).getMany();
|
||||
|
||||
return await GalleryPosts.packMany(posts, me);
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
import { GalleryPosts } from '@/models/index.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'GalleryPost',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const query = GalleryPosts.createQueryBuilder('post')
|
||||
.andWhere('post.likedCount > 0')
|
||||
.orderBy('post.likedCount', 'DESC');
|
||||
|
||||
const posts = await query.take(10).getMany();
|
||||
|
||||
return await GalleryPosts.packMany(posts, me);
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
import { GalleryPosts } from '@/models/index.js';
|
||||
import define from '../../define.js';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'GalleryPost',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
|
||||
.innerJoinAndSelect('post.user', 'user');
|
||||
|
||||
const posts = await query.take(ps.limit).getMany();
|
||||
|
||||
return await GalleryPosts.packMany(posts, me);
|
||||
});
|
|
@ -1,72 +0,0 @@
|
|||
import { DriveFiles, GalleryPosts } from '@/models/index.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { GalleryPost } from '@/models/entities/gallery-post.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import define from '../../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:gallery',
|
||||
|
||||
limit: {
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'GalleryPost',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', minLength: 1 },
|
||||
description: { type: 'string', nullable: true },
|
||||
fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 32, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
isSensitive: { type: 'boolean', default: false },
|
||||
},
|
||||
required: ['title', 'fileIds'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const files = (await Promise.all(ps.fileIds.map(fileId =>
|
||||
DriveFiles.findOneBy({
|
||||
id: fileId,
|
||||
userId: user.id,
|
||||
}),
|
||||
))).filter((file): file is DriveFile => file != null);
|
||||
|
||||
if (files.length !== ps.fileIds.length) {
|
||||
throw new ApiError(
|
||||
'INVALID_PARAM',
|
||||
{
|
||||
param: '#/properties/fileIds/items',
|
||||
reason: 'contains invalid file IDs',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const post = await GalleryPosts.insert(new GalleryPost({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
title: ps.title,
|
||||
description: ps.description,
|
||||
userId: user.id,
|
||||
isSensitive: ps.isSensitive,
|
||||
fileIds: files.map(file => file.id),
|
||||
})).then(x => GalleryPosts.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
return await GalleryPosts.pack(post, user);
|
||||
});
|
|
@ -1,33 +0,0 @@
|
|||
import { GalleryPosts } from '@/models/index.js';
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:gallery',
|
||||
|
||||
errors: ['NO_SUCH_POST'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
postId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['postId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const post = await GalleryPosts.findOneBy({
|
||||
id: ps.postId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (post == null) throw new ApiError('NO_SUCH_POST');
|
||||
|
||||
await GalleryPosts.delete(post.id);
|
||||
});
|
|
@ -1,46 +0,0 @@
|
|||
import { GalleryPosts, GalleryLikes } from '@/models/index.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:gallery-likes',
|
||||
|
||||
errors: ['NO_SUCH_POST', 'ALREADY_LIKED'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
postId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['postId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const post = await GalleryPosts.findOneBy({ id: ps.postId });
|
||||
if (post == null) throw new ApiError('NO_SUCH_POST');
|
||||
|
||||
// if already liked
|
||||
const exist = await GalleryLikes.countBy({
|
||||
postId: post.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (exist) throw new ApiError('ALREADY_LIKED');
|
||||
|
||||
// Create like
|
||||
await GalleryLikes.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
postId: post.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
GalleryPosts.increment({ id: post.id }, 'likedCount', 1);
|
||||
});
|
|
@ -1,36 +0,0 @@
|
|||
import { GalleryPosts } from '@/models/index.js';
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
errors: ['NO_SUCH_POST'],
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'GalleryPost',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
postId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['postId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const post = await GalleryPosts.findOneBy({
|
||||
id: ps.postId,
|
||||
});
|
||||
|
||||
if (post == null) throw new ApiError('NO_SUCH_POST');
|
||||
|
||||
return await GalleryPosts.pack(post, me);
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
import { GalleryPosts, GalleryLikes } from '@/models/index.js';
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:gallery-likes',
|
||||
|
||||
errors: ['NO_SUCH_POST', 'NOT_LIKED'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
postId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['postId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const post = await GalleryPosts.findOneBy({ id: ps.postId });
|
||||
if (post == null) throw new ApiError('NO_SUCH_POST');
|
||||
|
||||
const exist = await GalleryLikes.findOneBy({
|
||||
postId: post.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (exist == null) throw new ApiError('NOT_LIKED');
|
||||
|
||||
// Delete like
|
||||
await GalleryLikes.delete(exist.id);
|
||||
|
||||
GalleryPosts.decrement({ id: post.id }, 'likedCount', 1);
|
||||
});
|
|
@ -1,75 +0,0 @@
|
|||
import { DriveFiles, GalleryPosts } from '@/models/index.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { HOUR } from '@/const.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import define from '../../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:gallery',
|
||||
|
||||
limit: {
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'GalleryPost',
|
||||
},
|
||||
|
||||
errors: ['INVALID_PARAM'],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
postId: { type: 'string', format: 'misskey:id' },
|
||||
title: { type: 'string', minLength: 1 },
|
||||
description: { type: 'string', nullable: true },
|
||||
fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 32, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
isSensitive: { type: 'boolean', default: false },
|
||||
},
|
||||
required: ['postId', 'title', 'fileIds'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const files = (await Promise.all(ps.fileIds.map(fileId =>
|
||||
DriveFiles.findOneBy({
|
||||
id: fileId,
|
||||
userId: user.id,
|
||||
}),
|
||||
))).filter((file): file is DriveFile => file != null);
|
||||
|
||||
if (files.length !== ps.fileIds.length) {
|
||||
throw new ApiError(
|
||||
'INVALID_PARAM',
|
||||
{
|
||||
param: '#/properties/fileIds/items',
|
||||
reason: 'contains invalid file IDs',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await GalleryPosts.update({
|
||||
id: ps.postId,
|
||||
userId: user.id,
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
title: ps.title,
|
||||
description: ps.description,
|
||||
isSensitive: ps.isSensitive,
|
||||
fileIds: files.map(file => file.id),
|
||||
});
|
||||
|
||||
const post = await GalleryPosts.findOneByOrFail({ id: ps.postId });
|
||||
|
||||
return await GalleryPosts.pack(post, user);
|
||||
});
|
|
@ -1,55 +0,0 @@
|
|||
import { GalleryLikes } from '@/models/index.js';
|
||||
import define from '../../../define.js';
|
||||
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account', 'gallery'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:gallery-likes',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
post: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'GalleryPost',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = makePaginationQuery(GalleryLikes.createQueryBuilder('like'), ps.sinceId, ps.untilId)
|
||||
.andWhere('like.userId = :meId', { meId: user.id })
|
||||
.leftJoinAndSelect('like.post', 'post');
|
||||
|
||||
const likes = await query
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await GalleryLikes.packMany(likes, user);
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
import { GalleryPosts } from '@/models/index.js';
|
||||
import define from '../../../define.js';
|
||||
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account', 'gallery'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:gallery',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'GalleryPost',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
|
||||
.andWhere('post.userId = :meId', { meId: user.id });
|
||||
|
||||
const posts = await query
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await GalleryPosts.packMany(posts, user);
|
||||
});
|
|
@ -20,7 +20,6 @@ export const paramDef = {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ export const paramDef = {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.key = :key', { key: ps.key })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
|
|
@ -24,7 +24,6 @@ export const paramDef = {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.key = :key', { key: ps.key })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
|
|
@ -20,7 +20,6 @@ export const paramDef = {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ export const paramDef = {
|
|||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.select('item.key')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ export const paramDef = {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.key = :key', { key: ps.key })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
|
|
@ -17,7 +17,6 @@ export const paramDef = {
|
|||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.select('item.scope')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id });
|
||||
|
||||
const items = await query.getMany();
|
||||
|
|
|
@ -24,7 +24,6 @@ export const paramDef = {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.key = :key', { key: ps.key })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
@ -42,7 +41,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
userId: user.id,
|
||||
domain: null,
|
||||
scope: ps.scope,
|
||||
key: ps.key,
|
||||
value: ps.value,
|
||||
|
|
|
@ -25,6 +25,7 @@ export const meta = {
|
|||
v2: {
|
||||
method: 'get',
|
||||
alias: 'notes/:noteId/children',
|
||||
pathParameters: ['noteId'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ export const meta = {
|
|||
v2: {
|
||||
method: 'get',
|
||||
alias: 'notes/:noteId/clips',
|
||||
pathParameters: ['noteId'],
|
||||
},
|
||||
|
||||
errors: ['NO_SUCH_NOTE'],
|
||||
|
|
|
@ -22,6 +22,7 @@ export const meta = {
|
|||
v2: {
|
||||
method: 'get',
|
||||
alias: 'notes/:noteId/conversation',
|
||||
pathParameters: ['noteId'],
|
||||
},
|
||||
|
||||
errors: ['NO_SUCH_NOTE'],
|
||||
|
|
|
@ -14,13 +14,15 @@ export const meta = {
|
|||
|
||||
limit: {
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
minInterval: SECOND,
|
||||
max: 30,
|
||||
minInterval: 10 * SECOND,
|
||||
key: 'delete',
|
||||
},
|
||||
|
||||
v2: {
|
||||
method: 'delete',
|
||||
alias: 'notes/:noteId',
|
||||
pathParameters: ['noteId'],
|
||||
},
|
||||
|
||||
errors: ['ACCESS_DENIED', 'NO_SUCH_NOTE'],
|
||||
|
|
|
@ -25,7 +25,8 @@ export const meta = {
|
|||
|
||||
v2: {
|
||||
method: 'get',
|
||||
alias: 'notes/:noteId/reactions/:type?',
|
||||
alias: 'notes/:noteId/reactions',
|
||||
pathParameters: ['noteId'],
|
||||
},
|
||||
|
||||
errors: ['NO_SUCH_NOTE'],
|
||||
|
|
|
@ -13,8 +13,9 @@ export const meta = {
|
|||
|
||||
limit: {
|
||||
duration: HOUR,
|
||||
max: 60,
|
||||
minInterval: 3 * SECOND,
|
||||
max: 30,
|
||||
minInterval: 10 * SECOND,
|
||||
key: 'delete',
|
||||
},
|
||||
|
||||
errors: ['NO_SUCH_NOTE', 'NOT_REACTED'],
|
||||
|
|
|
@ -25,6 +25,7 @@ export const meta = {
|
|||
v2: {
|
||||
method: 'get',
|
||||
alias: 'notes/:noteId/renotes',
|
||||
pathParameters: ['noteId'],
|
||||
},
|
||||
|
||||
errors: ['NO_SUCH_NOTE'],
|
||||
|
|
|
@ -25,6 +25,7 @@ export const meta = {
|
|||
v2: {
|
||||
method: 'get',
|
||||
alias: 'notes/:noteId/replies',
|
||||
pathParameters: ['noteId'],
|
||||
},
|
||||
|
||||
errors: ['NO_SUCH_NOTE'],
|
||||
|
|
|
@ -17,6 +17,7 @@ export const meta = {
|
|||
v2: {
|
||||
method: 'get',
|
||||
alias: 'notes/:noteId',
|
||||
pathParameters: ['noteId'],
|
||||
},
|
||||
|
||||
errors: ['NO_SUCH_NOTE'],
|
||||
|
|
|
@ -30,6 +30,7 @@ export const meta = {
|
|||
v2: {
|
||||
method: 'get',
|
||||
alias: 'notes/:noteId/status',
|
||||
pathParameters: ['noteId'],
|
||||
},
|
||||
|
||||
errors: ['NO_SUCH_NOTE'],
|
||||
|
|
|
@ -57,7 +57,8 @@ export const meta = {
|
|||
|
||||
v2: {
|
||||
method: 'get',
|
||||
alias: 'notes/:noteId/translate/:targetLang/:sourceLang?',
|
||||
alias: 'notes/:noteId/translate/:targetLang',
|
||||
pathParameters: ['noteId', 'targetLang'],
|
||||
},
|
||||
|
||||
errors: ['NO_SUCH_NOTE'],
|
||||
|
|
|
@ -14,13 +14,15 @@ export const meta = {
|
|||
|
||||
limit: {
|
||||
duration: HOUR,
|
||||
max: 300,
|
||||
minInterval: SECOND,
|
||||
max: 30,
|
||||
minInterval: 10 * SECOND,
|
||||
key: 'delete',
|
||||
},
|
||||
|
||||
v2: {
|
||||
method: 'delete',
|
||||
alias: 'notes/:noteId/renotes',
|
||||
pathParameters: ['noteId'],
|
||||
},
|
||||
|
||||
errors: ['NO_SUCH_NOTE'],
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
import { GalleryPosts } from '@/models/index.js';
|
||||
import define from '../../../define.js';
|
||||
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users', 'gallery'],
|
||||
|
||||
description: 'Show all gallery posts by the given user.',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'GalleryPost',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = makePaginationQuery(GalleryPosts.createQueryBuilder('post'), ps.sinceId, ps.untilId)
|
||||
.andWhere('post.userId = :userId', { userId: ps.userId });
|
||||
|
||||
const posts = await query
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await GalleryPosts.packMany(posts, user);
|
||||
});
|
|
@ -29,8 +29,16 @@ export class ApiError extends Error {
|
|||
*/
|
||||
public apply(ctx: Koa.Context, endpoint: string): void {
|
||||
ctx.status = this.httpStatusCode;
|
||||
if (ctx.status === 401) {
|
||||
ctx.response.set('WWW-Authenticate', 'Bearer');
|
||||
// set additional headers
|
||||
switch (ctx.status) {
|
||||
case 401:
|
||||
ctx.response.set('WWW-Authenticate', 'Bearer');
|
||||
break;
|
||||
case 429:
|
||||
if (typeof this.info === 'object' && typeof this.info.reset === 'number') {
|
||||
ctx.respose.set('Retry-After', Math.floor(this.info.reset - (Date.now() / 1000)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
ctx.body = {
|
||||
error: {
|
||||
|
@ -73,7 +81,7 @@ export const errors: Record<string, { message: string, httpStatusCode: number }>
|
|||
httpStatusCode: 409,
|
||||
},
|
||||
ALREADY_LIKED: {
|
||||
message: 'You already liked that page or gallery post.',
|
||||
message: 'You already liked that page.',
|
||||
httpStatusCode: 409,
|
||||
},
|
||||
ALREADY_MUTING: {
|
||||
|
@ -292,10 +300,6 @@ export const errors: Record<string, { message: string, httpStatusCode: number }>
|
|||
message: 'No such parent folder.',
|
||||
httpStatusCode: 404,
|
||||
},
|
||||
NO_SUCH_POST: {
|
||||
message: 'No such gallery post.',
|
||||
httpStatusCode: 404,
|
||||
},
|
||||
NO_SUCH_RESET_REQUEST: {
|
||||
message: 'No such password reset request.',
|
||||
httpStatusCode: 404,
|
||||
|
@ -337,7 +341,7 @@ export const errors: Record<string, { message: string, httpStatusCode: number }>
|
|||
httpStatusCode: 409,
|
||||
},
|
||||
NOT_LIKED: {
|
||||
message: 'You have not liked that page or gallery post.',
|
||||
message: 'You have not liked that page.',
|
||||
httpStatusCode: 409,
|
||||
},
|
||||
NOT_MUTING: {
|
||||
|
|
|
@ -2,11 +2,12 @@ import Limiter from 'ratelimiter';
|
|||
import Logger from '@/services/logger.js';
|
||||
import { redisClient } from '@/db/redis.js';
|
||||
import { IEndpointMeta } from './endpoints.js';
|
||||
import { ApiError } from './error.js';
|
||||
|
||||
const logger = new Logger('limiter');
|
||||
|
||||
export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) => new Promise<void>((ok, reject) => {
|
||||
if (process.env.NODE_ENV === 'test') ok();
|
||||
export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) => new Promise<void>((resolve) => {
|
||||
if (process.env.NODE_ENV === 'test') resolve();
|
||||
|
||||
const hasShortTermLimit = typeof limitation.minInterval === 'number';
|
||||
|
||||
|
@ -19,10 +20,10 @@ export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<
|
|||
} else if (hasLongTermLimit) {
|
||||
max();
|
||||
} else {
|
||||
ok();
|
||||
resolve();
|
||||
}
|
||||
|
||||
// Short-term limit
|
||||
// Short-term limit, calls long term limit if appropriate.
|
||||
function min(): void {
|
||||
const minIntervalLimiter = new Limiter({
|
||||
id: `${actor}:${limitation.key}:min`,
|
||||
|
@ -33,18 +34,19 @@ export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<
|
|||
|
||||
minIntervalLimiter.get((err, info) => {
|
||||
if (err) {
|
||||
return reject('ERR');
|
||||
logger.error(err);
|
||||
throw new ApiError('INTERNAL_ERROR');
|
||||
}
|
||||
|
||||
logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
|
||||
|
||||
if (info.remaining === 0) {
|
||||
reject('BRIEF_REQUEST_INTERVAL');
|
||||
throw new ApiError('RATE_LIMIT_EXCEEDED', info);
|
||||
} else {
|
||||
if (hasLongTermLimit) {
|
||||
max();
|
||||
} else {
|
||||
ok();
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -61,15 +63,16 @@ export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<
|
|||
|
||||
limiter.get((err, info) => {
|
||||
if (err) {
|
||||
return reject('ERR');
|
||||
logger.error(err);
|
||||
throw new ApiError('INTERNAL_ERROR');
|
||||
}
|
||||
|
||||
logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
|
||||
|
||||
if (info.remaining === 0) {
|
||||
reject('RATE_LIMIT_EXCEEDED');
|
||||
throw new ApiError('RATE_LIMIT_EXCEEDED', info);
|
||||
} else {
|
||||
ok();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -126,11 +126,21 @@ export function genOpenapiSpec() {
|
|||
};
|
||||
}
|
||||
|
||||
let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n';
|
||||
let desc = endpoint.meta.description ?? 'No description provided.';
|
||||
desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`;
|
||||
if (endpoint.meta.kind) {
|
||||
const kind = endpoint.meta.kind;
|
||||
desc += ` / **Permission**: *${kind}*`;
|
||||
desc += '\n\n**Permission**: `' + endpoint.meta.kind + '`';
|
||||
}
|
||||
if (endpoint.meta.limit) {
|
||||
const limit = endpoint.meta.limit;
|
||||
|
||||
desc += '\n### Rate limit\nRate limiting group: `' + (limit.key ?? endpoint.name) + '`';
|
||||
if (limit.duration && limit.max) {
|
||||
desc += ` \nNo more than ${limit.max} requests every ${limit.duration} ms.`;
|
||||
}
|
||||
if (limit.minInterval) {
|
||||
desc += ` \nMinimum delay between each request: ${endpoint.meta.limit.minInterval} ms.`;
|
||||
}
|
||||
}
|
||||
|
||||
const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
|
||||
|
@ -183,6 +193,7 @@ export function genOpenapiSpec() {
|
|||
},
|
||||
},
|
||||
responses,
|
||||
deprecated: endpoint.meta.stability === 'deprecated',
|
||||
};
|
||||
|
||||
const path = {
|
||||
|
@ -209,11 +220,37 @@ export function genOpenapiSpec() {
|
|||
spec.paths['/' + endpoint.name] = path;
|
||||
|
||||
if (endpoint.meta.v2) {
|
||||
const route = `/v2/${endpoint.meta.v2.alias ?? endpoint.name.replace(/-/g, '_')}`;
|
||||
// we need a clone of the API endpoint info because otherwise we change it by reference
|
||||
const infoClone = structuredClone(info);
|
||||
const route = `/v2/${endpoint.meta.v2.alias ?? endpoint.name.replace(/-/g, '_')}`;
|
||||
// fix the way parameters are passed
|
||||
const hasBody = !(endpoint.meta.v2.method === 'get' || endpoint.meta.v2.method === 'delete');
|
||||
if (!hasBody) {
|
||||
// these methods do not (usually) have a body
|
||||
delete infoClone.requestBody;
|
||||
infoClone.parameters = [];
|
||||
for (const name in schema.properties) {
|
||||
infoClone.parameters.push({
|
||||
name,
|
||||
in: endpoint.meta.v2?.pathParameters?.includes(name) ? 'path' : 'query',
|
||||
schema: schema.properties[name],
|
||||
required: endpoint.meta.v2?.pathParameters?.includes(name) || schema.required?.includes(name) || false,
|
||||
});
|
||||
}
|
||||
} else if (endpoint.meta.v2.pathParameters) {
|
||||
for (const name in endpoint.meta.v2.pathParameters) {
|
||||
delete infoClone.requestBody.content[requestType].schema.properties[name];
|
||||
infoClone.parameters.push({
|
||||
name,
|
||||
in: 'path',
|
||||
schema: schema.properties[name],
|
||||
required: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
infoClone['operationId'] = infoClone['summary'] = route;
|
||||
infoClone['operationId'] = endpoint.meta.v2.method.toUpperCase() + '/' + route;
|
||||
infoClone['summary'] = endpoint.meta.v2.method.toUpperCase() + ' ' + route;
|
||||
|
||||
spec.paths[route] = {
|
||||
...spec.paths[route],
|
||||
|
|
|
@ -25,13 +25,8 @@ export default async (ctx: Koa.Context) => {
|
|||
new ApiError(e, info).apply(ctx, 'signin');
|
||||
}
|
||||
|
||||
try {
|
||||
// not more than 1 attempt per second and not more than 10 attempts per hour
|
||||
await limiter({ key: 'signin', duration: HOUR, max: 10, minInterval: SECOND }, getIpHash(ctx.ip));
|
||||
} catch (err) {
|
||||
error('RATE_LIMIT_EXCEEDED');
|
||||
return;
|
||||
}
|
||||
// not more than 1 attempt per second and not more than 10 attempts per hour
|
||||
await limiter({ key: 'signin', duration: HOUR, max: 10, minInterval: SECOND }, getIpHash(ctx.ip));
|
||||
|
||||
if (typeof username !== 'string') {
|
||||
error('INVALID_PARAM', { param: 'username', reason: 'not a string' });
|
||||
|
|
|
@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
|
|||
import * as http from 'node:http';
|
||||
import { WebSocketServer } from 'ws';
|
||||
|
||||
import { MINUTE } from '@/const.js';
|
||||
import { SECOND, MINUTE } from '@/const.js';
|
||||
import { subscriber as redisClient } from '@/db/redis.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import { Connection } from './stream/index.js';
|
||||
|
@ -43,6 +43,20 @@ export const initializeStreamingServer = (server: http.Server): void => {
|
|||
|
||||
const main = new Connection(socket, ev, user, app);
|
||||
|
||||
// ping/pong mechanism
|
||||
let pingTimeout = null;
|
||||
function startHeartbeat() {
|
||||
if (pingTimeout) clearTimeout(pingTimeout);
|
||||
|
||||
socket.ping();
|
||||
pingTimeout = setTimeout(() => {
|
||||
socket.terminate();
|
||||
}, 30 * SECOND);
|
||||
}
|
||||
startHeartbeat();
|
||||
socket.on('ping', () => { startHeartbeat(); });
|
||||
socket.on('pong', () => { startHeartbeat(); });
|
||||
|
||||
// keep user "online" while a stream is connected
|
||||
const intervalId = user ? setInterval(() => {
|
||||
Users.update(user.id, {
|
||||
|
@ -54,19 +68,13 @@ export const initializeStreamingServer = (server: http.Server): void => {
|
|||
lastActiveDate: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
socket.once('close', () => {
|
||||
ev.removeAllListeners();
|
||||
main.dispose();
|
||||
redisClient.off('message', onRedisMessage);
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
});
|
||||
|
||||
// ping/pong mechanism
|
||||
// TODO: the websocket protocol already specifies a ping/pong mechanism, why is this necessary?
|
||||
socket.on('message', async (data) => {
|
||||
if (data.type === 'utf8' && data.utf8Data === 'ping') {
|
||||
socket.send('pong');
|
||||
}
|
||||
if (pingTimeout) clearTimeout(pingTimeout);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ import { KoaAdapter } from '@bull-board/koa';
|
|||
import { In, IsNull } from 'typeorm';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import config from '@/config/index.js';
|
||||
import { Users, Notes, UserProfiles, Pages, Channels, Clips, GalleryPosts } from '@/models/index.js';
|
||||
import { Users, Notes, UserProfiles, Pages, Channels, Clips, DriveFiles } from '@/models/index.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
||||
import { queues } from '@/queue/queues.js';
|
||||
|
@ -324,15 +324,75 @@ router.get('/notes/:note', async (ctx, next) => {
|
|||
if (note) {
|
||||
try {
|
||||
// FIXME: packing with detail may throw an error if the reply or renote is not visible (#8774)
|
||||
const _note = await Notes.pack(note);
|
||||
const packedNote = await Notes.pack(note);
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
|
||||
const meta = await fetchMeta();
|
||||
|
||||
// If the note has a CW (is sensitive as a whole) or any of the files is sensitive or there are no
|
||||
// files, they are not used for a preview.
|
||||
let filesOpengraph = [];
|
||||
if (!packedNote.cw || packedNote.files.length > 0 || packedNote.files.all(file => !file.isSensitive)) {
|
||||
let limit = 4;
|
||||
for (const file of packedNote.files) {
|
||||
if (file.type.startsWith('image/')) {
|
||||
filesOpengraph.push([
|
||||
"og:image",
|
||||
DriveFiles.getPublicUrl(file, true),
|
||||
]);
|
||||
filesOpengraph.push([
|
||||
"og:image:type",
|
||||
file.type,
|
||||
]);
|
||||
if (file.properties != null) {
|
||||
filesOpengraph.push([
|
||||
"og:image:width",
|
||||
file.properties?.width,
|
||||
]);
|
||||
filesOpengraph.push([
|
||||
"og:image:height",
|
||||
file.properties?.height,
|
||||
]);
|
||||
}
|
||||
if (file.comment) {
|
||||
filesOpengraph.push([
|
||||
"og:image:alt",
|
||||
file.comment,
|
||||
]);
|
||||
}
|
||||
} else if (file.type.startsWith('audio/')) {
|
||||
filesOpengraph.push([
|
||||
"og:audio",
|
||||
DriveFiles.getPublicUrl(file),
|
||||
]);
|
||||
filesOpengraph.push([
|
||||
"og:audio:type",
|
||||
file.type,
|
||||
]);
|
||||
} else if (file.type.startsWith('video/')) {
|
||||
filesOpengraph.push([
|
||||
"og:video",
|
||||
DriveFiles.getPublicUrl(file),
|
||||
]);
|
||||
filesOpengraph.push([
|
||||
"og:video:type",
|
||||
file.type,
|
||||
]);
|
||||
} else {
|
||||
// doesn't count towards the limit
|
||||
continue;
|
||||
}
|
||||
|
||||
// limit the number of presented attachments
|
||||
if (--limit < 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.render('note', {
|
||||
note: _note,
|
||||
note: packedNote,
|
||||
profile,
|
||||
avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: note.userId })),
|
||||
filesOpengraph,
|
||||
// TODO: Let locale changeable by instance setting
|
||||
summary: getNoteSummary(_note),
|
||||
summary: getNoteSummary(packedNote),
|
||||
instanceName: meta.name || 'FoundKey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
|
@ -421,31 +481,6 @@ router.get('/clips/:clip', async (ctx, next) => {
|
|||
await next();
|
||||
});
|
||||
|
||||
// Gallery post
|
||||
router.get('/gallery/:post', async (ctx, next) => {
|
||||
const post = await GalleryPosts.findOneBy({ id: ctx.params.post });
|
||||
|
||||
if (post) {
|
||||
const _post = await GalleryPosts.pack(post);
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: post.userId });
|
||||
const meta = await fetchMeta();
|
||||
await ctx.render('gallery-post', {
|
||||
post: _post,
|
||||
profile,
|
||||
avatarUrl: await Users.getAvatarUrl(await Users.findOneByOrFail({ id: post.userId })),
|
||||
instanceName: meta.name || 'FoundKey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
});
|
||||
|
||||
ctx.set('Cache-Control', 'public, max-age=15');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
// Channel
|
||||
router.get('/channels/:channel', async (ctx, next) => {
|
||||
const channel = await Channels.findOneBy({
|
||||
|
|
|
@ -12,20 +12,12 @@ block desc
|
|||
meta(name='description' content= clip.description)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= clip.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='og:type' content='website')
|
||||
meta(property='og:title' content=title)
|
||||
meta(property='og:description' content=clip.description)
|
||||
meta(property='og:url' content=url)
|
||||
meta(property='og:image' content=avatarUrl)
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:clip-id' content=clip.id)
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
extends ./base
|
||||
|
||||
block vars
|
||||
- const user = post.user;
|
||||
- const title = post.title;
|
||||
- const url = `${config.url}/gallery/${post.id}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content= post.description)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= post.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= post.files[0].thumbnailUrl)
|
||||
|
||||
block meta
|
||||
if user.host || profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
||||
if !user.host
|
||||
link(rel='alternate' href=url type='application/activity+json')
|
|
@ -13,24 +13,19 @@ block desc
|
|||
meta(name='description' content= summary)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:article:published_time' content=note.createdAt.toISOString())
|
||||
meta(property='og:article:author:username' content=user.username)
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='og:url' content= url)
|
||||
for opengraphTag in filesOpengraph
|
||||
meta(property=opengraphTag[0] content=opengraphTag[1])
|
||||
|
||||
block meta
|
||||
if user.host || isRenote || profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:note-id' content=note.id)
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
||||
if note.prev
|
||||
link(rel='prev' href=`${config.url}/notes/${note.prev}`)
|
||||
if note.next
|
||||
|
|
|
@ -12,20 +12,13 @@ block desc
|
|||
meta(name='description' content= page.summary)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= page.summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:article:author:username' content=user.username)
|
||||
meta(property='og:title' content=title)
|
||||
meta(property='og:description' content=page.summary)
|
||||
meta(property='og:url' content=url)
|
||||
meta(property='og:image' content=page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:page-id' content=page.id)
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
|
|
@ -11,19 +11,16 @@ block desc
|
|||
meta(name='description' content= profile.description)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='blog')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:type' content='profile')
|
||||
meta(property='og:profile:username' content=user.username)
|
||||
meta(property='og:description' content= profile.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='og:url' content=url)
|
||||
meta(property='og:image' content=avatarUrl)
|
||||
|
||||
block meta
|
||||
if user.host || profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
|
||||
if profile.twitter
|
||||
meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
|
||||
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
<template>
|
||||
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel" tabindex="-1">
|
||||
<div class="thumbnail">
|
||||
<ImgWithBlurhash class="img" :src="post.files[0].thumbnailUrl" :hash="post.files[0].blurhash"/>
|
||||
</div>
|
||||
<article>
|
||||
<header>
|
||||
<MkAvatar :user="post.user" class="avatar"/>
|
||||
</header>
|
||||
<footer>
|
||||
<span class="title">{{ post.title }}</span>
|
||||
</footer>
|
||||
</article>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as foundkey from 'foundkey-js';
|
||||
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
|
||||
|
||||
defineProps<{
|
||||
post: foundkey.entities.GalleryPost;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ttasepnz {
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 200px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--accent);
|
||||
|
||||
> .thumbnail {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
> article {
|
||||
> footer {
|
||||
&:before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transition: all 0.5s ease;
|
||||
|
||||
> .img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
> article {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
> header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
> .avatar {
|
||||
margin-left: auto;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
> footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 8px #000;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.4), transparent);
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
> .title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -31,10 +31,6 @@ export default defineComponent({
|
|||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
i: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
customEmojis: {
|
||||
required: false,
|
||||
},
|
||||
|
|
|
@ -131,11 +131,6 @@ export const menuDef = reactive({
|
|||
icon: 'fas fa-file-alt',
|
||||
to: '/pages',
|
||||
},
|
||||
gallery: {
|
||||
title: 'gallery',
|
||||
icon: 'fas fa-icons',
|
||||
to: '/gallery',
|
||||
},
|
||||
clips: {
|
||||
title: 'clip',
|
||||
icon: 'fas fa-paperclip',
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormInput v-model="title">
|
||||
<template #label>{{ i18n.ts.title }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormTextarea v-model="description" :max="500">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<div class="">
|
||||
<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
|
||||
<div class="name">{{ file.name }}</div>
|
||||
<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<MkButton primary @click="selectFile"><i class="fas fa-plus"></i> {{ i18n.ts.attachFile }}</MkButton>
|
||||
</div>
|
||||
|
||||
<FormSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</FormSwitch>
|
||||
|
||||
<MkButton v-if="postId" primary @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-else primary @click="save"><i class="fas fa-save"></i> {{ i18n.ts.publish }}</MkButton>
|
||||
|
||||
<MkButton v-if="postId" danger @click="del"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, watch } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import { selectFiles } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import { useRouter } from '@/router';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
postId?: string;
|
||||
}>();
|
||||
|
||||
let init = $ref(null);
|
||||
let files = $ref([]);
|
||||
let description = $ref(null);
|
||||
let title = $ref(null);
|
||||
let isSensitive = $ref(false);
|
||||
|
||||
function selectFile(evt) {
|
||||
selectFiles(evt.currentTarget ?? evt.target, null).then(selected => {
|
||||
files = files.concat(selected);
|
||||
});
|
||||
}
|
||||
|
||||
function remove(file) {
|
||||
files = files.filter(f => f.id !== file.id);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (files.length === 0) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.attachmentRequired,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.postId) {
|
||||
await os.apiWithDialog('gallery/posts/update', {
|
||||
postId: props.postId,
|
||||
title,
|
||||
description,
|
||||
fileIds: files.map(file => file.id),
|
||||
isSensitive,
|
||||
});
|
||||
router.push(`/gallery/${props.postId}`);
|
||||
} else {
|
||||
const created = await os.apiWithDialog('gallery/posts/create', {
|
||||
title,
|
||||
description,
|
||||
fileIds: files.map(file => file.id),
|
||||
isSensitive,
|
||||
});
|
||||
router.push(`/gallery/${created.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.deleteConfirm,
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('gallery/posts/delete', {
|
||||
postId: props.postId,
|
||||
});
|
||||
router.push('/gallery');
|
||||
}
|
||||
|
||||
watch(() => props.postId, () => {
|
||||
init = () => props.postId ? os.api('gallery/posts/show', {
|
||||
postId: props.postId,
|
||||
}).then(post => {
|
||||
files = post.files;
|
||||
title = post.title;
|
||||
description = post.description;
|
||||
isSensitive = post.isSensitive;
|
||||
}) : Promise.resolve(null);
|
||||
}, { immediate: true });
|
||||
|
||||
definePageMetadata(computed(() => props.postId ? {
|
||||
title: i18n.ts.edit,
|
||||
icon: 'fas fa-pencil-alt',
|
||||
} : {
|
||||
title: i18n.ts.postToGallery,
|
||||
icon: 'fas fa-pencil-alt',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wqugxsfx {
|
||||
height: 200px;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
|
||||
> .name {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 9px;
|
||||
padding: 8px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
> .remove {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 9px;
|
||||
padding: 8px;
|
||||
background: var(--panel);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,137 +0,0 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="1400">
|
||||
<div class="_root">
|
||||
<div v-if="tab === 'explore'">
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-clock"></i>{{ i18n.ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
<MkFolder class="_gap">
|
||||
<template #header><i class="fas fa-fire-alt"></i>{{ i18n.ts.popularPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
</div>
|
||||
<div v-else-if="tab === 'liked'">
|
||||
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<div v-else-if="tab === 'my'">
|
||||
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="fas fa-plus"></i> {{ i18n.ts.postToGallery }}</MkA>
|
||||
<MkPagination v-slot="{items}" :pagination="myPostsPagination">
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, watch } from 'vue';
|
||||
import XUserList from '@/components/user-list.vue';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
|
||||
import number from '@/filters/number';
|
||||
import * as os from '@/os';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { i18n } from '@/i18n';
|
||||
import { useRouter } from '@/router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
}>();
|
||||
|
||||
let tab = $ref('explore');
|
||||
let tags = $ref([]);
|
||||
let tagsRef = $ref();
|
||||
|
||||
const recentPostsPagination = {
|
||||
endpoint: 'gallery/posts' as const,
|
||||
limit: 6,
|
||||
};
|
||||
const popularPostsPagination = {
|
||||
endpoint: 'gallery/featured' as const,
|
||||
limit: 5,
|
||||
};
|
||||
const myPostsPagination = {
|
||||
endpoint: 'i/gallery/posts' as const,
|
||||
limit: 5,
|
||||
};
|
||||
const likedPostsPagination = {
|
||||
endpoint: 'i/gallery/likes' as const,
|
||||
limit: 5,
|
||||
};
|
||||
|
||||
const tagUsersPagination = $computed(() => ({
|
||||
endpoint: 'hashtags/users' as const,
|
||||
limit: 30,
|
||||
params: {
|
||||
tag: this.tag,
|
||||
origin: 'combined',
|
||||
sort: '+follower',
|
||||
},
|
||||
}));
|
||||
|
||||
watch(() => props.tag, () => {
|
||||
if (tagsRef) tagsRef.tags.toggleContent(props.tag == null);
|
||||
});
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.create,
|
||||
handler: () => {
|
||||
router.push('/gallery/new');
|
||||
},
|
||||
}]);
|
||||
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'explore',
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'fas fa-icons',
|
||||
}, {
|
||||
key: 'liked',
|
||||
title: i18n.ts._gallery.liked,
|
||||
icon: 'fas fa-heart',
|
||||
}, {
|
||||
key: 'my',
|
||||
title: i18n.ts._gallery.my,
|
||||
icon: 'fas fa-edit',
|
||||
}]);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'fas fa-icons',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vfpdbgtk {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: 0 var(--margin);
|
||||
|
||||
> .post {
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,264 +0,0 @@
|
|||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions"/></template>
|
||||
<MkSpacer :content-max="1000" :margin-min="16" :margin-max="32">
|
||||
<div class="_root">
|
||||
<transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="post" class="rkxwuolj">
|
||||
<div class="files">
|
||||
<div v-for="file in post.files" :key="file.id" class="file">
|
||||
<img :src="file.url"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body _block">
|
||||
<div class="title">{{ post.title }}</div>
|
||||
<div class="description"><Mfm :text="post.description"/></div>
|
||||
<div class="info">
|
||||
<i class="fas fa-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="like">
|
||||
<MkButton v-if="post.isLiked" v-tooltip="i18n.ts._gallery.unlike" class="button" primary @click="unlike()"><i class="fas fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
|
||||
<MkButton v-else v-tooltip="i18n.ts._gallery.like" class="button" @click="like()"><i class="far fa-heart"></i><span v-if="post.likedCount > 0" class="count">{{ post.likedCount }}</span></MkButton>
|
||||
</div>
|
||||
<div class="other">
|
||||
<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" class="_button" @click="edit"><i class="fas fa-pencil-alt fa-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.shareWithNote" class="_button" @click="shareWithNote"><i class="fas fa-retweet fa-fw"></i></button>
|
||||
<button v-tooltip="i18n.ts.share" class="_button" @click="share"><i class="fas fa-share-alt fa-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="post.user" class="avatar"/>
|
||||
<div class="name">
|
||||
<MkUserName :user="post.user" style="display: block;"/>
|
||||
<MkAcct :user="post.user"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
|
||||
<MkContainer :max-height="300" :foldable="true" class="other">
|
||||
<template #header><i class="fas fa-clock"></i> {{ i18n.ts.recentPosts }}</template>
|
||||
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
|
||||
<div class="sdrarzaf">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</MkContainer>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</transition>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
|
||||
import MkFollowButton from '@/components/follow-button.vue';
|
||||
import { url } from '@/config';
|
||||
import { useRouter } from '@/router';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
postId: string;
|
||||
}>();
|
||||
|
||||
let post = $ref(null);
|
||||
let error = $ref(null);
|
||||
const otherPostsPagination = {
|
||||
endpoint: 'users/gallery/posts' as const,
|
||||
limit: 6,
|
||||
params: computed(() => ({
|
||||
userId: post.user.id,
|
||||
})),
|
||||
};
|
||||
|
||||
function fetchPost() {
|
||||
post = null;
|
||||
os.api('gallery/posts/show', {
|
||||
postId: props.postId,
|
||||
}).then(_post => {
|
||||
post = _post;
|
||||
}).catch(_error => {
|
||||
error = _error;
|
||||
});
|
||||
}
|
||||
|
||||
function share() {
|
||||
navigator.share({
|
||||
title: post.title,
|
||||
text: post.description,
|
||||
url: `${url}/gallery/${post.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
function shareWithNote() {
|
||||
os.post({
|
||||
initialText: `${post.title} ${url}/gallery/${post.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
function like() {
|
||||
os.apiWithDialog('gallery/posts/like', {
|
||||
postId: props.postId,
|
||||
}).then(() => {
|
||||
post.isLiked = true;
|
||||
post.likedCount++;
|
||||
});
|
||||
}
|
||||
|
||||
async function unlike() {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.unlikeConfirm,
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
os.apiWithDialog('gallery/posts/unlike', {
|
||||
postId: props.postId,
|
||||
}).then(() => {
|
||||
post.isLiked = false;
|
||||
post.likedCount--;
|
||||
});
|
||||
}
|
||||
|
||||
function edit() {
|
||||
router.push(`/gallery/${post.id}/edit`);
|
||||
}
|
||||
|
||||
watch(() => props.postId, fetchPost, { immediate: true });
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: i18n.ts.edit,
|
||||
handler: edit,
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => post ? {
|
||||
title: post.title,
|
||||
avatar: post.user,
|
||||
} : null));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.125s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.rkxwuolj {
|
||||
> .files {
|
||||
> .file {
|
||||
> img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
& + .file {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
padding: 32px;
|
||||
|
||||
> .title {
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
> .info {
|
||||
margin-top: 16px;
|
||||
font-size: 90%;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> .actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
padding: 16px 0 0 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> .like {
|
||||
> .button {
|
||||
--accent: rgb(241 97 132);
|
||||
--X8: rgb(241 92 128);
|
||||
--buttonBg: rgb(216 71 106 / 5%);
|
||||
--buttonHoverBg: rgb(216 71 106 / 10%);
|
||||
color: #ff002f;
|
||||
|
||||
::v-deep(.count) {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .other {
|
||||
margin-left: auto;
|
||||
|
||||
> button {
|
||||
padding: 8px;
|
||||
margin: 0 8px;
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .user {
|
||||
margin-top: 16px;
|
||||
padding: 16px 0 0 0;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
> .name {
|
||||
margin: 0 0 0 12px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
> .koudoku {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sdrarzaf {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin);
|
||||
|
||||
> .post {
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,38 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination">
|
||||
<div class="jrnovfpt">
|
||||
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import * as foundkey from 'foundkey-js';
|
||||
import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: foundkey.entities.User;
|
||||
}>(), {
|
||||
});
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'users/gallery/posts' as const,
|
||||
limit: 6,
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
})),
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.jrnovfpt {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin);
|
||||
}
|
||||
</style>
|
|
@ -8,7 +8,6 @@
|
|||
<XReactions v-else-if="tab === 'reactions'" :user="user"/>
|
||||
<XClips v-else-if="tab === 'clips'" :user="user"/>
|
||||
<XPages v-else-if="tab === 'pages'" :user="user"/>
|
||||
<XGallery v-else-if="tab === 'gallery'" :user="user"/>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetchUser()"/>
|
||||
<MkLoading v-else/>
|
||||
|
@ -33,7 +32,6 @@ const XHome = defineAsyncComponent(() => import('./home.vue'));
|
|||
const XReactions = defineAsyncComponent(() => import('./reactions.vue'));
|
||||
const XClips = defineAsyncComponent(() => import('./clips.vue'));
|
||||
const XPages = defineAsyncComponent(() => import('./pages.vue'));
|
||||
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
acct: string;
|
||||
|
@ -82,10 +80,6 @@ const headerTabs = $computed(() => [{
|
|||
key: 'pages',
|
||||
title: i18n.ts.pages,
|
||||
icon: 'fas fa-file-alt',
|
||||
}, {
|
||||
key: 'gallery',
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'fas fa-icons',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => user ? {
|
||||
|
|
|
@ -123,20 +123,6 @@ export const routes = [{
|
|||
}, {
|
||||
path: '/pages',
|
||||
component: page(() => import('./pages/pages.vue')),
|
||||
}, {
|
||||
path: '/gallery/:postId/edit',
|
||||
component: page(() => import('./pages/gallery/edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/gallery/new',
|
||||
component: page(() => import('./pages/gallery/edit.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/gallery/:postId',
|
||||
component: page(() => import('./pages/gallery/post.vue')),
|
||||
}, {
|
||||
path: '/gallery',
|
||||
component: page(() => import('./pages/gallery/index.vue')),
|
||||
}, {
|
||||
path: '/channels/:channelId/edit',
|
||||
component: page(() => import('./pages/channel-editor.vue')),
|
||||
|
|
|
@ -33,14 +33,6 @@ const dev: Ref<boolean> = ref(_DEV_);
|
|||
const onNotification = (notification: { type: string; id: any; }): void => {
|
||||
if ($i?.mutingNotificationTypes.includes(notification.type)) return;
|
||||
|
||||
// if push notifications are enabled there is no need to pass the notification along
|
||||
if (!instance.enableServiceWorker) {
|
||||
// service worker is not enabled or set up on the server, pass the notification along
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.active.postMessage({ type: 'notification', body: notification });
|
||||
});
|
||||
}
|
||||
|
||||
sound.play('notification');
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {
|
||||
Ad, Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, DetailedInstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, GalleryPost, Instance, InstanceMetadata,
|
||||
Ad, Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, DetailedInstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, Instance, InstanceMetadata,
|
||||
LiteInstanceMetadata,
|
||||
MeDetailed,
|
||||
Note, NoteFavorite, OriginType, Page, ServerInfo, Stats, User, UserDetailed, UserGroup, UserList, UserSorting, Notification, NoteReaction, Signin, MessagingMessage,
|
||||
|
@ -282,15 +282,6 @@ export type Endpoints = {
|
|||
'following/requests/cancel': { req: { userId: User['id'] }; res: User; };
|
||||
'following/requests/list': { req: NoParams; res: FollowRequest[]; };
|
||||
'following/requests/reject': { req: { userId: User['id'] }; res: null; };
|
||||
'gallery/featured': { req: TODO; res: TODO; };
|
||||
'gallery/popular': { req: TODO; res: TODO; };
|
||||
'gallery/posts': { req: TODO; res: TODO; };
|
||||
'gallery/posts/create': { req: TODO; res: TODO; };
|
||||
'gallery/posts/delete': { req: { postId: GalleryPost['id'] }; res: null; };
|
||||
'gallery/posts/like': { req: TODO; res: TODO; };
|
||||
'gallery/posts/show': { req: TODO; res: TODO; };
|
||||
'gallery/posts/unlike': { req: TODO; res: TODO; };
|
||||
'gallery/posts/update': { req: TODO; res: TODO; };
|
||||
'get-online-users-count': { req: TODO; res: TODO; };
|
||||
'hashtags/list': { req: TODO; res: TODO; };
|
||||
'hashtags/search': { req: TODO; res: TODO; };
|
||||
|
@ -315,8 +306,6 @@ export type Endpoints = {
|
|||
'i/export-notes': { req: TODO; res: TODO; };
|
||||
'i/export-user-lists': { req: TODO; res: TODO; };
|
||||
'i/favorites': { req: { limit?: number; sinceId?: NoteFavorite['id']; untilId?: NoteFavorite['id']; }; res: NoteFavorite[]; };
|
||||
'i/gallery/likes': { req: TODO; res: TODO; };
|
||||
'i/gallery/posts': { req: TODO; res: TODO; };
|
||||
'i/get-word-muted-notes-count': { req: TODO; res: TODO; };
|
||||
'i/import-blocking': { req: TODO; res: TODO; };
|
||||
'i/import-following': { req: TODO; res: TODO; };
|
||||
|
@ -488,7 +477,6 @@ export type Endpoints = {
|
|||
'users/clips': { req: TODO; res: TODO; };
|
||||
'users/followers': { req: { userId?: User['id']; username?: User['username']; host?: User['host'] | null; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFollowerPopulated[]; };
|
||||
'users/following': { req: { userId?: User['id']; username?: User['username']; host?: User['host'] | null; limit?: number; sinceId?: Following['id']; untilId?: Following['id']; }; res: FollowingFolloweePopulated[]; };
|
||||
'users/gallery/posts': { req: TODO; res: TODO; };
|
||||
'users/groups/create': { req: TODO; res: TODO; };
|
||||
'users/groups/delete': { req: { groupId: UserGroup['id'] }; res: null; };
|
||||
'users/groups/invitations/accept': { req: TODO; res: TODO; };
|
||||
|
|
|
@ -35,8 +35,4 @@ export const permissions = [
|
|||
'write:user-groups',
|
||||
'read:channels',
|
||||
'write:channels',
|
||||
'read:gallery',
|
||||
'write:gallery',
|
||||
'read:gallery-likes',
|
||||
'write:gallery-likes',
|
||||
];
|
||||
|
|
|
@ -126,8 +126,6 @@ export type DriveFile = {
|
|||
|
||||
export type DriveFolder = TODO;
|
||||
|
||||
export type GalleryPost = TODO;
|
||||
|
||||
export type Note = {
|
||||
id: ID;
|
||||
createdAt: DateString;
|
||||
|
|
|
@ -164,9 +164,6 @@ self.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message'
|
|||
case 'initialize':
|
||||
swLang.setLang(ev.data.lang);
|
||||
break;
|
||||
case 'notification':
|
||||
createNotification(ev.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})());
|
||||
|
|
Loading…
Reference in New Issue