Merge branch 'develop' into firefish-docs/develop

This commit is contained in:
naskya 2024-03-18 16:05:08 +09:00
commit ff2221f951
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
26 changed files with 477 additions and 153 deletions

View file

@ -1,6 +1,8 @@
BEGIN;
DELETE FROM "migrations" WHERE name IN (
'FixMutingIndices1710690239308',
'NoteFile1710304584214',
'RenameMetaColumns1705944717480',
'SeparateHardMuteWordsAndPatterns1706413792769',
'IndexAltTextAndCw1708872574733',
@ -16,6 +18,20 @@ DELETE FROM "migrations" WHERE name IN (
'RemoveNativeUtilsMigration1705877093218'
);
-- fix-muting-indices
DROP INDEX "IDX_renote_muting_createdAt";
DROP INDEX "IDX_renote_muting_muteeId";
DROP INDEX "IDX_renote_muting_muterId";
DROP INDEX "IDX_reply_muting_createdAt";
DROP INDEX "IDX_reply_muting_muteeId";
DROP INDEX "IDX_reply_muting_muterId";
CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt");
CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId");
CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId");
-- note-file
DROP TABLE "note_file";
-- rename-meta-columns
ALTER TABLE "meta" RENAME COLUMN "tosUrl" TO "ToSUrl";
ALTER TABLE "meta" RENAME COLUMN "objectStorageUseSsl" TO "objectStorageUseSSL";

View file

@ -1198,7 +1198,9 @@ searchWordsDescription: "To search for posts, enter the search term. Separate wo
search.\nFor example, 'morning night' will find posts that contain both 'morning'
and 'night', and 'morning OR night' will find posts that contain either 'morning'
or 'night' (or both).\nYou can also combine AND/OR conditions like '(morning OR
night) sleepy'.\n\nIf you want to go to a specific user page or post page, enter
night) sleepy'.\nIf you want to search for a sequence of words (e.g., a sentence), you
must put it in double quotes, not to make it an AND search: \"Today I learned\"\n\n
If you want to go to a specific user page or post page, enter
the ID or URL in this field and click the 'Lookup' button. Clicking 'Search' will
search for posts that literally contain the ID/URL."
searchUsers: "Posted by (optional)"

View file

@ -1008,7 +1008,8 @@ enableTimelineStreaming: "タイムラインを自動で更新する"
searchWords: "検索語句・照会するIDやURL"
searchWordsDescription: "投稿を検索するには、ここに検索語句を入力してください。空白区切りでAND検索になり、ORを挟むとOR検索になります。\n
例えば「朝 夜」と入力すると「朝」と「夜」が両方含まれた投稿を検索し、「朝 OR 夜」と入力すると「朝」または「夜」(または両方)が含まれた投稿を検索します。\n
「(朝 OR 夜) 眠い」のように、AND検索とOR検索を同時に行うこともできます。\n\n特定のユーザーや投稿のページに飛びたい場合には、この欄にID (@user@example.com)
「(朝 OR 夜) 眠い」のように、AND検索とOR検索を同時に行うこともできます。\n空白を含む文字列をAND検索ではなくそのまま検索したい場合、\"明日 買うもの\"\
\ のように二重引用符 (\") で囲む必要があります。\n\n特定のユーザーや投稿のページに飛びたい場合には、この欄にID (@user@example.com)
や投稿のURLを入力し「照会」を押してください。「検索」を押すとそのIDやURLが文字通り含まれる投稿を検索します。"
searchUsers: "投稿元(オプション)"
searchUsersDescription: "投稿検索で投稿者を絞りたい場合、@user@example.comローカルユーザーなら @userの形式で投稿者のIDを入力してください。ユーザーIDではなくドメイン名

View file

@ -1936,7 +1936,7 @@ moveFrom: 从旧账号迁移至此账号
defaultReaction: 发出和收到帖子的默认表情符号反应
sendModMail: 发送管理通知
moderationNote: "管理笔记"
ipFirstAcknowledged: "该日期是这个 IP 地址首次被获取到的日期"
ipFirstAcknowledged: "首次获取此 IP 地址的日期"
driveCapacityOverride: "网盘容量变更"
isLocked: 该账号设置了关注请求
_filters:
@ -2047,11 +2047,12 @@ publishTimelines: 为访客发布时间线
publishTimelinesDescription: 如果启用,在用户登出时本地和全局时间线也会显示在 {url} 上。
searchWordsDescription: "要搜索帖子,请输入关键词。交集搜索关键词之间使用空格进行区分,并集搜索关键词之间使用 OR 进行区分。\n例如 '早上
晚上' 将查找包含 '早上' 和 '晚上' 的帖子,而 '早上 OR 晚上' 将查找包含 '早上' 或 '晚上' (以及同时包含两者)的帖子。\n您还可以组合交集/并集条件,例如
'(早上 OR 晚上) 困了' 。\n\n如果您想转到特定的用户页面或帖子页面请在此字段中输入用户 ID 或 URL然后单击 “查询” 按钮。 单击 “搜索”
将搜索字面包含用户 ID/URL 的帖子。"
'(早上 OR 晚上) 困了' 。\n如果您想搜索单词序列例如一个英语句子您必须将其放在双引号中例如 \"Today I learned\" 以区分于交集搜索。\n
\n如果您想转到特定的用户页面或帖子页面请在此字段中输入用户 ID 或 URL然后单击 “查询” 按钮。 单击 “搜索” 将搜索字面包含用户 ID/URL
的帖子。"
searchRangeDescription: "如果您要过滤时间段请按以下格式输入20220615-20231031\n\n如果您省略年份例如 0105-0106
或 20231105-0110它将被解释为当前年份。\n\n您还可以省略开始日期或结束日期。 例如 -0102 将过滤搜索结果以仅显示今年 1 月 2 日之前发布的帖子,而
20231026- 将过滤结果以仅显示 2023 年 10 月 26 日之后发布的帖子。"
messagingUnencryptedInfo: "Firefish 上的聊天没有经过端到端加密,请不要在聊天中分享您的敏感信息。"
noAltTextWarning: 有些附件没有说明。您是否忘记写说明了?
showNoAltTextWarning: 当您尝试发布没有说明的帖子附件时显示警告
noAltTextWarning: 有些附件没有描述。您是否忘记写描述了?
showNoAltTextWarning: 当您尝试发布没有描述的帖子附件时显示警告

View file

@ -64,6 +64,8 @@ pub enum Relation {
DriveFolder,
#[sea_orm(has_many = "super::messaging_message::Entity")]
MessagingMessage,
#[sea_orm(has_many = "super::note_file::Entity")]
NoteFile,
#[sea_orm(has_many = "super::page::Entity")]
Page,
#[sea_orm(
@ -94,6 +96,12 @@ impl Related<super::messaging_message::Entity> for Entity {
}
}
impl Related<super::note_file::Entity> for Entity {
fn to() -> RelationDef {
Relation::NoteFile.def()
}
}
impl Related<super::page::Entity> for Entity {
fn to() -> RelationDef {
Relation::Page.def()

View file

@ -35,6 +35,7 @@ pub mod muting;
pub mod note;
pub mod note_edit;
pub mod note_favorite;
pub mod note_file;
pub mod note_reaction;
pub mod note_thread_muting;
pub mod note_unread;

View file

@ -38,8 +38,6 @@ pub struct Model {
#[sea_orm(column_name = "visibleUserIds")]
pub visible_user_ids: Vec<String>,
pub mentions: Vec<String>,
#[sea_orm(column_name = "mentionedRemoteUsers", column_type = "Text")]
pub mentioned_remote_users: String,
pub emojis: Vec<String>,
pub tags: Vec<String>,
#[sea_orm(column_name = "hasPoll")]
@ -100,6 +98,8 @@ pub enum Relation {
NoteEdit,
#[sea_orm(has_many = "super::note_favorite::Entity")]
NoteFavorite,
#[sea_orm(has_many = "super::note_file::Entity")]
NoteFile,
#[sea_orm(has_many = "super::note_reaction::Entity")]
NoteReaction,
#[sea_orm(has_many = "super::note_unread::Entity")]
@ -164,6 +164,12 @@ impl Related<super::note_favorite::Entity> for Entity {
}
}
impl Related<super::note_file::Entity> for Entity {
fn to() -> RelationDef {
Relation::NoteFile.def()
}
}
impl Related<super::note_reaction::Entity> for Entity {
fn to() -> RelationDef {
Relation::NoteReaction.def()

View file

@ -0,0 +1,48 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "note_file")]
pub struct Model {
#[sea_orm(column_name = "serialNo", primary_key)]
pub serial_no: i64,
#[sea_orm(column_name = "noteId")]
pub note_id: String,
#[sea_orm(column_name = "fileId")]
pub file_id: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::drive_file::Entity",
from = "Column::FileId",
to = "super::drive_file::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
DriveFile,
#[sea_orm(
belongs_to = "super::note::Entity",
from = "Column::NoteId",
to = "super::note::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Note,
}
impl Related<super::drive_file::Entity> for Entity {
fn to() -> RelationDef {
Relation::DriveFile.def()
}
}
impl Related<super::note::Entity> for Entity {
fn to() -> RelationDef {
Relation::Note.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -33,6 +33,7 @@ pub use super::muting::Entity as Muting;
pub use super::note::Entity as Note;
pub use super::note_edit::Entity as NoteEdit;
pub use super::note_favorite::Entity as NoteFavorite;
pub use super::note_file::Entity as NoteFile;
pub use super::note_reaction::Entity as NoteReaction;
pub use super::note_thread_muting::Entity as NoteThreadMuting;
pub use super::note_unread::Entity as NoteUnread;

View file

@ -73,6 +73,7 @@ import { UserPending } from "@/models/entities/user-pending.js";
import { Webhook } from "@/models/entities/webhook.js";
import { UserIp } from "@/models/entities/user-ip.js";
import { NoteEdit } from "@/models/entities/note-edit.js";
import { NoteFile } from "@/models/entities/note-file.js";
import { entities as charts } from "@/services/chart/entities.js";
import { dbLogger } from "./logger.js";
@ -143,6 +144,7 @@ export const entities = [
Note,
NoteEdit,
NoteFavorite,
NoteFile,
NoteReaction,
NoteWatching,
NoteThreadMuting,

View file

@ -105,10 +105,10 @@ export class RemoveNativeUtilsMigration1705877093218
`CREATE INDEX "IDX_9937ea48d7ae97ffb4f3f063a4" ON "antenna_note" ("read")`,
);
await queryRunner.query(
`ALTER TABLE "antenna_note" ADD CONSTRAINT IF NOT EXISTS "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "antenna_note" ADD CONSTRAINT IF NOT EXISTS "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
`ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_note_url"`);
await queryRunner.query(
@ -124,10 +124,10 @@ export class RemoveNativeUtilsMigration1705877093218
`CREATE INDEX IF NOT EXISTS "IDX_e247b23a3c9b45f89ec1299d06" ON "reversi_matching" ("childId")`,
);
await queryRunner.query(
`ALTER TABLE "reversi_matching" ADD CONSTRAINT IF NOT EXISTS "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "reversi_matching" ADD CONSTRAINT IF NOT EXISTS "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
`ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`COMMENT ON COLUMN "reversi_matching"."createdAt" IS 'The created date of the ReversiMatching.'`,
@ -139,10 +139,10 @@ export class RemoveNativeUtilsMigration1705877093218
`CREATE INDEX IF NOT EXISTS "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt")`,
);
await queryRunner.query(
`ALTER TABLE "reversi_game" ADD CONSTRAINT IF NOT EXISTS "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "reversi_game" ADD CONSTRAINT IF NOT EXISTS "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
`ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`COMMENT ON COLUMN "reversi_game"."createdAt" IS 'The created date of the ReversiGame.'`,

View file

@ -0,0 +1,41 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class NoteFile1710304584214 implements MigrationInterface {
name = "NoteFile1710304584214";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "note_file" (
"serialNo" bigserial PRIMARY KEY,
"noteId" varchar(32) NOT NULL,
"fileId" varchar(32) NOT NULL
)`,
);
await queryRunner.query(`
INSERT INTO "note_file" ("noteId", "fileId")
SELECT "t"."id", "t"."fid" FROM (
SELECT ROW_NUMBER() OVER () AS "rn", * FROM (
SELECT "id", UNNEST("fileIds") AS "fid" FROM "note"
) AS "s"
) AS "t"
INNER JOIN "drive_file" ON "drive_file"."id" = "t"."fid"
ORDER BY "rn"
`);
await queryRunner.query(
`ALTER TABLE "note_file" ADD FOREIGN KEY ("noteId") REFERENCES "note" ("id") ON DELETE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "note_file" ADD FOREIGN KEY ("fileId") REFERENCES "drive_file" ("id") ON DELETE CASCADE`,
);
await queryRunner.query(
`CREATE INDEX "IDX_note_file_noteId" ON "note_file" ("noteId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_note_file_fileId" ON "note_file" ("fileId")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "note_file"`);
}
}

View file

@ -0,0 +1,57 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class FixMutingIndices1710690239308 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_renote_muting_createdAt"`);
await queryRunner.query(`DROP INDEX "IDX_renote_muting_muteeId"`);
await queryRunner.query(`DROP INDEX "IDX_renote_muting_muterId"`);
await queryRunner.query(`DROP INDEX "IDX_reply_muting_createdAt"`);
await queryRunner.query(`DROP INDEX "IDX_reply_muting_muteeId"`);
await queryRunner.query(`DROP INDEX "IDX_reply_muting_muterId"`);
await queryRunner.query(
`CREATE INDEX "IDX_renote_muting_createdAt" ON "renote_muting" ("createdAt")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_renote_muting_muteeId" ON "renote_muting" ("muteeId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_renote_muting_muterId" ON "renote_muting" ("muterId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_reply_muting_createdAt" ON "reply_muting" ("createdAt")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_reply_muting_muteeId" ON "reply_muting" ("muteeId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_reply_muting_muterId" ON "reply_muting" ("muterId")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_renote_muting_createdAt"`);
await queryRunner.query(`DROP INDEX "IDX_renote_muting_muteeId"`);
await queryRunner.query(`DROP INDEX "IDX_renote_muting_muterId"`);
await queryRunner.query(`DROP INDEX "IDX_reply_muting_createdAt"`);
await queryRunner.query(`DROP INDEX "IDX_reply_muting_muteeId"`);
await queryRunner.query(`DROP INDEX "IDX_reply_muting_muterId"`);
await queryRunner.query(
`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_reply_muting_createdAt" ON "muting" ("createdAt")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_reply_muting_muteeId" ON "muting" ("muteeId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_reply_muting_muterId" ON "muting" ("muterId")`,
);
}
}

View file

@ -32,6 +32,7 @@ import { packedQueueCountSchema } from "@/models/schema/queue.js";
import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js";
import { packedEmojiSchema } from "@/models/schema/emoji.js";
import { packedNoteEdit } from "@/models/schema/note-edit.js";
import { packedNoteFileSchema } from "@/models/schema/note-file.js";
export const refs = {
UserLite: packedUserLiteSchema,
@ -47,6 +48,7 @@ export const refs = {
App: packedAppSchema,
MessagingMessage: packedMessagingMessageSchema,
Note: packedNoteSchema,
NoteFile: packedNoteFileSchema,
NoteEdit: packedNoteEdit,
NoteReaction: packedNoteReactionSchema,
NoteFavorite: packedNoteFavoriteSchema,

View file

@ -4,12 +4,17 @@ import {
Index,
JoinColumn,
Column,
ManyToMany,
ManyToOne,
OneToMany,
type Relation,
} from "typeorm";
import { id } from "../id.js";
import { Note } from "./note.js";
import { User } from "./user.js";
import { DriveFolder } from "./drive-folder.js";
import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
import { NoteFile } from "./note-file.js";
@Entity()
@Index(["userId", "folderId", "id"])
@ -31,12 +36,6 @@ export class DriveFile {
})
public userId: User["id"] | null;
@ManyToOne((type) => User, {
onDelete: "SET NULL",
})
@JoinColumn()
public user: User | null;
@Index()
@Column("varchar", {
length: 512,
@ -171,12 +170,6 @@ export class DriveFile {
})
public folderId: DriveFolder["id"] | null;
@ManyToOne((type) => DriveFolder, {
onDelete: "SET NULL",
})
@JoinColumn()
public folder: DriveFolder | null;
@Index()
@Column("boolean", {
default: false,
@ -205,4 +198,30 @@ export class DriveFile {
nullable: true,
})
public requestIp: string | null;
//#region Relations
@OneToMany(
() => NoteFile,
(noteFile: NoteFile) => noteFile.file,
)
public noteFiles: Relation<NoteFile[]>;
@ManyToMany(
() => Note,
(note: Note) => note.files,
)
public notes: Relation<Note[]>;
@ManyToOne(() => User, {
onDelete: "SET NULL",
})
@JoinColumn()
public user: User | null;
@ManyToOne(() => DriveFolder, {
onDelete: "SET NULL",
})
@JoinColumn()
public folder: DriveFolder | null;
//#endregion Relations
}

View file

@ -0,0 +1,45 @@
import {
Entity,
Index,
Column,
ManyToOne,
PrimaryGeneratedColumn,
type Relation,
} from "typeorm";
import { Note } from "./note.js";
import { DriveFile } from "./drive-file.js";
import { id } from "../id.js";
@Entity()
export class NoteFile {
@PrimaryGeneratedColumn("increment")
public serialNo: number;
@Index("IDX_note_file_noteId", { unique: false })
@Column({
...id(),
nullable: false,
})
public noteId: Note["id"];
@Index("IDX_note_file_fileId", { unique: false })
@Column({
...id(),
nullable: false,
})
public fileId: DriveFile["id"];
//#region Relations
@ManyToOne(
() => Note,
(note: Note) => note.noteFiles,
)
public note: Relation<Note>;
@ManyToOne(
() => DriveFile,
(file: DriveFile) => file.noteFiles,
)
public file: Relation<DriveFile>;
//#endregion Relations
}

View file

@ -2,15 +2,20 @@ import {
Entity,
Index,
JoinColumn,
JoinTable,
Column,
PrimaryColumn,
ManyToMany,
ManyToOne,
OneToMany,
type Relation,
} from "typeorm";
import { User } from "./user.js";
import type { DriveFile } from "./drive-file.js";
import { DriveFile } from "./drive-file.js";
import { id } from "../id.js";
import { noteVisibilities } from "../../types.js";
import { Channel } from "./channel.js";
import { NoteFile } from "./note-file.js";
@Entity()
@Index("IDX_NOTE_TAGS", { synchronize: false })
@ -34,12 +39,6 @@ export class Note {
})
public replyId: Note["id"] | null;
@ManyToOne((type) => Note, {
onDelete: "CASCADE",
})
@JoinColumn()
public reply: Note | null;
@Index()
@Column({
...id(),
@ -48,12 +47,6 @@ export class Note {
})
public renoteId: Note["id"] | null;
@ManyToOne((type) => Note, {
onDelete: "CASCADE",
})
@JoinColumn()
public renote: Note | null;
@Index()
@Column("varchar", {
length: 256,
@ -93,12 +86,6 @@ export class Note {
})
public userId: User["id"];
@ManyToOne((type) => User, {
onDelete: "CASCADE",
})
@JoinColumn()
public user: User | null;
@Column("boolean", {
default: false,
})
@ -151,6 +138,8 @@ export class Note {
})
public score: number;
// FIXME: file id is not removed from this array even if the file is deleted
// TODO: drop this column and use note_files
@Index()
@Column({
...id(),
@ -183,6 +172,7 @@ export class Note {
})
public mentions: User["id"][];
// FIXME: WHAT IS THIS
@Column("text", {
default: "[]",
})
@ -216,12 +206,55 @@ export class Note {
})
public channelId: Channel["id"] | null;
@ManyToOne((type) => Channel, {
//#region Relations
@OneToMany(
() => NoteFile,
(noteFile: NoteFile) => noteFile.note,
)
public noteFiles: Relation<NoteFile[]>;
@ManyToMany(
() => DriveFile,
(file: DriveFile) => file.notes,
)
@JoinTable({
name: "note_file",
joinColumn: {
name: "noteId",
referencedColumnName: "id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "id",
},
})
public files: Relation<DriveFile[]>;
@ManyToOne(() => Note, {
onDelete: "CASCADE",
})
@JoinColumn()
public reply: Note | null;
@ManyToOne(() => Note, {
onDelete: "CASCADE",
})
@JoinColumn()
public renote: Note | null;
@ManyToOne(() => Channel, {
onDelete: "CASCADE",
})
@JoinColumn()
public channel: Channel | null;
@ManyToOne(() => User, {
onDelete: "CASCADE",
})
@JoinColumn()
public user: User | null;
//#endregion Relations
//#region Denormalized fields
@Index()
@Column("varchar", {

View file

@ -66,12 +66,14 @@ import { InstanceRepository } from "./repositories/instance.js";
import { Webhook } from "./entities/webhook.js";
import { UserIp } from "./entities/user-ip.js";
import { NoteEdit } from "./entities/note-edit.js";
import { NoteFileRepository } from "./repositories/note-file.js";
export const Announcements = db.getRepository(Announcement);
export const AnnouncementReads = db.getRepository(AnnouncementRead);
export const Apps = AppRepository;
export const Notes = NoteRepository;
export const NoteEdits = db.getRepository(NoteEdit);
export const NoteFiles = NoteFileRepository;
export const NoteFavorites = NoteFavoriteRepository;
export const NoteWatchings = db.getRepository(NoteWatching);
export const NoteThreadMutings = db.getRepository(NoteThreadMuting);

View file

@ -0,0 +1,4 @@
import { db } from "@/db/postgre.js";
import { NoteFile } from "@/models/entities/note-file.js";
export const NoteFileRepository = db.getRepository(NoteFile).extend({});

View file

@ -0,0 +1,24 @@
export const packedNoteFileSchema = {
type: "object",
properties: {
serialNo: {
type: "number",
optional: false,
nullable: false,
},
noteId: {
type: "string",
optional: false,
nullable: false,
format: "id",
example: "xxxxxxxxxx",
},
fileId: {
type: "string",
optional: false,
nullable: false,
format: "id",
example: "xxxxxxxxxx",
},
},
} as const;

View file

@ -1,4 +1,3 @@
import { Brackets } from "typeorm";
import { Notes } from "@/models/index.js";
import { Note } from "@/models/entities/note.js";
import define from "@/server/api/define.js";
@ -7,6 +6,7 @@ import { generateVisibilityQuery } from "@/server/api/common/generate-visibility
import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js";
import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js";
import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
import type { SelectQueryBuilder } from "typeorm";
export const meta = {
tags: ["notes"],
@ -69,91 +69,123 @@ export const paramDef = {
} as const;
export default define(meta, paramDef, async (ps, me) => {
const query = makePaginationQuery(
Notes.createQueryBuilder("note"),
ps.sinceId,
ps.untilId,
ps.sinceDate ?? undefined,
ps.untilDate ?? undefined,
);
async function search(
modifier?: (query: SelectQueryBuilder<Note>) => void,
): Promise<Note[]> {
const query = makePaginationQuery(
Notes.createQueryBuilder("note"),
ps.sinceId,
ps.untilId,
ps.sinceDate ?? undefined,
ps.untilDate ?? undefined,
);
modifier?.(query);
if (ps.userId != null) {
query.andWhere("note.userId = :userId", { userId: ps.userId });
if (ps.userId != null) {
query.andWhere("note.userId = :userId", { userId: ps.userId });
}
if (ps.channelId != null) {
query.andWhere("note.channelId = :channelId", {
channelId: ps.channelId,
});
}
query.innerJoinAndSelect("note.user", "user");
// "from: me": search all (public, home, followers, specified) my posts
// otherwise: search public indexable posts only
if (ps.userId == null || ps.userId !== me?.id) {
query
.andWhere("note.visibility = 'public'")
.andWhere("user.isIndexable = TRUE");
}
if (ps.userId != null) {
query.andWhere("note.userId = :userId", { userId: ps.userId });
}
if (ps.host === null) {
query.andWhere("note.userHost IS NULL");
}
if (ps.host != null) {
query.andWhere("note.userHost = :userHost", { userHost: ps.host });
}
if (ps.withFiles === true) {
query.andWhere("note.fileIds != '{}'");
}
query
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateVisibilityQuery(query, me);
if (me) generateMutedUserQuery(query, me);
if (me) generateBlockedUserQuery(query, me);
return await query.take(ps.limit).getMany();
}
if (ps.channelId != null) {
query.andWhere("note.channelId = :channelId", {
channelId: ps.channelId,
});
}
let notes: Note[];
if (ps.query != null) {
const q = sqlLikeEscape(ps.query);
if (ps.searchCwAndAlt) {
query.andWhere(
new Brackets((qb) => {
qb.where("note.text &@~ :q", { q })
.orWhere("note.cw &@~ :q", { q })
.orWhere(
`EXISTS (
SELECT FROM "drive_file"
WHERE
comment &@~ :q
AND
drive_file."id" = ANY(note."fileIds")
)`,
{ q },
);
}),
);
// Whether we should return latest notes first
const isDescendingOrder =
(ps.sinceId == null || ps.untilId != null) &&
(ps.sinceId != null ||
ps.untilId != null ||
ps.sinceDate == null ||
ps.untilDate != null);
const compare = isDescendingOrder
? (lhs: Note, rhs: Note) =>
Math.sign(rhs.createdAt.getTime() - lhs.createdAt.getTime())
: (lhs: Note, rhs: Note) =>
Math.sign(lhs.createdAt.getTime() - rhs.createdAt.getTime());
notes = [
...new Map(
(
await Promise.all([
search((query) => {
query.andWhere("note.text &@~ :q", { q });
}),
search((query) => {
query.andWhere("note.cw &@~ :q", { q });
}),
search((query) => {
query
.andWhere("drive_file.comment &@~ :q", { q })
.innerJoin("note.files", "drive_file");
}),
])
)
.flatMap((e) => e)
.map((note) => [note.id, note]),
).values(),
]
.sort(compare)
.slice(0, ps.limit);
} else {
query.andWhere("note.text &@~ :q", { q });
notes = await search((query) => {
query.andWhere("note.text &@~ :q", { q });
});
}
} else {
notes = await search();
}
query.innerJoinAndSelect("note.user", "user");
// "from: me": search all (public, home, followers, specified) my posts
// otherwise: search public indexable posts only
if (ps.userId == null || ps.userId !== me?.id) {
query
.andWhere("note.visibility = 'public'")
.andWhere("user.isIndexable = TRUE");
}
if (ps.userId != null) {
query.andWhere("note.userId = :userId", { userId: ps.userId });
}
if (ps.host === null) {
query.andWhere("note.userHost IS NULL");
}
if (ps.host != null) {
query.andWhere("note.userHost = :userHost", { userHost: ps.host });
}
if (ps.withFiles === true) {
query.andWhere("note.fileIds != '{}'");
}
query
.leftJoinAndSelect("user.avatar", "avatar")
.leftJoinAndSelect("user.banner", "banner")
.leftJoinAndSelect("note.reply", "reply")
.leftJoinAndSelect("note.renote", "renote")
.leftJoinAndSelect("reply.user", "replyUser")
.leftJoinAndSelect("replyUser.avatar", "replyUserAvatar")
.leftJoinAndSelect("replyUser.banner", "replyUserBanner")
.leftJoinAndSelect("renote.user", "renoteUser")
.leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar")
.leftJoinAndSelect("renoteUser.banner", "renoteUserBanner");
generateVisibilityQuery(query, me);
if (me) generateMutedUserQuery(query, me);
if (me) generateBlockedUserQuery(query, me);
const notes: Note[] = await query.take(ps.limit).getMany();
return await Notes.packMany(notes, me);
});

View file

@ -31,6 +31,7 @@ import {
Channels,
ChannelFollowings,
NoteThreadMutings,
NoteFiles,
} from "@/models/index.js";
import type { DriveFile } from "@/models/entities/drive-file.js";
import type { App } from "@/models/entities/app.js";
@ -343,6 +344,12 @@ export default async (
const note = await insertNote(user, data, tags, emojis, mentionedUsers);
await NoteFiles.insert(
note.fileIds.map((fileId) => ({ noteId: note.id, fileId })),
).catch((e) => {
logger.error(inspect(e));
});
res(note);
// Register host

View file

@ -40,7 +40,6 @@ import { i18n } from "@/i18n";
import { fetchInstance, instance } from "@/instance";
import { isSignedIn, me } from "@/me";
import { alert, api, confirm, popup, post, toast } from "@/os";
import { compareFirefishVersions } from "@/scripts/compare-versions";
import { deviceKind } from "@/scripts/device-kind";
import { getAccountFromId } from "@/scripts/get-account-from-id";
import { makeHotkey } from "@/scripts/hotkey";
@ -246,11 +245,7 @@ function checkForSplash() {
try {
// 変なバージョン文字列来るとcompareVersionsでエラーになるため
if (
lastVersion != null &&
compareFirefishVersions(lastVersion, version) === 1 &&
defaultStore.state.showUpdates
) {
if (lastVersion < version && defaultStore.state.showUpdates) {
// ログインしてる場合だけ
if (me) {
popup(

View file

@ -85,7 +85,6 @@ import {
provideMetadataReceiver,
} from "@/scripts/page-metadata";
import icon from "@/scripts/icon";
import { compareFirefishVersions } from "@/scripts/compare-versions";
const isEmpty = (x: string | null) => x == null || x === "";
const el = ref<HTMLElement | null>(null);
@ -122,8 +121,7 @@ os.api("admin/abuse-user-reports", {
if (defaultStore.state.showAdminUpdates) {
os.api("latest-version").then((res) => {
updateAvailable.value =
compareFirefishVersions(version, res?.latest_version) === 1;
updateAvailable.value = version < res?.latest_version;
});
}

View file

@ -1,19 +0,0 @@
const less = -1;
const same = 0;
const more = 1;
export const compareFirefishVersions = (
oldVersion: string,
newVersion: string,
) => {
if (oldVersion === newVersion) return same;
const o = oldVersion.split("-");
const n = newVersion.split("-");
if (o[0] < n[0]) return more;
if (o[0] === n[0] && o[1] == null && n[1] != null) return more;
if (o[0] === n[0] && o[1] != null && n[1] != null && o[1] < n[1]) return more;
return less;
};

View file

@ -162,7 +162,6 @@ import { i18n } from "@/i18n";
import { instance } from "@/instance";
import { version } from "@/config";
import icon from "@/scripts/icon";
import { compareFirefishVersions } from "@/scripts/compare-versions";
const isEmpty = (x: string | null) => x == null || x === "";
@ -212,8 +211,7 @@ if (isAdmin) {
if (defaultStore.state.showAdminUpdates) {
os.api("latest-version").then((res) => {
updateAvailable.value =
compareFirefishVersions(version, res?.latest_version) === 1;
updateAvailable.value = version < res?.latest_version;
});
}