Compare commits

...

8 commits

14 changed files with 89 additions and 77 deletions

View file

@ -3,7 +3,7 @@ FROM docker.io/node:20-alpine as build
WORKDIR /firefish WORKDIR /firefish
# Install compilation dependencies # Install compilation dependencies
RUN apk update && apk add --no-cache build-base linux-headers curl ca-certificates python3 RUN apk update && apk add --no-cache build-base linux-headers curl ca-certificates python3 perl
RUN curl --proto '=https' --tlsv1.2 --silent --show-error --fail https://sh.rustup.rs | sh -s -- -y RUN curl --proto '=https' --tlsv1.2 --silent --show-error --fail https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}" ENV PATH="/root/.cargo/bin:${PATH}"
@ -34,6 +34,7 @@ RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm install -
# Copy in the rest of the rust files # Copy in the rest of the rust files
COPY packages/backend-rs packages/backend-rs/ COPY packages/backend-rs packages/backend-rs/
# COPY packages/macro-rs packages/macro-rs/
# Compile backend-rs # Compile backend-rs
RUN NODE_ENV='production' pnpm run --filter backend-rs build RUN NODE_ENV='production' pnpm run --filter backend-rs build

View file

@ -1183,6 +1183,7 @@ export interface PackedEmoji {
height: number | null height: number | null
} }
export function publishToBroadcastStream(emoji: PackedEmoji): void export function publishToBroadcastStream(emoji: PackedEmoji): void
export function publishToGroupChatStream(groupId: string, kind: ChatEvent, object: any): void
export interface AbuseUserReportLike { export interface AbuseUserReportLike {
id: string id: string
targetUserId: string targetUserId: string

View file

@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`) throw new Error(`Failed to load native binding`)
} }
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToModerationStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
module.exports.SECOND = SECOND module.exports.SECOND = SECOND
module.exports.MINUTE = MINUTE module.exports.MINUTE = MINUTE
@ -373,6 +373,7 @@ module.exports.publishToChatStream = publishToChatStream
module.exports.ChatIndexEvent = ChatIndexEvent module.exports.ChatIndexEvent = ChatIndexEvent
module.exports.publishToChatIndexStream = publishToChatIndexStream module.exports.publishToChatIndexStream = publishToChatIndexStream
module.exports.publishToBroadcastStream = publishToBroadcastStream module.exports.publishToBroadcastStream = publishToBroadcastStream
module.exports.publishToGroupChatStream = publishToGroupChatStream
module.exports.publishToModerationStream = publishToModerationStream module.exports.publishToModerationStream = publishToModerationStream
module.exports.getTimestamp = getTimestamp module.exports.getTimestamp = getTimestamp
module.exports.genId = genId module.exports.genId = genId

View file

@ -3,6 +3,7 @@ pub mod channel;
pub mod chat; pub mod chat;
pub mod chat_index; pub mod chat_index;
pub mod custom_emoji; pub mod custom_emoji;
pub mod group_chat;
pub mod moderation; pub mod moderation;
use crate::config::CONFIG; use crate::config::CONFIG;

View file

@ -0,0 +1,13 @@
use crate::service::stream::{chat::ChatEvent, publish_to_stream, Error, Stream};
// We want to merge `kind` and `object` into a single enum
// https://github.com/napi-rs/napi-rs/issues/2036
#[crate::export(js_name = "publishToGroupChatStream")]
pub fn publish(group_id: String, kind: ChatEvent, object: &serde_json::Value) -> Result<(), Error> {
publish_to_stream(
&Stream::GroupChat { group_id },
Some(kind.to_string()),
Some(serde_json::to_string(object)?),
)
}

View file

@ -7,20 +7,16 @@ import probeImageSize from "probe-image-size";
import isSvg from "is-svg"; import isSvg from "is-svg";
import sharp from "sharp"; import sharp from "sharp";
import { encode } from "blurhash"; import { encode } from "blurhash";
import { inspect } from "node:util";
export type FileInfo = { type FileInfo = {
size: number; size: number;
md5: string; md5: string;
type: { mime: string;
mime: string; fileExtension: string | null;
ext: string | null;
};
width?: number; width?: number;
height?: number; height?: number;
orientation?: number; orientation?: number;
blurhash?: string; blurhash?: string;
warnings: string[];
}; };
const TYPE_OCTET_STREAM = { const TYPE_OCTET_STREAM = {
@ -37,8 +33,6 @@ const TYPE_SVG = {
* Get file information * Get file information
*/ */
export async function getFileInfo(path: string): Promise<FileInfo> { export async function getFileInfo(path: string): Promise<FileInfo> {
const warnings = [] as string[];
const size = await getFileSize(path); const size = await getFileSize(path);
const md5 = await calcHash(path); const md5 = await calcHash(path);
@ -63,14 +57,12 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
"image/avif", "image/avif",
].includes(type.mime) ].includes(type.mime)
) { ) {
const imageSize = await detectImageSize(path).catch((e) => { const imageSize = await detectImageSize(path).catch((_) => {
warnings.push(`detectImageSize failed:\n${inspect(e)}`);
return undefined; return undefined;
}); });
// うまく判定できない画像は octet-stream にする // うまく判定できない画像は octet-stream にする
if (!imageSize) { if (!imageSize) {
warnings.push("cannot detect image dimensions");
type = TYPE_OCTET_STREAM; type = TYPE_OCTET_STREAM;
} else if (imageSize.wUnits === "px") { } else if (imageSize.wUnits === "px") {
width = imageSize.width; width = imageSize.width;
@ -79,11 +71,8 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
// 制限を超えている画像は octet-stream にする // 制限を超えている画像は octet-stream にする
if (imageSize.width > 16383 || imageSize.height > 16383) { if (imageSize.width > 16383 || imageSize.height > 16383) {
warnings.push("image dimensions exceeds limits");
type = TYPE_OCTET_STREAM; type = TYPE_OCTET_STREAM;
} }
} else {
warnings.push(`unsupported unit type: ${imageSize.wUnits}`);
} }
} }
@ -100,8 +89,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
"image/avif", "image/avif",
].includes(type.mime) ].includes(type.mime)
) { ) {
blurhash = await getBlurhash(path).catch((e) => { blurhash = await getBlurhash(path).catch((_) => {
warnings.push(`getBlurhash failed:\n${inspect(e)}`);
return undefined; return undefined;
}); });
} }
@ -109,12 +97,12 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
return { return {
size, size,
md5, md5,
type, mime: type.mime,
fileExtension: type.ext,
width, width,
height, height,
orientation, orientation,
blurhash, blurhash,
warnings,
}; };
} }

View file

@ -1,9 +1,7 @@
import { import { publishMainStream } from "@/services/stream.js";
publishMainStream,
publishGroupMessagingStream,
} from "@/services/stream.js";
import { import {
publishToChatStream, publishToChatStream,
publishToGroupChatStream,
publishToChatIndexStream, publishToChatIndexStream,
sendPushNotification, sendPushNotification,
ChatEvent, ChatEvent,
@ -130,9 +128,9 @@ export async function readGroupMessagingMessage(
} }
// Publish event // Publish event
publishGroupMessagingStream(groupId, "read", { publishToGroupChatStream(groupId, ChatEvent.Read, {
ids: reads, ids: reads,
userId: userId, userId,
}); });
publishToChatIndexStream(userId, ChatIndexEvent.Read, reads); publishToChatIndexStream(userId, ChatIndexEvent.Read, reads);

View file

@ -14,10 +14,10 @@ import {
} from "@/models/index.js"; } from "@/models/index.js";
import type { AccessToken } from "@/models/entities/access-token.js"; import type { AccessToken } from "@/models/entities/access-token.js";
import type { UserProfile } from "@/models/entities/user-profile.js"; import type { UserProfile } from "@/models/entities/user-profile.js";
import { publishGroupMessagingStream } from "@/services/stream.js";
import { import {
publishToChannelStream, publishToChannelStream,
publishToChatStream, publishToChatStream,
publishToGroupChatStream,
ChatEvent, ChatEvent,
} from "backend-rs"; } from "backend-rs";
import type { UserGroup } from "@/models/entities/user-group.js"; import type { UserGroup } from "@/models/entities/user-group.js";
@ -531,8 +531,8 @@ export default class Connection {
ChatEvent.Typing, ChatEvent.Typing,
this.user.id, this.user.id,
); );
} else if (param.group) { } else if (param.group != null) {
publishGroupMessagingStream(param.group, "typing", this.user.id); publishToGroupChatStream(param.group, ChatEvent.Typing, this.user.id);
} }
} }
} }

View file

@ -477,17 +477,20 @@ export async function addFile({
requestHeaders = null, requestHeaders = null,
usageHint = null, usageHint = null,
}: AddFileArgs): Promise<DriveFile> { }: AddFileArgs): Promise<DriveFile> {
const info = await getFileInfo(path); const fileInfo = await getFileInfo(path);
logger.info(`${JSON.stringify(info)}`); logger.info(`${JSON.stringify(fileInfo)}`);
// detect name // detect name
const detectedName = const detectedName =
name || (info.type.ext ? `untitled.${info.type.ext}` : "untitled"); name ||
(fileInfo.fileExtension
? `untitled.${fileInfo.fileExtension}`
: "untitled");
if (user && !force) { if (user && !force) {
// Check if there is a file with the same hash // Check if there is a file with the same hash
const much = await DriveFiles.findOneBy({ const much = await DriveFiles.findOneBy({
md5: info.md5, md5: fileInfo.md5,
userId: user.id, userId: user.id,
}); });
@ -515,7 +518,7 @@ export async function addFile({
logger.debug("drive capacity override applied"); logger.debug("drive capacity override applied");
logger.debug( logger.debug(
`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${ `overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${
usage + info.size usage + fileInfo.size
}bytes`, }bytes`,
); );
} }
@ -523,7 +526,7 @@ export async function addFile({
logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
// If usage limit exceeded // If usage limit exceeded
if (usage + info.size > driveCapacity) { if (usage + fileInfo.size > driveCapacity) {
if (Users.isLocalUser(user)) { if (Users.isLocalUser(user)) {
throw new IdentifiableError( throw new IdentifiableError(
"c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6", "c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6",
@ -533,7 +536,7 @@ export async function addFile({
// (アバターまたはバナーを含まず)最も古いファイルを削除する // (アバターまたはバナーを含まず)最も古いファイルを削除する
expireOldFile( expireOldFile(
(await Users.findOneByOrFail({ id: user.id })) as IRemoteUser, (await Users.findOneByOrFail({ id: user.id })) as IRemoteUser,
driveCapacity - info.size, driveCapacity - fileInfo.size,
); );
} }
} }
@ -561,12 +564,12 @@ export async function addFile({
orientation?: number; orientation?: number;
} = {}; } = {};
if (info.width) { if (fileInfo.width != null && fileInfo.height != null) {
properties["width"] = info.width; properties.width = fileInfo.width;
properties["height"] = info.height; properties.height = fileInfo.height;
} }
if (info.orientation != null) { if (fileInfo.orientation != null) {
properties["orientation"] = info.orientation; properties.orientation = fileInfo.orientation;
} }
const profile = user const profile = user
@ -584,7 +587,7 @@ export async function addFile({
file.folderId = folder != null ? folder.id : null; file.folderId = folder != null ? folder.id : null;
file.comment = comment; file.comment = comment;
file.properties = properties; file.properties = properties;
file.blurhash = info.blurhash || null; file.blurhash = fileInfo.blurhash ?? null;
file.isLink = isLink; file.isLink = isLink;
file.requestIp = requestIp; file.requestIp = requestIp;
file.requestHeaders = requestHeaders; file.requestHeaders = requestHeaders;
@ -617,9 +620,9 @@ export async function addFile({
if (isLink) { if (isLink) {
try { try {
file.size = 0; file.size = 0;
file.md5 = info.md5; file.md5 = fileInfo.md5;
file.name = detectedName; file.name = detectedName;
file.type = info.type.mime; file.type = fileInfo.mime;
file.storedInternal = false; file.storedInternal = false;
file = await DriveFiles.insert(file).then((x) => file = await DriveFiles.insert(file).then((x) =>
@ -644,9 +647,9 @@ export async function addFile({
file, file,
path, path,
detectedName, detectedName,
info.type.mime, fileInfo.mime,
info.md5, fileInfo.md5,
info.size, fileInfo.size,
usageHint, usageHint,
); );
} }

View file

@ -11,6 +11,7 @@ import {
genId, genId,
sendPushNotification, sendPushNotification,
publishToChatStream, publishToChatStream,
publishToGroupChatStream,
publishToChatIndexStream, publishToChatIndexStream,
toPuny, toPuny,
ChatEvent, ChatEvent,
@ -18,10 +19,7 @@ import {
PushNotificationKind, PushNotificationKind,
} from "backend-rs"; } from "backend-rs";
import type { MessagingMessage } from "@/models/entities/messaging-message.js"; import type { MessagingMessage } from "@/models/entities/messaging-message.js";
import { import { publishMainStream } from "@/services/stream.js";
publishMainStream,
publishGroupMessagingStream,
} from "@/services/stream.js";
import { Not } from "typeorm"; import { Not } from "typeorm";
import type { Note } from "@/models/entities/note.js"; import type { Note } from "@/models/entities/note.js";
import renderNote from "@/remote/activitypub/renderer/note.js"; import renderNote from "@/remote/activitypub/renderer/note.js";
@ -87,11 +85,11 @@ export async function createMessage(
); );
publishMainStream(recipientUser.id, "messagingMessage", messageObj); publishMainStream(recipientUser.id, "messagingMessage", messageObj);
} }
} else if (recipientGroup) { } else if (recipientGroup != null) {
// グループのストリーム // group's stream
publishGroupMessagingStream(recipientGroup.id, "message", messageObj); publishToGroupChatStream(recipientGroup.id, ChatEvent.Message, messageObj);
// メンバーのストリーム // member's stream
const joinings = await UserGroupJoinings.findBy({ const joinings = await UserGroupJoinings.findBy({
userGroupId: recipientGroup.id, userGroupId: recipientGroup.id,
}); });

View file

@ -1,8 +1,11 @@
import { config } from "@/config.js"; import { config } from "@/config.js";
import { MessagingMessages, Users } from "@/models/index.js"; import { MessagingMessages, Users } from "@/models/index.js";
import type { MessagingMessage } from "@/models/entities/messaging-message.js"; import type { MessagingMessage } from "@/models/entities/messaging-message.js";
import { publishGroupMessagingStream } from "@/services/stream.js"; import {
import { publishToChatStream, ChatEvent } from "backend-rs"; publishToChatStream,
publishToGroupChatStream,
ChatEvent,
} from "backend-rs";
import { renderActivity } from "@/remote/activitypub/renderer/index.js"; import { renderActivity } from "@/remote/activitypub/renderer/index.js";
import renderDelete from "@/remote/activitypub/renderer/delete.js"; import renderDelete from "@/remote/activitypub/renderer/delete.js";
import renderTombstone from "@/remote/activitypub/renderer/tombstone.js"; import renderTombstone from "@/remote/activitypub/renderer/tombstone.js";
@ -42,7 +45,7 @@ async function postDeleteMessage(message: MessagingMessage) {
); );
deliver(user, activity, recipient.inbox); deliver(user, activity, recipient.inbox);
} }
} else if (message.groupId) { } else if (message.groupId != null) {
publishGroupMessagingStream(message.groupId, "deleted", message.id); publishToGroupChatStream(message.groupId, ChatEvent.Deleted, message.id);
} }
} }

View file

@ -2,7 +2,7 @@ import { redisClient } from "@/db/redis.js";
import type { User } from "@/models/entities/user.js"; import type { User } from "@/models/entities/user.js";
import type { Note } from "@/models/entities/note.js"; import type { Note } from "@/models/entities/note.js";
import type { UserList } from "@/models/entities/user-list.js"; import type { UserList } from "@/models/entities/user-list.js";
import type { UserGroup } from "@/models/entities/user-group.js"; // import type { UserGroup } from "@/models/entities/user-group.js";
import { config } from "@/config.js"; import { config } from "@/config.js";
// import type { Antenna } from "@/models/entities/antenna.js"; // import type { Antenna } from "@/models/entities/antenna.js";
// import type { Channel } from "@/models/entities/channel.js"; // import type { Channel } from "@/models/entities/channel.js";
@ -13,7 +13,7 @@ import type {
// BroadcastTypes, // BroadcastTypes,
// ChannelStreamTypes, // ChannelStreamTypes,
DriveStreamTypes, DriveStreamTypes,
GroupMessagingStreamTypes, // GroupMessagingStreamTypes,
InternalStreamTypes, InternalStreamTypes,
MainStreamTypes, MainStreamTypes,
// MessagingIndexStreamTypes, // MessagingIndexStreamTypes,
@ -163,19 +163,20 @@ class Publisher {
// ); // );
// }; // };
public publishGroupMessagingStream = < /* ported to backend-rs */
K extends keyof GroupMessagingStreamTypes, // public publishGroupMessagingStream = <
>( // K extends keyof GroupMessagingStreamTypes,
groupId: UserGroup["id"], // >(
type: K, // groupId: UserGroup["id"],
value?: GroupMessagingStreamTypes[K], // type: K,
): void => { // value?: GroupMessagingStreamTypes[K],
this.publish( // ): void => {
`messagingStream:${groupId}`, // this.publish(
type, // `messagingStream:${groupId}`,
typeof value === "undefined" ? null : value, // type,
); // typeof value === "undefined" ? null : value,
}; // );
// };
/* ported to backend-rs */ /* ported to backend-rs */
// public publishMessagingIndexStream = < // public publishMessagingIndexStream = <
@ -225,7 +226,6 @@ export const publishNotesStream = publisher.publishNotesStream;
export const publishUserListStream = publisher.publishUserListStream; export const publishUserListStream = publisher.publishUserListStream;
// export const publishAntennaStream = publisher.publishAntennaStream; // export const publishAntennaStream = publisher.publishAntennaStream;
// export const publishMessagingStream = publisher.publishMessagingStream; // export const publishMessagingStream = publisher.publishMessagingStream;
export const publishGroupMessagingStream = // export const publishGroupMessagingStream = publisher.publishGroupMessagingStream;
publisher.publishGroupMessagingStream;
// export const publishMessagingIndexStream = publisher.publishMessagingIndexStream; // export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
// export const publishAdminStream = publisher.publishAdminStream; // export const publishAdminStream = publisher.publishAdminStream;

View file

@ -379,6 +379,7 @@ export function inputText(props: {
default?: string | null; default?: string | null;
minLength?: number; minLength?: number;
maxLength?: number; maxLength?: number;
isPlaintext?: boolean;
}): Promise< }): Promise<
| { canceled: true; result?: undefined } | { canceled: true; result?: undefined }
| { | {
@ -400,6 +401,7 @@ export function inputText(props: {
minLength: props.minLength, minLength: props.minLength,
maxLength: props.maxLength, maxLength: props.maxLength,
}, },
isPlaintext: props.isPlaintext,
}, },
{ {
done: (result) => { done: (result) => {

View file

@ -20,6 +20,8 @@ if (isSignedIn(me)) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: "question", type: "question",
text: i18n.ts.useThisAccountConfirm, text: i18n.ts.useThisAccountConfirm,
okText: i18n.ts.yes,
cancelText: i18n.ts.no,
}); });
// use the current account // use the current account
@ -32,6 +34,7 @@ if (isSignedIn(me)) {
// Otherwise ask the user what the other account ID is // Otherwise ask the user what the other account ID is
const remoteAccountId = await os.inputText({ const remoteAccountId = await os.inputText({
text: i18n.ts.inputAccountId, text: i18n.ts.inputAccountId,
isPlaintext: true,
}); });
// If the user do not want enter uri, the user will be redirected to the user page. // If the user do not want enter uri, the user will be redirected to the user page.