Merge branch 'develop' into iceshrimp_mastodon
This commit is contained in:
commit
16f26bc6d7
23 changed files with 607 additions and 582 deletions
50
Cargo.lock
generated
50
Cargo.lock
generated
|
@ -4,9 +4,9 @@ version = 3
|
|||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.22.0"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
|
||||
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
@ -239,9 +239,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.72"
|
||||
version = "0.3.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
|
||||
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
|
@ -928,6 +928,12 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "finl_unicode"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.30"
|
||||
|
@ -1117,9 +1123,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.29.0"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
|
@ -1539,9 +1545,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.18"
|
||||
version = "1.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e"
|
||||
checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
|
@ -1885,9 +1891,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.35.0"
|
||||
version = "0.32.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
|
||||
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -2023,9 +2029,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
|
|||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
|
@ -3218,13 +3224,13 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
|||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
|
||||
checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6"
|
||||
dependencies = [
|
||||
"finl_unicode",
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3585,12 +3591,6 @@ dependencies = [
|
|||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-properties"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.11.0"
|
||||
|
@ -3982,9 +3982,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
|||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.9"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6"
|
||||
checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -4017,9 +4017,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
BEGIN;
|
||||
|
||||
DELETE FROM "migrations" WHERE name IN (
|
||||
'SwSubscriptionAccessToken1709395223611',
|
||||
'AddMastodonSubscriptionType1715181461692',
|
||||
'ClientCredentials1713108561474',
|
||||
'RefactorScheduledPosts1716804636187',
|
||||
'RemoveEnumTypenameSuffix1716462794927',
|
||||
'CreateScheduledNote1714728200194',
|
||||
'AddBackTimezone1715351290096',
|
||||
|
@ -30,10 +34,7 @@ DELETE FROM "migrations" WHERE name IN (
|
|||
'EmojiModerator1692825433698',
|
||||
'RemoveNsfwDetection1705848938166',
|
||||
'FirefishUrlMove1707850084123',
|
||||
'SwSubscriptionAccessToken1709395223611'
|
||||
'UserProfileMentions1711075007936',
|
||||
'ClientCredentials1713108561474',
|
||||
'AddMastodonSubscriptionType1715181461692'
|
||||
'UserProfileMentions1711075007936'
|
||||
);
|
||||
|
||||
-- addMastodonSubscriptionType
|
||||
|
@ -50,6 +51,38 @@ ALTER TABLE "user_profile" DROP COLUMN "mentions";
|
|||
-- client-credential-support
|
||||
ALTER TABLE "access_token" ALTER COLUMN "userId" SET NOT NULL;
|
||||
|
||||
-- refactor-scheduled-post
|
||||
CREATE TABLE "scheduled_note" (
|
||||
"id" character varying(32) NOT NULL PRIMARY KEY,
|
||||
"noteId" character varying(32) NOT NULL,
|
||||
"userId" character varying(32) NOT NULL,
|
||||
"scheduledAt" TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
COMMENT ON COLUMN "scheduled_note"."noteId" IS 'The ID of the temporarily created note that corresponds to the schedule.';
|
||||
CREATE EXTENSION pgcrypto;
|
||||
CREATE FUNCTION generate_scheduled_note_id(size int) RETURNS text AS $$ DECLARE
|
||||
characters text := 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
bytes bytea := gen_random_bytes(size);
|
||||
l int := length(characters);
|
||||
i int := 0;
|
||||
output text := '';
|
||||
BEGIN
|
||||
WHILE i < size LOOP
|
||||
output := output || substr(characters, get_byte(bytes, i) % l + 1, 1);
|
||||
i := i + 1;
|
||||
END LOOP;
|
||||
RETURN output;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql VOLATILE;
|
||||
INSERT INTO "scheduled_note" ("id", "noteId", "userId", "scheduledAt") (SELECT generate_scheduled_note_id(16), "id", "userId", "scheduledAt" FROM "note" WHERE "note"."scheduledAt" IS NOT NULL);
|
||||
DROP EXTENSION pgcrypto;
|
||||
DROP FUNCTION "generate_scheduled_note_id";
|
||||
CREATE INDEX "IDX_noteId_ScheduledNote" ON "scheduled_note" ("noteId");
|
||||
CREATE INDEX "IDX_userId_ScheduledNote" ON "scheduled_note" ("userId");
|
||||
ALTER TABLE "scheduled_note" ADD FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
ALTER TABLE "scheduled_note" ADD FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION;
|
||||
ALTER TABLE "note" DROP COLUMN "scheduledAt";
|
||||
|
||||
-- remove-enum-typename-suffix
|
||||
ALTER TYPE "antenna_src" RENAME TO "antenna_src_enum";
|
||||
ALTER TYPE "drive_file_usage_hint" RENAME TO "drive_file_usage_hint_enum";
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"@biomejs/cli-linux-arm64": "1.8.0",
|
||||
"@biomejs/cli-linux-x64": "1.8.0",
|
||||
"@types/node": "20.14.2",
|
||||
"execa": "9.1.0",
|
||||
"execa": "9.2.0",
|
||||
"pnpm": "9.2.0",
|
||||
"typescript": "5.4.5"
|
||||
}
|
||||
|
|
7
packages/backend-rs/index.d.ts
vendored
7
packages/backend-rs/index.d.ts
vendored
|
@ -927,6 +927,7 @@ export interface Note {
|
|||
threadId: string | null
|
||||
updatedAt: DateTimeWithTimeZone | null
|
||||
lang: string | null
|
||||
scheduledAt: DateTimeWithTimeZone | null
|
||||
}
|
||||
export interface NoteEdit {
|
||||
id: string
|
||||
|
@ -1086,12 +1087,6 @@ export interface ReplyMuting {
|
|||
muteeId: string
|
||||
muterId: string
|
||||
}
|
||||
export interface ScheduledNote {
|
||||
id: string
|
||||
noteId: string
|
||||
userId: string
|
||||
scheduledAt: DateTimeWithTimeZone
|
||||
}
|
||||
export enum AntennaSrc {
|
||||
All = 'all',
|
||||
Group = 'group',
|
||||
|
|
|
@ -53,7 +53,6 @@ pub mod registry_item;
|
|||
pub mod relay;
|
||||
pub mod renote_muting;
|
||||
pub mod reply_muting;
|
||||
pub mod scheduled_note;
|
||||
pub mod sea_orm_active_enums;
|
||||
pub mod signin;
|
||||
pub mod sw_subscription;
|
||||
|
|
|
@ -68,6 +68,8 @@ pub struct Model {
|
|||
#[sea_orm(column_name = "updatedAt")]
|
||||
pub updated_at: Option<DateTimeWithTimeZone>,
|
||||
pub lang: Option<String>,
|
||||
#[sea_orm(column_name = "scheduledAt")]
|
||||
pub scheduled_at: Option<DateTimeWithTimeZone>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
@ -124,8 +126,6 @@ pub enum Relation {
|
|||
PromoNote,
|
||||
#[sea_orm(has_many = "super::promo_read::Entity")]
|
||||
PromoRead,
|
||||
#[sea_orm(has_many = "super::scheduled_note::Entity")]
|
||||
ScheduledNote,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
|
@ -228,12 +228,6 @@ impl Related<super::promo_read::Entity> for Entity {
|
|||
}
|
||||
}
|
||||
|
||||
impl Related<super::scheduled_note::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ScheduledNote.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
|
|
|
@ -51,7 +51,6 @@ pub use super::registry_item::Entity as RegistryItem;
|
|||
pub use super::relay::Entity as Relay;
|
||||
pub use super::renote_muting::Entity as RenoteMuting;
|
||||
pub use super::reply_muting::Entity as ReplyMuting;
|
||||
pub use super::scheduled_note::Entity as ScheduledNote;
|
||||
pub use super::signin::Entity as Signin;
|
||||
pub use super::sw_subscription::Entity as SwSubscription;
|
||||
pub use super::used_username::Entity as UsedUsername;
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[sea_orm(table_name = "scheduled_note")]
|
||||
#[cfg_attr(
|
||||
feature = "napi",
|
||||
napi_derive::napi(object, js_name = "ScheduledNote", use_nullable = true)
|
||||
)]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
#[sea_orm(column_name = "noteId")]
|
||||
pub note_id: String,
|
||||
#[sea_orm(column_name = "userId")]
|
||||
pub user_id: String,
|
||||
#[sea_orm(column_name = "scheduledAt")]
|
||||
pub scheduled_at: DateTimeWithTimeZone,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::note::Entity",
|
||||
from = "Column::NoteId",
|
||||
to = "super::note::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Note,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::note::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Note.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -153,8 +153,6 @@ pub enum Relation {
|
|||
PromoRead,
|
||||
#[sea_orm(has_many = "super::registry_item::Entity")]
|
||||
RegistryItem,
|
||||
#[sea_orm(has_many = "super::scheduled_note::Entity")]
|
||||
ScheduledNote,
|
||||
#[sea_orm(has_many = "super::signin::Entity")]
|
||||
Signin,
|
||||
#[sea_orm(has_many = "super::sw_subscription::Entity")]
|
||||
|
@ -347,12 +345,6 @@ impl Related<super::registry_item::Entity> for Entity {
|
|||
}
|
||||
}
|
||||
|
||||
impl Related<super::scheduled_note::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::ScheduledNote.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::signin::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Signin.def()
|
||||
|
|
|
@ -22,9 +22,9 @@
|
|||
"@swc/core-android-arm64": "1.3.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "5.20.0",
|
||||
"@bull-board/koa": "5.20.0",
|
||||
"@bull-board/ui": "5.20.0",
|
||||
"@bull-board/api": "5.20.1",
|
||||
"@bull-board/koa": "5.20.1",
|
||||
"@bull-board/ui": "5.20.1",
|
||||
"@discordapp/twemoji": "15.0.3",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@koa/multer": "3.0.2",
|
||||
|
@ -38,7 +38,7 @@
|
|||
"archiver": "7.0.1",
|
||||
"async-lock": "1.4.0",
|
||||
"async-mutex": "0.5.0",
|
||||
"aws-sdk": "2.1635.0",
|
||||
"aws-sdk": "2.1636.0",
|
||||
"axios": "1.7.2",
|
||||
"backend-rs": "workspace:*",
|
||||
"blurhash": "2.0.5",
|
||||
|
@ -59,7 +59,7 @@
|
|||
"firefish-js": "workspace:*",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.0",
|
||||
"got": "14.4.0",
|
||||
"got": "14.4.1",
|
||||
"gunzip-maybe": "1.4.2",
|
||||
"hpagent": "1.2.0",
|
||||
"ioredis": "5.4.1",
|
||||
|
|
|
@ -74,7 +74,6 @@ 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 { ScheduledNote } from "@/models/entities/scheduled-note.js";
|
||||
|
||||
import { entities as charts } from "@/services/chart/entities.js";
|
||||
import { dbLogger } from "./logger.js";
|
||||
|
@ -183,7 +182,6 @@ export const entities = [
|
|||
UserPending,
|
||||
Webhook,
|
||||
UserIp,
|
||||
ScheduledNote,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import type { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class RefactorScheduledPosts1716804636187 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "note" ADD COLUMN "scheduledAt" timestamp with time zone`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TEMP TABLE "tmp_scheduled_note" (LIKE "note")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "tmp_scheduled_note" (SELECT * FROM "note" WHERE "note"."id" IN (SELECT "noteId" FROM "scheduled_note"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE "tmp_scheduled_note" SET "scheduledAt" = "scheduled_note"."scheduledAt" FROM "scheduled_note" WHERE "tmp_scheduled_note"."id" = "scheduled_note"."noteId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "note" WHERE "note"."id" IN (SELECT "noteId" FROM "scheduled_note")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "note" SELECT * FROM "tmp_scheduled_note"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "scheduled_note"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "scheduled_note" (
|
||||
"id" character varying(32) NOT NULL PRIMARY KEY,
|
||||
"noteId" character varying(32) NOT NULL,
|
||||
"userId" character varying(32) NOT NULL,
|
||||
"scheduledAt" TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
)`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`COMMENT ON COLUMN "scheduled_note"."noteId" IS 'The ID of the temporarily created note that corresponds to the schedule.'`,
|
||||
);
|
||||
// temp function to populate "scheduled_note"."id" with random values (as it's unused)
|
||||
await queryRunner.query("CREATE EXTENSION pgcrypto");
|
||||
await queryRunner.query(`
|
||||
CREATE FUNCTION generate_scheduled_note_id(size int) RETURNS text AS $$ DECLARE
|
||||
characters text := 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
bytes bytea := gen_random_bytes(size);
|
||||
l int := length(characters);
|
||||
i int := 0;
|
||||
output text := '';
|
||||
BEGIN
|
||||
WHILE i < size LOOP
|
||||
output := output || substr(characters, get_byte(bytes, i) % l + 1, 1);
|
||||
i := i + 1;
|
||||
END LOOP;
|
||||
RETURN output;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql VOLATILE;
|
||||
`);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "scheduled_note" ("id", "noteId", "userId", "scheduledAt") (SELECT generate_scheduled_note_id(16), "id", "userId", "scheduledAt" FROM "note" WHERE "note"."scheduledAt" IS NOT NULL)`,
|
||||
);
|
||||
await queryRunner.query("DROP EXTENSION pgcrypto");
|
||||
await queryRunner.query(`DROP FUNCTION "generate_scheduled_note_id"`);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_noteId_ScheduledNote" ON "scheduled_note" ("noteId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_userId_ScheduledNote" ON "scheduled_note" ("userId")`,
|
||||
);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "scheduled_note"
|
||||
ADD FOREIGN KEY ("noteId") REFERENCES "note"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "scheduled_note"
|
||||
ADD FOREIGN KEY ("userId") REFERENCES "user"("id")
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE NO ACTION
|
||||
`);
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "scheduledAt"`);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import fetch from "node-fetch";
|
|||
import { Converter } from "opencc-js";
|
||||
import { getAgentByUrl } from "@/misc/fetch.js";
|
||||
import { fetchMeta } from "backend-rs";
|
||||
import type { PostLanguage } from "@/misc/langmap";
|
||||
import type { PostLanguage } from "firefish-js";
|
||||
import * as deepl from "deepl-node";
|
||||
|
||||
// DeepL translate and LibreTranslate don't provide
|
||||
|
|
|
@ -31,6 +31,11 @@ export class Note {
|
|||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Column("timestamp with time zone", {
|
||||
nullable: true,
|
||||
})
|
||||
public scheduledAt: Date | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import {
|
||||
Entity,
|
||||
JoinColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
OneToOne,
|
||||
PrimaryColumn,
|
||||
Index,
|
||||
type Relation,
|
||||
} from "typeorm";
|
||||
import { Note } from "./note.js";
|
||||
import { id } from "../id.js";
|
||||
import { User } from "./user.js";
|
||||
|
||||
@Entity()
|
||||
export class ScheduledNote {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment:
|
||||
"The ID of the temporarily created note that corresponds to the schedule.",
|
||||
})
|
||||
public noteId: Note["id"];
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public userId: User["id"];
|
||||
|
||||
@Column("timestamp with time zone")
|
||||
public scheduledAt: Date;
|
||||
|
||||
//#region Relations
|
||||
@OneToOne(() => Note, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn()
|
||||
public note: Relation<Note>;
|
||||
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: Relation<User>;
|
||||
//#endregion
|
||||
}
|
|
@ -67,7 +67,6 @@ import { UserIp } from "./entities/user-ip.js";
|
|||
import { NoteFileRepository } from "./repositories/note-file.js";
|
||||
import { NoteEditRepository } from "./repositories/note-edit.js";
|
||||
import { UserProfileRepository } from "./repositories/user-profile.js";
|
||||
import { ScheduledNote } from "./entities/scheduled-note.js";
|
||||
|
||||
export const Announcements = db.getRepository(Announcement);
|
||||
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
||||
|
@ -136,4 +135,3 @@ export const RegistryItems = db.getRepository(RegistryItem);
|
|||
export const Webhooks = db.getRepository(Webhook);
|
||||
export const Ads = db.getRepository(Ad);
|
||||
export const PasswordResetRequests = db.getRepository(PasswordResetRequest);
|
||||
export const ScheduledNotes = db.getRepository(ScheduledNote);
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
Channels,
|
||||
UserProfiles,
|
||||
Notes,
|
||||
ScheduledNotes,
|
||||
} from "../index.js";
|
||||
import type { Packed } from "@/misc/schema.js";
|
||||
import { countReactions, decodeReaction, nyaify } from "backend-rs";
|
||||
|
@ -224,19 +223,17 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
host,
|
||||
);
|
||||
|
||||
let scheduledAt: string | undefined;
|
||||
if (note.visibility === "specified" && note.visibleUserIds.length === 0) {
|
||||
scheduledAt = (
|
||||
await ScheduledNotes.findOneBy({
|
||||
noteId: note.id,
|
||||
})
|
||||
)?.scheduledAt?.toISOString();
|
||||
}
|
||||
|
||||
const reactionEmoji = await populateEmojis(reactionEmojiNames, host);
|
||||
const packed: Packed<"Note"> = await awaitAll({
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
// FIXME: note.scheduledAt should be a `Date`
|
||||
scheduledAt:
|
||||
note.scheduledAt == null
|
||||
? undefined
|
||||
: typeof note.scheduledAt === "string"
|
||||
? note.scheduledAt
|
||||
: note.scheduledAt?.toISOString(),
|
||||
userId: note.userId,
|
||||
user: Users.pack(note.user ?? note.userId, me, {
|
||||
detail: false,
|
||||
|
@ -266,7 +263,6 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
},
|
||||
})
|
||||
: undefined,
|
||||
scheduledAt,
|
||||
reactions: countReactions(note.reactions),
|
||||
reactionEmojis: reactionEmoji,
|
||||
emojis: noteEmoji,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Users, Notes, ScheduledNotes, DriveFiles } from "@/models/index.js";
|
||||
import { Users, Notes, DriveFiles } from "@/models/index.js";
|
||||
import type { DbUserScheduledNoteData } from "@/queue/types.js";
|
||||
import { queueLogger } from "../../logger.js";
|
||||
import type Bull from "bull";
|
||||
|
@ -14,52 +14,82 @@ export async function scheduledNote(
|
|||
): Promise<void> {
|
||||
logger.info(`Creating: ${job.data.noteId}`);
|
||||
|
||||
const user = await Users.findOneBy({ id: job.data.user.id });
|
||||
const [user, draftNote] = await Promise.all([
|
||||
Users.findOneBy({ id: job.data.user.id }),
|
||||
Notes.findOneBy({ id: job.data.noteId }),
|
||||
]);
|
||||
|
||||
if (user == null) {
|
||||
logger.warn(`User ${job.data.user.id} does not exist, aborting`);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await Notes.findOneBy({ id: job.data.noteId });
|
||||
if (note == null) {
|
||||
if (draftNote == null) {
|
||||
logger.warn(`Note ${job.data.noteId} does not exist, aborting`);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
const files = await DriveFiles.findBy({ id: In(note.fileIds) });
|
||||
|
||||
if (user.isSuspended) {
|
||||
deleteNote(user, note);
|
||||
logger.info(
|
||||
`Cancelled due to user ${job.data.user.id} being suspended, aborting`,
|
||||
);
|
||||
await deleteNote(user, draftNote);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
await ScheduledNotes.delete({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
const [visibleUsers, reply, renote, files] = await Promise.all([
|
||||
job.data.option.visibleUserIds
|
||||
? Users.findBy({
|
||||
id: In(job.data.option.visibleUserIds),
|
||||
})
|
||||
: [],
|
||||
job.data.option.replyId != null
|
||||
? Notes.findOneBy({ id: job.data.option.replyId })
|
||||
: undefined,
|
||||
job.data.option.renoteId != null
|
||||
? Notes.findOneBy({ id: job.data.option.renoteId })
|
||||
: undefined,
|
||||
DriveFiles.findBy({ id: In(draftNote.fileIds) }),
|
||||
]);
|
||||
|
||||
const visibleUsers = job.data.option.visibleUserIds
|
||||
? await Users.findBy({
|
||||
id: In(job.data.option.visibleUserIds),
|
||||
})
|
||||
: [];
|
||||
if (job.data.option.replyId != null && reply == null) {
|
||||
logger.warn(
|
||||
`Note ${job.data.option.replyId} (reply) does not exist, aborting`,
|
||||
);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
if (job.data.option.renoteId != null && renote == null) {
|
||||
logger.warn(
|
||||
`Note ${job.data.option.renoteId} (renote) does not exist, aborting`,
|
||||
);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create scheduled (actual) note
|
||||
await createNote(user, {
|
||||
createdAt: new Date(),
|
||||
scheduledAt: null,
|
||||
files,
|
||||
poll: job.data.option.poll,
|
||||
text: note.text || undefined,
|
||||
lang: note.lang,
|
||||
reply: note.reply,
|
||||
renote: note.renote,
|
||||
cw: note.cw,
|
||||
localOnly: note.localOnly,
|
||||
text: draftNote.text || undefined,
|
||||
lang: draftNote.lang,
|
||||
reply,
|
||||
renote,
|
||||
cw: draftNote.cw,
|
||||
localOnly: draftNote.localOnly,
|
||||
visibility: job.data.option.visibility,
|
||||
visibleUsers,
|
||||
channel: note.channel,
|
||||
channel: draftNote.channel,
|
||||
});
|
||||
|
||||
await deleteNote(user, note);
|
||||
// Delete temporal (draft) note
|
||||
await deleteNote(user, draftNote);
|
||||
|
||||
logger.info("Success");
|
||||
|
||||
|
|
|
@ -62,6 +62,8 @@ export type DbUserScheduledNoteData = {
|
|||
option: {
|
||||
visibility: string;
|
||||
visibleUserIds?: string[] | null;
|
||||
replyId?: string;
|
||||
renoteId?: string;
|
||||
poll?: IPoll;
|
||||
};
|
||||
noteId: Note["id"];
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
Notes,
|
||||
Channels,
|
||||
Blockings,
|
||||
ScheduledNotes,
|
||||
} from "@/models/index.js";
|
||||
import type { DriveFile } from "@/models/entities/drive-file.js";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
|
@ -16,7 +15,7 @@ import { config } from "@/config.js";
|
|||
import { noteVisibilities } from "@/types.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
import define from "@/server/api/define.js";
|
||||
import { HOUR, genIdAt } from "backend-rs";
|
||||
import { HOUR } from "backend-rs";
|
||||
import { getNote } from "@/server/api/common/getters.js";
|
||||
import { langmap } from "firefish-js";
|
||||
import { createScheduledNoteJob } from "@/queue/index.js";
|
||||
|
@ -95,6 +94,12 @@ export const meta = {
|
|||
code: "ACCOUNT_LOCKED",
|
||||
id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3",
|
||||
},
|
||||
|
||||
scheduledTimeIsPast: {
|
||||
message: "The scheduled time is past.",
|
||||
code: "SCHEDULED_TIME_IS_PAST",
|
||||
id: "277f91df-8d8e-4647-b4e3-5885fda8978a",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -299,26 +304,26 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
|
||||
let delay: number | null = null;
|
||||
if (ps.scheduledAt) {
|
||||
if (ps.scheduledAt != null) {
|
||||
delay = ps.scheduledAt - Date.now();
|
||||
if (delay < 0) {
|
||||
delay = null;
|
||||
throw new ApiError(meta.errors.scheduledTimeIsPast);
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Create a post
|
||||
const note = await create(
|
||||
user,
|
||||
{
|
||||
createdAt: now,
|
||||
createdAt: new Date(),
|
||||
scheduledAt: delay != null ? new Date(ps.scheduledAt!) : null,
|
||||
files: files,
|
||||
poll: ps.poll
|
||||
? {
|
||||
choices: ps.poll.choices,
|
||||
multiple: ps.poll.multiple,
|
||||
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
|
||||
expiresAt:
|
||||
ps.poll.expiresAt != null ? new Date(ps.poll.expiresAt) : null,
|
||||
}
|
||||
: undefined,
|
||||
text: ps.text || undefined,
|
||||
|
@ -344,13 +349,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
false,
|
||||
delay
|
||||
? async (note) => {
|
||||
await ScheduledNotes.insert({
|
||||
id: genIdAt(now),
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
scheduledAt: new Date(ps.scheduledAt as number),
|
||||
});
|
||||
|
||||
createScheduledNoteJob(
|
||||
{
|
||||
user: { id: user.id },
|
||||
|
@ -367,6 +365,8 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
: undefined,
|
||||
visibility: ps.visibility,
|
||||
visibleUserIds: ps.visibleUserIds,
|
||||
replyId: ps.replyId ?? undefined,
|
||||
renoteId: ps.renoteId ?? undefined,
|
||||
},
|
||||
},
|
||||
delay,
|
||||
|
|
|
@ -133,15 +133,10 @@ class NotificationManager {
|
|||
}
|
||||
}
|
||||
|
||||
type MinimumUser = {
|
||||
id: User["id"];
|
||||
host: User["host"];
|
||||
username: User["username"];
|
||||
uri: User["uri"];
|
||||
};
|
||||
|
||||
type Option = {
|
||||
type UserLike = Pick<User, "id" | "host" | "username" | "uri">;
|
||||
type NoteLike = {
|
||||
createdAt?: Date | null;
|
||||
scheduledAt?: Date | null;
|
||||
name?: string | null;
|
||||
text?: string | null;
|
||||
lang?: string | null;
|
||||
|
@ -152,9 +147,9 @@ type Option = {
|
|||
localOnly?: boolean | null;
|
||||
cw?: string | null;
|
||||
visibility?: string;
|
||||
visibleUsers?: MinimumUser[] | null;
|
||||
visibleUsers?: UserLike[] | null;
|
||||
channel?: Channel | null;
|
||||
apMentions?: MinimumUser[] | null;
|
||||
apMentions?: UserLike[] | null;
|
||||
apHashtags?: string[] | null;
|
||||
apEmojis?: string[] | null;
|
||||
uri?: string | null;
|
||||
|
@ -163,16 +158,11 @@ type Option = {
|
|||
};
|
||||
|
||||
export default async (
|
||||
user: {
|
||||
id: User["id"];
|
||||
username: User["username"];
|
||||
host: User["host"];
|
||||
isSilenced: User["isSilenced"];
|
||||
createdAt: User["createdAt"];
|
||||
isBot: User["isBot"];
|
||||
inbox?: User["inbox"];
|
||||
},
|
||||
data: Option,
|
||||
user: Pick<
|
||||
User,
|
||||
"id" | "username" | "host" | "isSilenced" | "createdAt" | "isBot"
|
||||
> & { inbox?: User["inbox"] },
|
||||
data: NoteLike,
|
||||
silent = false,
|
||||
waitToPublish?: (note: Note) => Promise<void>,
|
||||
) =>
|
||||
|
@ -181,6 +171,9 @@ export default async (
|
|||
const dontFederateInitially =
|
||||
data.visibility?.startsWith("hidden") === true;
|
||||
|
||||
// Whether this is a scheduled "draft" post (yet to be published)
|
||||
const isDraft = data.scheduledAt != null;
|
||||
|
||||
// If you reply outside the channel, match the scope of the target.
|
||||
// TODO (I think it's a process that could be done on the client side, but it's server side for now.)
|
||||
if (
|
||||
|
@ -208,6 +201,7 @@ export default async (
|
|||
data.createdAt > now
|
||||
)
|
||||
data.createdAt = now;
|
||||
|
||||
if (data.visibility == null) data.visibility = "public";
|
||||
if (data.localOnly == null) data.localOnly = false;
|
||||
if (data.channel != null) data.visibility = "public";
|
||||
|
@ -277,13 +271,9 @@ export default async (
|
|||
data.localOnly = true;
|
||||
}
|
||||
|
||||
if (data.text) {
|
||||
data.text = data.text.trim();
|
||||
} else {
|
||||
data.text = null;
|
||||
}
|
||||
data.text = data.text?.trim() ?? null;
|
||||
|
||||
if (data.lang) {
|
||||
if (data.lang != null) {
|
||||
if (!Object.keys(langmap).includes(data.lang.toLowerCase()))
|
||||
throw new Error("invalid param");
|
||||
data.lang = data.lang.toLowerCase();
|
||||
|
@ -297,10 +287,10 @@ export default async (
|
|||
|
||||
// Parse MFM if needed
|
||||
if (!(tags && emojis && mentionedUsers)) {
|
||||
const tokens = data.text ? mfm.parse(data.text)! : [];
|
||||
const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
|
||||
const tokens = data.text ? mfm.parse(data.text) : [];
|
||||
const cwTokens = data.cw ? mfm.parse(data.cw) : [];
|
||||
const choiceTokens = data.poll?.choices
|
||||
? concat(data.poll.choices.map((choice) => mfm.parse(choice)!))
|
||||
? concat(data.poll.choices.map((choice) => mfm.parse(choice)))
|
||||
: [];
|
||||
|
||||
const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
|
||||
|
@ -318,16 +308,16 @@ export default async (
|
|||
.splice(0, 32);
|
||||
|
||||
if (
|
||||
data.reply &&
|
||||
data.reply != null &&
|
||||
user.id !== data.reply.userId &&
|
||||
!mentionedUsers.some((u) => u.id === data.reply!.userId)
|
||||
!mentionedUsers.some((u) => u.id === data.reply?.userId)
|
||||
) {
|
||||
mentionedUsers.push(
|
||||
await Users.findOneByOrFail({ id: data.reply!.userId }),
|
||||
await Users.findOneByOrFail({ id: data.reply.userId }),
|
||||
);
|
||||
}
|
||||
|
||||
if (data.visibility === "specified") {
|
||||
if (!isDraft && data.visibility === "specified") {
|
||||
if (data.visibleUsers == null) throw new Error("invalid param");
|
||||
|
||||
for (const u of data.visibleUsers) {
|
||||
|
@ -338,10 +328,10 @@ export default async (
|
|||
|
||||
if (
|
||||
data.reply &&
|
||||
!data.visibleUsers.some((x) => x.id === data.reply!.userId)
|
||||
!data.visibleUsers.some((x) => x.id === data.reply?.userId)
|
||||
) {
|
||||
data.visibleUsers.push(
|
||||
await Users.findOneByOrFail({ id: data.reply!.userId }),
|
||||
await Users.findOneByOrFail({ id: data.reply?.userId }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -365,314 +355,321 @@ export default async (
|
|||
});
|
||||
}
|
||||
|
||||
// ハッシュタグ更新
|
||||
if (data.visibility === "public" || data.visibility === "home") {
|
||||
updateHashtags(user, tags);
|
||||
}
|
||||
if (!isDraft) {
|
||||
// ハッシュタグ更新
|
||||
if (data.visibility === "public" || data.visibility === "home") {
|
||||
updateHashtags(user, tags);
|
||||
}
|
||||
|
||||
// Increment notes count (user)
|
||||
incNotesCountOfUser(user);
|
||||
// Increment notes count (user)
|
||||
incNotesCountOfUser(user);
|
||||
|
||||
// Word mutes & antenna
|
||||
const thisNoteIsMutedBy: string[] = [];
|
||||
// Word mutes & antenna
|
||||
const thisNoteIsMutedBy: string[] = [];
|
||||
|
||||
await hardMutesCache
|
||||
.fetch(null, () =>
|
||||
UserProfiles.find({
|
||||
where: {
|
||||
enableWordMute: true,
|
||||
},
|
||||
select: ["userId", "mutedWords", "mutedPatterns"],
|
||||
}),
|
||||
)
|
||||
.then(async (us) => {
|
||||
for (const u of us) {
|
||||
if (u.userId === user.id) return;
|
||||
await checkWordMute(note, u.mutedWords, u.mutedPatterns).then(
|
||||
(shouldMute: boolean) => {
|
||||
if (shouldMute) {
|
||||
thisNoteIsMutedBy.push(u.userId);
|
||||
MutedNotes.insert({
|
||||
id: genId(),
|
||||
userId: u.userId,
|
||||
noteId: note.id,
|
||||
reason: "word",
|
||||
});
|
||||
}
|
||||
await hardMutesCache
|
||||
.fetch(null, () =>
|
||||
UserProfiles.find({
|
||||
where: {
|
||||
enableWordMute: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
select: ["userId", "mutedWords", "mutedPatterns"],
|
||||
}),
|
||||
)
|
||||
.then(async (us) => {
|
||||
for (const u of us) {
|
||||
if (u.userId === user.id) return;
|
||||
await checkWordMute(note, u.mutedWords, u.mutedPatterns).then(
|
||||
(shouldMute: boolean) => {
|
||||
if (shouldMute) {
|
||||
thisNoteIsMutedBy.push(u.userId);
|
||||
MutedNotes.insert({
|
||||
id: genId(),
|
||||
userId: u.userId,
|
||||
noteId: note.id,
|
||||
reason: "word",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// type errors will be resolved by https://github.com/napi-rs/napi-rs/pull/2054
|
||||
const _note = toRustObject(note);
|
||||
if (note.renoteId == null || isQuote(_note)) {
|
||||
await updateAntennasOnNewNote(_note, user, thisNoteIsMutedBy);
|
||||
}
|
||||
// type errors will be resolved by https://github.com/napi-rs/napi-rs/pull/2054
|
||||
const _note = toRustObject(note);
|
||||
if (note.renoteId == null || isQuote(_note)) {
|
||||
await updateAntennasOnNewNote(_note, user, thisNoteIsMutedBy);
|
||||
}
|
||||
|
||||
// Channel
|
||||
if (note.channelId) {
|
||||
ChannelFollowings.findBy({ followeeId: note.channelId }).then(
|
||||
(followings) => {
|
||||
for (const following of followings) {
|
||||
insertNoteUnread(following.followerId, note, {
|
||||
isSpecified: false,
|
||||
// Channel
|
||||
if (note.channelId != null) {
|
||||
ChannelFollowings.findBy({ followeeId: note.channelId }).then(
|
||||
(followings) => {
|
||||
for (const following of followings) {
|
||||
insertNoteUnread(following.followerId, note, {
|
||||
isSpecified: false,
|
||||
isMentioned: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (data.reply) {
|
||||
saveReply(data.reply, note);
|
||||
}
|
||||
|
||||
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
|
||||
if (
|
||||
data.renote &&
|
||||
!user.isBot &&
|
||||
(await countSameRenotes(user.id, data.renote.id, note.id)) === 0
|
||||
) {
|
||||
incRenoteCount(data.renote);
|
||||
}
|
||||
|
||||
if (data.poll?.expiresAt) {
|
||||
const delay = data.poll.expiresAt.getTime() - Date.now();
|
||||
endedPollNotificationQueue.add(
|
||||
{
|
||||
noteId: note.id,
|
||||
},
|
||||
{
|
||||
delay,
|
||||
removeOnComplete: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
if (Users.isLocalUser(user)) activeUsersChart.write(user);
|
||||
|
||||
// 未読通知を作成
|
||||
if (data.visibility === "specified") {
|
||||
if (data.visibleUsers == null) throw new Error("invalid param");
|
||||
|
||||
for (const u of data.visibleUsers) {
|
||||
// ローカルユーザーのみ
|
||||
if (!Users.isLocalUser(u)) continue;
|
||||
|
||||
insertNoteUnread(u.id, note, {
|
||||
isSpecified: true,
|
||||
isMentioned: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (data.reply) {
|
||||
saveReply(data.reply, note);
|
||||
}
|
||||
|
||||
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
|
||||
if (
|
||||
data.renote &&
|
||||
!user.isBot &&
|
||||
(await countSameRenotes(user.id, data.renote.id, note.id)) === 0
|
||||
) {
|
||||
incRenoteCount(data.renote);
|
||||
}
|
||||
|
||||
if (data.poll?.expiresAt) {
|
||||
const delay = data.poll.expiresAt.getTime() - Date.now();
|
||||
endedPollNotificationQueue.add(
|
||||
{
|
||||
noteId: note.id,
|
||||
},
|
||||
{
|
||||
delay,
|
||||
removeOnComplete: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
if (Users.isLocalUser(user)) activeUsersChart.write(user);
|
||||
|
||||
// 未読通知を作成
|
||||
if (data.visibility === "specified") {
|
||||
if (data.visibleUsers == null) throw new Error("invalid param");
|
||||
|
||||
for (const u of data.visibleUsers) {
|
||||
// ローカルユーザーのみ
|
||||
if (!Users.isLocalUser(u)) continue;
|
||||
|
||||
insertNoteUnread(u.id, note, {
|
||||
isSpecified: true,
|
||||
isMentioned: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (const u of mentionedUsers) {
|
||||
// ローカルユーザーのみ
|
||||
if (!Users.isLocalUser(u)) continue;
|
||||
|
||||
insertNoteUnread(u.id, note, {
|
||||
isSpecified: false,
|
||||
isMentioned: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!dontFederateInitially) {
|
||||
let publishKey: string;
|
||||
let noteToPublish: Note;
|
||||
const relays = await getCachedRelays();
|
||||
|
||||
// Some relays (e.g., aode-relay) deliver posts by boosting them as
|
||||
// Announce activities. In that case, user is the relay's actor.
|
||||
const boostedByRelay =
|
||||
!!user.inbox &&
|
||||
relays.map((relay) => relay.inbox).includes(user.inbox);
|
||||
|
||||
if (boostedByRelay && data.renote && data.renote.userHost) {
|
||||
publishKey = `publishedNote:${data.renote.id}`;
|
||||
noteToPublish = data.renote;
|
||||
} else {
|
||||
publishKey = `publishedNote:${note.id}`;
|
||||
noteToPublish = note;
|
||||
}
|
||||
for (const u of mentionedUsers) {
|
||||
// ローカルユーザーのみ
|
||||
if (!Users.isLocalUser(u)) continue;
|
||||
|
||||
const lock = new Mutex(redisClient, "publishedNote");
|
||||
await lock.acquire();
|
||||
try {
|
||||
const published = (await redisClient.get(publishKey)) != null;
|
||||
if (!published) {
|
||||
await redisClient.set(publishKey, "done", "EX", 30);
|
||||
if (noteToPublish.renoteId) {
|
||||
// Prevents other threads from publishing the boosting post
|
||||
await redisClient.set(
|
||||
`publishedNote:${noteToPublish.renoteId}`,
|
||||
"done",
|
||||
"EX",
|
||||
30,
|
||||
);
|
||||
}
|
||||
publishNotesStream(noteToPublish);
|
||||
}
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
if (note.replyId != null) {
|
||||
// Only provide the reply note id here as the recipient may not be authorized to see the note.
|
||||
publishNoteStream(note.replyId, "replied", {
|
||||
id: note.id,
|
||||
});
|
||||
}
|
||||
|
||||
const webhooks = await getActiveWebhooks().then((webhooks) =>
|
||||
webhooks.filter((x) => x.userId === user.id && x.on.includes("note")),
|
||||
);
|
||||
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, "note", {
|
||||
note: await Notes.pack(note, user),
|
||||
});
|
||||
}
|
||||
|
||||
const nm = new NotificationManager(user, note);
|
||||
const nmRelatedPromises = [];
|
||||
|
||||
await createMentionedEvents(mentionedUsers, note, nm);
|
||||
|
||||
// If has in reply to note
|
||||
if (data.reply) {
|
||||
// Fetch watchers
|
||||
nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm));
|
||||
|
||||
// 通知
|
||||
if (data.reply.userHost === null) {
|
||||
const threadMuted = await NoteThreadMutings.findOneBy({
|
||||
userId: data.reply.userId,
|
||||
threadId: data.reply.threadId || data.reply.id,
|
||||
});
|
||||
|
||||
if (!threadMuted) {
|
||||
nm.push(data.reply.userId, "reply");
|
||||
|
||||
const packedReply = await Notes.pack(note, {
|
||||
id: data.reply.userId,
|
||||
insertNoteUnread(u.id, note, {
|
||||
isSpecified: false,
|
||||
isMentioned: true,
|
||||
});
|
||||
publishMainStream(data.reply.userId, "reply", packedReply);
|
||||
}
|
||||
}
|
||||
|
||||
if (note.replyId != null) {
|
||||
// Only provide the reply note id here as the recipient may not be authorized to see the note.
|
||||
publishNoteStream(note.replyId, "replied", {
|
||||
id: note.id,
|
||||
});
|
||||
}
|
||||
|
||||
const webhooks = await getActiveWebhooks().then((webhooks) =>
|
||||
webhooks.filter((x) => x.userId === user.id && x.on.includes("note")),
|
||||
);
|
||||
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, "note", {
|
||||
note: await Notes.pack(note, user),
|
||||
});
|
||||
}
|
||||
|
||||
const nm = new NotificationManager(user, note);
|
||||
const nmRelatedPromises = [];
|
||||
|
||||
await createMentionedEvents(mentionedUsers, note, nm);
|
||||
|
||||
// If has in reply to note
|
||||
if (data.reply != null) {
|
||||
// Fetch watchers
|
||||
nmRelatedPromises.push(
|
||||
notifyToWatchersOfReplyee(data.reply, user, nm),
|
||||
);
|
||||
|
||||
// 通知
|
||||
if (data.reply.userHost === null) {
|
||||
const threadMuted = await NoteThreadMutings.findOneBy({
|
||||
userId: data.reply.userId,
|
||||
threadId: data.reply.threadId || data.reply.id,
|
||||
});
|
||||
|
||||
if (!threadMuted) {
|
||||
nm.push(data.reply.userId, "reply");
|
||||
|
||||
const packedReply = await Notes.pack(note, {
|
||||
id: data.reply.userId,
|
||||
});
|
||||
publishMainStream(data.reply.userId, "reply", packedReply);
|
||||
|
||||
const webhooks = (await getActiveWebhooks()).filter(
|
||||
(x) =>
|
||||
x.userId === data.reply?.userId && x.on.includes("reply"),
|
||||
);
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, "reply", {
|
||||
note: packedReply,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it is renote
|
||||
if (data.renote != null) {
|
||||
const type = data.text ? "quote" : "renote";
|
||||
|
||||
// Notify
|
||||
if (data.renote.userHost === null) {
|
||||
const threadMuted = await NoteThreadMutings.findOneBy({
|
||||
userId: data.renote.userId,
|
||||
threadId: data.renote.threadId || data.renote.id,
|
||||
});
|
||||
|
||||
if (!threadMuted) {
|
||||
nm.push(data.renote.userId, type);
|
||||
}
|
||||
}
|
||||
// Fetch watchers
|
||||
nmRelatedPromises.push(
|
||||
notifyToWatchersOfRenotee(data.renote, user, nm, type),
|
||||
);
|
||||
|
||||
// Publish event
|
||||
if (user.id !== data.renote.userId && data.renote.userHost === null) {
|
||||
const packedRenote = await Notes.pack(note, {
|
||||
id: data.renote.userId,
|
||||
});
|
||||
publishMainStream(data.renote.userId, "renote", packedRenote);
|
||||
|
||||
const renote = data.renote;
|
||||
const webhooks = (await getActiveWebhooks()).filter(
|
||||
(x) => x.userId === data.reply!.userId && x.on.includes("reply"),
|
||||
(x) => x.userId === renote.userId && x.on.includes("renote"),
|
||||
);
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, "reply", {
|
||||
note: packedReply,
|
||||
webhookDeliver(webhook, "renote", {
|
||||
note: packedRenote,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it is renote
|
||||
if (data.renote) {
|
||||
const type = data.text ? "quote" : "renote";
|
||||
Promise.all(nmRelatedPromises).then(() => {
|
||||
nm.deliver();
|
||||
});
|
||||
|
||||
// Notify
|
||||
if (data.renote.userHost === null) {
|
||||
const threadMuted = await NoteThreadMutings.findOneBy({
|
||||
userId: data.renote.userId,
|
||||
threadId: data.renote.threadId || data.renote.id,
|
||||
});
|
||||
//#region AP deliver
|
||||
if (Users.isLocalUser(user) && !dontFederateInitially) {
|
||||
(async () => {
|
||||
const noteActivity = await renderNoteOrRenoteActivity(data, note);
|
||||
const dm = new DeliverManager(user, noteActivity);
|
||||
|
||||
if (!threadMuted) {
|
||||
nm.push(data.renote.userId, type);
|
||||
}
|
||||
}
|
||||
// Fetch watchers
|
||||
nmRelatedPromises.push(
|
||||
notifyToWatchersOfRenotee(data.renote, user, nm, type),
|
||||
);
|
||||
|
||||
// Publish event
|
||||
if (user.id !== data.renote.userId && data.renote.userHost === null) {
|
||||
const packedRenote = await Notes.pack(note, {
|
||||
id: data.renote.userId,
|
||||
});
|
||||
publishMainStream(data.renote.userId, "renote", packedRenote);
|
||||
|
||||
const renote = data.renote;
|
||||
const webhooks = (await getActiveWebhooks()).filter(
|
||||
(x) => x.userId === renote.userId && x.on.includes("renote"),
|
||||
);
|
||||
for (const webhook of webhooks) {
|
||||
webhookDeliver(webhook, "renote", {
|
||||
note: packedRenote,
|
||||
});
|
||||
}
|
||||
// メンションされたリモートユーザーに配送
|
||||
for (const u of mentionedUsers.filter((u) =>
|
||||
Users.isRemoteUser(u),
|
||||
)) {
|
||||
dm.addDirectRecipe(u as IRemoteUser);
|
||||
}
|
||||
|
||||
// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
|
||||
if (data.reply?.userHost != null) {
|
||||
const u = await Users.findOneBy({ id: data.reply.userId });
|
||||
if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
|
||||
}
|
||||
|
||||
// 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
|
||||
if (data.renote?.userHost != null) {
|
||||
const u = await Users.findOneBy({ id: data.renote.userId });
|
||||
if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
|
||||
}
|
||||
|
||||
// フォロワーに配送
|
||||
if (["public", "home", "followers"].includes(note.visibility)) {
|
||||
dm.addFollowersRecipe();
|
||||
}
|
||||
|
||||
if (["public"].includes(note.visibility)) {
|
||||
deliverToRelays(user, noteActivity);
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
})();
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
Promise.all(nmRelatedPromises).then(() => {
|
||||
nm.deliver();
|
||||
});
|
||||
if (data.channel) {
|
||||
Channels.increment({ id: data.channel.id }, "notesCount", 1);
|
||||
Channels.update(data.channel.id, {
|
||||
lastNotedAt: new Date(),
|
||||
});
|
||||
|
||||
//#region AP deliver
|
||||
if (Users.isLocalUser(user) && !dontFederateInitially) {
|
||||
(async () => {
|
||||
const noteActivity = await renderNoteOrRenoteActivity(data, note);
|
||||
const dm = new DeliverManager(user, noteActivity);
|
||||
|
||||
// メンションされたリモートユーザーに配送
|
||||
for (const u of mentionedUsers.filter((u) => Users.isRemoteUser(u))) {
|
||||
dm.addDirectRecipe(u as IRemoteUser);
|
||||
await Notes.countBy({
|
||||
userId: user.id,
|
||||
channelId: data.channel.id,
|
||||
}).then((count) => {
|
||||
// この処理が行われるのはノート作成後なので、ノートが一つしかなかったら最初の投稿だと判断できる
|
||||
// TODO: とはいえノートを削除して何回も投稿すればその分だけインクリメントされる雑さもあるのでどうにかしたい
|
||||
if (count === 1 && data.channel != null) {
|
||||
Channels.increment({ id: data.channel.id }, "usersCount", 1);
|
||||
}
|
||||
|
||||
// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
|
||||
if (data.reply?.userHost != null) {
|
||||
const u = await Users.findOneBy({ id: data.reply.userId });
|
||||
if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
|
||||
}
|
||||
|
||||
// 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
|
||||
if (data.renote?.userHost != null) {
|
||||
const u = await Users.findOneBy({ id: data.renote.userId });
|
||||
if (u && Users.isRemoteUser(u)) dm.addDirectRecipe(u);
|
||||
}
|
||||
|
||||
// フォロワーに配送
|
||||
if (["public", "home", "followers"].includes(note.visibility)) {
|
||||
dm.addFollowersRecipe();
|
||||
}
|
||||
|
||||
if (["public"].includes(note.visibility)) {
|
||||
deliverToRelays(user, noteActivity);
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
})();
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
if (data.channel) {
|
||||
Channels.increment({ id: data.channel.id }, "notesCount", 1);
|
||||
Channels.update(data.channel.id, {
|
||||
lastNotedAt: new Date(),
|
||||
});
|
||||
if (!dontFederateInitially) {
|
||||
let publishKey: string;
|
||||
let noteToPublish: Note;
|
||||
const relays = await getCachedRelays();
|
||||
|
||||
await Notes.countBy({
|
||||
userId: user.id,
|
||||
channelId: data.channel.id,
|
||||
}).then((count) => {
|
||||
// この処理が行われるのはノート作成後なので、ノートが一つしかなかったら最初の投稿だと判断できる
|
||||
// TODO: とはいえノートを削除して何回も投稿すればその分だけインクリメントされる雑さもあるのでどうにかしたい
|
||||
if (count === 1 && data.channel != null) {
|
||||
Channels.increment({ id: data.channel.id }, "usersCount", 1);
|
||||
// Some relays (e.g., aode-relay) deliver posts by boosting them as
|
||||
// Announce activities. In that case, user is the relay's actor.
|
||||
const boostedByRelay =
|
||||
!!user.inbox && relays.map((relay) => relay.inbox).includes(user.inbox);
|
||||
|
||||
if (boostedByRelay && data.renote && data.renote.userHost) {
|
||||
publishKey = `publishedNote:${data.renote.id}`;
|
||||
noteToPublish = data.renote;
|
||||
} else {
|
||||
publishKey = `publishedNote:${note.id}`;
|
||||
noteToPublish = note;
|
||||
}
|
||||
|
||||
const lock = new Mutex(redisClient, "publishedNote");
|
||||
await lock.acquire();
|
||||
try {
|
||||
const published = (await redisClient.get(publishKey)) != null;
|
||||
if (!published) {
|
||||
await redisClient.set(publishKey, "done", "EX", 30);
|
||||
if (noteToPublish.renoteId) {
|
||||
// Prevents other threads from publishing the boosting post
|
||||
await redisClient.set(
|
||||
`publishedNote:${noteToPublish.renoteId}`,
|
||||
"done",
|
||||
"EX",
|
||||
30,
|
||||
);
|
||||
}
|
||||
publishNotesStream(noteToPublish);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function renderNoteOrRenoteActivity(data: Option, note: Note) {
|
||||
async function renderNoteOrRenoteActivity(data: NoteLike, note: Note) {
|
||||
if (data.localOnly) return null;
|
||||
|
||||
const content =
|
||||
|
@ -704,17 +701,17 @@ function incRenoteCount(renote: Note) {
|
|||
|
||||
async function insertNote(
|
||||
user: { id: User["id"]; host: User["host"] },
|
||||
data: Option,
|
||||
data: NoteLike,
|
||||
tags: string[],
|
||||
emojis: string[],
|
||||
mentionedUsers: MinimumUser[],
|
||||
mentionedUsers: UserLike[],
|
||||
) {
|
||||
if (data.createdAt === null || data.createdAt === undefined) {
|
||||
data.createdAt = new Date();
|
||||
}
|
||||
const insert = new Note({
|
||||
data.createdAt ??= new Date();
|
||||
|
||||
const note = new Note({
|
||||
id: genIdAt(data.createdAt),
|
||||
createdAt: data.createdAt,
|
||||
scheduledAt: data.scheduledAt ?? null,
|
||||
fileIds: data.files ? data.files.map((file) => file.id) : [],
|
||||
replyId: data.reply ? data.reply.id : null,
|
||||
renoteId: data.renote ? data.renote.id : null,
|
||||
|
@ -743,7 +740,7 @@ async function insertNote(
|
|||
|
||||
attachedFileTypes: data.files ? data.files.map((file) => file.type) : [],
|
||||
|
||||
// 以下非正規化データ
|
||||
// denormalized fields
|
||||
replyUserId: data.reply ? data.reply.userId : null,
|
||||
replyUserHost: data.reply ? data.reply.userHost : null,
|
||||
renoteUserId: data.renote ? data.renote.userId : null,
|
||||
|
@ -751,22 +748,22 @@ async function insertNote(
|
|||
userHost: user.host,
|
||||
});
|
||||
|
||||
if (data.uri != null) insert.uri = data.uri;
|
||||
if (data.url != null) insert.url = data.url;
|
||||
if (data.uri != null) note.uri = data.uri;
|
||||
if (data.url != null) note.url = data.url;
|
||||
|
||||
// Append mentions data
|
||||
if (mentionedUsers.length > 0) {
|
||||
insert.mentions = mentionedUsers.map((u) => u.id);
|
||||
const profiles = await UserProfiles.findBy({ userId: In(insert.mentions) });
|
||||
insert.mentionedRemoteUsers = JSON.stringify(
|
||||
note.mentions = mentionedUsers.map((u) => u.id);
|
||||
const profiles = await UserProfiles.findBy({ userId: In(note.mentions) });
|
||||
note.mentionedRemoteUsers = JSON.stringify(
|
||||
mentionedUsers
|
||||
.filter((u) => Users.isRemoteUser(u))
|
||||
.map((u) => {
|
||||
const profile = profiles.find((p) => p.userId === u.id);
|
||||
const url = profile != null ? profile.url : null;
|
||||
const url = profile?.url ?? null;
|
||||
return {
|
||||
uri: u.uri,
|
||||
url: url == null ? undefined : url,
|
||||
url: url ?? undefined,
|
||||
username: u.username,
|
||||
host: u.host,
|
||||
} as IMentionedRemoteUsers[0];
|
||||
|
@ -776,12 +773,12 @@ async function insertNote(
|
|||
|
||||
// 投稿を作成
|
||||
try {
|
||||
if (insert.hasPoll) {
|
||||
if (note.hasPoll) {
|
||||
// Start transaction
|
||||
await db.transaction(async (transactionalEntityManager) => {
|
||||
if (!data.poll) throw new Error("Empty poll data");
|
||||
|
||||
await transactionalEntityManager.insert(Note, insert);
|
||||
await transactionalEntityManager.insert(Note, note);
|
||||
|
||||
let expiresAt: Date | null;
|
||||
if (
|
||||
|
@ -794,12 +791,12 @@ async function insertNote(
|
|||
}
|
||||
|
||||
const poll = new Poll({
|
||||
noteId: insert.id,
|
||||
noteId: note.id,
|
||||
choices: data.poll.choices,
|
||||
expiresAt,
|
||||
multiple: data.poll.multiple,
|
||||
votes: new Array(data.poll.choices.length).fill(0),
|
||||
noteVisibility: insert.visibility,
|
||||
noteVisibility: note.visibility,
|
||||
userId: user.id,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
@ -807,10 +804,10 @@ async function insertNote(
|
|||
await transactionalEntityManager.insert(Poll, poll);
|
||||
});
|
||||
} else {
|
||||
await Notes.insert(insert);
|
||||
await Notes.insert(note);
|
||||
}
|
||||
|
||||
return insert;
|
||||
return note;
|
||||
} catch (e) {
|
||||
// duplicate key error
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
|
@ -857,7 +854,7 @@ async function notifyToWatchersOfReplyee(
|
|||
}
|
||||
|
||||
async function createMentionedEvents(
|
||||
mentionedUsers: MinimumUser[],
|
||||
mentionedUsers: UserLike[],
|
||||
note: Note,
|
||||
nm: NotificationManager,
|
||||
) {
|
||||
|
|
|
@ -42,8 +42,12 @@ export default async function (
|
|||
) {
|
||||
const deletedAt = new Date();
|
||||
|
||||
// Whether this is a scheduled "draft" post
|
||||
const isDraft = note.scheduledAt != null;
|
||||
|
||||
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
|
||||
if (
|
||||
!isDraft &&
|
||||
note.renoteId &&
|
||||
(await countSameRenotes(user.id, note.renoteId, note.id)) === 0 &&
|
||||
deleteFromDb
|
||||
|
@ -52,7 +56,7 @@ export default async function (
|
|||
Notes.decrement({ id: note.renoteId }, "score", 1);
|
||||
}
|
||||
|
||||
if (note.replyId && deleteFromDb) {
|
||||
if (!isDraft && note.replyId != null && deleteFromDb) {
|
||||
await Notes.decrement({ id: note.replyId }, "repliesCount", 1);
|
||||
}
|
||||
|
||||
|
@ -74,7 +78,7 @@ export default async function (
|
|||
}
|
||||
|
||||
//#region ローカルの投稿なら削除アクティビティを配送
|
||||
if (Users.isLocalUser(user) && !note.localOnly) {
|
||||
if (!isDraft && Users.isLocalUser(user) && !note.localOnly) {
|
||||
let renote: Note | null = null;
|
||||
|
||||
// if deletd note is renote
|
||||
|
|
|
@ -31,8 +31,8 @@ importers:
|
|||
specifier: 20.14.2
|
||||
version: 20.14.2
|
||||
execa:
|
||||
specifier: 9.1.0
|
||||
version: 9.1.0
|
||||
specifier: 9.2.0
|
||||
version: 9.2.0
|
||||
pnpm:
|
||||
specifier: 9.2.0
|
||||
version: 9.2.0
|
||||
|
@ -43,14 +43,14 @@ importers:
|
|||
packages/backend:
|
||||
dependencies:
|
||||
'@bull-board/api':
|
||||
specifier: 5.20.0
|
||||
version: 5.20.0(@bull-board/ui@5.20.0)
|
||||
specifier: 5.20.1
|
||||
version: 5.20.1(@bull-board/ui@5.20.1)
|
||||
'@bull-board/koa':
|
||||
specifier: 5.20.0
|
||||
version: 5.20.0(@types/koa@2.15.0)(lodash@4.17.21)(pug@3.0.3)
|
||||
specifier: 5.20.1
|
||||
version: 5.20.1(@types/koa@2.15.0)(lodash@4.17.21)(pug@3.0.3)
|
||||
'@bull-board/ui':
|
||||
specifier: 5.20.0
|
||||
version: 5.20.0
|
||||
specifier: 5.20.1
|
||||
version: 5.20.1
|
||||
'@discordapp/twemoji':
|
||||
specifier: 15.0.3
|
||||
version: 15.0.3
|
||||
|
@ -91,8 +91,8 @@ importers:
|
|||
specifier: 0.5.0
|
||||
version: 0.5.0
|
||||
aws-sdk:
|
||||
specifier: 2.1635.0
|
||||
version: 2.1635.0
|
||||
specifier: 2.1636.0
|
||||
version: 2.1636.0
|
||||
axios:
|
||||
specifier: 1.7.2
|
||||
version: 1.7.2
|
||||
|
@ -154,8 +154,8 @@ importers:
|
|||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
got:
|
||||
specifier: 14.4.0
|
||||
version: 14.4.0
|
||||
specifier: 14.4.1
|
||||
version: 14.4.1
|
||||
gunzip-maybe:
|
||||
specifier: 1.4.2
|
||||
version: 1.4.2
|
||||
|
@ -1043,11 +1043,13 @@ packages:
|
|||
'@biomejs/cli-darwin-arm64@1.8.0':
|
||||
resolution: {integrity: sha512-dBAYzfIJ1JmWigKlWourT3sJ3I60LZPjqNwwlsyFjiv5AV7vPeWlHVVIImV2BpINwNjZQhpXnwDfVnGS4vr7AA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-darwin-x64@1.8.0':
|
||||
resolution: {integrity: sha512-ZTTSD0bP0nn9UpRDGQrQNTILcYSj+IkxTYr3CAV64DWBDtQBomlk2oVKWzDaA1LOhpAsTh0giLCbPJaVk2jfMQ==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@1.8.0':
|
||||
|
@ -1059,6 +1061,7 @@ packages:
|
|||
'@biomejs/cli-linux-arm64@1.8.0':
|
||||
resolution: {integrity: sha512-cx725jTlJS6dskvJJwwCQaaMRBKE2Qss7ukzmx27Rn/DXRxz6tnnBix4FUGPf1uZfwrERkiJlbWM05JWzpvvXg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@1.8.0':
|
||||
|
@ -1070,6 +1073,7 @@ packages:
|
|||
'@biomejs/cli-linux-x64@1.8.0':
|
||||
resolution: {integrity: sha512-cmgmhlD4QUxMhL1VdaNqnB81xBHb3R7huVNyYnPYzP+AykZ7XqJbPd1KcWAszNjUk2AHdx0aLKEBwCOWemxb2g==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-win32-arm64@1.8.0':
|
||||
|
@ -1084,16 +1088,16 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@bull-board/api@5.20.0':
|
||||
resolution: {integrity: sha512-WUSuHdunODSWX8rkZmTnXpJsrUHqLS6+H3IGN0MNh6ylM/plZxGJJ3jW4nTXePWASeLNoDC0gjRcziDjlodPnQ==}
|
||||
'@bull-board/api@5.20.1':
|
||||
resolution: {integrity: sha512-45aDhnOzWRrtUUAKHxdClSnLIus5f8BK3ATzb2IwI/BRgOi1lWTe1YG266hVqDdWXsUDxKzf75DAANKfAoEsRA==}
|
||||
peerDependencies:
|
||||
'@bull-board/ui': 5.20.0
|
||||
'@bull-board/ui': 5.20.1
|
||||
|
||||
'@bull-board/koa@5.20.0':
|
||||
resolution: {integrity: sha512-jto1WNIBZscCSJMkGm4G4qrb5KhblyJT/Q/59k5hHg9fdogzW9PfR+UQj7OOjLTMNX3phtPNvL0jG7/3HfmtIA==}
|
||||
'@bull-board/koa@5.20.1':
|
||||
resolution: {integrity: sha512-xUEEpMsdzZWHZQMnfk4a9kFhCz4PQkMBnb/t/C2xYwG15nCLapHWqMd5xO59OxnWOZ5XIiLsJwtCuf0l5G+L8A==}
|
||||
|
||||
'@bull-board/ui@5.20.0':
|
||||
resolution: {integrity: sha512-EuijEHzQ9EAaA8WD4B+iXpTWTvCpTU2pskVUOs8CFkp0AYBqIhQDOi1W3f+e3FMqwS81l2/WkVHKkd6QRzbRzA==}
|
||||
'@bull-board/ui@5.20.1':
|
||||
resolution: {integrity: sha512-RzNinC4FKHNuxzkIRsCL+n9iO5RxmF5YM7byCuuv1/UeFjtCtsLHFi6TI9ZgJsXETA2Uxq9Mg7ppncojUjrINw==}
|
||||
|
||||
'@cbor-extract/cbor-extract-darwin-arm64@2.2.0':
|
||||
resolution: {integrity: sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==}
|
||||
|
@ -2887,8 +2891,8 @@ packages:
|
|||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
aws-sdk@2.1635.0:
|
||||
resolution: {integrity: sha512-zab4y8ftjgcctYao33c1fr8Yx1wMuRlEbZT7hwEXcK8Ta3+LAEXJs1wKjPpium+KUwl6zw7wxhaIdEibiz6InA==}
|
||||
aws-sdk@2.1636.0:
|
||||
resolution: {integrity: sha512-0w/jOCYnwewLYjH4UCh3GTBjR/NMdvEKNrd1pnM4FvfJSmjfzCinDvmf5Qc6xeIrqPfrdYOoQh7NJhYeJScCIQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
axios@0.24.0:
|
||||
|
@ -4264,9 +4268,9 @@ packages:
|
|||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
execa@9.1.0:
|
||||
resolution: {integrity: sha512-lSgHc4Elo2m6bUDhc3Hl/VxvUDJdQWI40RZ4KMY9bKRc+hgMOT7II/JjbNDhI8VnMtrCb7U/fhpJIkLORZozWw==}
|
||||
engines: {node: '>=18'}
|
||||
execa@9.2.0:
|
||||
resolution: {integrity: sha512-vpOyYg7UAVKLAWWtRS2gAdgkT7oJbCn0me3gmUmxZih4kd3MF/oo8kNTBTIbkO3yuuF5uB4ZCZfn8BOolITYhg==}
|
||||
engines: {node: ^18.19.0 || >=20.5.0}
|
||||
|
||||
executable@4.1.1:
|
||||
resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==}
|
||||
|
@ -4606,8 +4610,8 @@ packages:
|
|||
resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==}
|
||||
engines: {node: '>=10.19.0'}
|
||||
|
||||
got@14.4.0:
|
||||
resolution: {integrity: sha512-baa2HMfREJ9UQSXOPwWe0DNK+FT8Okcxe9kmTJvaetv2q/MUxq0qFzEnfSbxo+wj45/QioGcH5ZhuT9VBIPJ5Q==}
|
||||
got@14.4.1:
|
||||
resolution: {integrity: sha512-IvDJbJBUeexX74xNQuMIVgCRRuNOm5wuK+OC3Dc2pnSoh1AOmgc7JVj7WC+cJ4u0aPcO9KZ2frTXcqK4W/5qTQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
|
@ -8041,15 +8045,15 @@ snapshots:
|
|||
'@biomejs/cli-win32-x64@1.8.0':
|
||||
optional: true
|
||||
|
||||
'@bull-board/api@5.20.0(@bull-board/ui@5.20.0)':
|
||||
'@bull-board/api@5.20.1(@bull-board/ui@5.20.1)':
|
||||
dependencies:
|
||||
'@bull-board/ui': 5.20.0
|
||||
'@bull-board/ui': 5.20.1
|
||||
redis-info: 3.1.0
|
||||
|
||||
'@bull-board/koa@5.20.0(@types/koa@2.15.0)(lodash@4.17.21)(pug@3.0.3)':
|
||||
'@bull-board/koa@5.20.1(@types/koa@2.15.0)(lodash@4.17.21)(pug@3.0.3)':
|
||||
dependencies:
|
||||
'@bull-board/api': 5.20.0(@bull-board/ui@5.20.0)
|
||||
'@bull-board/ui': 5.20.0
|
||||
'@bull-board/api': 5.20.1(@bull-board/ui@5.20.1)
|
||||
'@bull-board/ui': 5.20.1
|
||||
ejs: 3.1.10
|
||||
koa: 2.15.3
|
||||
koa-mount: 4.0.0
|
||||
|
@ -8112,9 +8116,9 @@ snapshots:
|
|||
- walrus
|
||||
- whiskers
|
||||
|
||||
'@bull-board/ui@5.20.0':
|
||||
'@bull-board/ui@5.20.1':
|
||||
dependencies:
|
||||
'@bull-board/api': 5.20.0(@bull-board/ui@5.20.0)
|
||||
'@bull-board/api': 5.20.1(@bull-board/ui@5.20.1)
|
||||
|
||||
'@cbor-extract/cbor-extract-darwin-arm64@2.2.0':
|
||||
optional: true
|
||||
|
@ -10043,7 +10047,7 @@ snapshots:
|
|||
dependencies:
|
||||
possible-typed-array-names: 1.0.0
|
||||
|
||||
aws-sdk@2.1635.0:
|
||||
aws-sdk@2.1636.0:
|
||||
dependencies:
|
||||
buffer: 4.9.2
|
||||
events: 1.1.1
|
||||
|
@ -11585,7 +11589,7 @@ snapshots:
|
|||
signal-exit: 3.0.7
|
||||
strip-final-newline: 2.0.0
|
||||
|
||||
execa@9.1.0:
|
||||
execa@9.2.0:
|
||||
dependencies:
|
||||
'@sindresorhus/merge-streams': 4.0.0
|
||||
cross-spawn: 7.0.3
|
||||
|
@ -11976,7 +11980,7 @@ snapshots:
|
|||
p-cancelable: 2.1.1
|
||||
responselike: 2.0.1
|
||||
|
||||
got@14.4.0:
|
||||
got@14.4.1:
|
||||
dependencies:
|
||||
'@sindresorhus/is': 6.3.1
|
||||
'@szmarczak/http-timer': 5.0.1
|
||||
|
@ -11989,6 +11993,7 @@ snapshots:
|
|||
lowercase-keys: 3.0.0
|
||||
p-cancelable: 4.0.1
|
||||
responselike: 3.0.0
|
||||
type-fest: 4.19.0
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
|
|
Loading…
Reference in a new issue