Compare commits

...

52 Commits

Author SHA1 Message Date
laozhoubuluo bad9aae2d0 Merge branch 'feat/post_import_export' into 'develop'
feat: import firefish renote and reply from export, import self-reply from mastodon export

Closes #9947, #10661, and #10807

See merge request firefish/firefish!10689
2024-05-16 05:25:58 +00:00
naskya 128fc72778 Merge branch 'renovate/lock-file-maintenance' into 'develop'
chore(deps): lock file maintenance

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10853
2024-05-16 05:17:04 +00:00
CI 310059f6a0 chore(deps): lock file maintenance 2024-05-16 04:05:56 +00:00
naskya 7d4d1c1fbd
fix merge mistake 2024-05-16 08:45:50 +09:00
naskya dbd205972f Merge branch 'refactor/push-notification' into 'develop'
refactor: port push notification sender to backend-rs


See merge request firefish/firefish!10760
2024-05-15 22:19:58 +00:00
naskya 41b32c5535 refactor (backend): port push notification sender to backend-rs 2024-05-15 22:19:58 +00:00
naskya 56be2f034e Merge branch 'renovate/syn-2.x' into 'develop'
chore(deps): update rust crate syn to 2.0.63

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10851
2024-05-15 21:22:39 +00:00
naskya e15bcee86c Merge branch 'renovate/aws-sdk-2.x' into 'develop'
fix(deps): update dependency aws-sdk to v2.1621.0

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10852
2024-05-15 21:19:48 +00:00
naskya 43326cdf8d Merge branch 'renovate/serde-monorepo' into 'develop'
chore(deps): update rust crate serde to 1.0.202

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10850
2024-05-15 21:16:55 +00:00
CI 7d1947792d fix(deps): update dependency aws-sdk to v2.1621.0 2024-05-15 21:05:33 +00:00
CI d28fe77d9f chore(deps): update rust crate syn to 2.0.63 2024-05-15 21:05:04 +00:00
CI acc13e9b10 chore(deps): update rust crate serde to 1.0.202 2024-05-15 21:04:59 +00:00
naskya 4e31e11f81
docs: use permalink 2024-05-16 05:04:47 +09:00
naskya dddd2779c0
chore: update auto-generated files 2024-05-16 04:57:48 +09:00
naskya 832fc7cd1d
docs: update changelog 2024-05-16 04:56:26 +09:00
naskya a18ad132be
fix: remove $[center] MFM function 2024-05-16 04:51:51 +09:00
naskya 4b96063c23
chore: format 2024-05-16 04:22:41 +09:00
naskya 0de54e02f8
chore (backend): use literals and consts 2024-05-16 04:22:23 +09:00
naskya 101e50926b
chore: remove import assertion 2024-05-16 04:12:10 +09:00
naskya 9cf88f0df6
chore: remove import assertion 2024-05-16 03:49:31 +09:00
naskya efb6cc9132 Merge branch 'renovate/execa-9.x' into 'develop'
chore(deps): update dependency execa to v9.1.0

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10848
2024-05-15 18:36:18 +00:00
Hosted Weblate 58f3eb4924
Merge branch 'origin/develop' into Weblate 2024-05-15 18:29:30 +00:00
Gary O'Regan Kelly 5adc0e581d
locale: update translations (French)
Currently translated at 100.0% (1932 of 1932 strings)

Translation: Firefish/locales
Translate-URL: https://hosted.weblate.org/projects/firefish/locales/fr/
2024-05-15 20:29:25 +02:00
naskya c0b760cda5 Merge branch 'develop' into 'renovate/execa-9.x'
# Conflicts:
#   package.json
2024-05-15 18:17:35 +00:00
naskya eb967564f9 Merge branch 'renovate/aws-sdk-2.x' into 'develop'
fix(deps): update dependency aws-sdk to v2.1620.0

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10849
2024-05-15 18:13:47 +00:00
naskya 0085105e72 Merge branch 'renovate/websocket-1.x' into 'develop'
fix(deps): update dependency websocket to v1.0.35

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10847
2024-05-15 18:09:16 +00:00
naskya 217b3ecf80 Merge branch 'renovate/is-svg-5.x' into 'develop'
fix(deps): update dependency is-svg to v5.0.1

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10846
2024-05-15 18:07:52 +00:00
naskya ffeeb3b444 Merge branch 'renovate/bull-4.x' into 'develop'
fix(deps): update dependency bull to v4.12.4

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10845
2024-05-15 18:06:29 +00:00
naskya 2f00947a24 Merge branch 'renovate/swc-monorepo' into 'develop'
chore(deps): update swc monorepo

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10844
2024-05-15 18:05:01 +00:00
naskya 5608129913 Merge branch 'renovate/syn-2.x-lockfile' into 'develop'
chore(deps): update rust crate syn to v2.0.63

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10843
2024-05-15 18:02:51 +00:00
naskya 8923e1f2a7 Merge branch 'renovate/serde-monorepo' into 'develop'
chore(deps): update rust crate serde to v1.0.202

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10842
2024-05-15 17:54:30 +00:00
naskya 8765e6ba54
ci: update renovate config 2024-05-16 02:42:59 +09:00
naskya 7c72738983 Merge branch 'renovate/pnpm-9.x' into 'develop'
chore(deps): update pnpm to v9.1.1

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10841
2024-05-15 17:40:54 +00:00
naskya ff446de7e8 Merge branch 'renovate/vue-tsc-2.x' into 'develop'
chore(deps): update dependency vue-tsc to v2.0.18

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10840
2024-05-15 17:36:25 +00:00
naskya 411d00a7af Merge branch 'renovate/vue-draggable-plus-0.x' into 'develop'
chore(deps): update dependency vue-draggable-plus to v0.4.1

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10839
2024-05-15 17:35:22 +00:00
CI 65a1fa870b fix(deps): update dependency aws-sdk to v2.1620.0 2024-05-15 17:29:54 +00:00
CI 1d25c78866 chore(deps): update dependency execa to v9.1.0 2024-05-15 17:29:35 +00:00
CI 6067eaef04 fix(deps): update dependency websocket to v1.0.35 2024-05-15 17:29:14 +00:00
CI 92299423a3 fix(deps): update dependency is-svg to v5.0.1 2024-05-15 17:28:54 +00:00
CI 65a8984c09 fix(deps): update dependency bull to v4.12.4 2024-05-15 17:28:34 +00:00
CI 99eb364778 chore(deps): update swc monorepo 2024-05-15 17:28:11 +00:00
CI 266c81df1e chore(deps): update rust crate syn to v2.0.63 2024-05-15 17:27:48 +00:00
CI 6a2e91efa1 chore(deps): update rust crate serde to v1.0.202 2024-05-15 17:27:42 +00:00
CI 17cbb9cd1e chore(deps): update pnpm to v9.1.1 2024-05-15 17:26:51 +00:00
CI d6ebb55556 chore(deps): update dependency vue-tsc to v2.0.18 2024-05-15 17:26:26 +00:00
CI 4dd1cff80b chore(deps): update dependency vue-draggable-plus to v0.4.1 2024-05-15 17:26:06 +00:00
naskya 752c6dc75b
ci: update renovate config 2024-05-16 02:24:06 +09:00
naskya cede0fdae2 Merge branch 'renovate/node-20.x' into 'develop'
chore(deps): update dependency @types/node to v20.12.12

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10837
2024-05-15 15:01:54 +00:00
naskya 35d706e45d Merge branch 'renovate/swiper-11.x' into 'develop'
chore(deps): update dependency swiper to v11.1.3

Co-authored-by: CI <project_7_bot_1bfaee5701aed20091a86249a967a6c1@noreply.firefish.dev>

See merge request firefish/firefish!10838
2024-05-15 15:01:26 +00:00
CI 9075050a67 chore(deps): update dependency swiper to v11.1.3 2024-05-15 10:05:12 +00:00
CI edc2a7d890 chore(deps): update dependency @types/node to v20.12.12 2024-05-15 10:04:53 +00:00
老周部落 469ca68e2e
feat: import firefish renote and reply from export, import self-reply from mastodon export 2024-05-12 09:42:19 +08:00
39 changed files with 1402 additions and 739 deletions

570
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -30,11 +30,11 @@ redis = "0.25.3"
regex = "1.10.4"
rmp-serde = "1.3.0"
sea-orm = "0.12.15"
serde = "1.0.201"
serde = "1.0.202"
serde_json = "1.0.117"
serde_yaml = "0.9.34"
strum = "0.26.2"
syn = "2.0.62"
syn = "2.0.63"
sysinfo = "0.30.12"
thiserror = "1.0.60"
tokio = "1.37.0"
@ -42,6 +42,7 @@ tracing = "0.1.40"
tracing-subscriber = "0.3.18"
url = "2.5.0"
urlencoding = "2.1.3"
web-push = { git = "https://github.com/pimeys/rust-web-push", rev = "40febe4085e3cef9cdfd539c315e3e945aba0656" }
[profile.release]
lto = true

View File

@ -7,7 +7,9 @@ Critical security updates are indicated by the :warning: icon.
## Unreleased
- Improve timeline UX
- Improve timeline UX (you can restore the original appearance by settings)
- Remove `$[center]` MFM function
- This function was suddenly added last year (https://firefish.dev/firefish/firefish/-/commit/1a971efa689323d54eebb4d3646e102fb4d1d95a), but according to the [MFM spec](https://github.com/misskey-dev/mfm.js/blob/6aaf68089023c6adebe44123eebbc4dcd75955e0/docs/syntax.md#fn), `$[something]` must be an inline element (while `center` is a block element), so such a syntax is not expected by MFM renderers. Please use `<center></center>` instead.
- Fix bugs
## [v20240504](https://firefish.dev/firefish/firefish/-/merge_requests/10790/commits)

View File

@ -2,6 +2,10 @@
You can skip intermediate versions when upgrading from an old version, but please read the notices and follow the instructions for each intermediate version before [upgrading](./upgrade.md).
## Unreleased
Firefish is now compatible with [Node v22](https://nodejs.org/en/blog/announcements/v22-release-announce).
## v20240430
### For all users

View File

@ -74,6 +74,34 @@ mentions: "Mentions"
directNotes: "Direct messages"
cw: "Content warning"
importAndExport: "Import/Export Data"
importAndExportWarn: "The Import/Export Data feature is an experimental feature and
implementation may change at any time without prior notice.\n
Due to differences in the exported data of different software versions, the actual
conditions of the import program, and the server health of the exported data link,
the imported data may be incomplete or the access permissions may not be set
correctly (for example, there is no access permission mark in the
Mastodon/Akkoma/Pleroma exported data, so all posts makes public after import),
so please be sure to check the imported data carefully integrity and configure
the correct access permissions for it."
importAndExportInfo: "Since some data cannot be obtained after the original account is
frozen or the original server goes offline, it is strongly recommendedthat you import
the data before the original account is frozen (migrated, logged out) or the original
server goes offline.\n
If the original account is frozen or the original server is offline but you have the
original images, you can try uploading them to the network disk before importing the
data, which may help with data import.\n
Since some data is obtained from its server using your current account when importing
data, data that the current account does not have permission to access will be regarded
as broken. Please make adjustments including but not limited to access permissions,
Manually following accounts and other methods allow the current account to obtain
relevant data, so that the import program can normally obtain the data it needs to
obtain to help you import.\n
Since it is impossible to confirm whether the broken link content posted by someone other
than you is posted by him/her, if there is broken link content posted by others in the
discussion thread, the related content and subsequent replies will not be imported.\n
Since data import is greatly affected by network communication, it is recommended that you
pay attention to data recovery after a period of time. If the data is still not restored,
you can try importing the same backup file again and try again."
import: "Import"
export: "Export"
files: "Files"

View File

@ -1142,8 +1142,8 @@ _wordMute:
mutedNotes: "Publications masquées"
muteLangsDescription2: Utiliser les codes de langue (i.e en, fr, ja, zh).
lang: Langue
langDescription: Cacher du fil de publication les publications qui correspondent
à ces langues.
langDescription: Cachez les publications qui correspondent à la langue définie dans
le fil d'actualité.
muteLangs: Langues filtrées
muteLangsDescription: Séparer avec des espaces ou des retours à la ligne pour une
condition OU (OR).
@ -1260,7 +1260,7 @@ _tutorial:
step2_2: "En fournissant quelques informations sur qui vous êtes, il sera plus facile
pour les autres de savoir s'ils veulent voir vos publcations ou s'abonner à vous."
step3_1: "Maintenant il est temps de vous abonner à des gens!"
step3_2: "Vos fils d'actualités Principal et Social sont basés sur les personnes
step3_2: "Vos fils d'actualité Principal et Social sont basés sur les personnes
que vous êtes abonné, alors essayez de vous abonner à quelques comptes pour commencer.\n
Cliquez sur le cercle « plus » en haut à droite d'un profil pour vous abonner."
step4_1: "On y va."
@ -2334,7 +2334,7 @@ copyRemoteFollowUrl: Copier l'URL d'abonnement à distance
slashQuote: Citation enchaînée
i18nServerInfo: Les nouveaux clients seront en {language} par défaut.
i18nServerChange: Utilisez {language} à la place.
i18nServerSet: Utilisez {langue} pour les nouveaux clients.
i18nServerSet: Utilisez {language} pour les nouveaux clients.
mergeThreadInTimeline: Fusionner plusieurs publications dans le même fil dans les
fils d'actualités
fils d'actualité
mergeRenotesInTimeline: Regrouper plusieurs boosts du même publication

View File

@ -61,6 +61,16 @@ mention: "提及"
mentions: "提及"
directNotes: "私信"
importAndExport: "导入 / 导出数据"
importAndExportWarn: "导入 / 导出数据功能是一项实验性功能,实现可能会随时变化而无预先通知。\n
由于不同软件不同版本的导出数据、导入程序实际情况以及导出数据链接的服务器运行状况不同,导入的数据可能会不完整或未被正确设置访问权限
(例如 Mastodon/Akkoma/Pleroma 导出数据内无访问权限标记,因此所有帖子导入后均为公开状态),因此请务必谨慎核对导入数据的完整性,
并为其配置正确的访问权限。"
importAndExportInfo: "由于原账号冻结或者原服务器下线后部分数据无法获取,因此强烈建议您在原账号冻结(迁移、注销)或原服务器下线前导入数据。\n
在原账号冻结或者原服务器下线但您拥有原始图片的情况下,可以尝试在导入数据之前将其上传到网盘上,可能对数据导入有所帮助。\n
由于导入数据时部分数据是使用您当前账号到其服务器上获取,因此当前账号无权访问的数据会视为断链。请通过包括但不限于访问权限调整、
手动关注账户等方式让当前帐号可以获取到相关数据,以便导入程序能够正常获取到需要获取的数据从而帮助您进行导入。\n
由于无法确认非您本人发表的断链内容的是否由其本人发表,因此如果讨论串内有其他人发表的断链内容,则相关内容以及后续回复不会被导入。\n
由于数据导入受网络通信影响较大,因此建议您一段时间之后再关注数据恢复情况。如果数据仍未恢复可以尝试再次导入同样的备份文件重试一次。"
import: "导入"
export: "导出"
files: "文件"

View File

@ -5,7 +5,7 @@
"type": "git",
"url": "https://firefish.dev/firefish/firefish.git"
},
"packageManager": "pnpm@9.1.0",
"packageManager": "pnpm@9.1.1",
"private": true,
"scripts": {
"rebuild": "pnpm run clean && pnpm run build",
@ -46,9 +46,9 @@
"@biomejs/cli-darwin-x64": "1.7.3",
"@biomejs/cli-linux-arm64": "1.7.3",
"@biomejs/cli-linux-x64": "1.7.3",
"@types/node": "20.12.11",
"execa": "9.0.2",
"pnpm": "9.1.0",
"@types/node": "20.12.12",
"execa": "9.1.0",
"pnpm": "9.1.1",
"typescript": "5.4.5"
}
}

View File

@ -46,6 +46,7 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }
web-push = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }

View File

@ -41,7 +41,6 @@ export interface ServerConfig {
proxySmtp?: string
proxyBypassHosts?: Array<string>
allowedPrivateNetworks?: Array<string>
/** `NapiValue` is not implemented for `u64` */
maxFileSize?: number
accessLog?: string
clusterLimits?: WorkerConfigInternal
@ -212,8 +211,8 @@ export interface Acct {
}
export function stringToAcct(acct: string): Acct
export function acctToString(acct: Acct): string
export function initializeRustLogger(): void
export function showServerInfo(): void
export function initializeRustLogger(): void
export function addNoteToAntenna(antennaId: string, note: Note): void
/**
* Checks if a server is blocked.
@ -237,7 +236,6 @@ export function isSilencedServer(host: string): Promise<boolean>
* `host` - punycoded instance host
*/
export function isAllowedServer(host: string): Promise<boolean>
/** TODO: handle name collisions better */
export interface NoteLikeForCheckWordMute {
fileIds: Array<string>
userId: string | null
@ -262,7 +260,6 @@ export interface ImageSize {
height: number
}
export function getImageSizeFromUrl(url: string): Promise<ImageSize>
/** TODO: handle name collisions better */
export interface NoteLikeForGetNoteSummary {
fileIds: Array<string>
text: string | null
@ -270,6 +267,28 @@ export interface NoteLikeForGetNoteSummary {
hasPoll: boolean
}
export function getNoteSummary(note: NoteLikeForGetNoteSummary): string
export interface Cpu {
model: string
cores: number
}
export interface Memory {
/** Total memory amount in bytes */
total: number
/** Used memory amount in bytes */
used: number
/** Available (for (re)use) memory amount in bytes */
available: number
}
export interface Storage {
/** Total storage space in bytes */
total: number
/** Used storage space in bytes */
used: number
}
export function cpuInfo(): Cpu
export function cpuUsage(): number
export function memoryUsage(): Memory
export function storageUsage(): Storage | null
export function isSafeUrl(url: string): boolean
export function latestVersion(): Promise<string>
export function toMastodonId(firefishId: string): string | null
@ -301,28 +320,6 @@ export function countReactions(reactions: Record<string, number>): Record<string
export function toDbReaction(reaction?: string | undefined | null, host?: string | undefined | null): Promise<string>
/** Delete all entries in the "attestation_challenge" table created at more than 5 minutes ago */
export function removeOldAttestationChallenges(): Promise<void>
export interface Cpu {
model: string
cores: number
}
export interface Memory {
/** Total memory amount in bytes */
total: number
/** Used memory amount in bytes */
used: number
/** Available (for (re)use) memory amount in bytes */
available: number
}
export interface Storage {
/** Total storage space in bytes */
total: number
/** Used storage space in bytes */
used: number
}
export function cpuInfo(): Cpu
export function cpuUsage(): number
export function memoryUsage(): Memory
export function storageUsage(): Storage | null
export interface AbuseUserReport {
id: string
createdAt: Date
@ -1282,6 +1279,15 @@ export interface Users {
}
export function watchNote(watcherId: string, noteAuthorId: string, noteId: string): Promise<void>
export function unwatchNote(watcherId: string, noteId: string): Promise<void>
export enum PushNotificationKind {
Generic = 'generic',
Chat = 'chat',
ReadAllChats = 'readAllChats',
ReadAllChatsInTheRoom = 'readAllChatsInTheRoom',
ReadNotifications = 'readNotifications',
ReadAllNotifications = 'readAllNotifications'
}
export function sendPushNotification(receiverUserId: string, kind: PushNotificationKind, content: any): Promise<void>
export function publishToChannelStream(channelId: string, userId: string): void
export enum ChatEvent {
Message = 'message',

View File

@ -310,7 +310,7 @@ if (!nativeBinding) {
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, initializeRustLogger, showServerInfo, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, showServerInfo, initializeRustLogger, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, cpuInfo, cpuUsage, memoryUsage, storageUsage, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding
module.exports.SECOND = SECOND
module.exports.MINUTE = MINUTE
@ -323,8 +323,8 @@ module.exports.loadEnv = loadEnv
module.exports.loadConfig = loadConfig
module.exports.stringToAcct = stringToAcct
module.exports.acctToString = acctToString
module.exports.initializeRustLogger = initializeRustLogger
module.exports.showServerInfo = showServerInfo
module.exports.initializeRustLogger = initializeRustLogger
module.exports.addNoteToAntenna = addNoteToAntenna
module.exports.isBlockedServer = isBlockedServer
module.exports.isSilencedServer = isSilencedServer
@ -341,6 +341,10 @@ module.exports.safeForSql = safeForSql
module.exports.formatMilliseconds = formatMilliseconds
module.exports.getImageSizeFromUrl = getImageSizeFromUrl
module.exports.getNoteSummary = getNoteSummary
module.exports.cpuInfo = cpuInfo
module.exports.cpuUsage = cpuUsage
module.exports.memoryUsage = memoryUsage
module.exports.storageUsage = storageUsage
module.exports.isSafeUrl = isSafeUrl
module.exports.latestVersion = latestVersion
module.exports.toMastodonId = toMastodonId
@ -355,10 +359,6 @@ module.exports.decodeReaction = decodeReaction
module.exports.countReactions = countReactions
module.exports.toDbReaction = toDbReaction
module.exports.removeOldAttestationChallenges = removeOldAttestationChallenges
module.exports.cpuInfo = cpuInfo
module.exports.cpuUsage = cpuUsage
module.exports.memoryUsage = memoryUsage
module.exports.storageUsage = storageUsage
module.exports.AntennaSrcEnum = AntennaSrcEnum
module.exports.DriveFileUsageHintEnum = DriveFileUsageHintEnum
module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
@ -378,6 +378,8 @@ module.exports.Inbound = Inbound
module.exports.Outbound = Outbound
module.exports.watchNote = watchNote
module.exports.unwatchNote = unwatchNote
module.exports.PushNotificationKind = PushNotificationKind
module.exports.sendPushNotification = sendPushNotification
module.exports.publishToChannelStream = publishToChannelStream
module.exports.ChatEvent = ChatEvent
module.exports.publishToChatStream = publishToChatStream

View File

@ -1,4 +1,8 @@
use serde::{Deserialize, Serialize};
// TODO: handle name collisions in a better way
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[crate::export(object, js_name = "NoteLikeForGetNoteSummary")]
pub struct NoteLike {
pub file_ids: Vec<String>,

View File

@ -1,3 +1,4 @@
pub mod nodeinfo;
pub mod note;
pub mod push_notification;
pub mod stream;

View File

@ -0,0 +1,232 @@
use crate::database::db_conn;
use crate::misc::get_note_summary::{get_note_summary, NoteLike};
use crate::misc::meta::fetch_meta;
use crate::model::entity::sw_subscription;
use crate::util::http_client;
use once_cell::sync::OnceCell;
use sea_orm::{prelude::*, DbErr};
use web_push::{
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
WebPushClient, WebPushError, WebPushMessageBuilder,
};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Database error: {0}")]
DbErr(#[from] DbErr),
#[error("Web Push error: {0}")]
WebPushErr(#[from] WebPushError),
#[error("Failed to (de)serialize an object: {0}")]
SerializeErr(#[from] serde_json::Error),
#[error("Invalid content: {0}")]
InvalidContentErr(String),
#[error("HTTP client aquisition error: {0}")]
HttpClientErr(#[from] http_client::Error),
}
static CLIENT: OnceCell<IsahcWebPushClient> = OnceCell::new();
fn get_client() -> Result<IsahcWebPushClient, Error> {
Ok(CLIENT
.get_or_try_init(|| http_client::client().map(IsahcWebPushClient::from))
.cloned()?)
}
#[derive(strum::Display, PartialEq)]
#[crate::export(string_enum = "camelCase")]
pub enum PushNotificationKind {
#[strum(serialize = "notification")]
Generic,
#[strum(serialize = "unreadMessagingMessage")]
Chat,
#[strum(serialize = "readAllMessagingMessages")]
ReadAllChats,
#[strum(serialize = "readAllMessagingMessagesOfARoom")]
ReadAllChatsInTheRoom,
#[strum(serialize = "readNotifications")]
ReadNotifications,
#[strum(serialize = "readAllNotifications")]
ReadAllNotifications,
}
fn compact_content(
kind: &PushNotificationKind,
mut content: serde_json::Value,
) -> Result<serde_json::Value, Error> {
if kind != &PushNotificationKind::Generic {
return Ok(content);
}
if !content.is_object() {
return Err(Error::InvalidContentErr("not a JSON object".to_string()));
}
let object = content.as_object_mut().unwrap();
if !object.contains_key("note") {
return Ok(content);
}
let mut note = if object.contains_key("type") && object.get("type").unwrap() == "renote" {
object
.get("note")
.unwrap()
.get("renote")
.ok_or(Error::InvalidContentErr(
"renote object is missing".to_string(),
))?
} else {
object.get("note").unwrap()
}
.clone();
if !note.is_object() {
return Err(Error::InvalidContentErr(
"(re)note is not an object".to_string(),
));
}
let note_like: NoteLike = serde_json::from_value(note.clone())?;
let text = get_note_summary(note_like);
let note_object = note.as_object_mut().unwrap();
note_object.remove("reply");
note_object.remove("renote");
note_object.remove("user");
note_object.insert("text".to_string(), text.into());
object.insert("note".to_string(), note);
Ok(serde_json::from_value(Json::Object(object.clone()))?)
}
async fn handle_web_push_failure(
db: &DatabaseConnection,
err: WebPushError,
subscription_id: &str,
error_message: &str,
) -> Result<(), DbErr> {
match err {
WebPushError::BadRequest(_)
| WebPushError::ServerError(_)
| WebPushError::InvalidUri
| WebPushError::EndpointNotValid
| WebPushError::EndpointNotFound
| WebPushError::TlsError
| WebPushError::SslError
| WebPushError::InvalidPackageName
| WebPushError::MissingCryptoKeys
| WebPushError::InvalidCryptoKeys
| WebPushError::InvalidResponse => {
sw_subscription::Entity::delete_by_id(subscription_id)
.exec(db)
.await?;
tracing::info!("{}; {} was unsubscribed", error_message, subscription_id);
tracing::debug!("reason: {:#?}", err);
}
_ => {
tracing::warn!("{}; subscription id: {}", error_message, subscription_id);
tracing::info!("reason: {:#?}", err);
}
};
Ok(())
}
#[crate::export]
pub async fn send_push_notification(
receiver_user_id: &str,
kind: PushNotificationKind,
content: &serde_json::Value,
) -> Result<(), Error> {
let meta = fetch_meta(true).await?;
if !meta.enable_service_worker || meta.sw_public_key.is_none() || meta.sw_private_key.is_none()
{
return Ok(());
}
let db = db_conn().await?;
let signature_builder = VapidSignatureBuilder::from_base64_no_sub(
meta.sw_private_key.unwrap().as_str(),
web_push::URL_SAFE_NO_PAD,
)?;
let subscriptions = sw_subscription::Entity::find()
.filter(sw_subscription::Column::UserId.eq(receiver_user_id))
.all(db)
.await?;
let payload = format!(
"{{\"type\":\"{}\",\"userId\":\"{}\",\"dateTime\":{},\"body\":{}}}",
kind,
receiver_user_id,
chrono::Utc::now().timestamp_millis(),
serde_json::to_string(&compact_content(&kind, content.clone())?)?
);
tracing::trace!("payload: {:#?}", payload);
for subscription in subscriptions.iter() {
if !subscription.send_read_message
&& [
PushNotificationKind::ReadAllChats,
PushNotificationKind::ReadAllChatsInTheRoom,
PushNotificationKind::ReadAllNotifications,
PushNotificationKind::ReadNotifications,
]
.contains(&kind)
{
continue;
}
let subscription_info = SubscriptionInfo {
endpoint: subscription.endpoint.to_owned(),
keys: SubscriptionKeys {
// convert standard base64 into base64url
// https://en.wikipedia.org/wiki/Base64#Variants_summary_table
p256dh: subscription
.publickey
.replace('+', "-")
.replace('/', "_")
.to_owned(),
auth: subscription
.auth
.replace('+', "-")
.replace('/', "_")
.to_owned(),
},
};
let signature = signature_builder
.clone()
.add_sub_info(&subscription_info)
.build();
if let Err(err) = signature {
handle_web_push_failure(db, err, &subscription.id, "failed to build a signature")
.await?;
continue;
}
let mut message_builder = WebPushMessageBuilder::new(&subscription_info);
message_builder.set_ttl(1000);
message_builder.set_payload(ContentEncoding::Aes128Gcm, payload.as_bytes());
message_builder.set_vapid_signature(signature.unwrap());
let message = message_builder.build();
if let Err(err) = message {
handle_web_push_failure(db, err, &subscription.id, "failed to build a payload").await?;
continue;
}
if let Err(err) = get_client()?.send(message.unwrap()).await {
handle_web_push_failure(db, err, &subscription.id, "failed to send").await?;
continue;
}
tracing::debug!("success; subscription id: {}", subscription.id);
}
Ok(())
}

View File

@ -36,11 +36,11 @@
"adm-zip": "0.5.10",
"ajv": "8.13.0",
"archiver": "7.0.1",
"aws-sdk": "2.1618.0",
"aws-sdk": "2.1621.0",
"axios": "1.6.8",
"backend-rs": "workspace:*",
"blurhash": "2.0.5",
"bull": "4.12.3",
"bull": "4.12.4",
"cacheable-lookup": "TheEssem/cacheable-lookup",
"cbor-x": "1.5.9",
"chalk": "5.3.0",
@ -62,7 +62,7 @@
"hpagent": "1.2.0",
"ioredis": "5.4.1",
"ip-cidr": "4.0.0",
"is-svg": "5.0.0",
"is-svg": "5.0.1",
"jsdom": "24.0.0",
"json5": "2.2.3",
"jsonld": "8.3.2",
@ -117,13 +117,12 @@
"typeorm": "0.3.20",
"ulid": "2.3.0",
"uuid": "9.0.1",
"web-push": "3.6.7",
"websocket": "1.0.34",
"websocket": "1.0.35",
"xev": "3.0.2"
},
"devDependencies": {
"@swc/cli": "0.3.12",
"@swc/core": "1.5.5",
"@swc/core": "1.5.7",
"@types/adm-zip": "0.5.5",
"@types/color-convert": "2.0.3",
"@types/content-disposition": "0.5.8",
@ -144,7 +143,7 @@
"@types/koa__multer": "2.0.7",
"@types/koa__router": "12.0.4",
"@types/mocha": "10.0.6",
"@types/node": "20.12.11",
"@types/node": "20.12.12",
"@types/node-fetch": "2.6.11",
"@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.4",

View File

@ -335,6 +335,7 @@ export function createImportMastoPostJob(
user: ThinUser,
post: any,
signatureCheck: boolean,
parent: Note | null = null,
) {
return dbQueue.add(
"importMastoPost",
@ -342,6 +343,7 @@ export function createImportMastoPostJob(
user: user,
post: post,
signatureCheck: signatureCheck,
parent: parent,
},
{
removeOnComplete: true,

View File

@ -11,6 +11,7 @@ import type { Poll } from "@/models/entities/poll.js";
import type { DbUserJobData } from "@/queue/types.js";
import { createTemp } from "@/misc/create-temp.js";
import { inspect } from "node:util";
import { config } from "@/config.js";
const logger = queueLogger.createSubLogger("export-notes");
@ -131,5 +132,6 @@ async function serialize(
visibility: note.visibility,
visibleUserIds: note.visibleUserIds,
localOnly: note.localOnly,
objectUrl: `${config.url}/notes/${note.id}`,
};
}

View File

@ -1,12 +1,14 @@
import * as Post from "@/misc/post.js";
import create from "@/services/note/create.js";
import { NoteFiles, Users } from "@/models/index.js";
import Resolver from "@/remote/activitypub/resolver.js";
import { DriveFiles, NoteFiles, Users } from "@/models/index.js";
import type { DbUserImportMastoPostJobData } from "@/queue/types.js";
import { queueLogger } from "../../logger.js";
import { uploadFromUrl } from "@/services/drive/upload-from-url.js";
import type { DriveFile } from "@/models/entities/drive-file.js";
import type Bull from "bull";
import { createImportCkPostJob } from "@/queue/index.js";
import { resolveNote } from "@/remote/activitypub/models/note.js";
import { Notes, NoteEdits } from "@/models/index.js";
import type { Note } from "@/models/entities/note.js";
import { genId } from "backend-rs";
@ -23,20 +25,37 @@ export async function importCkPost(
return;
}
const post = job.data.post;
/*
if (post.replyId != null) {
done();
return;
const parent = job.data.parent;
const isRenote = post.renoteId !== null;
let reply: Note | null = null;
let renote: Note | null = null;
job.progress(20);
if (!isRenote && post.replyId !== null) {
if (
!parent &&
typeof post.objectUrl !== "undefined" &&
post.objectUrl !== null
) {
const resolver = new Resolver();
const originalNote = await resolver.resolve(post.objectUrl);
reply = await resolveNote(originalNote.inReplyTo);
} else {
reply = post.replyId !== null ? parent : null;
}
}
if (post.renoteId != null) {
done();
return;
// renote also need resolve original note
if (
isRenote &&
!parent &&
typeof post.objectUrl !== "undefined" &&
post.objectUrl !== null
) {
const resolver = new Resolver();
const originalNote = await resolver.resolve(post.objectUrl);
renote = await resolveNote(originalNote.quoteUrl);
} else {
renote = isRenote ? parent : null;
}
if (post.visibility !== "public") {
done();
return;
}
*/
const urls = (post.files || [])
.map((x: any) => x.url)
.filter((x: String) => x.startsWith("http"));
@ -49,7 +68,17 @@ export async function importCkPost(
});
files.push(file);
} catch (e) {
logger.info(`Skipped adding file to drive: ${url}`);
// try to get the same md5 file from user drive
const md5 = post.files.map((x: any) => x.url).find(url).md5;
const much = await DriveFiles.findOneBy({
md5: md5,
userId: user.id,
});
if (much) {
files.push(much);
} else {
logger.info(`Skipped adding file to drive: ${url}`);
}
}
}
const { text, cw, localOnly, createdAt, visibility } = Post.parse(post);
@ -88,8 +117,8 @@ export async function importCkPost(
files: files.length === 0 ? undefined : files,
poll: undefined,
text: text || undefined,
reply: post.replyId ? job.data.parent : null,
renote: post.renoteId ? job.data.parent : null,
reply,
renote,
cw: cw,
localOnly,
visibility: visibility,

View File

@ -10,6 +10,7 @@ import type { DriveFile } from "@/models/entities/drive-file.js";
import { Notes, NoteEdits } from "@/models/index.js";
import type { Note } from "@/models/entities/note.js";
import { genId } from "backend-rs";
import { createImportMastoPostJob } from "@/queue/index.js";
const logger = queueLogger.createSubLogger("import-masto-post");
@ -23,12 +24,17 @@ export async function importMastoPost(
return;
}
const post = job.data.post;
const parent = job.data.parent;
const isRenote = post.type === "Announce";
let reply: Note | null = null;
let renote: Note | null = null;
job.progress(20);
if (!isRenote && post.object.inReplyTo != null) {
reply = await resolveNote(post.object.inReplyTo);
if (parent == null) {
reply = await resolveNote(post.object.inReplyTo);
} else {
reply = parent;
}
}
// renote also need resolve original note
if (isRenote) {
@ -135,4 +141,14 @@ export async function importMastoPost(
done();
logger.info("Imported");
if (post.childNotes) {
for (const child of post.childNotes) {
createImportMastoPostJob(
job.data.user,
child,
job.data.signatureCheck,
note,
);
}
}
}

View File

@ -40,7 +40,10 @@ export async function importPosts(
file.url,
job.data.user.id,
);
for (const post of outbox.orderedItems) {
logger.info("Parsing mastodon style posts");
const arr = recreateChainForMastodon(outbox.orderedItems);
logger.debug(JSON.stringify(arr, null, 2));
for (const post of arr) {
createImportMastoPostJob(job.data.user, post, job.data.signatureCheck);
}
} catch (e) {
@ -60,12 +63,15 @@ export async function importPosts(
if (Array.isArray(parsed)) {
logger.info("Parsing *key posts");
const arr = recreateChain(parsed);
logger.debug(JSON.stringify(arr, null, 2));
for (const post of arr) {
createImportCkPostJob(job.data.user, post, job.data.signatureCheck);
}
} else if (parsed instanceof Object) {
logger.info("Parsing Mastodon posts");
for (const post of parsed.orderedItems) {
const arr = recreateChainForMastodon(parsed.orderedItems);
logger.debug(JSON.stringify(arr, null, 2));
for (const post of arr) {
createImportMastoPostJob(job.data.user, post, job.data.signatureCheck);
}
}
@ -96,9 +102,56 @@ function recreateChain(arr: any[]): any {
let parent = null;
if (note.replyId != null) {
parent = lookup[`${note.replyId}`];
// Accept URL, let import process to resolveNote
if (
!parent &&
typeof note.objectUrl !== "undefined" &&
note.objectUrl.startsWith("http")
) {
notesTree.push(note);
}
}
if (note.renoteId != null) {
parent = lookup[`${note.renoteId}`];
// Accept URL, let import process to resolveNote
if (
!parent &&
typeof note.objectUrl !== "undefined" &&
note.objectUrl.startsWith("http")
) {
notesTree.push(note);
}
}
if (parent) {
parent.childNotes.push(note);
}
}
return notesTree;
}
function recreateChainForMastodon(arr: any[]): any {
type NotesMap = {
[id: string]: any;
};
const notesTree: any[] = [];
const lookup: NotesMap = {};
for (const note of arr) {
lookup[`${note.id}`] = note;
note.childNotes = [];
if (note.object.inReplyTo == null) {
notesTree.push(note);
}
}
for (const note of arr) {
let parent = null;
if (note.object.inReplyTo != null) {
const inReplyToIdForLookup = `${note.object.inReplyTo}/activity`;
parent = lookup[`${inReplyToIdForLookup}`];
// Accept URL, let import process to resolveNote
if (!parent && note.object.inReplyTo.startsWith("http")) {
notesTree.push(note);
}
}
if (parent) {

View File

@ -3,10 +3,11 @@ import {
publishToChatStream,
publishToGroupChatStream,
publishToChatIndexStream,
sendPushNotification,
ChatEvent,
ChatIndexEvent,
PushNotificationKind,
} from "backend-rs";
import { pushNotification } from "@/services/push-notification.js";
import type { User, IRemoteUser } from "@/models/entities/user.js";
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
import { MessagingMessages, UserGroupJoinings, Users } from "@/models/index.js";
@ -62,20 +63,19 @@ export async function readUserMessagingMessage(
if (!(await Users.getHasUnreadMessagingMessage(userId))) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishMainStream(userId, "readAllMessagingMessages");
pushNotification(userId, "readAllMessagingMessages", undefined);
sendPushNotification(userId, PushNotificationKind.ReadAllChats, {});
} else {
// そのユーザーとのメッセージで未読がなければイベント発行
const count = await MessagingMessages.count({
const hasUnread = await MessagingMessages.exists({
where: {
userId: otherpartyId,
recipientId: userId,
isRead: false,
},
take: 1,
});
if (!count) {
pushNotification(userId, "readAllMessagingMessagesOfARoom", {
if (!hasUnread) {
sendPushNotification(userId, PushNotificationKind.ReadAllChatsInTheRoom, {
userId: otherpartyId,
});
}
@ -137,10 +137,10 @@ export async function readGroupMessagingMessage(
if (!(await Users.getHasUnreadMessagingMessage(userId))) {
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
publishMainStream(userId, "readAllMessagingMessages");
pushNotification(userId, "readAllMessagingMessages", undefined);
sendPushNotification(userId, PushNotificationKind.ReadAllChats, {});
} else {
// そのグループにおいて未読がなければイベント発行
const unreadExist = await MessagingMessages.createQueryBuilder("message")
const hasUnread = await MessagingMessages.createQueryBuilder("message")
.where("message.groupId = :groupId", { groupId: groupId })
.andWhere("message.userId != :userId", { userId: userId })
.andWhere("NOT (:userId = ANY(message.reads))", { userId: userId })
@ -150,8 +150,10 @@ export async function readGroupMessagingMessage(
.getOne()
.then((x) => x != null);
if (!unreadExist) {
pushNotification(userId, "readAllMessagingMessagesOfARoom", { groupId });
if (!hasUnread) {
sendPushNotification(userId, PushNotificationKind.ReadAllChatsInTheRoom, {
groupId,
});
}
}
}

View File

@ -1,6 +1,6 @@
import { In } from "typeorm";
import { publishMainStream } from "@/services/stream.js";
import { pushNotification } from "@/services/push-notification.js";
import { sendPushNotification, PushNotificationKind } from "backend-rs";
import type { User } from "@/models/entities/user.js";
import type { Notification } from "@/models/entities/notification.js";
import { Notifications, Users } from "@/models/index.js";
@ -47,7 +47,11 @@ export async function readNotificationByQuery(
function postReadAllNotifications(userId: User["id"]) {
publishMainStream(userId, "readAllNotifications");
return pushNotification(userId, "readAllNotifications", undefined);
return sendPushNotification(
userId,
PushNotificationKind.ReadAllNotifications,
{},
);
}
function postReadNotifications(
@ -55,5 +59,7 @@ function postReadNotifications(
notificationIds: Notification["id"][],
) {
publishMainStream(userId, "readNotifications", notificationIds);
return pushNotification(userId, "readNotifications", { notificationIds });
return sendPushNotification(userId, PushNotificationKind.ReadNotifications, {
notificationIds,
});
}

View File

@ -2,11 +2,7 @@ import * as os from "node:os";
import define from "@/server/api/define.js";
import { redisClient } from "@/db/redis.js";
import { db } from "@/db/postgre.js";
import {
cpuInfo,
memoryUsage,
storageUsage,
} from "backend-rs";
import { cpuInfo, memoryUsage, storageUsage } from "backend-rs";
export const meta = {
requireCredential: true,

View File

@ -1,5 +1,5 @@
import { publishMainStream } from "@/services/stream.js";
import { pushNotification } from "@/services/push-notification.js";
import { sendPushNotification, PushNotificationKind } from "backend-rs";
import { Notifications } from "@/models/index.js";
import define from "@/server/api/define.js";
@ -17,7 +17,7 @@ export const paramDef = {
required: [],
} as const;
export default define(meta, paramDef, async (ps, user) => {
export default define(meta, paramDef, async (_, user) => {
// Update documents
await Notifications.update(
{
@ -31,5 +31,5 @@ export default define(meta, paramDef, async (ps, user) => {
// 全ての通知を読みましたよというイベントを発行
publishMainStream(user.id, "readAllNotifications");
pushNotification(user.id, "readAllNotifications", undefined);
sendPushNotification(user.id, PushNotificationKind.ReadAllNotifications, {});
});

View File

@ -149,7 +149,9 @@ router.get<{ Params: { path: string } }>("/emoji/:path(.*)", async (ctx) => {
return;
}
let url = new URL(`${config.mediaProxy || config.url + "/proxy"}/emoji.webp`);
const url = new URL(
`${config.mediaProxy || `${config.url}/proxy`}/emoji.webp`,
);
// || emoji.originalUrl してるのは後方互換性のため
url.searchParams.append("url", emoji.publicUrl || emoji.originalUrl);
url.searchParams.append("emoji", "1");
@ -370,9 +372,8 @@ const getFeed = async (
};
// As the /@user[.json|.rss|.atom]/sub endpoint is complicated, we will use a regex to switch between them.
const reUser = new RegExp(
"^/@(?<user>[^/]+?)(?:.(?<feed>json|rss|atom)(?:\\?[^/]*)?)?(?:/(?<sub>[^/]+))?$",
);
const reUser =
/^\/@(?<user>[^\/]+?)(?:.(?<feed>json|rss|atom)(?:\?[^\/]*)?)?(?:\/(?<sub>[^\/]+))?$/;
router.get(reUser, async (ctx, next) => {
const groups = reUser.exec(ctx.originalUrl)?.groups;
if (!groups) {

View File

@ -1,73 +0,0 @@
{
"short_name": "Firefish",
"name": "Firefish",
"description": "An open source, decentralized social media platform that's free forever!",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#1f1d2e",
"theme_color": "#31748f",
"orientation": "natural",
"icons": [
{
"src": "/static-assets/icons/192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static-assets/icons/512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static-assets/icons/maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static-assets/icons/monochrome.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "monochrome"
}
],
"share_target": {
"action": "/share/",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
},
"screenshots": [
{
"src": "/static-assets/screenshots/1.webp",
"sizes": "1080x2340",
"type": "image/webp",
"platform": "narrow",
"label": "Profile page"
},
{
"src": "/static-assets/screenshots/2.webp",
"sizes": "1080x2340",
"type": "image/webp",
"platform": "narrow",
"label": "Posts"
}
],
"shortcuts": [
{
"name": "Notifications",
"short_name": "Notifs",
"url": "/my/notifications"
},
{
"name": "Chats",
"url": "/my/messaging"
}
],
"categories": ["social"]
}

View File

@ -1,27 +1,97 @@
import type Koa from "koa";
import { fetchMeta } from "backend-rs";
import { config } from "@/config.js";
import manifest from "./manifest.json" assert { type: "json" };
const manifest = {
short_name: "Firefish",
name: "Firefish",
description:
"An open source, decentralized social media platform that's free forever!",
start_url: "/",
scope: "/",
display: "standalone",
background_color: "#1f1d2e",
theme_color: "#31748f",
orientation: "natural",
icons: [
{
src: "/static-assets/icons/192.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "/static-assets/icons/512.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
{
src: "/static-assets/icons/maskable.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
{
src: "/static-assets/icons/monochrome.png",
sizes: "512x512",
type: "image/png",
purpose: "monochrome",
},
],
share_target: {
action: "/share/",
params: {
title: "title",
text: "text",
url: "url",
},
},
screenshots: [
{
src: "/static-assets/screenshots/1.webp",
sizes: "1080x2340",
type: "image/webp",
platform: "narrow",
label: "Profile page",
},
{
src: "/static-assets/screenshots/2.webp",
sizes: "1080x2340",
type: "image/webp",
platform: "narrow",
label: "Posts",
},
],
shortcuts: [
{
name: "Notifications",
short_name: "Notifs",
url: "/my/notifications",
},
{
name: "Chats",
url: "/my/messaging",
},
],
categories: ["social"],
};
export const manifestHandler = async (ctx: Koa.Context) => {
// TODO
//const res = structuredClone(manifest);
const res = JSON.parse(JSON.stringify(manifest));
const instance = await fetchMeta(true);
const instance = await fetchMeta(false);
res.short_name = instance.name || "Firefish";
res.name = instance.name || "Firefish";
if (instance.themeColor) res.theme_color = instance.themeColor;
for (const icon of res.icons) {
manifest.short_name = instance.name || "Firefish";
manifest.name = instance.name || "Firefish";
if (instance.themeColor) manifest.theme_color = instance.themeColor;
for (const icon of manifest.icons) {
icon.src = `${icon.src}?v=${config.version.replace(/[^0-9]/g, "")}`;
}
for (const screenshot of res.screenshots) {
for (const screenshot of manifest.screenshots) {
screenshot.src = `${screenshot.src}?v=${config.version.replace(
/[^0-9]/g,
"",
)}`;
}
ctx.set("Cache-Control", "max-age=300");
ctx.body = res;
ctx.body = manifest;
};

View File

@ -1,5 +1,4 @@
import { publishMainStream } from "@/services/stream.js";
import { pushNotification } from "@/services/push-notification.js";
import {
Notifications,
Mutings,
@ -8,7 +7,12 @@ import {
Users,
Followings,
} from "@/models/index.js";
import { genId, isSilencedServer } from "backend-rs";
import {
genId,
isSilencedServer,
sendPushNotification,
PushNotificationKind,
} from "backend-rs";
import type { User } from "@/models/entities/user.js";
import type { Notification } from "@/models/entities/notification.js";
import { sendEmailNotification } from "./send-email-notification.js";
@ -81,7 +85,7 @@ export async function createNotification(
if (fresh == null) return; // 既に削除されているかもしれない
// We execute this before, because the server side "read" check doesnt work well with push notifications, the app and service worker will decide themself
// when it is best to show push notifications
pushNotification(notifieeId, "notification", packed);
sendPushNotification(notifieeId, PushNotificationKind.Generic, packed);
if (fresh.isRead) return;
//#region ただしミュートしているユーザーからの通知なら無視

View File

@ -9,16 +9,17 @@ import {
} from "@/models/index.js";
import {
genId,
sendPushNotification,
publishToChatStream,
publishToGroupChatStream,
publishToChatIndexStream,
toPuny,
ChatEvent,
ChatIndexEvent,
PushNotificationKind,
} from "backend-rs";
import type { MessagingMessage } from "@/models/entities/messaging-message.js";
import { publishMainStream } from "@/services/stream.js";
import { pushNotification } from "@/services/push-notification.js";
import { Not } from "typeorm";
import type { Note } from "@/models/entities/note.js";
import renderNote from "@/remote/activitypub/renderer/note.js";
@ -118,7 +119,11 @@ export async function createMessage(
//#endregion
publishMainStream(recipientUser.id, "unreadMessagingMessage", messageObj);
pushNotification(recipientUser.id, "unreadMessagingMessage", messageObj);
sendPushNotification(
recipientUser.id,
PushNotificationKind.Chat,
messageObj,
);
} else if (recipientGroup) {
const joinings = await UserGroupJoinings.findBy({
userGroupId: recipientGroup.id,
@ -127,7 +132,11 @@ export async function createMessage(
for (const joining of joinings) {
if (freshMessage.reads.includes(joining.userId)) return; // 既読
publishMainStream(joining.userId, "unreadMessagingMessage", messageObj);
pushNotification(joining.userId, "unreadMessagingMessage", messageObj);
sendPushNotification(
joining.userId,
PushNotificationKind.Chat,
messageObj,
);
}
}
}, 2000);

View File

@ -1,115 +0,0 @@
import push from "web-push";
import { config } from "@/config.js";
import { SwSubscriptions } from "@/models/index.js";
import { fetchMeta, getNoteSummary } from "backend-rs";
import type { Packed } from "@/misc/schema.js";
// Defined also packages/sw/types.ts#L14-L21
type pushNotificationsTypes = {
notification: Packed<"Notification">;
unreadMessagingMessage: Packed<"MessagingMessage">;
readNotifications: { notificationIds: string[] };
readAllNotifications: undefined;
readAllMessagingMessages: undefined;
readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string };
};
// プッシュメッセージサーバーには文字数制限があるため、内容を削減します
function truncateNotification(notification: Packed<"Notification">): any {
if (notification.note != null) {
return {
...notification,
note: {
...notification.note,
// replace the text with summary
text: getNoteSummary(
notification.type === "renote" && notification.note.renote != null
? notification.note.renote
: notification.note,
),
cw: undefined,
reply: undefined,
renote: undefined,
user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる
},
};
}
return notification;
}
export async function pushNotification<T extends keyof pushNotificationsTypes>(
userId: string,
type: T,
body: pushNotificationsTypes[T],
) {
const meta = await fetchMeta(true);
if (
!meta.enableServiceWorker ||
meta.swPublicKey == null ||
meta.swPrivateKey == null
)
return;
// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録
push.setVapidDetails(config.url, meta.swPublicKey, meta.swPrivateKey);
// Fetch
const subscriptions = await SwSubscriptions.findBy({
userId: userId,
});
for (const subscription of subscriptions) {
if (
[
"readNotifications",
"readAllNotifications",
"readAllMessagingMessages",
"readAllMessagingMessagesOfARoom",
].includes(type) &&
!subscription.sendReadMessage
)
continue;
const pushSubscription = {
endpoint: subscription.endpoint,
keys: {
auth: subscription.auth,
p256dh: subscription.publickey,
},
};
push
.sendNotification(
pushSubscription,
JSON.stringify({
type,
body:
type === "notification"
? truncateNotification(body as Packed<"Notification">)
: body,
userId,
dateTime: Date.now(),
}),
{
proxy: config.proxy,
},
)
.catch((err: any) => {
//swLogger.info(err.statusCode);
//swLogger.info(err.headers);
//swLogger.info(err.body);
if (err.statusCode === 410) {
SwSubscriptions.delete({
userId: userId,
endpoint: subscription.endpoint,
auth: subscription.auth,
publickey: subscription.publickey,
});
}
});
}
}

View File

@ -75,7 +75,7 @@
"sass": "1.77.1",
"seedrandom": "3.0.5",
"stringz": "2.1.0",
"swiper": "11.1.1",
"swiper": "11.1.3",
"syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0",
"three": "0.164.1",
@ -88,9 +88,9 @@
"vite": "5.2.11",
"vite-plugin-compression": "0.5.1",
"vue": "3.4.27",
"vue-draggable-plus": "0.4.0",
"vue-draggable-plus": "0.4.1",
"vue-plyr": "7.0.0",
"vue-prism-editor": "2.0.0-alpha.2",
"vue-tsc": "2.0.17"
"vue-tsc": "2.0.18"
}
}

View File

@ -349,17 +349,6 @@ export default defineComponent({
),
];
}
case "center": {
return [
h(
"div",
{
style: "text-align: center;",
},
genEl(token.children),
),
];
}
}
if (style == null) {
return [

View File

@ -464,9 +464,7 @@ const preview_bold = ref(`**${i18n.ts._mfm.dummy}**`);
const preview_small = ref(
`<small>${i18n.ts._mfm.dummy}</small> $[small ${i18n.ts._mfm.dummy}]`,
);
const preview_center = ref(
`<center>${i18n.ts._mfm.dummy}</center>\n$[center ${i18n.ts._mfm.dummy}]`,
);
const preview_center = ref(`<center>${i18n.ts._mfm.dummy}</center>`);
const preview_inlineCode = ref('`<: "Hello, world!"`');
const preview_blockCode = ref(
'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```',

View File

@ -1,5 +1,14 @@
<template>
<div class="_formRoot">
<FormSection>
<template #label>{{ i18n.ts.importAndExport }}</template>
<FormInfo warn class="_formBlock">{{
i18n.ts.importAndExportWarn
}}</FormInfo>
<FormInfo class="_formBlock">{{
i18n.ts.importAndExportInfo
}}</FormInfo>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts._exportOrImport.allNotes }}</template>
<FormFolder>
@ -177,6 +186,7 @@
<script lang="ts" setup>
import { ref } from "vue";
import FormInfo from "@/components/MkInfo.vue";
import MkButton from "@/components/MkButton.vue";
import FormSection from "@/components/form/section.vue";
import FormFolder from "@/components/form/folder.vue";

View File

@ -22,5 +22,4 @@ export const MFM_TAGS = [
"rotate",
"fade",
"small",
"center",
];

View File

@ -21,10 +21,10 @@
},
"devDependencies": {
"@swc/cli": "0.3.12",
"@swc/core": "1.5.5",
"@swc/types": "0.1.6",
"@swc/core": "1.5.7",
"@swc/types": "0.1.7",
"@types/jest": "29.5.12",
"@types/node": "20.12.11",
"@types/node": "20.12.12",
"jest": "29.7.0",
"jest-fetch-mock": "3.0.3",
"jest-websocket-mock": "2.5.0",

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,9 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"rangeStrategy": "bump",
"branchConcurrentLimit": 0,
"prHourlyLimit": 20,
"prConcurrentLimit": 20,
"enabledManagers": ["npm", "cargo"],
"baseBranches": ["develop"],

View File

@ -15,7 +15,7 @@ await (async () => {
]);
const locales = (await import("../locales/index.mjs")).default;
const meta = (await import("../built/meta.json", { assert: { type: "json" } })).default;
const meta = JSON.parse(await fs.readFile(file("built/meta.json")));
for await (const [lang, locale] of Object.entries(locales)) {
await fs.writeFile(