diff --git a/.config/example.yml b/.config/example.yml index 7e08651c51..2b2874a86e 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -184,6 +184,9 @@ reservedUsernames: [ # deliverJobMaxAttempts: 12 # inboxJobMaxAttempts: 8 +# Local address used for outgoing requests +#outgoingAddress: 127.0.0.1 + # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 diff --git a/Dockerfile b/Dockerfile index dca605a5bc..dbd34d18d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,5 +76,5 @@ COPY --from=build /firefish/packages/backend/native-utils/built /firefish/packag RUN corepack enable && corepack prepare pnpm@latest --activate ENV NODE_ENV=production VOLUME "/firefish/files" -ENTRYPOINT [ "/sbin/tini", "--" ] +ENTRYPOINT [ "/usr/bin/tini", "--" ] CMD [ "pnpm", "run", "migrateandstart" ] diff --git a/locales/de-DE.yml b/locales/de-DE.yml index fbfa23db56..d58896da84 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -2099,7 +2099,7 @@ customKaTeXMacro: Individuelle KaTeX Makros enableCustomKaTeXMacro: Individuelle KaTeX-Makros aktivieren replayTutorial: Wiederhole die Benutzeranleitung apps: Apps -caption: Automatische Untertitelung +caption: Automatische Beschreibung pwa: PWA installieren cw: Inhaltswarnung older: älter @@ -2204,3 +2204,7 @@ deletePasskeysConfirm: Alle Passkeys und Security-Keys werden unwiderruflich von inputNotMatch: Eingabe stimmt nicht überein addRe: Ein "re:" am Anfang des Kommentars hinzufügen, um einem Beitrag mit einer Inhaltswarnung zu antworten +confirm: Bestätigen +importZip: ZIP Importieren +emojiPackCreator: Emoji-Pack Ersteller +exportZip: ZIP Exportieren diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f266b1f820..43436b996d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -303,7 +303,7 @@ emptyDrive: "ドライブは空です" emptyFolder: "フォルダーは空です" unableToDelete: "削除できません" inputNewFileName: "新しいファイル名を入力してください" -inputNewDescription: "新しいキャプションを入力" +inputNewDescription: "新しい説明を入力" inputNewFolderName: "新しいフォルダ名を入力してください" circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーです。" hasChildFilesOrFolders: "このフォルダは空でないため、削除できません。" @@ -577,8 +577,8 @@ disablePlayer: "プレイヤーを閉じる" expandTweet: "ツイートを展開する" themeEditor: "テーマエディター" description: "説明" -describeFile: "キャプションを追加" -enterFileDescription: "キャプションを入力" +describeFile: "説明を追加" +enterFileDescription: "説明を入力" author: "作者" leaveConfirm: "未保存の変更があります。破棄しますか?" manage: "管理" @@ -949,7 +949,7 @@ customSplashIconsDescription: "ユーザがページをロード/リロードす showUpdates: "Firefishの更新時にポップアップを表示する" recommendedInstances: "おすすめサーバー" recommendedInstancesDescription: "おすすめタイムラインに表示するサーバーを改行区切りで入力してください。" -caption: "自動でキャプションをつける" +caption: "自動で説明をつける" splash: "スプラッシュスクリーン" updateAvailable: "アップデートがありますよ!" swipeOnDesktop: "デスクトップでモバイルスタイルのスワイプを可能にする" diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index 84f3bfa8f6..54361c0a67 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -85,6 +85,7 @@ export type Source = { fingerprint?: string; }; + outgoingAddress?: string; outgoingAddressFamily?: "ipv4" | "ipv6" | "dual"; deliverJobConcurrency?: number; diff --git a/packages/backend/src/misc/fetch.ts b/packages/backend/src/misc/fetch.ts index 0e673ba3a8..e47ef0d47a 100644 --- a/packages/backend/src/misc/fetch.ts +++ b/packages/backend/src/misc/fetch.ts @@ -99,6 +99,7 @@ const _http = new http.Agent({ keepAlive: true, keepAliveMsecs: 30 * 1000, lookup: cache.lookup, + localAddress: config.outgoingAddress, } as http.AgentOptions); /** @@ -108,6 +109,7 @@ const _https = new https.Agent({ keepAlive: true, keepAliveMsecs: 30 * 1000, lookup: cache.lookup, + localAddress: config.outgoingAddress, } as https.AgentOptions); const maxSockets = Math.max(256, config.deliverJobConcurrency || 128); @@ -123,6 +125,7 @@ export const httpAgent = config.proxy maxFreeSockets: 256, scheduling: "lifo", proxy: config.proxy, + localAddress: config.outgoingAddress, }) : _http; @@ -137,6 +140,7 @@ export const httpsAgent = config.proxy maxFreeSockets: 256, scheduling: "lifo", proxy: config.proxy, + localAddress: config.outgoingAddress, }) : _https; diff --git a/packages/backend/src/remote/activitypub/deliver-manager.ts b/packages/backend/src/remote/activitypub/deliver-manager.ts index 400e047774..c97d1c99ae 100644 --- a/packages/backend/src/remote/activitypub/deliver-manager.ts +++ b/packages/backend/src/remote/activitypub/deliver-manager.ts @@ -122,19 +122,31 @@ export default class DeliverManager { ) .forEach((recipe) => inboxes.add(recipe.to.inbox!)); + // Validate Inboxes first + const validInboxes = []; + for (const inbox of inboxes) { + try { + validInboxes.push({ + inbox, + host: new URL(inbox).host, + }); + } catch (error) { + console.error(error); + console.error(`Invalid Inbox ${inbox}`); + } + } + const instancesToSkip = await skippedInstances( // get (unique) list of hosts - Array.from( - new Set(Array.from(inboxes).map((inbox) => new URL(inbox).host)), - ), + Array.from(new Set(validInboxes.map((valid) => valid.host))), ); // deliver - for (const inbox of inboxes) { + for (const valid of validInboxes) { // skip instances as indicated - if (instancesToSkip.includes(new URL(inbox).host)) continue; + if (instancesToSkip.includes(valid.host)) continue; - deliver(this.actor, this.activity, inbox); + deliver(this.actor, this.activity, valid.inbox); } } } diff --git a/packages/backend/src/server/api/endpoints/admin/send-mod-mail.ts b/packages/backend/src/server/api/endpoints/admin/send-mod-mail.ts index f7a9ad7782..db12ab6c23 100644 --- a/packages/backend/src/server/api/endpoints/admin/send-mod-mail.ts +++ b/packages/backend/src/server/api/endpoints/admin/send-mod-mail.ts @@ -1,4 +1,4 @@ -import * as sanitizeHtml from "sanitize-html"; +import sanitizeHtml from "sanitize-html"; import define from "../../define.js"; import { Users, UserProfiles } from "@/models/index.js"; import { ApiError } from "../../error.js"; diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 44d3f9b500..1368f9e2b9 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -1,12 +1,14 @@ -import * as sanitizeHtml from "sanitize-html"; +import * as mfm from "mfm-js"; +import sanitizeHtml from "sanitize-html"; import { publishAdminStream } from "@/services/stream.js"; -import { AbuseUserReports, Users } from "@/models/index.js"; +import { AbuseUserReports, UserProfiles, Users } from "@/models/index.js"; import { genId } from "@/misc/gen-id.js"; import { sendEmail } from "@/services/send-email.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; import { getUser } from "../../common/getters.js"; import { ApiError } from "../../error.js"; import define from "../../define.js"; +import { toHtml } from "@/mfm/to-html.js"; export const meta = { tags: ["users"], @@ -84,6 +86,7 @@ export default define(meta, paramDef, async (ps, me) => { ], }); + const meta = await fetchMeta(); for (const moderator of moderators) { publishAdminStream(moderator.id, "newAbuseUserReport", { id: report.id, @@ -91,16 +94,16 @@ export default define(meta, paramDef, async (ps, me) => { reporterId: report.reporterId, comment: report.comment, }); - } - const meta = await fetchMeta(); - if (meta.email) { - sendEmail( - meta.email, - "New abuse report", - sanitizeHtml(ps.comment), - sanitizeHtml(ps.comment), - ); + const profile = await UserProfiles.findOneBy({ userId: moderator.id }); + if (profile?.email) { + sendEmail( + profile.email, + "New abuse report", + sanitizeHtml(toHtml(mfm.parse(ps.comment))!), + sanitizeHtml(toHtml(mfm.parse(ps.comment))!), + ); + } } }); }); diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts index 9084643795..6469d9c97b 100644 --- a/packages/backend/src/server/api/mastodon/converters.ts +++ b/packages/backend/src/server/api/mastodon/converters.ts @@ -74,3 +74,13 @@ export function convertStatus(status: Entity.Status) { return status; } + +export function convertConversation(conversation: Entity.Conversation) { + conversation.id = convertId(conversation.id, IdType.MastodonId); + conversation.accounts = conversation.accounts.map(convertAccount); + if (conversation.last_status) { + conversation.last_status = convertStatus(conversation.last_status); + } + + return conversation; +} diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index d54594327a..0a4da322d7 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -1,7 +1,12 @@ import Router from "@koa/router"; import { getClient } from "../ApiMastodonCompatibleService.js"; import { ParsedUrlQuery } from "querystring"; -import { convertAccount, convertList, convertStatus } from "../converters.js"; +import { + convertAccount, + convertConversation, + convertList, + convertStatus, +} from "../converters.js"; import { convertId, IdType } from "../../index.js"; export function limitToInt(q: ParsedUrlQuery) { @@ -136,7 +141,9 @@ export function apiTimelineMastodon(router: Router): void { const data = await client.getConversationTimeline( convertTimelinesArgsId(limitToInt(ctx.query)), ); - ctx.body = data.data; + ctx.body = data.data.map((conversation) => + convertConversation(conversation), + ); } catch (e: any) { console.error(e); console.error(e.response.data);