Merge remote-tracking branch 'calckey/develop' into feat/announcement-popup

This commit is contained in:
naskya 2023-07-08 19:52:49 +00:00
commit 290f047a6e
32 changed files with 432 additions and 255 deletions

View File

@ -67,6 +67,20 @@ redis:
#db: 1
#user: default
# ┌─────────────────────────────┐
#───┘ Cache server configuration └─────────────────────────────────────
# A Redis-compatible server (DragonflyDB, Keydb, Redis) for caching
# If left blank, it will use the Redis server from above
#cacheServer:
#host: localhost
#port: 6379
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
#pass: example-pass
#prefix: example-prefix
#db: 1
# Please configure either MeiliSearch *or* Sonic.
# If both MeiliSearch and Sonic configurations are present, MeiliSearch will take precedence.

View File

@ -1,12 +1,8 @@
# Visual Studio Code
/.vscode
!/.vscode/extensions.json
.vscode
# Intelij-IDEA
/.idea
packages/backend/.idea/backend.iml
packages/backend/.idea/modules.xml
packages/backend/.idea/vcs.xml
.idea
# Node.js
node_modules
@ -14,7 +10,7 @@ node_modules
report.*.json
# Rust
packages/backend/native-utils/target/*
packages/backend/native-utils/target
# Cypress
cypress/screenshots
@ -24,9 +20,7 @@ cypress/videos
coverage
# config
/.config/*
!/.config/example.yml
!/.config/docker_example.env
/.config
# misskey
built

View File

@ -21,6 +21,7 @@ COPY packages/backend/package.json packages/backend/package.json
COPY packages/client/package.json packages/client/package.json
COPY packages/sw/package.json packages/sw/package.json
COPY packages/calckey-js/package.json packages/calckey-js/package.json
COPY packages/megalodon/package.json packages/megalodon/package.json
COPY packages/backend/native-utils/package.json packages/backend/native-utils/package.json
COPY packages/backend/native-utils/npm/linux-x64-musl/package.json packages/backend/native-utils/npm/linux-x64-musl/package.json
COPY packages/backend/native-utils/npm/linux-arm64-musl/package.json packages/backend/native-utils/npm/linux-arm64-musl/package.json
@ -29,10 +30,7 @@ COPY packages/backend/native-utils/npm/linux-arm64-musl/package.json packages/ba
RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm i --frozen-lockfile
# Copy in the rest of the native-utils rust files
COPY packages/backend/native-utils/.cargo packages/backend/native-utils/.cargo
COPY packages/backend/native-utils/build.rs packages/backend/native-utils/
COPY packages/backend/native-utils/src packages/backend/native-utils/src/
COPY packages/backend/native-utils/migration/src packages/backend/native-utils/migration/src/
COPY packages/backend/native-utils packages/backend/native-utils/
# Compile native-utils
RUN pnpm run --filter native-utils build
@ -53,6 +51,8 @@ RUN apk add --no-cache --no-progress tini ffmpeg vips-dev zip unzip nodejs-curre
COPY . ./
COPY --from=build /calckey/packages/megalodon /calckey/packages/megalodon
# Copy node modules
COPY --from=build /calckey/node_modules /calckey/node_modules
COPY --from=build /calckey/packages/backend/node_modules /calckey/packages/backend/node_modules

View File

@ -88,7 +88,6 @@ If you have access to a server that supports one of the sources below, I recomme
## 🧑‍💻 Dependencies
- 🐢 At least [NodeJS](https://nodejs.org/en/) v18.16.0 (v20 recommended)
- Install with [nvm](https://github.com/nvm-sh/nvm)
- 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12 (v14 recommended)
- 🍱 At least [Redis](https://redis.io/) v6 (v7 recommended)
- Web Proxy (one of the following)
@ -104,6 +103,10 @@ If you have access to a server that supports one of the sources below, I recomme
- 🦔 [Sonic](https://crates.io/crates/sonic-server)
- [MeiliSearch](https://www.meilisearch.com/)
- [ElasticSearch](https://www.elastic.co/elasticsearch/)
- Caching server (one of the following)
- 🐲 [DragonflyDB](https://www.dragonflydb.io/) (recommended)
- 👻 [KeyDB](https://keydb.dev/)
- 🍱 Another [Redis](https://redis.io/) server
### 🏗️ Build dependencies
@ -161,6 +164,10 @@ psql postgres -c "create database calckey with encoding = 'UTF8';"
In Calckey's directory, fill out the `db` section of `.config/default.yml` with the correct information, where the `db` key is `calckey`.
## 💰 Caching server
If you experience a lot of traffic, it's a good idea to set up another Redis-compatible caching server. If you don't set one one up, it'll fall back to the mandatory Redis server. DragonflyDB is the recommended option due to its unrivaled performance and ease of use.
## 🔎 Set up search
### 🦔 Sonic

View File

@ -1066,6 +1066,7 @@ _aboutMisskey:
donate: "Calckeyに寄付"
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます! 🥰"
patrons: "支援者"
patronsList: 寄付額ではなく時系列順に並んでいます。上記のリンクから寄付を行ってここにあなたのIDを載せましょう
_nsfw:
respect: "閲覧注意のメディアは隠す"
ignore: "閲覧注意のメディアを隠さない"
@ -1377,11 +1378,12 @@ _permissions:
_auth:
shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?"
shareAccessAsk: "アカウントへのアクセスを許可しますか?"
permissionAsk: "このアプリケーションは次の権限を要求しています"
permissionAsk: "このアプリケーションは次の権限を要求しています:"
pleaseGoBack: "アプリケーションに戻り続行してください"
callback: "アプリケーションに戻っています"
denied: "アクセスを拒否しました"
copyAsk: "以下の認証コードをアプリケーションにコピーしてください"
copyAsk: "以下の認証コードをアプリケーションにコピーしてください:"
allPermissions: 全てのアクセス権
_antennaSources:
all: "全ての投稿"
homeTimeline: "フォローしているユーザーの投稿"
@ -1455,11 +1457,11 @@ _poll:
remainingSeconds: "終了まであと{s}秒"
_visibility:
public: "公開"
publicDescription: "全てのユーザーに公開"
publicDescription: "全ての公開タイムラインに配信されます"
home: "未収載"
homeDescription: "ホームタイムラインのみに公開"
followers: "フォロワー"
followersDescription: "自分のフォロワーのみに公開"
followersDescription: "フォロワーと会話相手のみに公開"
specified: "ダイレクト"
specifiedDescription: "指定したユーザーのみに公開"
localOnly: "ローカルのみ"
@ -1934,3 +1936,14 @@ isBot: このアカウントはBotです
isLocked: このアカウントのフォローは承認制です
isAdmin: 管理者
isPatron: Calckey 後援者
_skinTones:
light: ペールオレンジ
mediumLight: ミディアムライト
medium: ミディアム
mediumDark: ミディアムダーク
yellow: 黄色
dark: 茶色
removeReaction: リアクションを取り消す
alt: 代替テキスト
swipeOnMobile: ページ間のスワイプを有効にする
reactionPickerSkinTone: 優先する絵文字のスキン色

View File

@ -1,6 +1,6 @@
_lang_: "繁體中文"
headlineMisskey: "貼文連繫網路"
introMisskey: "歡迎! Calckey是一個免費,開放原碼,去中心化的社群網路🚀"
introMisskey: "歡迎! Calckey是一個開源、去中心化且永遠免費的社群網路平台!🚀"
monthAndDay: "{month}月 {day}日"
search: "搜尋"
notifications: "通知"
@ -21,7 +21,7 @@ basicSettings: "基本設定"
otherSettings: "其他設定"
openInWindow: "在新視窗開啟"
profile: "個人檔案"
timeline: "時間"
timeline: "時間"
noAccountDescription: "此用戶還沒有自我介紹。"
login: "登入"
loggingIn: "登入中"
@ -31,7 +31,7 @@ uploading: "上傳中..."
save: "儲存"
users: "使用者"
addUser: "新增使用者"
favorite: "我的最愛"
favorite: "添加至我的最愛"
favorites: "我的最愛"
unfavorite: "從我的最愛中移除"
favorited: "已添加至我的最愛。"
@ -43,7 +43,7 @@ copyContent: "複製內容"
copyLink: "複製連結"
delete: "刪除"
deleteAndEdit: "刪除並編輯"
deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有情感、轉發和回覆也將會消失。"
deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也會消失。"
addToList: "加入至清單"
sendMessage: "發送訊息"
copyUsername: "複製使用者名稱"
@ -64,7 +64,7 @@ export: "匯出"
files: "檔案"
download: "下載"
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。"
unfollowConfirm: "確定要取消追隨{name}嗎?"
unfollowConfirm: "確定要取消追隨{name}嗎?"
exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。"
importRequested: "已請求匯入。這可能會花一點時間。"
lists: "清單"
@ -95,9 +95,9 @@ followRequestPending: "追隨許可批准中"
enterEmoji: "輸入表情符號"
renote: "轉發"
unrenote: "取消轉發"
renoted: "已轉。"
renoted: "已轉。"
cantRenote: "無法轉發此貼文。"
cantReRenote: "無法轉傳之前已經轉傳過的內容。"
cantReRenote: "無法轉發之前已經轉發過的內容。"
quote: "引用"
pinnedNote: "已置頂的貼文"
pinned: "置頂"
@ -105,7 +105,7 @@ you: "您"
clickToShow: "按一下以顯示"
sensitive: "敏感內容"
add: "新增"
reaction: "情感"
reaction: "反應"
enableEmojiReaction: "啟用表情符號反應"
showEmojisInReactionNotifications: "在反應通知中顯示表情符號"
reactionSetting: "在選擇器中顯示反應"
@ -140,14 +140,14 @@ emojiUrl: "表情符號URL"
addEmoji: "加入表情符號"
settingGuide: "推薦設定"
cacheRemoteFiles: "快取遠端檔案"
cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外連接數據。"
flagAsBot: "此使用者是機器人"
cacheRemoteFilesDescription: "禁用此設定會停止遠端檔案的緩存,從而節省儲存空間,但資料會因直接連線從而產生額外數據花費。"
flagAsBot: "標記此帳號是機器人"
flagAsBotDescription: "如果本帳戶是由程式控制請啟用此選項。啟用後會作為標示幫助其他開發者防止機器人之間產生無限互動的行為並會調整Calckey內部系統將本帳戶識別為機器人。"
flagAsCat: "此使用者是貓"
flagAsCat: "你是喵咪嗎w😺"
flagAsCatDescription: "如果想將本帳戶標示為一隻貓,請開啟此標示!"
flagShowTimelineReplies: "在時間上顯示貼文的回覆"
flagShowTimelineReplies: "在時間上顯示貼文的回覆"
flagShowTimelineRepliesDescription: "啟用時,時間線除了顯示用戶的貼文以外,還會顯示用戶對其他貼文的回覆。"
autoAcceptFollowed: "自動追隨中使用者的追隨請求"
autoAcceptFollowed: "自動准予追隨中使用者的追隨請求"
addAccount: "添加帳戶"
loginFailed: "登入失敗"
showOnRemote: "轉到所在伺服器顯示"
@ -157,7 +157,7 @@ setWallpaper: "設定桌布"
removeWallpaper: "移除桌布"
searchWith: "搜尋: {q}"
youHaveNoLists: "你沒有任何清單"
followConfirm: "你真的要追隨{name}嗎?"
followConfirm: "你真的要追隨{name}嗎?"
proxyAccount: "代理帳戶"
proxyAccountDescription: "代理帳戶是在某些情況下充當其他伺服器用戶的帳戶。例如,當使用者將一個來自其他伺服器的帳戶放在列表中時,由於沒有其他使用者追蹤該帳戶,該指令不會傳送到該伺服器上,因此會由代理帳戶追蹤。"
host: "主機"
@ -166,7 +166,7 @@ recipient: "收件人"
annotation: "註解"
federation: "站台聯邦"
instances: "伺服器"
registeredAt: "初次觀測"
registeredAt: "初次註冊"
latestRequestSentAt: "上次發送的請求"
latestRequestReceivedAt: "上次收到的請求"
latestStatus: "最後狀態"
@ -234,19 +234,19 @@ lookup: "查詢"
announcements: "公告"
imageUrl: "圖片URL"
remove: "刪除"
removed: "已刪除"
removed: "已成功刪除"
removeAreYouSure: "確定要刪掉「{x}」嗎?"
deleteAreYouSure: "確定要刪掉「{x}」嗎?"
resetAreYouSure: "確定要重設嗎?"
saved: "已儲存"
messaging: "傳送訊息"
messaging: "訊息"
upload: "上傳"
keepOriginalUploading: "保留原圖"
keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時生成一張用於web發布的圖片。"
keepOriginalUploadingDescription: "上傳圖片時保留原始圖片。關閉時,瀏覽器會在上傳時自動產生用於貼文發布的圖片。"
fromDrive: "從雲端空間"
fromUrl: "從URL"
fromUrl: "從網址"
uploadFromUrl: "從網址上傳"
uploadFromUrlDescription: "您要上傳的文件的URL"
uploadFromUrlDescription: "您要上傳的文件的網址"
uploadFromUrlRequested: "已請求上傳"
uploadFromUrlMayTakeTime: "還需要一些時間才能完成上傳。"
explore: "探索"
@ -258,7 +258,7 @@ agreeTo: "我同意{0}"
tos: "使用條款"
start: "開始"
home: "首頁"
remoteUserCaution: "由於該使用者來自遠端實例,因此資訊可能非即時的。"
remoteUserCaution: "由於該使用者來自遠端實例,因此資料可能是非即時的。"
activity: "動態"
images: "圖片"
birthday: "生日"
@ -267,12 +267,12 @@ registeredDate: "註冊日期"
location: "位置"
theme: "外觀主題"
themeForLightMode: "在淺色模式下使用的主題"
themeForDarkMode: "在模式下使用的主題"
themeForDarkMode: "在黑模式下使用的主題"
light: "淺色"
dark: ""
dark: "黑"
lightThemes: "明亮主題"
darkThemes: "主題"
syncDeviceDarkMode: "將黑暗模式與設備設置同步"
darkThemes: "黑主題"
syncDeviceDarkMode: "闇黑模式使用裝置設定"
drive: "雲端硬碟"
fileName: "檔案名稱"
selectFile: "選擇檔案"
@ -281,19 +281,19 @@ selectFolder: "選擇資料夾"
selectFolders: "選擇資料夾"
renameFile: "重新命名檔案"
folderName: "資料夾名稱"
createFolder: "新增資料夾"
createFolder: "創建資料夾"
renameFolder: "重新命名資料夾"
deleteFolder: "刪除資料夾"
addFile: "加入附件"
emptyDrive: "雲端硬碟為空"
emptyFolder: "資料夾為空"
emptyDrive: "你的雲端硬碟沒有任何東西( ̄▽ ̄)\""
emptyFolder: "資料夾裡面沒有東西(⊙_⊙;)"
unableToDelete: "無法刪除"
inputNewFileName: "輸入檔案名稱"
inputNewDescription: "請輸入新標題"
inputNewFolderName: "輸入新資料夾的名稱"
circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件夾。"
hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。"
copyUrl: "複製URL"
copyUrl: "複製網址"
rename: "重新命名"
avatar: "大頭貼"
banner: "橫幅"
@ -304,7 +304,7 @@ reload: "重新整理"
doNothing: "無視"
reloadConfirm: "確定要重新整理嗎?"
watch: "關注"
unwatch: "取消追隨"
unwatch: "取消關注"
accept: "接受"
reject: "拒絕"
normal: "正常"
@ -312,7 +312,7 @@ instanceName: "伺服器名稱"
instanceDescription: "伺服器說明"
maintainerName: "管理員名稱"
maintainerEmail: "管理員郵箱"
tosUrl: "服務條款URL"
tosUrl: "服務條款網址"
thisYear: "本年"
thisMonth: "本月"
today: "本日"
@ -323,23 +323,23 @@ pages: "頁面"
integration: "整合"
connectService: "己連結"
disconnectService: "己斷開"
enableLocalTimeline: "開啟本地時間"
enableGlobalTimeline: "啟用公開時間"
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。"
enableLocalTimeline: "開啟本地時間"
enableGlobalTimeline: "啟用公開時間"
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和版主始終可以訪問所有的時間線。"
registration: "註冊"
enableRegistration: "開啟新使用者註冊"
invite: "邀請"
driveCapacityPerLocalAccount: "每個本地用戶的雲端空間大小"
driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量"
inMb: "以Mbps為單位"
iconUrl: "圖像URL"
bannerUrl: "橫幅圖像URL"
inMb: "以MB為單位"
iconUrl: "圖標網址"
bannerUrl: "橫幅圖像網址"
backgroundImageUrl: "背景圖片的來源網址"
basicInfo: "基本資訊"
pinnedUsers: "置頂用戶"
pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的使用者。"
pinnedPages: "釘選頁面"
pinnedPagesDescription: "輸入要固定至伺服器首頁的頁面路徑,以換行符分隔。"
pinnedUsersDescription: "在「探索」頁面中使用換行標記想要置頂的使用者。"
pinnedPages: "釘選頁面"
pinnedPagesDescription: "輸入要固定至伺服器首頁的頁面路徑,一行一個。"
pinnedClipId: "置頂的摘錄ID"
pinnedNotes: "已置頂的貼文"
hcaptcha: "hCaptcha"
@ -482,7 +482,7 @@ promotion: "推廣"
promote: "推廣"
numberOfDays: "有效天數"
hideThisNote: "隱藏此貼文"
showFeaturedNotesInTimeline: "在時間上顯示熱門推薦"
showFeaturedNotesInTimeline: "在時間上顯示熱門推薦"
objectStorage: "Object Storage (物件儲存)"
useObjectStorage: "使用Object Storage"
objectStorageBaseUrl: "根URL"
@ -502,7 +502,7 @@ objectStorageUseProxyDesc: "如果不使用代理進行API連接請關閉"
objectStorageSetPublicRead: "上傳時設定為\"public-read\""
serverLogs: "伺服器日誌"
deleteAll: "刪除所有記錄"
showFixedPostForm: "於時間頁頂顯示「發送貼文」方框"
showFixedPostForm: "於時間頁頂顯示「發送貼文」方框"
newNoteRecived: "發現新的貼文"
sounds: "音效"
listen: "聆聽"
@ -661,8 +661,8 @@ repliedCount: "回覆數量"
renotedCount: "轉發次數"
followingCount: "正在跟隨的用戶數量"
followersCount: "跟隨者數量"
sentReactionsCount: "情感發送次數"
receivedReactionsCount: "情感收到次數"
sentReactionsCount: "反應發送次數"
receivedReactionsCount: "反應收到次數"
pollVotesCount: "已統計的投票數"
pollVotedCount: "已投票數"
yes: "確定"
@ -688,7 +688,7 @@ experimentalFeatures: "實驗中的功能"
developer: "開發者"
makeExplorable: "使自己的帳戶能夠在“探索”頁面中顯示"
makeExplorableDescription: "如果關閉,帳戶將不會被顯示在\"探索\"頁面中。"
showGapBetweenNotesInTimeline: "分開顯示時間上的貼文"
showGapBetweenNotesInTimeline: "分開顯示時間上的貼文"
duplicate: "複製"
left: "左"
center: "置中"
@ -1089,8 +1089,8 @@ _wordMute:
muteWords: "加入靜音文字"
muteWordsDescription: "用空格分隔指定AND用換行分隔指定OR。"
muteWordsDescription2: "將關鍵字用斜線括起來表示正規表達式。"
softDescription: "隱藏時間中指定條件的貼文。"
hardDescription: "具有指定條件的貼文將不添加到時間。 即使您更改條件,未被添加的貼文也會被排除在外。"
softDescription: "隱藏時間中指定條件的貼文。"
hardDescription: "具有指定條件的貼文將不添加到時間。 即使您更改條件,未被添加的貼文也會被排除在外。"
soft: "軟性靜音"
hard: "硬性靜音"
mutedNotes: "已靜音的貼文"
@ -1203,16 +1203,16 @@ _tutorial:
step2_1: "首先,請完成你的個人資料。"
step2_2: "通過提供一些關於你自己的資料,其他人會更容易了解他們是否想看到你的帖子或關注你。"
step3_1: "現在是時候追隨一些人了!"
step3_2: "你的主頁和社交時間軸是基於你所追蹤的人,所以試著先追蹤幾個賬戶。\n點擊個人資料右上角的加號圈就可以關注它。"
step3_2: "你的主頁和社交時間線是基於你所追蹤的人,所以試著先追蹤幾個帳戶。\n點擊個人資料右上角的加號圈就可以關注它。"
step4_1: "讓我們出去找你。"
step4_2: "對於他們的第一條信息,有些人喜歡做 {introduction} 或一個簡單的 \"hello world!\""
step5_1: "時間軸,到處都是時間軸"
step5_2: "您的伺服器已啟用了{timelines}個時間。"
step5_3: "主 {icon} 時間軸是顯示你追蹤的帳號的帖子。"
step5_4: "本地 {icon} 時間軸是你可以看到伺服器中所有其他用戶的信息的時間軸。"
step5_5: "社交 {icon} 時間軸是顯示你的主時間軸 + 本地時間軸。"
step5_6: "推薦 {icon} 時間軸是顯示你的伺服器管理員推薦的帖文。"
step5_7: "全球 {icon} 時間軸是顯示來自所有其他連接的伺服器的帖文。"
step5_1: "時間線,到處都是時間線"
step5_2: "您的伺服器已啟用了{timelines}個時間。"
step5_3: "首頁 {icon} 時間線是顯示你追蹤的帳號的帖子。"
step5_4: "本地 {icon} 時間線是你可以看到伺服器中所有其他用戶的貼文的時間線。"
step5_5: "社交 {icon} 時間線是你的 首頁時間線 和 本地時間線 的結合體。"
step5_6: "推薦 {icon} 時間線是顯示你的伺服器管理員推薦的貼文。"
step5_7: "全球 {icon} 時間線是顯示來自所有其他連接的伺服器的貼文。"
step6_1: "那麼,這裡是什麼地方?"
step6_2: "你不只是加入Calckey。你已經加入了Fediverse的一個門戶這是一個由成千上萬台服務器組成的互聯網絡。"
step6_3: "每個服務器也有不同而並不是所有的服務器都運行Calckey。但這個服務器確實是運行Calckey的! 你可能會覺得有點複雜,但你很快就會明白的。"
@ -1245,8 +1245,8 @@ _permissions:
"write:notes": "撰寫或刪除貼文"
"read:notifications": "查看通知"
"write:notifications": "編輯通知"
"read:reactions": "查看情感"
"write:reactions": "編輯情感"
"read:reactions": "查看反應"
"write:reactions": "編輯反應"
"write:votes": "投票"
"read:pages": "顯示頁面"
"write:pages": "編輯頁面"
@ -1284,7 +1284,7 @@ _weekday:
_widgets:
memo: "備忘錄"
notifications: "通知"
timeline: "時間"
timeline: "時間"
calendar: "行事曆"
trends: "發燒貼文"
clock: "時鐘"
@ -1335,7 +1335,7 @@ _visibility:
public: "公開"
publicDescription: "發布給所有用戶"
home: "不在主頁顯示"
homeDescription: "僅發送至首頁的時間"
homeDescription: "僅發送至首頁的時間"
followers: "追隨者"
followersDescription: "僅發送至關注者"
specified: "指定使用者"
@ -1403,7 +1403,7 @@ _instanceCharts:
_timelines:
home: "首頁"
local: "本地"
social: "社"
social: "社"
global: "公開"
recommended: 推薦
_pages:
@ -1726,7 +1726,7 @@ _notification:
pollEnded: "問卷調查結束"
receiveFollowRequest: "已收到追隨請求"
followRequestAccepted: "追隨請求已接受"
groupInvited: "加入社群邀請"
groupInvited: "群組加入邀請"
app: "應用程式通知"
_actions:
followBack: "回關"
@ -1755,7 +1755,7 @@ _deck:
main: "主列"
widgets: "小工具"
notifications: "通知"
tl: "時間"
tl: "時間"
antenna: "天線"
list: "清單"
mentions: "提及"
@ -1782,11 +1782,11 @@ enterSendsMessage: 在 Messaging 中按 Return 發送消息 (如關閉則是 Ctr
migrationConfirm: "您確定要將你的帳戶遷移到 {account} 嗎? 一旦這樣做,你將無法復原,而你將無法再次正常使用您的帳戶。\n另外請確保你已將此當前帳戶設置為您要遷移的帳戶。"
customSplashIconsDescription: 每次用戶加載/重新加載頁面時,以換行符號分隔的自定啟動畫面圖標的網址將隨機顯示。請確保圖片位於靜態網址上,最好所有圖片解析度調整為
192x192。
accountMoved: '該使用者已移至新帳戶:'
accountMoved: '該使用者已移至新帳戶:'
showAds: 顯示廣告
noThankYou: 不用了,謝謝
selectInstance: 選擇伺服器
enableRecommendedTimeline: 啟用推薦時間
enableRecommendedTimeline: 啟用推薦時間
antennaInstancesDescription: 分行列出一個伺服器
moveTo: 遷移此帳戶到新帳戶
moveToLabel: '請輸入你將會遷移到的帳戶:'
@ -1838,8 +1838,8 @@ pushNotification: 推送通知
subscribePushNotification: 啟用推送通知
unsubscribePushNotification: 禁用推送通知
pushNotificationAlreadySubscribed: 推送通知已經啟用
recommendedInstancesDescription: 以每行分隔的推薦服務器出現在推薦的時間軸中。 不要添加 `https://`,只添加域名。
searchPlaceholder: 搜尋 Calckey
recommendedInstancesDescription: 以每行分隔的推薦伺服器出現在推薦的時間線中。 不要添加 `https://`,只添加域名。
searchPlaceholder: 在聯邦網路上搜尋
cw: 內容警告
selectChannel: 選擇一個頻道
newer: 較新
@ -1848,3 +1848,10 @@ jumpToPrevious: 跳到上一個
removeReaction: 移除你的反應
listsDesc: 清單可以創建一個只有您指定用戶的時間線。 可以從時間線頁面訪問它們。
flagSpeakAsCatDescription: 在喵咪模式下你的貼文會被喵化ヾ(•ω•`)o
antennasDesc: "天線會顯示符合您設置條件的新貼文!\n 可以從時間線訪問它們。"
expandOnNoteClick: 點擊以打開貼文
expandOnNoteClickDesc: 如果禁用,您仍然可以通過右鍵單擊菜單或單擊時間戳來打開貼文。
hiddenTagsDescription: '列出您希望隱藏趨勢和探索的主題標籤(不帶 #)。 隱藏的主題標籤仍然可以通過其他方式發現。'
userSaysSomethingReasonQuote: '{name} 引用了一篇包含 {reason} 的貼文'
silencedInstancesDescription: 列出您想要靜音的伺服器的網址。 您列出的伺服器內的帳戶將被視為“沉默”,只能發出追隨請求,如果不追隨則不能提及本地帳戶。
這不會影響被阻止的伺服器。

View File

@ -7,4 +7,4 @@ This directory contains all of the packages Calckey uses.
- `client`: Web interface written in Vue3 and TypeScript
- `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript
- `calckey-js`: TypeScript SDK for both backend and client, also published on [NPM](https://www.npmjs.com/package/calckey-js) for public use
- `megalodon`: TypeScript library used for Mastodon compatibility
- `megalodon`: TypeScript library used for partial Mastodon API compatibility

View File

@ -28,13 +28,11 @@
"@bull-board/api": "5.2.0",
"@bull-board/koa": "5.2.0",
"@bull-board/ui": "5.2.0",
"megalodon": "workspace:*",
"@discordapp/twemoji": "14.1.2",
"@elastic/elasticsearch": "7.17.0",
"@koa/cors": "3.4.3",
"@koa/multer": "3.0.2",
"@koa/router": "9.0.1",
"@msgpack/msgpack": "3.0.0-beta2",
"@peertube/http-signature": "1.7.0",
"@redocly/openapi-core": "1.0.0-beta.120",
"@sinonjs/fake-timers": "9.1.2",
@ -87,9 +85,11 @@
"koa-send": "5.0.1",
"koa-slow": "2.1.0",
"koa-views": "7.0.2",
"megalodon": "workspace:*",
"meilisearch": "0.33.0",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"msgpackr": "1.9.5",
"multer": "1.4.4-lts.1",
"native-utils": "link:native-utils",
"nested-property": "4.0.0",

View File

@ -55,6 +55,8 @@ export default function load() {
mixin.clientEntry = clientManifest["src/init.ts"];
if (!config.redis.prefix) config.redis.prefix = mixin.host;
if (config.cacheServer && !config.cacheServer.prefix)
config.cacheServer.prefix = mixin.host;
return Object.assign(config, mixin);
}

View File

@ -26,6 +26,16 @@ export type Source = {
user?: string;
tls?: { [y: string]: string };
};
cacheServer?: {
host: string;
port: number;
family?: number;
pass?: string;
db?: number;
prefix?: string;
user?: string;
tls?: { [z: string]: string };
};
elasticsearch: {
host: string;
port: number;

View File

@ -2,15 +2,19 @@ import Redis from "ioredis";
import config from "@/config/index.js";
export function createConnection() {
let source = config.redis;
if (config.cacheServer) {
source = config.cacheServer;
}
return new Redis({
port: config.redis.port,
host: config.redis.host,
family: config.redis.family ?? 0,
password: config.redis.pass,
username: config.redis.user ?? "default",
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db || 0,
tls: config.redis.tls,
port: source.port,
host: source.host,
family: source.family ?? 0,
password: source.pass,
username: source.user ?? "default",
keyPrefix: `${source.prefix}:`,
db: source.db || 0,
tls: source.tls,
});
}

View File

@ -1,5 +1,5 @@
import { redisClient } from "@/db/redis.js";
import { encode, decode } from "@msgpack/msgpack";
import { encode, decode } from "msgpackr";
import { ChainableCommander } from "ioredis";
export class Cache<T> {

View File

@ -138,7 +138,7 @@ export async function packActivity(note: Note): Promise<any> {
) {
const renote = await Notes.findOneByOrFail({ id: note.renoteId });
return renderAnnounce(
renote.uri ?? `${config.url}/notes/${renote.id}`,
renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`,
note,
);
}

View File

@ -10,7 +10,7 @@ import type { Note } from "@/models/entities/note.js";
import type { CacheableLocalUser, User } from "@/models/entities/user.js";
import { isActor, isPost, getApId } from "@/remote/activitypub/type.js";
import type { SchemaType } from "@/misc/schema.js";
import { HOUR } from "@/const.js";
import { MINUTE } from "@/const.js";
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
import { updateQuestion } from "@/remote/activitypub/models/question.js";
import { populatePoll } from "@/models/repositories/note.js";
@ -22,8 +22,8 @@ export const meta = {
requireCredential: true,
limit: {
duration: HOUR,
max: 30,
duration: MINUTE,
max: 10,
},
errors: {

View File

@ -2,8 +2,10 @@ import { Entity } from "megalodon";
import { convertId, IdType } from "../index.js";
function simpleConvert(data: any) {
data.id = convertId(data.id, IdType.MastodonId);
return data;
// copy the object to bypass weird pass by reference bugs
const result = Object.assign({}, data);
result.id = convertId(data.id, IdType.MastodonId);
return result;
}
export function convertAccount(account: Entity.Account) {

View File

@ -30,21 +30,26 @@ export function apiSearchMastodon(router: Router): void {
try {
const query: any = convertTimelinesArgsId(limitToInt(ctx.query));
const type = query.type;
if (type) {
const data = await client.search(query.q, type, query);
ctx.body = data.data.accounts.map((account) => convertAccount(account));
} else {
const acct = await client.search(query.q, "accounts", query);
const stat = await client.search(query.q, "statuses", query);
const tags = await client.search(query.q, "hashtags", query);
ctx.body = {
accounts: acct.data.accounts.map((account) =>
convertAccount(account),
),
statuses: stat.data.statuses.map((status) => convertStatus(status)),
hashtags: tags.data.hashtags,
};
}
const acct =
!type || type === "accounts"
? await client.search(query.q, "accounts", query)
: null;
const stat =
!type || type === "statuses"
? await client.search(query.q, "statuses", query)
: null;
const tags =
!type || type === "hashtags"
? await client.search(query.q, "hashtags", query)
: null;
ctx.body = {
accounts:
acct?.data?.accounts.map((account) => convertAccount(account)) ?? [],
statuses:
stat?.data?.statuses.map((status) => convertStatus(status)) ?? [],
hashtags: tags?.data?.hashtags ?? [],
};
} catch (e: any) {
console.error(e);
ctx.status = 401;

View File

@ -197,7 +197,19 @@ export function apiStatusMastodon(router: Router): void {
router.get<{ Params: { id: string } }>(
"/v1/statuses/:id/favourited_by",
async (ctx) => {
ctx.body = [];
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getStatusFavouritedBy(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(

View File

@ -45,13 +45,14 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const query: any = ctx.query;
const data = query.local
? await client.getLocalTimeline(
convertTimelinesArgsId(argsToBools(limitToInt(query))),
)
: await client.getPublicTimeline(
convertTimelinesArgsId(argsToBools(limitToInt(query))),
);
const data =
query.local === "true"
? await client.getLocalTimeline(
convertTimelinesArgsId(argsToBools(limitToInt(query))),
)
: await client.getPublicTimeline(
convertTimelinesArgsId(argsToBools(limitToInt(query))),
);
ctx.body = data.data.map((status) => convertStatus(status));
} catch (e: any) {
console.error(e);

View File

@ -67,7 +67,6 @@ import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
import meilisearch from "../../db/meilisearch.js";
import { redisClient } from "@/db/redis.js";
import { Mutex } from "redis-semaphore";
import { packActivity } from "@/server/activitypub/outbox.js";
const mutedWordsCache = new Cache<
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
@ -597,13 +596,9 @@ export default async (
});
//#region AP deliver
if (
Users.isLocalUser(user) &&
!data.localOnly &&
!dontFederateInitially
) {
if (Users.isLocalUser(user) && !dontFederateInitially) {
(async () => {
const noteActivity = renderActivity(await packActivity(note));
const noteActivity = await renderNoteOrRenoteActivity(data, note);
const dm = new DeliverManager(user, noteActivity);
// メンションされたリモートユーザーに配送
@ -660,6 +655,25 @@ export default async (
await index(note, false);
});
async function renderNoteOrRenoteActivity(data: Option, note: Note) {
if (data.localOnly) return null;
const content =
data.renote &&
data.text == null &&
data.poll == null &&
(data.files == null || data.files.length === 0)
? renderAnnounce(
data.renote.uri
? data.renote.uri
: `${config.url}/notes/${data.renote.id}`,
note,
)
: renderCreate(await renderNote(note, false), note);
return renderActivity(content);
}
function incRenoteCount(renote: Note) {
Notes.createQueryBuilder()
.update()

View File

@ -15,7 +15,7 @@ import { createSystemUser } from "./create-system-user.js";
const ACTOR_USERNAME = "relay.actor" as const;
const relaysCache = new Cache<Relay[]>("relay", 60 * 10);
const relaysCache = new Cache<Relay[]>("relay", 60 * 60);
export async function getRelayActor(): Promise<ILocalUser> {
const user = await Users.findOneBy({

View File

@ -162,7 +162,7 @@ export async function openAccountMenu(
{
done: (res) => {
addAccount(res.id, res.i);
success();
switchAccountWithToken(res.i);
},
},
"closed",

View File

@ -328,7 +328,6 @@ if (noteViewInterruptors.length > 0) {
const isRenote =
note.renote != null &&
note.text == null &&
note.cw == null &&
note.fileIds.length === 0 &&
note.poll == null;

View File

@ -26,6 +26,7 @@
class="banner"
:style="{
backgroundImage: `url('${user.bannerUrl}')`,
'--backgroundImageStatic': defaultStore.state.useBlurEffect ? `url('${getStaticImageUrl(user.bannerUrl)}')` : null
}"
></div>
<div class="fade"></div>
@ -384,8 +385,10 @@ import MkRemoteCaution from "@/components/MkRemoteCaution.vue";
import MkInfo from "@/components/MkInfo.vue";
import MkMoved from "@/components/MkMoved.vue";
import { getScrollPosition } from "@/scripts/scroll";
import { getStaticImageUrl } from "@/scripts/get-static-image-url";
import number from "@/filters/number";
import { userPage } from "@/filters/user";
import { defaultStore } from "@/store";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { $i } from "@/account";
@ -513,7 +516,7 @@ onUnmounted(() => {
content: "";
position: fixed;
inset: 0;
background: var(--blur, inherit);
background: var(--backgroundImageStatic);
background-size: cover;
background-position: center;
pointer-events: none;

View File

@ -1,19 +0,0 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript",
"dynamicImport": true,
"decorators": true
},
"transform": {
"decoratorMetadata": true
},
"target": "es2022"
},
"minify": false,
"module": {
"type": "commonjs",
"strict": true
}
}

View File

@ -4,7 +4,7 @@
"main": "./lib/src/index.js",
"typings": "./lib/src/index.d.ts",
"scripts": {
"build": "pnpm swc src -d built -D",
"build": "tsc -p ./",
"lint": "eslint --ext .js,.ts src",
"doc": "typedoc --out ../docs ./src",
"test": "NODE_ENV=test jest -u --maxWorkers=3"
@ -49,8 +49,6 @@
"async-lock": "1.4.0"
},
"devDependencies": {
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.62",
"@types/core-js": "^2.5.0",
"@types/form-data": "^2.5.0",
"@types/jest": "^29.4.0",
@ -79,8 +77,5 @@
"directories": {
"lib": "lib",
"test": "test"
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11"
}
}
}

View File

@ -9,5 +9,6 @@ namespace Entity {
votes_count: number
options: Array<PollOption>
voted: boolean
own_votes: Array<number>
}
}

View File

@ -841,7 +841,7 @@ export interface MegalodonInterface {
* @param choices Array of own votes containing index for each option (starting from 0).
* @return Poll
*/
votePoll(id: string, choices: Array<number>, status_id?: string | null): Promise<Response<Entity.Poll>>
votePoll(id: string, choices: Array<number>): Promise<Response<Entity.Poll>>
// ======================================
// statuses/scheduled_statuses
// ======================================

View File

@ -10,6 +10,7 @@ import Entity from './entity'
import { MegalodonInterface, WebSocketInterface, NoImplementedError, ArgumentError, UnexpectedError } from './megalodon'
import MegalodonEntity from "@/entity";
import fs from "node:fs";
import MisskeyNotificationType from "./misskey/notification";
type AccountCache = {
locks: AsyncLock,
@ -332,7 +333,7 @@ export default class Misskey implements MegalodonInterface {
if (res.data.pinnedNotes) {
return {
...res,
data: await Promise.all(res.data.pinnedNotes.map(n => this.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache)))
data: await Promise.all(res.data.pinnedNotes.map(n => this.noteWithDetails(n, this.baseUrlToHost(this.baseUrl), accountCache)))
}
}
return {...res, data: []}
@ -384,7 +385,7 @@ export default class Misskey implements MegalodonInterface {
})
}
return this.client.post<Array<MisskeyAPI.Entity.Note>>('/api/users/notes', params).then(async res => {
const statuses: Array<Entity.Status> = await Promise.all(res.data.map(note => this.noteWithMentions(note, this.baseUrlToHost(this.baseUrl), accountCache)))
const statuses: Array<Entity.Status> = await Promise.all(res.data.map(note => this.noteWithDetails(note, this.baseUrlToHost(this.baseUrl), accountCache)))
return Object.assign(res, {
data: statuses
})
@ -423,7 +424,7 @@ export default class Misskey implements MegalodonInterface {
}
return this.client.post<Array<MisskeyAPI.Entity.Favorite>>('/api/users/reactions', params).then(async res => {
return Object.assign(res, {
data: await Promise.all(res.data.map(fav => this.noteWithMentions(fav.note, this.baseUrlToHost(this.baseUrl), accountCache)))
data: await Promise.all(res.data.map(fav => this.noteWithDetails(fav.note, this.baseUrlToHost(this.baseUrl), accountCache)))
})
})
}
@ -763,7 +764,7 @@ export default class Misskey implements MegalodonInterface {
}
return this.client.post<Array<MisskeyAPI.Entity.Favorite>>('/api/i/favorites', params).then(async res => {
return Object.assign(res, {
data: await Promise.all(res.data.map(s => this.noteWithMentions(s.note, this.baseUrlToHost(this.baseUrl), accountCache)))
data: await Promise.all(res.data.map(s => this.noteWithDetails(s.note, this.baseUrlToHost(this.baseUrl), accountCache)))
})
})
}
@ -1221,7 +1222,7 @@ export default class Misskey implements MegalodonInterface {
.post<MisskeyAPI.Entity.CreatedNote>('/api/notes/create', params)
.then(async res => ({
...res,
data: await this.noteWithMentions(res.data.createdNote, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())
data: await this.noteWithDetails(res.data.createdNote, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())
}))
}
@ -1233,7 +1234,7 @@ export default class Misskey implements MegalodonInterface {
.post<MisskeyAPI.Entity.Note>('/api/notes/show', {
noteId: id
})
.then(async res => ({ ...res, data: await this.noteWithMentions(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}));
.then(async res => ({ ...res, data: await this.noteWithDetails(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}));
}
private getFreshAccountCache() :AccountCache {
@ -1243,45 +1244,76 @@ export default class Misskey implements MegalodonInterface {
}
}
public async noteWithMentions(n: MisskeyAPI.Entity.Note, host: string, cache: AccountCache): Promise<MegalodonEntity.Status> {
const status = await this.converter.note(n, host);
return status.mentions.length === 0 ? this.addMentionsToStatus(status, cache) : status;
public async notificationWithDetails(n: MisskeyAPI.Entity.Notification, host: string, cache: AccountCache): Promise<MegalodonEntity.Notification> {
const notification = this.converter.notification(n, host);
if (n.note)
notification.status = await this.noteWithDetails(n.note, host, cache);
return notification;
}
public async noteWithDetails(n: MisskeyAPI.Entity.Note, host: string, cache: AccountCache): Promise<MegalodonEntity.Status> {
const status = await this.addUserDetailsToStatus(this.converter.note(n, host), cache);
return this.addMentionsToStatus(status, cache);
}
public async addUserDetailsToStatus(status: Entity.Status, cache: AccountCache) : Promise<Entity.Status> {
if (status.account.followers_count === 0 && status.account.followers_count === 0 && status.account.statuses_count === 0)
status.account = await this.getAccountCached(status.account.id, status.account.acct, cache) ?? status.account;
if (status.reblog != null)
status.reblog = await this.addUserDetailsToStatus(status.reblog, cache);
if (status.quote != null)
status.quote = await this.addUserDetailsToStatus(status.quote, cache);
return status;
}
public async addMentionsToStatus(status: Entity.Status, cache: AccountCache) : Promise<Entity.Status> {
if (status.mentions.length > 0)
return status;
if (status.reblog != null)
status.reblog = await this.addMentionsToStatus(status.reblog, cache);
if (status.quote != null)
status.quote = await this.addMentionsToStatus(status.quote, cache);
status.mentions = (await this.getMentions(status.plain_content!, cache)).filter(p => p != null);
for (const m of status.mentions.filter((value, index, array) => array.indexOf(value) === index)) {
if (m.acct == m.username)
status.content = status.content.replace(`@${m.acct}@${this.baseUrlToHost(this.baseUrl)}`, `@${m.acct}`);
status.content = status.content.replace(`@${m.acct}`, `<a href="${m.url}" class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@${m.acct}</a>`);
}
return status;
}
public async getMentions(text: string, cache: AccountCache): Promise<Entity.Mention[]> {
console.log(`getting mentions for message: '${text}'`);
const mentions :Entity.Mention[] = [];
if (text == undefined)
return mentions;
console.log('text is not undefined, continuing');
const mentionMatch = text.matchAll(/(?<=^|\s)@(?<user>.*?)(?:@(?<host>.*?)|)(?=\s|$)/g);
for (const m of mentionMatch) {
if (m.groups == null)
continue;
try {
if (m.groups == null)
continue;
const account = await this.getAccountByNameCached(m.groups.user, m.groups.host, cache);
const account = await this.getAccountByNameCached(m.groups.user, m.groups.host, cache);
if (account == null)
continue;
if (account == null)
continue;
mentions.push({
id: account.id,
url: account.url,
username: account.username,
acct: account.acct
});
mentions.push({
id: account.id,
url: account.url,
username: account.username,
acct: account.acct
});
}
catch {}
}
return mentions;
@ -1306,6 +1338,23 @@ export default class Misskey implements MegalodonInterface {
})
}
public async getAccountCached(id: string, acct: string, cache: AccountCache): Promise<Entity.Account | undefined | null> {
return await cache.locks.acquire(acct, async () => {
const cacheHit = cache.accounts.find(p => p.id === id);
const account = cacheHit ?? (await this.getAccount(id)).data;
if (!account) {
return null;
}
if (cacheHit == null) {
cache.accounts.push(account);
}
return account;
})
}
public async editStatus(
_id: string,
_options: {
@ -1374,11 +1423,11 @@ export default class Misskey implements MegalodonInterface {
return this.client.post<Array<MisskeyAPI.Entity.Note>>('/api/notes/children', params).then(async res => {
const accountCache = this.getFreshAccountCache();
const conversation = await this.client.post<Array<MisskeyAPI.Entity.Note>>('/api/notes/conversation', params);
const parents = await Promise.all(conversation.data.map(n => this.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache)));
const parents = await Promise.all(conversation.data.map(n => this.noteWithDetails(n, this.baseUrlToHost(this.baseUrl), accountCache)));
const context: Entity.Context = {
ancestors: parents.reverse(),
descendants: this.dfs(await Promise.all(res.data.map(n => this.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache))))
descendants: this.dfs(await Promise.all(res.data.map(n => this.noteWithDetails(n, this.baseUrlToHost(this.baseUrl), accountCache))))
}
return {
...res,
@ -1446,17 +1495,21 @@ export default class Misskey implements MegalodonInterface {
.post<Array<MisskeyAPI.Entity.Note>>('/api/notes/renotes', {
noteId: id
})
.then(res => ({
.then(async res => ({
...res,
data: res.data.map(n => this.converter.user(n.user))
data: (await Promise.all(res.data.map(n => this.getAccount(n.user.id)))).map(p => p.data)
}))
}
public async getStatusFavouritedBy(_id: string): Promise<Response<Array<Entity.Account>>> {
return new Promise((_, reject) => {
const err = new NoImplementedError('misskey does not support')
reject(err)
})
public async getStatusFavouritedBy(id: string): Promise<Response<Array<Entity.Account>>> {
return this.client
.post<Array<MisskeyAPI.Entity.Reaction>>('/api/notes/reactions', {
noteId: id
})
.then(async res => ({
...res,
data: (await Promise.all(res.data.map(n => this.getAccount(n.user.id)))).map(p => p.data)
}))
}
public async favouriteStatus(id: string): Promise<Response<Entity.Status>> {
@ -1491,7 +1544,7 @@ export default class Misskey implements MegalodonInterface {
})
.then(async res => ({
...res,
data: await this.noteWithMentions(res.data.createdNote, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())
data: await this.noteWithDetails(res.data.createdNote, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())
}))
}
@ -1506,7 +1559,7 @@ export default class Misskey implements MegalodonInterface {
.post<MisskeyAPI.Entity.Note>('/api/notes/show', {
noteId: id
})
.then(async res => ({...res, data: await this.noteWithMentions(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
.then(async res => ({...res, data: await this.noteWithDetails(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
}
/**
@ -1520,7 +1573,7 @@ export default class Misskey implements MegalodonInterface {
.post<MisskeyAPI.Entity.Note>('/api/notes/show', {
noteId: id
})
.then(async res => ({...res, data: await this.noteWithMentions(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
.then(async res => ({...res, data: await this.noteWithDetails(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
}
/**
@ -1534,7 +1587,7 @@ export default class Misskey implements MegalodonInterface {
.post<MisskeyAPI.Entity.Note>('/api/notes/show', {
noteId: id
})
.then(async res => ({...res, data: await this.noteWithMentions(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
.then(async res => ({...res, data: await this.noteWithDetails(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
}
public async muteStatus(_id: string): Promise<Response<Entity.Status>> {
@ -1562,7 +1615,7 @@ export default class Misskey implements MegalodonInterface {
.post<MisskeyAPI.Entity.Note>('/api/notes/show', {
noteId: id
})
.then(async res => ({...res, data: await this.noteWithMentions(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
.then(async res => ({...res, data: await this.noteWithDetails(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
}
/**
@ -1576,7 +1629,7 @@ export default class Misskey implements MegalodonInterface {
.post<MisskeyAPI.Entity.Note>('/api/notes/show', {
noteId: id
})
.then(async res => ({...res, data: await this.noteWithMentions(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
.then(async res => ({...res, data: await this.noteWithDetails(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
}
// ======================================
@ -1635,34 +1688,38 @@ export default class Misskey implements MegalodonInterface {
// ======================================
// statuses/polls
// ======================================
public async getPoll(_id: string): Promise<Response<Entity.Poll>> {
return new Promise((_, reject) => {
const err = new NoImplementedError('misskey does not support')
reject(err)
})
public async getPoll(id: string): Promise<Response<Entity.Poll>> {
const res = await this.getStatus(id);
if (res.data.poll == null)
throw new Error('poll not found');
return { ...res, data: res.data.poll }
}
/**
* POST /api/notes/polls/vote
*/
public async votePoll(_id: string, choices: Array<number>, status_id?: string | null): Promise<Response<Entity.Poll>> {
if (!status_id) {
public async votePoll(id: string, choices: Array<number>): Promise<Response<Entity.Poll>> {
if (!id) {
return new Promise((_, reject) => {
const err = new ArgumentError('status_id is required')
const err = new ArgumentError('id is required')
reject(err)
})
}
const params = {
noteId: status_id,
choice: choices[0]
}
await this.client.post<{}>('/api/notes/polls/vote', params)
for (const c of choices) {
const params = {
noteId: id,
choice: +c
}
await this.client.post<{}>('/api/notes/polls/vote', params)
}
const res = await this.client
.post<MisskeyAPI.Entity.Note>('/api/notes/show', {
noteId: status_id
noteId: id
})
.then(async res => {
const note = await this.noteWithMentions(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())
const note = await this.noteWithDetails(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())
return {...res, data: note.poll}
})
if (!res.data) {
@ -1767,7 +1824,7 @@ export default class Misskey implements MegalodonInterface {
.post<Array<MisskeyAPI.Entity.Note>>('/api/notes/global-timeline', params)
.then(async res => ({
...res,
data: await Promise.all(res.data.map(n => this.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache)))
data: await Promise.all(res.data.map(n => this.noteWithDetails(n, this.baseUrlToHost(this.baseUrl), accountCache)))
}))
}
@ -1825,7 +1882,7 @@ export default class Misskey implements MegalodonInterface {
.post<Array<MisskeyAPI.Entity.Note>>('/api/notes/local-timeline', params)
.then(async res => ({
...res,
data: await Promise.all(res.data.map(n => this.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache)))
data: await Promise.all(res.data.map(n => this.noteWithDetails(n, this.baseUrlToHost(this.baseUrl), accountCache)))
}))
}
@ -1889,7 +1946,7 @@ export default class Misskey implements MegalodonInterface {
.post<Array<MisskeyAPI.Entity.Note>>('/api/notes/search-by-tag', params)
.then(async res => ({
...res,
data: await Promise.all(res.data.map(n => this.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache)))
data: await Promise.all(res.data.map(n => this.noteWithDetails(n, this.baseUrlToHost(this.baseUrl), accountCache)))
}))
}
@ -1944,7 +2001,7 @@ export default class Misskey implements MegalodonInterface {
.post<Array<MisskeyAPI.Entity.Note>>('/api/notes/timeline', params)
.then(async res => ({
...res,
data: await Promise.all(res.data.map(n => this.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache)))
data: await Promise.all(res.data.map(n => this.noteWithDetails(n, this.baseUrlToHost(this.baseUrl), accountCache)))
}))
}
@ -2000,7 +2057,7 @@ export default class Misskey implements MegalodonInterface {
}
return this.client
.post<Array<MisskeyAPI.Entity.Note>>('/api/notes/user-list-timeline', params)
.then(async res => ({ ...res, data: await Promise.all(res.data.map(n => this.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache))) }))
.then(async res => ({ ...res, data: await Promise.all(res.data.map(n => this.noteWithDetails(n, this.baseUrlToHost(this.baseUrl), accountCache))) }))
}
// ======================================
@ -2236,9 +2293,15 @@ export default class Misskey implements MegalodonInterface {
limit: 20
})
}
const cache = this.getFreshAccountCache();
return this.client
.post<Array<MisskeyAPI.Entity.Notification>>('/api/i/notifications', params)
.then(res => ({ ...res, data: res.data.map(n => this.converter.notification(n, this.baseUrlToHost(this.baseUrl))) }))
.then(async res => ({
...res,
data: await Promise.all(res.data
.filter(p => p.type != MisskeyNotificationType.FollowRequestAccepted) // these aren't supported on mastodon
.map(n => this.notificationWithDetails(n, this.baseUrlToHost(this.baseUrl), cache)))
}))
}
public async getNotification(_id: string): Promise<Response<Entity.Notification>> {
@ -2332,6 +2395,32 @@ export default class Misskey implements MegalodonInterface {
switch (type) {
case 'accounts': {
if (q.startsWith("http://") || q.startsWith("https://")) {
return this.client.post('/api/ap/show', {uri: q}).then(async res => {
if (res.status != 200 || res.data.type != 'User') {
res.status = 200;
res.statusText = "OK";
res.data = {
accounts: [],
statuses: [],
hashtags: []
};
return res;
}
const account = await this.converter.userDetail(res.data.object as MisskeyAPI.Entity.UserDetail, this.baseUrlToHost(this.baseUrl));
return {
...res,
data: {
accounts: options?.max_id && options?.max_id >= account.id ? [] : [account],
statuses: [],
hashtags: []
}
};
})
}
let params = {
query: q
}
@ -2405,6 +2494,32 @@ export default class Misskey implements MegalodonInterface {
}))
}
case 'statuses': {
if (q.startsWith("http://") || q.startsWith("https://")) {
return this.client.post('/api/ap/show', {uri: q}).then(async res => {
if (res.status != 200 || res.data.type != 'Note') {
res.status = 200;
res.statusText = "OK";
res.data = {
accounts: [],
statuses: [],
hashtags: []
};
return res;
}
const post = await this.noteWithDetails(res.data.object as MisskeyAPI.Entity.Note, this.baseUrlToHost(this.baseUrl), accountCache);
return {
...res,
data: {
accounts: [],
statuses: options?.max_id && options.max_id >= post.id ? [] : [post],
hashtags: []
}
}
})
}
let params = {
query: q
}
@ -2439,7 +2554,7 @@ export default class Misskey implements MegalodonInterface {
...res,
data: {
accounts: [],
statuses: await Promise.all(res.data.map(n => this.noteWithMentions(n, this.baseUrlToHost(this.baseUrl), accountCache))),
statuses: await Promise.all(res.data.map(n => this.noteWithDetails(n, this.baseUrlToHost(this.baseUrl), accountCache))),
hashtags: []
}
}))
@ -2577,7 +2692,7 @@ export default class Misskey implements MegalodonInterface {
.post<MisskeyAPI.Entity.Note>('/api/notes/show', {
noteId: id
})
.then(async res => ({...res, data: await this.noteWithMentions(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
.then(async res => ({...res, data: await this.noteWithDetails(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
}
/**
@ -2591,7 +2706,7 @@ export default class Misskey implements MegalodonInterface {
.post<MisskeyAPI.Entity.Note>('/api/notes/show', {
noteId: id
})
.then(async res => ({...res, data: await this.noteWithMentions(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
.then(async res => ({...res, data: await this.noteWithDetails(res.data, this.baseUrlToHost(this.baseUrl), this.getFreshAccountCache())}))
}
public async getEmojiReactions(id: string): Promise<Response<Array<Entity.Reaction>>> {

View File

@ -161,7 +161,7 @@ namespace MisskeyAPI {
followers_count: u.followersCount,
following_count: u.followingCount,
statuses_count: u.notesCount,
note: u.description,
note: u.description?.replace(/\n|\\n/g, '<br>') ?? '',
url: acctUrl,
avatar: u.avatarUrl,
avatar_static: u.avatarUrl,
@ -275,18 +275,19 @@ namespace MisskeyAPI {
}
}
poll = (p: Entity.Poll): MegalodonEntity.Poll => {
poll = (p: Entity.Poll, id: string): MegalodonEntity.Poll => {
const now = dayjs()
const expire = dayjs(p.expiresAt)
const count = p.choices.reduce((sum, choice) => sum + choice.votes, 0)
return {
id: '',
id: id,
expires_at: p.expiresAt,
expired: now.isAfter(expire),
multiple: p.multiple,
votes_count: count,
options: p.choices.map(c => this.choice(c)),
voted: p.choices.some(c => c.isVoted)
voted: p.choices.some(c => c.isVoted),
own_votes: p.choices.filter(c => c.isVoted).map(c => p.choices.indexOf(c))
}
}
@ -318,7 +319,7 @@ namespace MisskeyAPI {
mentions: [],
tags: [],
card: null,
poll: n.poll ? this.poll(n.poll) : null,
poll: n.poll ? this.poll(n.poll, n.id) : null,
application: null,
language: null,
pinned: null,

View File

@ -105,9 +105,6 @@ importers:
'@koa/router':
specifier: 9.0.1
version: 9.0.1
'@msgpack/msgpack':
specifier: 3.0.0-beta2
version: 3.0.0-beta2
'@peertube/http-signature':
specifier: 1.7.0
version: 1.7.0
@ -276,6 +273,9 @@ importers:
mime-types:
specifier: 2.1.35
version: 2.1.35
msgpackr:
specifier: 1.9.5
version: 1.9.5
multer:
specifier: 1.4.4-lts.1
version: 1.4.4-lts.1
@ -789,7 +789,7 @@ importers:
version: 2.30.0
emojilib:
specifier: github:thatonecalculator/emojilib
version: github.com/thatonecalculator/emojilib/15fd9504f943763a057ff803ee2009ec0524c96b
version: github.com/thatonecalculator/emojilib/9d16541664dc8fef3201ae9b647477070676a52e
escape-regexp:
specifier: 0.0.1
version: 0.0.1
@ -970,17 +970,7 @@ importers:
ws:
specifier: 8.12.0
version: 8.12.0
optionalDependencies:
'@swc/core-android-arm64':
specifier: 1.3.11
version: 1.3.11
devDependencies:
'@swc/cli':
specifier: ^0.1.62
version: 0.1.62(@swc/core@1.3.62)(chokidar@3.3.1)
'@swc/core':
specifier: ^1.3.62
version: 1.3.62
'@types/async-lock':
specifier: 1.4.0
version: 1.4.0
@ -2607,11 +2597,6 @@ packages:
os-filter-obj: 2.0.0
dev: true
/@msgpack/msgpack@3.0.0-beta2:
resolution: {integrity: sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==}
engines: {node: '>= 14'}
dev: false
/@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2:
resolution: {integrity: sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==}
cpu: [arm64]
@ -17444,8 +17429,8 @@ packages:
url-polyfill: 1.1.12
dev: true
github.com/thatonecalculator/emojilib/15fd9504f943763a057ff803ee2009ec0524c96b:
resolution: {tarball: https://codeload.github.com/thatonecalculator/emojilib/tar.gz/15fd9504f943763a057ff803ee2009ec0524c96b}
github.com/thatonecalculator/emojilib/9d16541664dc8fef3201ae9b647477070676a52e:
resolution: {tarball: https://codeload.github.com/thatonecalculator/emojilib/tar.gz/9d16541664dc8fef3201ae9b647477070676a52e}
name: emojilib
version: 3.0.10
dev: true

View File

@ -46,6 +46,14 @@ const { join } = require("node:path");
recursive: true,
force: true,
});
fs.rmSync(join(__dirname, "/../packages/megalodon/lib"), {
recursive: true,
force: true,
});
fs.rmSync(join(__dirname, "/../packages/megalodon/node_modules"), {
recursive: true,
force: true,
});
fs.rmSync(join(__dirname, "/../built"), { recursive: true, force: true });
fs.rmSync(join(__dirname, "/../node_modules"), {

View File

@ -23,5 +23,9 @@ const { join } = require("node:path");
recursive: true,
force: true,
});
fs.rmSync(join(__dirname, "/../packages/megalodon/lib"), {
recursive: true,
force: true,
});
fs.rmSync(join(__dirname, "/../built"), { recursive: true, force: true });
})();