Merge branch 'develop' of https://codeberg.org/calckey/calckey into logged-out
This commit is contained in:
commit
03c796e5fa
178 changed files with 10968 additions and 1101 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -48,6 +48,9 @@ packages/backend/assets/sounds/None.mp3
|
|||
|
||||
!packages/backend/src/db
|
||||
|
||||
packages/megalodon/lib
|
||||
packages/megalodon/.idea
|
||||
|
||||
# blender backups
|
||||
*.blend1
|
||||
*.blend2
|
||||
|
|
|
@ -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
|
||||
|
|
19
README.md
19
README.md
|
@ -72,6 +72,14 @@
|
|||
|
||||
# 🌠 Getting started
|
||||
|
||||
Want to just join a Calckey server? View the list here, pick one, and join:
|
||||
|
||||
### https://calckey.org/join
|
||||
|
||||
---
|
||||
|
||||
Want to make your own? Keep reading!
|
||||
|
||||
This guide will work for both **starting from scratch** and **migrating from Misskey**.
|
||||
|
||||
## 🔰 Easy installers
|
||||
|
@ -88,7 +96,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,7 +111,11 @@ 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
|
||||
|
||||
- 🦀 At least [Rust](https://www.rust-lang.org/) v1.68.0
|
||||
|
@ -161,6 +172,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
|
||||
|
|
|
@ -83,9 +83,9 @@ NODE_ENV=production pnpm run migrate
|
|||
cd packages/backend
|
||||
|
||||
LINE_NUM="$(npx typeorm migration:show -d ormconfig.js | grep -n uniformThemecolor1652859567549 | cut -d ':' -f 1)"
|
||||
NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | nl)"
|
||||
NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | wc -l)"
|
||||
|
||||
for i in $(seq 1 $NUM_MIGRAIONS); do
|
||||
for i in $(seq 1 $NUM_MIGRATIONS); do
|
||||
npx typeorm migration:revert -d ormconfig.js
|
||||
done
|
||||
|
||||
|
|
|
@ -2144,3 +2144,6 @@ _skinTones:
|
|||
swipeOnMobile: Permet lliscar entre pàgines
|
||||
enableIdenticonGeneration: Habilitar la generació d'Identicon
|
||||
enableServerMachineStats: Habilitar les estadístiques del maquinari del servidor
|
||||
showPopup: Notificar els usuaris amb una finestra emergent
|
||||
showWithSparkles: Mostra amb espurnes
|
||||
youHaveUnreadAnnouncements: Tens anuncis sense llegir
|
||||
|
|
|
@ -1119,6 +1119,9 @@ timelines: "Timelines"
|
|||
characterLimit: "Character limit"
|
||||
poweredBy: "Powered by Calckey, part of an interconnected network of communities in the Fediverse."
|
||||
storagePerUser: "Storage per user"
|
||||
showPopup: "Notify users with popup"
|
||||
showWithSparkles: "Show with sparkles"
|
||||
youHaveUnreadAnnouncements: "You have unread announcements"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing
|
||||
|
|
|
@ -980,6 +980,9 @@ preventAiLearningDescription: "投稿したノート、添付した画像など
|
|||
noGraze: "ブラウザの拡張機能「Graze for Mastodon」は、Calckeyの動作を妨げるため、無効にしてください。"
|
||||
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
||||
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
||||
showPopup: "ポップアップを表示してユーザーに知らせる"
|
||||
showWithSparkles: "タイトルをキラキラさせる"
|
||||
youHaveUnreadAnnouncements: "未読のお知らせがあります"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。"
|
||||
|
@ -1064,6 +1067,7 @@ _aboutMisskey:
|
|||
donate: "Calckeyに寄付"
|
||||
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます! 🥰"
|
||||
patrons: "支援者"
|
||||
patronsList: 寄付額ではなく時系列順に並んでいます。上記のリンクから寄付を行ってここにあなたのIDを載せましょう!
|
||||
_nsfw:
|
||||
respect: "閲覧注意のメディアは隠す"
|
||||
ignore: "閲覧注意のメディアを隠さない"
|
||||
|
@ -1375,11 +1379,12 @@ _permissions:
|
|||
_auth:
|
||||
shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?"
|
||||
shareAccessAsk: "アカウントへのアクセスを許可しますか?"
|
||||
permissionAsk: "このアプリケーションは次の権限を要求しています"
|
||||
permissionAsk: "このアプリケーションは次の権限を要求しています:"
|
||||
pleaseGoBack: "アプリケーションに戻り続行してください"
|
||||
callback: "アプリケーションに戻っています"
|
||||
denied: "アクセスを拒否しました"
|
||||
copyAsk: "以下の認証コードをアプリケーションにコピーしてください"
|
||||
copyAsk: "以下の認証コードをアプリケーションにコピーしてください:"
|
||||
allPermissions: 全てのアクセス権
|
||||
_antennaSources:
|
||||
all: "全ての投稿"
|
||||
homeTimeline: "フォローしているユーザーの投稿"
|
||||
|
@ -1453,11 +1458,11 @@ _poll:
|
|||
remainingSeconds: "終了まであと{s}秒"
|
||||
_visibility:
|
||||
public: "公開"
|
||||
publicDescription: "全てのユーザーに公開"
|
||||
publicDescription: "全ての公開タイムラインに配信されます"
|
||||
home: "未収載"
|
||||
homeDescription: "ホームタイムラインのみに公開"
|
||||
followers: "フォロワー"
|
||||
followersDescription: "自分のフォロワーのみに公開"
|
||||
followersDescription: "フォロワーと会話相手のみに公開"
|
||||
specified: "ダイレクト"
|
||||
specifiedDescription: "指定したユーザーのみに公開"
|
||||
localOnly: "ローカルのみ"
|
||||
|
@ -1890,14 +1895,14 @@ hiddenTags: 非表示にするハッシュタグ
|
|||
apps: "アプリ"
|
||||
_experiments:
|
||||
title: 試験的な機能
|
||||
postImportsCaption:
|
||||
postImportsCaption:
|
||||
ユーザーが過去の投稿をCalckey・Misskey・Mastodon・Akkoma・Pleromaからインポートすることを許可します。キューが溜まっているときにインポートするとサーバーに負荷がかかる可能性があります。
|
||||
enablePostImports: 投稿のインポートを有効にする
|
||||
sendModMail: モデレーション通知を送る
|
||||
deleted: 削除済み
|
||||
editNote: 投稿を編集
|
||||
edited: '編集済み: {date} {time}'
|
||||
signupsDisabled:
|
||||
signupsDisabled:
|
||||
現在、このサーバーでは新規登録が一般開放されていません。招待コードをお持ちの場合には、以下の欄に入力してください。招待コードをお持ちでない場合にも、新規登録を開放している他のサーバーには入れますよ!
|
||||
findOtherInstance: 他のサーバーを探す
|
||||
newer: 新しい投稿
|
||||
|
@ -1932,3 +1937,14 @@ isBot: このアカウントはBotです
|
|||
isLocked: このアカウントのフォローは承認制です
|
||||
isAdmin: 管理者
|
||||
isPatron: Calckey 後援者
|
||||
_skinTones:
|
||||
light: ペールオレンジ
|
||||
mediumLight: ミディアムライト
|
||||
medium: ミディアム
|
||||
mediumDark: ミディアムダーク
|
||||
yellow: 黄色
|
||||
dark: 茶色
|
||||
removeReaction: リアクションを取り消す
|
||||
alt: 代替テキスト
|
||||
swipeOnMobile: ページ間のスワイプを有効にする
|
||||
reactionPickerSkinTone: 優先する絵文字のスキン色
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
---
|
||||
_lang_: "Türkçe"
|
||||
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Calckey'e hoş geldiniz.\nMisskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\" oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\nHerkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀."
|
||||
introMisskey: "Açık kaynaklı bir dağıtılmış mikroblog hizmeti olan Calckey'e hoş geldiniz.\n
|
||||
Misskey, neler olup bittiğini paylaşmak ve herkese sizden bahsetmek için \"notlar\"\
|
||||
\ oluşturmanıza olanak tanıyan, açık kaynaklı, dağıtılmış bir mikroblog hizmetidir.\n
|
||||
Herkesin notlarına kendi tepkilerinizi hızlıca eklemek için \"Tepkiler\" özelliğini
|
||||
de kullanabilirsiniz👍.\nYeni bir dünyayı keşfedin🚀."
|
||||
monthAndDay: "{month}Ay {day}Gün"
|
||||
search: "Arama"
|
||||
notifications: "Bildirim"
|
||||
notifications: "Bildirimler"
|
||||
username: "Kullanıcı Adı"
|
||||
password: "Şifre"
|
||||
forgotPassword: "şifremi unuttum"
|
||||
|
@ -11,7 +14,7 @@ ok: "TAMAM"
|
|||
gotIt: "Anladım"
|
||||
cancel: "İptal"
|
||||
enterUsername: "Kullanıcı adınızı giriniz"
|
||||
noNotes: "Notlar mevcut değil."
|
||||
noNotes: "Gönderiler mevcut değil."
|
||||
noNotifications: "Bildirim bulunmuyor"
|
||||
settings: "Ayarlar"
|
||||
basicSettings: "Temel Ayarlar"
|
||||
|
@ -37,7 +40,8 @@ copyContent: "İçeriği kopyala"
|
|||
copyLink: "Bağlantıyı Kopyala"
|
||||
delete: "Sil"
|
||||
deleteAndEdit: "Sil ve yeniden düzenle"
|
||||
deleteAndEditConfirm: "Bu notu silip yeniden düzenlemek istiyor musunuz? Bu nota ilişkin tüm Tepkiler, Yeniden Notlar ve Yanıtlar da silinecektir."
|
||||
deleteAndEditConfirm: "Bu gönderiyi silip yeniden düzenlemek istiyor musunuz? Bu gönderiye
|
||||
ilişkin tüm tepkiler, destekler ve yanıtlar silinecektir."
|
||||
addToList: "Listeye ekle"
|
||||
sendMessage: "Mesaj Gönder"
|
||||
copyUsername: "Kullanıcı Adını Kopyala"
|
||||
|
@ -61,3 +65,19 @@ _deck:
|
|||
_columns:
|
||||
notifications: "Bildirim"
|
||||
tl: "Zaman çizelgesi"
|
||||
searchPlaceholder: Calckey'de Ara
|
||||
reply: Yanıtla
|
||||
jumpToPrevious: Öncekini görüntüle
|
||||
deleted: Silindi
|
||||
editNote: Notu düzenle
|
||||
noThankYou: Hayır, teşekkürler
|
||||
addInstance: Bir sunucu ekle
|
||||
cantFavorite: Favorilere eklenemedi.
|
||||
edited: '{date} tarihinde ve {time} vaktinde düzenlendi'
|
||||
loggingIn: Giriş Yapılıyor
|
||||
save: Kaydet
|
||||
headlineMisskey: Sonsuza kadar ücretsiz, açık kaynak kodlu, merkeziyetsiz sosyal medya
|
||||
platformu! 🚀
|
||||
loadMore: Daha fazla yükle
|
||||
instance: Sunucu
|
||||
fetchingAsApObject: Fedevren'den çekiliyor
|
||||
|
|
|
@ -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: "置中"
|
||||
|
@ -702,7 +702,8 @@ onlineUsersCount: "{n}人正在線上"
|
|||
nUsers: "{n}用戶"
|
||||
nNotes: "{n}貼文"
|
||||
sendErrorReports: "傳送錯誤報告"
|
||||
sendErrorReportsDescription: "啟用後,問題報告將傳送至Calckey開發者以提升軟體品質。\n問題報告可能包括OS版本,瀏覽器類型,行為歷史記錄等。"
|
||||
sendErrorReportsDescription: "開啟後,錯誤出現時將會與 Calckey 分享詳細紀錄,對於 Calckey 的開發會有非常大的幫助。\n
|
||||
這將包括您的操作系統版本、使用的瀏覽器、您在 Calckey 中的活動等資料。"
|
||||
myTheme: "我的佈景主題"
|
||||
backgroundColor: "背景"
|
||||
accentColor: "重點色彩"
|
||||
|
@ -862,7 +863,7 @@ check: "檢查"
|
|||
driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
|
||||
driveCapOverrideCaption: "如果指定0以下的值,就會被取消。"
|
||||
requireAdminForView: "必須以管理者帳號登入才可以檢視。"
|
||||
isSystemAccount: "由系統自動建立與管理的帳號。"
|
||||
isSystemAccount: "該帳號由系統自動創建並運行。 千千萬萬不要審核、編輯、刪除或以其他方式修改此帳戶,否則可能會破壞您的伺服器。"
|
||||
typeToConfirm: "要執行這項操作,請輸入 {x}"
|
||||
deleteAccount: "刪除帳號"
|
||||
document: "文件"
|
||||
|
@ -1089,8 +1090,8 @@ _wordMute:
|
|||
muteWords: "加入靜音文字"
|
||||
muteWordsDescription: "用空格分隔指定AND,用換行分隔指定OR。"
|
||||
muteWordsDescription2: "將關鍵字用斜線括起來表示正規表達式。"
|
||||
softDescription: "隱藏時間軸中指定條件的貼文。"
|
||||
hardDescription: "具有指定條件的貼文將不添加到時間軸。 即使您更改條件,未被添加的貼文也會被排除在外。"
|
||||
softDescription: "隱藏時間線中指定條件的貼文。"
|
||||
hardDescription: "具有指定條件的貼文將不添加到時間線。 即使您更改條件,未被添加的貼文也會被排除在外。"
|
||||
soft: "軟性靜音"
|
||||
hard: "硬性靜音"
|
||||
mutedNotes: "已靜音的貼文"
|
||||
|
@ -1203,16 +1204,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 +1246,8 @@ _permissions:
|
|||
"write:notes": "撰寫或刪除貼文"
|
||||
"read:notifications": "查看通知"
|
||||
"write:notifications": "編輯通知"
|
||||
"read:reactions": "查看情感"
|
||||
"write:reactions": "編輯情感"
|
||||
"read:reactions": "查看反應"
|
||||
"write:reactions": "編輯反應"
|
||||
"write:votes": "投票"
|
||||
"read:pages": "顯示頁面"
|
||||
"write:pages": "編輯頁面"
|
||||
|
@ -1284,7 +1285,7 @@ _weekday:
|
|||
_widgets:
|
||||
memo: "備忘錄"
|
||||
notifications: "通知"
|
||||
timeline: "時間軸"
|
||||
timeline: "時間線"
|
||||
calendar: "行事曆"
|
||||
trends: "發燒貼文"
|
||||
clock: "時鐘"
|
||||
|
@ -1335,7 +1336,7 @@ _visibility:
|
|||
public: "公開"
|
||||
publicDescription: "發布給所有用戶"
|
||||
home: "不在主頁顯示"
|
||||
homeDescription: "僅發送至首頁的時間軸"
|
||||
homeDescription: "僅發送至首頁的時間線"
|
||||
followers: "追隨者"
|
||||
followersDescription: "僅發送至關注者"
|
||||
specified: "指定使用者"
|
||||
|
@ -1403,7 +1404,7 @@ _instanceCharts:
|
|||
_timelines:
|
||||
home: "首頁"
|
||||
local: "本地"
|
||||
social: "社群"
|
||||
social: "社交"
|
||||
global: "公開"
|
||||
recommended: 推薦
|
||||
_pages:
|
||||
|
@ -1726,7 +1727,7 @@ _notification:
|
|||
pollEnded: "問卷調查結束"
|
||||
receiveFollowRequest: "已收到追隨請求"
|
||||
followRequestAccepted: "追隨請求已接受"
|
||||
groupInvited: "加入社群邀請"
|
||||
groupInvited: "群組加入邀請"
|
||||
app: "應用程式通知"
|
||||
_actions:
|
||||
followBack: "回關"
|
||||
|
@ -1755,7 +1756,7 @@ _deck:
|
|||
main: "主列"
|
||||
widgets: "小工具"
|
||||
notifications: "通知"
|
||||
tl: "時間軸"
|
||||
tl: "時間線"
|
||||
antenna: "天線"
|
||||
list: "清單"
|
||||
mentions: "提及"
|
||||
|
@ -1782,11 +1783,11 @@ enterSendsMessage: 在 Messaging 中按 Return 發送消息 (如關閉則是 Ctr
|
|||
migrationConfirm: "您確定要將你的帳戶遷移到 {account} 嗎? 一旦這樣做,你將無法復原,而你將無法再次正常使用您的帳戶。\n另外,請確保你已將此當前帳戶設置為您要遷移的帳戶。"
|
||||
customSplashIconsDescription: 每次用戶加載/重新加載頁面時,以換行符號分隔的自定啟動畫面圖標的網址將隨機顯示。請確保圖片位於靜態網址上,最好所有圖片解析度調整為
|
||||
192x192。
|
||||
accountMoved: '該使用者已移至新帳戶:'
|
||||
accountMoved: '該使用者已遷移至新帳戶:'
|
||||
showAds: 顯示廣告
|
||||
noThankYou: 不用了,謝謝
|
||||
selectInstance: 選擇伺服器
|
||||
enableRecommendedTimeline: 啟用推薦時間軸
|
||||
enableRecommendedTimeline: 啟用推薦時間線
|
||||
antennaInstancesDescription: 分行列出一個伺服器
|
||||
moveTo: 遷移此帳戶到新帳戶
|
||||
moveToLabel: '請輸入你將會遷移到的帳戶:'
|
||||
|
@ -1838,10 +1839,31 @@ pushNotification: 推送通知
|
|||
subscribePushNotification: 啟用推送通知
|
||||
unsubscribePushNotification: 禁用推送通知
|
||||
pushNotificationAlreadySubscribed: 推送通知已經啟用
|
||||
recommendedInstancesDescription: 以每行分隔的推薦服務器出現在推薦的時間軸中。 不要添加 `https://`,只添加域名。
|
||||
searchPlaceholder: 搜尋 Calckey
|
||||
recommendedInstancesDescription: 以每行分隔的推薦伺服器出現在推薦的時間線中。 不要添加 `https://`,只添加域名。
|
||||
searchPlaceholder: 在聯邦網路上搜尋
|
||||
cw: 內容警告
|
||||
selectChannel: 選擇一個頻道
|
||||
newer: 較新
|
||||
older: 較舊
|
||||
jumpToPrevious: 跳到上一個
|
||||
removeReaction: 移除你的反應
|
||||
listsDesc: 清單可以創建一個只有您指定用戶的時間線。 可以從時間線頁面訪問它們。
|
||||
flagSpeakAsCatDescription: 在喵咪模式下你的貼文會被喵化ヾ(•ω•`)o
|
||||
antennasDesc: "天線會顯示符合您設置條件的新貼文!\n 可以從時間線訪問它們。"
|
||||
expandOnNoteClick: 點擊以打開貼文
|
||||
expandOnNoteClickDesc: 如果禁用,您仍然可以通過右鍵單擊菜單或單擊時間戳來打開貼文。
|
||||
hiddenTagsDescription: '列出您希望隱藏趨勢和探索的主題標籤(不帶 #)。 隱藏的主題標籤仍然可以通過其他方式發現。'
|
||||
userSaysSomethingReasonQuote: '{name} 引用了一篇包含 {reason} 的貼文'
|
||||
silencedInstancesDescription: 列出您想要靜音的伺服器的網址。 您列出的伺服器內的帳戶將被視為“沉默”,只能發出追隨請求,如果不追隨則不能提及本地帳戶。
|
||||
這不會影響被阻止的伺服器。
|
||||
video: 影片
|
||||
audio: 音訊
|
||||
sendPushNotificationReadMessageCaption: 包含文本 “{emptyPushNotificationMessage}” 的通知將顯示一小段時間。
|
||||
這可能會增加您設備的電池使用量(如果適用)。
|
||||
channelFederationWarn: 頻道功能尚未與聯邦宇宙連動
|
||||
swipeOnMobile: 允許在頁面之間滑動
|
||||
sendPushNotificationReadMessage: 閱讀相關通知或消息後刪除推送通知
|
||||
image: 圖片
|
||||
seperateRenoteQuote: 分別獨立的轉傳及引用按鈕
|
||||
clipsDesc: 摘錄就像一個可以分享的書籤。 你可以從每個貼文的菜單創建新摘錄或將貼文加入已有的摘錄。
|
||||
noteId: 貼文 ID
|
||||
|
|
23
package.json
23
package.json
|
@ -1,16 +1,16 @@
|
|||
{
|
||||
"name": "calckey",
|
||||
"version": "14.0.0-rc3",
|
||||
"version": "14.0.0-dev60",
|
||||
"codename": "aqua",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/calckey/calckey.git"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.6",
|
||||
"packageManager": "pnpm@8.6.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"rebuild": "pnpm run clean && pnpm node ./scripts/build-greet.js && pnpm -r run build && pnpm run gulp",
|
||||
"build": "pnpm node ./scripts/build-greet.js && pnpm -r run build && pnpm run gulp",
|
||||
"rebuild": "pnpm run clean && pnpm node ./scripts/build-greet.js && pnpm -r --parallel run build && pnpm run gulp",
|
||||
"build": "pnpm node ./scripts/build-greet.js && pnpm -r --parallel run build && pnpm run gulp",
|
||||
"start": "pnpm --filter backend run start",
|
||||
"start:test": "pnpm --filter backend run start:test",
|
||||
"init": "pnpm run migrate",
|
||||
|
@ -21,13 +21,13 @@
|
|||
"watch": "pnpm run dev",
|
||||
"dev": "pnpm node ./scripts/dev.js",
|
||||
"dev:staging": "NODE_OPTIONS=--max_old_space_size=3072 NODE_ENV=development pnpm run build && pnpm run start",
|
||||
"lint": "pnpm -r run lint",
|
||||
"lint": "pnpm -r --parallel run lint",
|
||||
"cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||
"cy:run": "cypress run",
|
||||
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
|
||||
"mocha": "pnpm --filter backend run mocha",
|
||||
"test": "pnpm run mocha",
|
||||
"format": "pnpm -r run format",
|
||||
"format": "pnpm -r --parallel run format",
|
||||
"clean": "pnpm node ./scripts/clean.js",
|
||||
"clean-all": "pnpm node ./scripts/clean-all.js",
|
||||
"cleanall": "pnpm run clean-all"
|
||||
|
@ -36,16 +36,17 @@
|
|||
"chokidar": "^3.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "5.2.0",
|
||||
"@bull-board/ui": "5.2.0",
|
||||
"@bull-board/api": "5.6.0",
|
||||
"@bull-board/ui": "5.6.0",
|
||||
"@napi-rs/cli": "^2.16.1",
|
||||
"@tensorflow/tfjs": "^3.21.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"seedrandom": "^3.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/gulp": "4.0.13",
|
||||
"@types/gulp-rename": "2.0.2",
|
||||
"@types/node": "20.4.1",
|
||||
"chalk": "4.1.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
|
@ -58,6 +59,6 @@
|
|||
"install-peers": "^1.0.4",
|
||||
"rome": "^12.1.3",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"typescript": "4.9.4"
|
||||
"typescript": "5.1.6"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,3 +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 partial Mastodon API compatibility
|
||||
|
|
BIN
packages/backend/assets/transparent.png
Normal file
BIN
packages/backend/assets/transparent.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 B |
|
@ -0,0 +1,21 @@
|
|||
export class AnnouncementPopup1688845537045 {
|
||||
name = "AnnouncementPopup1688845537045";
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "announcement" ADD "showPopup" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "announcement" ADD "isGoodNews" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "announcement" DROP COLUMN "isGoodNews"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "announcement" DROP COLUMN "showPopup"`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ pub enum IdConvertType {
|
|||
|
||||
#[napi]
|
||||
pub fn convert_id(in_id: String, id_convert_type: IdConvertType) -> napi::Result<String> {
|
||||
println!("converting id: {}", in_id);
|
||||
use IdConvertType::*;
|
||||
match id_convert_type {
|
||||
MastodonId => {
|
||||
|
|
|
@ -25,18 +25,16 @@
|
|||
"@tensorflow/tfjs-node": "3.21.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "5.2.0",
|
||||
"@bull-board/koa": "5.2.0",
|
||||
"@bull-board/ui": "5.2.0",
|
||||
"@calckey/megalodon": "5.2.0",
|
||||
"@bull-board/api": "5.6.0",
|
||||
"@bull-board/koa": "5.6.0",
|
||||
"@bull-board/ui": "5.6.0",
|
||||
"@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",
|
||||
"@redocly/openapi-core": "1.0.0-beta.131",
|
||||
"@sinonjs/fake-timers": "9.1.2",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@tensorflow/tfjs": "^4.2.0",
|
||||
|
@ -47,17 +45,17 @@
|
|||
"autobind-decorator": "2.4.0",
|
||||
"autolinker": "4.0.0",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.1277.0",
|
||||
"aws-sdk": "2.1413.0",
|
||||
"axios": "^1.4.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "1.1.5",
|
||||
"blurhash": "2.0.5",
|
||||
"bull": "4.10.4",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"calckey-js": "workspace:*",
|
||||
"cbor": "8.1.0",
|
||||
"chalk": "5.2.0",
|
||||
"chalk": "5.3.0",
|
||||
"chalk-template": "0.4.0",
|
||||
"chokidar": "3.5.3",
|
||||
"chokidar": "^3.5.3",
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
|
@ -70,15 +68,15 @@
|
|||
"got": "12.5.3",
|
||||
"hpagent": "0.1.2",
|
||||
"ioredis": "5.3.2",
|
||||
"ip-cidr": "3.0.11",
|
||||
"ip-cidr": "3.1.0",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "20.0.3",
|
||||
"jsonld": "8.2.0",
|
||||
"jsrsasign": "10.6.1",
|
||||
"koa": "2.13.4",
|
||||
"jsrsasign": "10.8.6",
|
||||
"koa": "2.14.2",
|
||||
"koa-body": "^6.0.1",
|
||||
"koa-bodyparser": "4.3.0",
|
||||
"koa-bodyparser": "4.4.1",
|
||||
"koa-favicon": "2.1.0",
|
||||
"koa-json-body": "5.3.0",
|
||||
"koa-logger": "3.2.1",
|
||||
|
@ -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",
|
||||
|
@ -98,9 +98,9 @@
|
|||
"nsfwjs": "2.4.2",
|
||||
"oauth": "^0.10.0",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "^9.1.2",
|
||||
"otpauth": "^9.1.3",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.11.0",
|
||||
"pg": "8.11.1",
|
||||
"private-ip": "2.3.4",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
|
@ -110,7 +110,7 @@
|
|||
"qs": "6.11.2",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.19.0",
|
||||
"re2": "1.19.1",
|
||||
"redis-lock": "0.1.4",
|
||||
"redis-semaphore": "5.3.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
|
@ -119,7 +119,7 @@
|
|||
"rss-parser": "3.13.0",
|
||||
"sanitize-html": "2.10.0",
|
||||
"seedrandom": "^3.0.5",
|
||||
"semver": "7.5.1",
|
||||
"semver": "7.5.4",
|
||||
"sharp": "0.32.1",
|
||||
"sonic-channel": "^1.3.1",
|
||||
"stringz": "2.1.0",
|
||||
|
@ -130,27 +130,26 @@
|
|||
"tinycolor2": "1.5.2",
|
||||
"tmp": "0.2.1",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.11",
|
||||
"typeorm": "0.3.17",
|
||||
"ulid": "2.3.0",
|
||||
"uuid": "9.0.0",
|
||||
"web-push": "3.6.1",
|
||||
"web-push": "3.6.3",
|
||||
"websocket": "1.0.34",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.62",
|
||||
"@swc/core": "^1.3.68",
|
||||
"@types/adm-zip": "^0.5.0",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.15.9",
|
||||
"@types/cbor": "6.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/fluent-ffmpeg": "2.1.20",
|
||||
"@types/fluent-ffmpeg": "2.1.21",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsdom": "20.0.1",
|
||||
"@types/jsonld": "1.5.8",
|
||||
"@types/jsrsasign": "10.5.4",
|
||||
"@types/koa": "2.13.5",
|
||||
"@types/jsdom": "21.1.1",
|
||||
"@types/jsonld": "1.5.9",
|
||||
"@types/jsrsasign": "10.5.8",
|
||||
"@types/koa": "2.13.6",
|
||||
"@types/koa-bodyparser": "4.3.10",
|
||||
"@types/koa-cors": "0.0.2",
|
||||
"@types/koa-favicon": "2.0.21",
|
||||
|
@ -169,7 +168,7 @@
|
|||
"@types/probe-image-size": "^7.2.0",
|
||||
"@types/pug": "2.0.6",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.5.0",
|
||||
"@types/qrcode": "1.5.1",
|
||||
"@types/qs": "6.9.7",
|
||||
"@types/random-seed": "0.3.3",
|
||||
"@types/ratelimiter": "3.4.4",
|
||||
|
@ -177,17 +176,16 @@
|
|||
"@types/rename": "1.0.4",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
"@types/semver": "7.5.0",
|
||||
"@types/sharp": "0.31.1",
|
||||
"@types/sinonjs__fake-timers": "8.1.2",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/tmp": "0.2.3",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/uuid": "9.0.2",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@types/ws": "8.5.5",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint": "^8.44.0",
|
||||
"execa": "6.1.0",
|
||||
"json5": "2.2.3",
|
||||
"json5-loader": "4.0.1",
|
||||
|
@ -195,11 +193,11 @@
|
|||
"pug": "3.0.2",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
"ts-loader": "9.4.3",
|
||||
"ts-loader": "9.4.4",
|
||||
"ts-node": "10.9.1",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.1.3",
|
||||
"webpack": "^5.85.1",
|
||||
"typescript": "5.1.6",
|
||||
"webpack": "^5.88.1",
|
||||
"ws": "8.13.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -21,8 +21,9 @@ export default function () {
|
|||
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50));
|
||||
});
|
||||
|
||||
const meta = fetchMeta();
|
||||
if (!meta.enableServerMachineStats) return;
|
||||
fetchMeta().then((meta) => {
|
||||
if (!meta.enableServerMachineStats) return;
|
||||
});
|
||||
|
||||
async function tick() {
|
||||
const cpu = await cpuUsage();
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -20,5 +20,9 @@ export function nyaize(text: string): string {
|
|||
)
|
||||
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, "다냥")
|
||||
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, "냥")
|
||||
// el-GR
|
||||
.replaceAll("να", "νια")
|
||||
.replaceAll("ΝΑ", "ΝΙΑ")
|
||||
.replaceAll("Να", "Νια")
|
||||
);
|
||||
}
|
||||
|
|
|
@ -36,6 +36,16 @@ export class Announcement {
|
|||
})
|
||||
public imageUrl: string | null;
|
||||
|
||||
@Column("boolean", {
|
||||
default: false,
|
||||
})
|
||||
public showPopup: boolean;
|
||||
|
||||
@Column("boolean", {
|
||||
default: false,
|
||||
})
|
||||
public isGoodNews: boolean;
|
||||
|
||||
constructor(data: Partial<Announcement>) {
|
||||
if (data == null) return;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type Bull from "bull";
|
||||
import type { DoneCallback } from "bull";
|
||||
|
||||
import { queueLogger } from "../../logger.js";
|
||||
import { Notes } from "@/models/index.js";
|
||||
|
@ -11,7 +12,7 @@ const logger = queueLogger.createSubLogger("index-all-notes");
|
|||
|
||||
export default async function indexAllNotes(
|
||||
job: Bull.Job<Record<string, unknown>>,
|
||||
done: () => void,
|
||||
done: DoneCallback,
|
||||
): Promise<void> {
|
||||
logger.info("Indexing all notes...");
|
||||
|
||||
|
@ -20,7 +21,7 @@ export default async function indexAllNotes(
|
|||
let total: number = (job.data.total as number) ?? 0;
|
||||
|
||||
let running = true;
|
||||
const take = 100000;
|
||||
const take = 10000;
|
||||
const batch = 100;
|
||||
while (running) {
|
||||
logger.info(
|
||||
|
@ -41,13 +42,14 @@ export default async function indexAllNotes(
|
|||
},
|
||||
relations: ["user"],
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
logger.error(`Failed to query notes ${e}`);
|
||||
continue;
|
||||
done(e);
|
||||
break;
|
||||
}
|
||||
|
||||
if (notes.length === 0) {
|
||||
job.progress(100);
|
||||
await job.progress(100);
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
|
@ -55,7 +57,7 @@ export default async function indexAllNotes(
|
|||
try {
|
||||
const count = await Notes.count();
|
||||
total = count;
|
||||
job.update({ indexedCount, cursor, total });
|
||||
await job.update({ indexedCount, cursor, total });
|
||||
} catch (e) {}
|
||||
|
||||
for (let i = 0; i < notes.length; i += batch) {
|
||||
|
@ -69,12 +71,12 @@ export default async function indexAllNotes(
|
|||
|
||||
indexedCount += chunk.length;
|
||||
const pct = (indexedCount / total) * 100;
|
||||
job.update({ indexedCount, cursor, total });
|
||||
job.progress(+pct.toFixed(1));
|
||||
await job.update({ indexedCount, cursor, total });
|
||||
await job.progress(+pct.toFixed(1));
|
||||
logger.info(`Indexed notes ${indexedCount}/${total ? total : "?"}`);
|
||||
}
|
||||
cursor = notes[notes.length - 1].id;
|
||||
job.update({ indexedCount, cursor, total });
|
||||
await job.update({ indexedCount, cursor, total });
|
||||
|
||||
if (notes.length < take) {
|
||||
running = false;
|
||||
|
|
|
@ -47,6 +47,16 @@ export const meta = {
|
|||
optional: false,
|
||||
nullable: true,
|
||||
},
|
||||
showPopup: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
},
|
||||
isGoodNews: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
@ -57,6 +67,8 @@ export const paramDef = {
|
|||
title: { type: "string", minLength: 1 },
|
||||
text: { type: "string", minLength: 1 },
|
||||
imageUrl: { type: "string", nullable: true, minLength: 1 },
|
||||
showPopup: { type: "boolean" },
|
||||
isGoodNews: { type: "boolean" },
|
||||
},
|
||||
required: ["title", "text", "imageUrl"],
|
||||
} as const;
|
||||
|
@ -69,6 +81,8 @@ export default define(meta, paramDef, async (ps) => {
|
|||
title: ps.title,
|
||||
text: ps.text,
|
||||
imageUrl: ps.imageUrl,
|
||||
showPopup: ps.showPopup ?? false,
|
||||
isGoodNews: ps.isGoodNews ?? false,
|
||||
}).then((x) => Announcements.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
return Object.assign({}, announcement, {
|
||||
|
|
|
@ -57,6 +57,16 @@ export const meta = {
|
|||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
showPopup: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
},
|
||||
isGoodNews: {
|
||||
type: "boolean",
|
||||
optional: true,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -100,5 +110,7 @@ export default define(meta, paramDef, async (ps) => {
|
|||
text: announcement.text,
|
||||
imageUrl: announcement.imageUrl,
|
||||
reads: reads.get(announcement)!,
|
||||
showPopup: announcement.showPopup,
|
||||
isGoodNews: announcement.isGoodNews,
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -24,6 +24,8 @@ export const paramDef = {
|
|||
title: { type: "string", minLength: 1 },
|
||||
text: { type: "string", minLength: 1 },
|
||||
imageUrl: { type: "string", nullable: true, minLength: 1 },
|
||||
showPopup: { type: "boolean" },
|
||||
isGoodNews: { type: "boolean" },
|
||||
},
|
||||
required: ["id", "title", "text", "imageUrl"],
|
||||
} as const;
|
||||
|
@ -38,5 +40,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
title: ps.title,
|
||||
text: ps.text,
|
||||
imageUrl: ps.imageUrl,
|
||||
showPopup: ps.showPopup ?? false,
|
||||
isGoodNews: ps.isGoodNews ?? false,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -56,6 +56,16 @@ export const meta = {
|
|||
optional: true,
|
||||
nullable: false,
|
||||
},
|
||||
showPopup: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
isGoodNews: {
|
||||
type: "boolean",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -29,6 +29,11 @@ export const meta = {
|
|||
code: "TOO_MANY_ANTENNAS",
|
||||
id: "c3a5a51e-04d4-11ee-be56-0242ac120002",
|
||||
},
|
||||
noKeywords: {
|
||||
message: "No keywords",
|
||||
code: "NO_KEYWORDS",
|
||||
id: "aa975b74-1ddb-11ee-be56-0242ac120002",
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
|
@ -100,6 +105,7 @@ export const paramDef = {
|
|||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
if (user.movedToUri != null) throw new ApiError(meta.errors.noSuchUserGroup);
|
||||
if (ps.keywords.length === 0) throw new ApiError(meta.errors.noKeywords);
|
||||
let userList;
|
||||
let userGroupJoining;
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -33,7 +33,7 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
if (ps.key !== "reactions") return;
|
||||
if (ps.key !== "reactions" && ps.key !== "defaultNoteVisibility") return;
|
||||
const query = RegistryItems.createQueryBuilder("item")
|
||||
.where("item.domain IS NULL")
|
||||
.andWhere("item.userId = :userId", { userId: user.id })
|
||||
|
|
|
@ -21,6 +21,7 @@ export const meta = {
|
|||
message: "No such note.",
|
||||
code: "NO_SUCH_NOTE",
|
||||
id: "24fcbfc6-2e37-42b6-8388-c29b3861a08d",
|
||||
httpStatusCode: 404,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -42,7 +42,10 @@ export default define(meta, paramDef, async (ps) => {
|
|||
.then((response) => response.json())
|
||||
.catch(() => {
|
||||
const staticPatrons = JSON.parse(
|
||||
fs.readFileSync(`${_dirname}/../../../../../../patrons.json`, "utf-8"),
|
||||
fs.readFileSync(
|
||||
`${_dirname}/../../../../../../patrons.json`,
|
||||
"utf-8",
|
||||
),
|
||||
);
|
||||
patrons = cachedPatrons ? JSON.parse(cachedPatrons) : staticPatrons;
|
||||
});
|
||||
|
|
|
@ -49,7 +49,7 @@ export const paramDef = {
|
|||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const profile = await UserProfiles.findOneByOrFail({ userId: ps.userId });
|
||||
|
||||
if (me == null || (me.id !== ps.userId && !profile.publicReactions)) {
|
||||
if (me.id !== ps.userId && !profile.publicReactions) {
|
||||
throw new ApiError(meta.errors.reactionsNotPublic);
|
||||
}
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => {
|
|||
ctx.status = 401;
|
||||
return;
|
||||
}
|
||||
const data = await client.uploadMedia(multipartData);
|
||||
const data = await client.uploadMedia(multipartData, ctx.request.body);
|
||||
ctx.body = convertAttachment(data.data as Entity.Attachment);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Router from "@koa/router";
|
||||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import megalodon, { MegalodonInterface } from "megalodon";
|
||||
import { apiAuthMastodon } from "./endpoints/auth.js";
|
||||
import { apiAccountMastodon } from "./endpoints/account.js";
|
||||
import { apiStatusMastodon } from "./endpoints/status.js";
|
||||
|
@ -18,11 +18,7 @@ export function getClient(
|
|||
const accessTokenArr = authorization?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator(
|
||||
"misskey",
|
||||
BASE_URL,
|
||||
accessToken,
|
||||
) as MegalodonInterface;
|
||||
const client = generator(BASE_URL, accessToken) as MegalodonInterface;
|
||||
return client;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { Entity } from "@calckey/megalodon";
|
||||
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) {
|
||||
|
@ -21,6 +23,9 @@ export function convertFilter(filter: Entity.Filter) {
|
|||
export function convertList(list: Entity.List) {
|
||||
return simpleConvert(list);
|
||||
}
|
||||
export function convertFeaturedTag(tag: Entity.FeaturedTag) {
|
||||
return simpleConvert(tag);
|
||||
}
|
||||
|
||||
export function convertNotification(notification: Entity.Notification) {
|
||||
notification.account = convertAccount(notification.account);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { argsToBools, convertTimelinesArgsId, limitToInt } from "./timeline.js";
|
|||
import { convertId, IdType } from "../../index.js";
|
||||
import {
|
||||
convertAccount,
|
||||
convertFeaturedTag,
|
||||
convertList,
|
||||
convertRelationship,
|
||||
convertStatus,
|
||||
|
@ -42,12 +43,12 @@ export function apiAccountMastodon(router: Router): void {
|
|||
acct.url = `${BASE_URL}/@${acct.url}`;
|
||||
acct.note = acct.note || "";
|
||||
acct.avatar_static = acct.avatar;
|
||||
acct.header = acct.header || "https://http.cat/404";
|
||||
acct.header_static = acct.header || "https://http.cat/404";
|
||||
acct.header = acct.header || "/static-assets/transparent.png";
|
||||
acct.header_static = acct.header || "/static-assets/transparent.png";
|
||||
acct.source = {
|
||||
note: acct.note,
|
||||
fields: acct.fields,
|
||||
privacy: "public",
|
||||
privacy: await client.getDefaultPostPrivacy(),
|
||||
sensitive: false,
|
||||
language: "",
|
||||
};
|
||||
|
@ -164,6 +165,25 @@ export function apiAccountMastodon(router: Router): void {
|
|||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/featured_tags",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getAccountFeaturedTags(
|
||||
convertId(ctx.params.id, IdType.CalckeyId),
|
||||
);
|
||||
ctx.body = data.data.map((tag) => convertFeaturedTag(tag));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/accounts/:id/followers",
|
||||
async (ctx) => {
|
||||
|
@ -342,6 +362,34 @@ export function apiAccountMastodon(router: Router): void {
|
|||
}
|
||||
},
|
||||
);
|
||||
router.get("/v1/featured_tags", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getFeaturedTags();
|
||||
ctx.body = data.data.map((tag) => convertFeaturedTag(tag));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/followed_tags", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getFollowedTags();
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/bookmarks", async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import megalodon, { MegalodonInterface } from "megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { koaBody } from "koa-body";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import megalodon, { MegalodonInterface } from "megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import { IdType, convertId } from "../../index.js";
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Entity } from "@calckey/megalodon";
|
||||
import { Entity } from "megalodon";
|
||||
import config from "@/config/index.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { Users, Notes } from "@/models/index.js";
|
||||
import { IsNull, MoreThan } from "typeorm";
|
||||
|
@ -17,14 +18,14 @@ export async function getInstance(response: Entity.Instance) {
|
|||
response.description ||
|
||||
"This is a vanilla Calckey Instance. It doesnt seem to have a description. BTW you are using the Mastodon api to access this server :)",
|
||||
email: response.email || "",
|
||||
version: "3.0.0 compatible (3.5+ Calckey)", //I hope this version string is correct, we will need to test it.
|
||||
version: `3.0.0 (compatible; Calckey ${config.version})`,
|
||||
urls: response.urls,
|
||||
stats: {
|
||||
user_count: await totalUsers,
|
||||
status_count: await totalStatuses,
|
||||
domain_count: response.stats.domain_count,
|
||||
},
|
||||
thumbnail: response.thumbnail || "https://http.cat/404",
|
||||
thumbnail: response.thumbnail || "/static-assets/transparent.png",
|
||||
languages: meta.langs,
|
||||
registrations: !meta.disableRegistration || response.registrations,
|
||||
approval_required: !response.registrations,
|
||||
|
@ -96,8 +97,8 @@ export async function getInstance(response: Entity.Instance) {
|
|||
url: `${response.uri}/`,
|
||||
avatar: `${response.uri}/static-assets/badges/info.png`,
|
||||
avatar_static: `${response.uri}/static-assets/badges/info.png`,
|
||||
header: "https://http.cat/404",
|
||||
header_static: "https://http.cat/404",
|
||||
header: "/static-assets/transparent.png",
|
||||
header_static: "/static-assets/transparent.png",
|
||||
followers_count: -1,
|
||||
following_count: 0,
|
||||
statuses_count: 0,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import megalodon, { MegalodonInterface } from "megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { koaBody } from "koa-body";
|
||||
import { convertId, IdType } from "../../index.js";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import { convertTimelinesArgsId, toTextWithReaction } from "./timeline.js";
|
||||
import { convertTimelinesArgsId } from "./timeline.js";
|
||||
import { convertNotification } from "../converters.js";
|
||||
function toLimitToInt(q: any) {
|
||||
if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10);
|
||||
|
@ -25,10 +25,6 @@ export function apiNotificationsMastodon(router: Router): void {
|
|||
n = convertNotification(n);
|
||||
if (n.type !== "follow" && n.type !== "follow_request") {
|
||||
if (n.type === "reaction") n.type = "favourite";
|
||||
n.status = toTextWithReaction(
|
||||
n.status ? [n.status] : [],
|
||||
ctx.hostname,
|
||||
)[0];
|
||||
return n;
|
||||
} else {
|
||||
return n;
|
||||
|
@ -52,11 +48,13 @@ export function apiNotificationsMastodon(router: Router): void {
|
|||
convertId(ctx.params.id, IdType.CalckeyId),
|
||||
);
|
||||
const data = convertNotification(dataRaw.data);
|
||||
if (data.type !== "follow" && data.type !== "follow_request") {
|
||||
if (data.type === "reaction") data.type = "favourite";
|
||||
ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0];
|
||||
} else {
|
||||
ctx.body = data;
|
||||
ctx.body = data;
|
||||
if (
|
||||
data.type !== "follow" &&
|
||||
data.type !== "follow_request" &&
|
||||
data.type === "reaction"
|
||||
) {
|
||||
data.type = "favourite";
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import megalodon, { MegalodonInterface } from "megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import axios from "axios";
|
||||
import { Converter } from "@calckey/megalodon";
|
||||
import { Converter } from "megalodon";
|
||||
import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
|
||||
import { convertAccount, convertStatus } from "../converters.js";
|
||||
|
||||
|
@ -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;
|
||||
|
@ -103,7 +108,7 @@ async function getHighlight(
|
|||
i: accessToken,
|
||||
});
|
||||
const data: MisskeyEntity.Note[] = api.data;
|
||||
return data.map((note) => Converter.note(note, domain));
|
||||
return data.map((note) => new Converter(BASE_URL).note(note, domain));
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
console.log(e.response.data);
|
||||
|
@ -131,7 +136,7 @@ async function getFeaturedUser(
|
|||
return data.map((u) => {
|
||||
return {
|
||||
source: "past_interactions",
|
||||
account: Converter.userDetail(u, host),
|
||||
account: new Converter(BASE_URL).userDetail(u, host),
|
||||
};
|
||||
});
|
||||
} catch (e: any) {
|
||||
|
|
|
@ -59,9 +59,33 @@ export function apiStatusMastodon(router: Router): void {
|
|||
}
|
||||
if (!body.media_ids) body.media_ids = undefined;
|
||||
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
|
||||
if (body.media_ids) {
|
||||
body.media_ids = (body.media_ids as string[]).map((p) =>
|
||||
convertId(p, IdType.CalckeyId),
|
||||
);
|
||||
}
|
||||
const { sensitive } = body;
|
||||
body.sensitive =
|
||||
typeof sensitive === "string" ? sensitive === "true" : sensitive;
|
||||
|
||||
if (body.poll) {
|
||||
if (
|
||||
body.poll.expires_in != null &&
|
||||
typeof body.poll.expires_in === "string"
|
||||
)
|
||||
body.poll.expires_in = parseInt(body.poll.expires_in);
|
||||
if (
|
||||
body.poll.multiple != null &&
|
||||
typeof body.poll.multiple === "string"
|
||||
)
|
||||
body.poll.multiple = body.poll.multiple == "true";
|
||||
if (
|
||||
body.poll.hide_totals != null &&
|
||||
typeof body.poll.hide_totals === "string"
|
||||
)
|
||||
body.poll.hide_totals = body.poll.hide_totals == "true";
|
||||
}
|
||||
|
||||
const data = await client.postStatus(text, body);
|
||||
ctx.body = convertStatus(data.data);
|
||||
} catch (e: any) {
|
||||
|
@ -81,7 +105,7 @@ export function apiStatusMastodon(router: Router): void {
|
|||
ctx.body = convertStatus(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
ctx.status = ctx.status == 404 ? 404 : 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
|
@ -118,27 +142,7 @@ export function apiStatusMastodon(router: Router): void {
|
|||
id,
|
||||
convertTimelinesArgsId(limitToInt(ctx.query as any)),
|
||||
);
|
||||
const status = await client.getStatus(id);
|
||||
let reqInstance = axios.create({
|
||||
headers: {
|
||||
Authorization: ctx.headers.authorization,
|
||||
},
|
||||
});
|
||||
const reactionsAxios = await reqInstance.get(
|
||||
`${BASE_URL}/api/notes/reactions?noteId=${id}`,
|
||||
);
|
||||
const reactions: IReaction[] = reactionsAxios.data;
|
||||
const text = reactions
|
||||
.map((r) => `${r.type.replace("@.", "")} ${r.user.username}`)
|
||||
.join("<br />");
|
||||
data.data.descendants.unshift(
|
||||
statusModel(
|
||||
status.data.id,
|
||||
status.data.account.id,
|
||||
status.data.emojis,
|
||||
text,
|
||||
),
|
||||
);
|
||||
|
||||
data.data.ancestors = data.data.ancestors.map((status) =>
|
||||
convertStatus(status),
|
||||
);
|
||||
|
@ -153,6 +157,24 @@ export function apiStatusMastodon(router: Router): void {
|
|||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/history",
|
||||
async (ctx) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getStatusHistory(
|
||||
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.get<{ Params: { id: string } }>(
|
||||
"/v1/statuses/:id/reblogged_by",
|
||||
async (ctx) => {
|
||||
|
@ -174,7 +196,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 } }>(
|
||||
|
@ -421,65 +455,3 @@ async function getFirstReaction(
|
|||
return react;
|
||||
}
|
||||
}
|
||||
|
||||
export function statusModel(
|
||||
id: string | null,
|
||||
acctId: string | null,
|
||||
emojis: MastodonEntity.Emoji[],
|
||||
content: string,
|
||||
) {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: "9atm5frjhb",
|
||||
uri: "https://http.cat/404", // ""
|
||||
url: "https://http.cat/404", // "",
|
||||
account: {
|
||||
id: "9arzuvv0sw",
|
||||
username: "Reactions",
|
||||
acct: "Reactions",
|
||||
display_name: "Reactions to this post",
|
||||
locked: false,
|
||||
created_at: now,
|
||||
followers_count: 0,
|
||||
following_count: 0,
|
||||
statuses_count: 0,
|
||||
note: "",
|
||||
url: "https://http.cat/404",
|
||||
avatar: "/static-assets/badges/info.png",
|
||||
avatar_static: "/static-assets/badges/info.png",
|
||||
header: "https://http.cat/404", // ""
|
||||
header_static: "https://http.cat/404", // ""
|
||||
emojis: [],
|
||||
fields: [],
|
||||
moved: null,
|
||||
bot: false,
|
||||
},
|
||||
in_reply_to_id: id,
|
||||
in_reply_to_account_id: acctId,
|
||||
reblog: null,
|
||||
content: `<p>${content}</p>`,
|
||||
plain_content: null,
|
||||
created_at: now,
|
||||
emojis: emojis,
|
||||
replies_count: 0,
|
||||
reblogs_count: 0,
|
||||
favourites_count: 0,
|
||||
favourited: false,
|
||||
reblogged: false,
|
||||
muted: false,
|
||||
sensitive: false,
|
||||
spoiler_text: "",
|
||||
visibility: "public" as const,
|
||||
media_attachments: [],
|
||||
mentions: [],
|
||||
tags: [],
|
||||
card: null,
|
||||
poll: null,
|
||||
application: null,
|
||||
language: null,
|
||||
pinned: false,
|
||||
emoji_reactions: [],
|
||||
bookmarked: false,
|
||||
quote: null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import Router from "@koa/router";
|
||||
import megalodon, { Entity, MegalodonInterface } from "@calckey/megalodon";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import { statusModel } from "./status.js";
|
||||
import Autolinker from "autolinker";
|
||||
import { ParsedUrlQuery } from "querystring";
|
||||
import { convertAccount, convertList, convertStatus } from "../converters.js";
|
||||
import { convertId, IdType } from "../../index.js";
|
||||
|
@ -41,66 +38,6 @@ export function convertTimelinesArgsId(q: ParsedUrlQuery) {
|
|||
return q;
|
||||
}
|
||||
|
||||
export function toTextWithReaction(status: Entity.Status[], host: string) {
|
||||
return status.map((t) => {
|
||||
if (!t) return statusModel(null, null, [], "no content");
|
||||
t.quote = null as any;
|
||||
if (!t.emoji_reactions) return t;
|
||||
if (t.reblog) t.reblog = toTextWithReaction([t.reblog], host)[0];
|
||||
const reactions = t.emoji_reactions.map((r) => {
|
||||
const emojiNotation = r.url ? `:${r.name.replace("@.", "")}:` : r.name;
|
||||
return `${emojiNotation} (${r.count}${r.me ? `* ` : ""})`;
|
||||
});
|
||||
const reaction = t.emoji_reactions as Entity.Reaction[];
|
||||
const emoji = t.emojis || [];
|
||||
for (const r of reaction) {
|
||||
if (!r.url) continue;
|
||||
emoji.push({
|
||||
shortcode: r.name,
|
||||
url: r.url,
|
||||
static_url: r.url,
|
||||
visible_in_picker: true,
|
||||
category: "",
|
||||
});
|
||||
}
|
||||
const isMe = reaction.findIndex((r) => r.me) > -1;
|
||||
const total = reaction.reduce((sum, reaction) => sum + reaction.count, 0);
|
||||
t.favourited = isMe;
|
||||
t.favourites_count = total;
|
||||
t.emojis = emoji;
|
||||
t.content = `<p>${autoLinker(t.content, host)}</p><p>${reactions.join(
|
||||
", ",
|
||||
)}</p>`;
|
||||
return t;
|
||||
});
|
||||
}
|
||||
export function autoLinker(input: string, host: string) {
|
||||
return Autolinker.link(input, {
|
||||
hashtag: "twitter",
|
||||
mention: "twitter",
|
||||
email: false,
|
||||
stripPrefix: false,
|
||||
replaceFn: function (match) {
|
||||
switch (match.type) {
|
||||
case "url":
|
||||
return true;
|
||||
case "mention":
|
||||
console.log("Mention: ", match.getMention());
|
||||
console.log("Mention Service Name: ", match.getServiceName());
|
||||
return `<a href="https://${host}/@${encodeURIComponent(
|
||||
match.getMention(),
|
||||
)}" target="_blank">@${match.getMention()}</a>`;
|
||||
case "hashtag":
|
||||
console.log("Hashtag: ", match.getHashtag());
|
||||
return `<a href="https://${host}/tags/${encodeURIComponent(
|
||||
match.getHashtag(),
|
||||
)}" target="_blank">#${match.getHashtag()}</a>`;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function apiTimelineMastodon(router: Router): void {
|
||||
router.get("/v1/timelines/public", async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
|
@ -108,15 +45,15 @@ 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))),
|
||||
);
|
||||
let resp = data.data.map((status) => convertStatus(status));
|
||||
ctx.body = toTextWithReaction(resp, ctx.hostname);
|
||||
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);
|
||||
console.error(e.response.data);
|
||||
|
@ -135,8 +72,7 @@ export function apiTimelineMastodon(router: Router): void {
|
|||
ctx.params.hashtag,
|
||||
convertTimelinesArgsId(argsToBools(limitToInt(ctx.query))),
|
||||
);
|
||||
let resp = data.data.map((status) => convertStatus(status));
|
||||
ctx.body = toTextWithReaction(resp, ctx.hostname);
|
||||
ctx.body = data.data.map((status) => convertStatus(status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
|
@ -153,8 +89,7 @@ export function apiTimelineMastodon(router: Router): void {
|
|||
const data = await client.getHomeTimeline(
|
||||
convertTimelinesArgsId(limitToInt(ctx.query)),
|
||||
);
|
||||
let resp = data.data.map((status) => convertStatus(status));
|
||||
ctx.body = toTextWithReaction(resp, ctx.hostname);
|
||||
ctx.body = data.data.map((status) => convertStatus(status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
|
@ -173,8 +108,7 @@ export function apiTimelineMastodon(router: Router): void {
|
|||
convertId(ctx.params.listId, IdType.CalckeyId),
|
||||
convertTimelinesArgsId(limitToInt(ctx.query)),
|
||||
);
|
||||
let resp = data.data.map((status) => convertStatus(status));
|
||||
ctx.body = toTextWithReaction(resp, ctx.hostname);
|
||||
ctx.body = data.data.map((status) => convertStatus(status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
|
|
|
@ -25,9 +25,8 @@ import { readNotification } from "../common/read-notification.js";
|
|||
import channels from "./channels/index.js";
|
||||
import type Channel from "./channel.js";
|
||||
import type { StreamEventEmitter, StreamMessages } from "./types.js";
|
||||
import { Converter } from "@calckey/megalodon";
|
||||
import { Converter } from "megalodon";
|
||||
import { getClient } from "../mastodon/ApiMastodonCompatibleService.js";
|
||||
import { toTextWithReaction } from "../mastodon/endpoints/timeline.js";
|
||||
|
||||
/**
|
||||
* Main stream connection
|
||||
|
@ -400,12 +399,7 @@ export default class Connection {
|
|||
JSON.stringify({
|
||||
stream: [payload.id],
|
||||
event: "update",
|
||||
payload: JSON.stringify(
|
||||
toTextWithReaction(
|
||||
[Converter.note(payload.body, this.host)],
|
||||
this.host,
|
||||
)[0],
|
||||
),
|
||||
payload: JSON.stringify(Converter.note(payload.body, this.host)),
|
||||
}),
|
||||
);
|
||||
this.onSubscribeNote({
|
||||
|
@ -415,7 +409,7 @@ export default class Connection {
|
|||
// reaction
|
||||
const client = getClient(this.host, this.accessToken);
|
||||
client.getStatus(payload.id).then((data) => {
|
||||
const newPost = toTextWithReaction([data.data], this.host);
|
||||
const newPost = [data.data];
|
||||
const targetPost = newPost[0];
|
||||
for (const stream of this.currentSubscribe) {
|
||||
this.wsConnection.send(
|
||||
|
@ -442,10 +436,6 @@ export default class Connection {
|
|||
if (payload.id === "user") {
|
||||
const body = Converter.notification(payload.body, this.host);
|
||||
if (body.type === "reaction") body.type = "favourite";
|
||||
body.status = toTextWithReaction(
|
||||
body.status ? [body.status] : [],
|
||||
"",
|
||||
)[0];
|
||||
this.wsConnection.send(
|
||||
JSON.stringify({
|
||||
stream: ["user"],
|
||||
|
|
|
@ -22,7 +22,7 @@ import { createTemp } from "@/misc/create-temp.js";
|
|||
import { publishMainStream } from "@/services/stream.js";
|
||||
import * as Acct from "@/misc/acct.js";
|
||||
import { envOption } from "@/env.js";
|
||||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import megalodon, { MegalodonInterface } from "megalodon";
|
||||
import activityPub from "./activitypub.js";
|
||||
import nodeinfo from "./nodeinfo.js";
|
||||
import wellKnown from "./well-known.js";
|
||||
|
@ -166,7 +166,7 @@ mastoRouter.post("/oauth/token", async (ctx) => {
|
|||
let client_id: any = body.client_id;
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator("misskey", BASE_URL, null) as MegalodonInterface;
|
||||
const client = generator(BASE_URL, null) as MegalodonInterface;
|
||||
let m = null;
|
||||
let token = null;
|
||||
if (body.code) {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"@syuilo/aiscript": "0.11.1",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/gulp": "4.0.11",
|
||||
"@types/gulp": "4.0.13",
|
||||
"@types/gulp-rename": "2.0.2",
|
||||
"@types/katex": "0.16.0",
|
||||
"@types/matter-js": "0.18.2",
|
||||
|
@ -29,8 +29,8 @@
|
|||
"@vue/compiler-sfc": "3.3.4",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "5.0.2",
|
||||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.19.1",
|
||||
"blurhash": "2.0.5",
|
||||
"broadcast-channel": "5.1.0",
|
||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer",
|
||||
"calckey-js": "workspace:*",
|
||||
"chart.js": "4.3.0",
|
||||
|
@ -39,51 +39,52 @@
|
|||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"city-timezones": "^1.2.1",
|
||||
"compare-versions": "5.0.3",
|
||||
"compare-versions": "6.0.0",
|
||||
"cropperjs": "2.0.0-beta.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
"date-fns": "2.30.0",
|
||||
"emojilib": "github:thatonecalculator/emojilib",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eventemitter3": "4.0.7",
|
||||
"focus-trap": "^7.4.3",
|
||||
"eventemitter3": "5.0.1",
|
||||
"fast-blurhash": "^1.1.2",
|
||||
"focus-trap": "^7.5.2",
|
||||
"focus-trap-vue": "^4.0.2",
|
||||
"gsap": "^3.11.5",
|
||||
"gsap": "^3.12.2",
|
||||
"idb-keyval": "6.2.1",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"json5": "2.2.3",
|
||||
"katex": "0.16.7",
|
||||
"katex": "0.16.8",
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.23.3",
|
||||
"photoswipe": "5.3.7",
|
||||
"photoswipe": "5.3.8",
|
||||
"prettier": "3.0.0",
|
||||
"prettier-plugin-vue": "1.1.6",
|
||||
"prismjs": "1.29.0",
|
||||
"punycode": "2.1.1",
|
||||
"punycode": "2.3.0",
|
||||
"querystring": "0.2.1",
|
||||
"rndstr": "1.0.0",
|
||||
"rollup": "3.23.1",
|
||||
"rollup": "3.26.2",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.62.1",
|
||||
"sass": "1.63.6",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"swiper": "9.3.2",
|
||||
"swiper": "10.0.4",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.146.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.5.2",
|
||||
"tsc-alias": "1.8.6",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tsc-alias": "1.8.7",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "5.1.3",
|
||||
"typescript": "5.1.6",
|
||||
"unicode-emoji-json": "^0.4.0",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "4.3.9",
|
||||
"vite": "4.4.2",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "3.3.4",
|
||||
"vue-isyourpasswordsafe": "^2.0.0",
|
||||
|
|
|
@ -162,7 +162,7 @@ export async function openAccountMenu(
|
|||
{
|
||||
done: (res) => {
|
||||
addAccount(res.id, res.i);
|
||||
success();
|
||||
switchAccountWithToken(res.i);
|
||||
},
|
||||
},
|
||||
"closed",
|
||||
|
|
85
packages/client/src/components/MkAnnouncement.vue
Normal file
85
packages/client/src/components/MkAnnouncement.vue
Normal file
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<MkModal ref="modal" :z-priority="'middle'" @closed="$emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.title">
|
||||
<MkSparkle v-if="isGoodNews">{{ title }}</MkSparkle>
|
||||
<p v-else>{{ title }}</p>
|
||||
</div>
|
||||
<div :class="$style.time">
|
||||
<MkTime :time="announcement.createdAt" />
|
||||
<div v-if="announcement.updatedAt">
|
||||
{{ i18n.ts.updatedAt }}:
|
||||
<MkTime :time="announcement.createdAt" />
|
||||
</div>
|
||||
</div>
|
||||
<Mfm :text="text" />
|
||||
<img
|
||||
v-if="imageUrl != null"
|
||||
:key="imageUrl"
|
||||
:src="imageUrl"
|
||||
alt="attached image"
|
||||
/>
|
||||
<MkButton :class="$style.gotIt" primary full @click="gotIt()">{{
|
||||
i18n.ts.gotIt
|
||||
}}</MkButton>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from "vue";
|
||||
import MkModal from "@/components/MkModal.vue";
|
||||
import MkSparkle from "@/components/MkSparkle.vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
import * as os from "@/os";
|
||||
|
||||
const props = defineProps<{
|
||||
announcement: Announcement;
|
||||
}>();
|
||||
|
||||
const { id, text, title, imageUrl, isGoodNews } = props.announcement;
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
|
||||
const gotIt = () => {
|
||||
modal.value.close();
|
||||
os.api("i/read-announcement", { announcementId: id });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
|
||||
> img {
|
||||
border-radius: 10px;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.gotIt {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
</style>
|
|
@ -13,7 +13,7 @@
|
|||
<template #default="{ width, height }">
|
||||
<div
|
||||
class="mk-cropper-dialog"
|
||||
:style="`--vw: ${width}px; --vh: ${height}px;`"
|
||||
:style="`--vw: ${width ? `${width}px` : '100%'}; --vh: ${height ? `${height}px` : '100%'};`"
|
||||
>
|
||||
<Transition name="fade">
|
||||
<div v-if="loading" class="loading">
|
||||
|
|
|
@ -220,12 +220,12 @@ const unicodeEmojiSkinTones = [
|
|||
];
|
||||
|
||||
const unicodeEmojiSkinToneLabels = [
|
||||
i18n.ts._skinTones.yellow,
|
||||
i18n.ts._skinTones.light,
|
||||
i18n.ts._skinTones.mediumLight,
|
||||
i18n.ts._skinTones.medium,
|
||||
i18n.ts._skinTones.mediumDark,
|
||||
i18n.ts._skinTones.dark,
|
||||
i18n.ts._skinTones?.yellow ?? "Yellow",
|
||||
i18n.ts._skinTones?.light ?? "Light",
|
||||
i18n.ts._skinTones?.mediumLight ?? "Medium Light",
|
||||
i18n.ts._skinTones?.medium ?? "Medium",
|
||||
i18n.ts._skinTones?.mediumDark ?? "Medium Dark",
|
||||
i18n.ts._skinTones?.dark ?? "Dark",
|
||||
];
|
||||
|
||||
const size = computed(() =>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import { decode } from "blurhash";
|
||||
import { decodeBlurHash } from "fast-blurhash";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -47,8 +47,8 @@ const canvas = $ref<HTMLCanvasElement>();
|
|||
let loaded = $ref(false);
|
||||
|
||||
function draw() {
|
||||
if (props.hash == null) return;
|
||||
const pixels = decode(props.hash, props.size, props.size);
|
||||
if (props.hash == null || canvas == null) return;
|
||||
const pixels = decodeBlurHash(props.hash, props.size, props.size);
|
||||
const ctx = canvas.getContext("2d");
|
||||
const imageData = ctx!.createImageData(props.size, props.size);
|
||||
imageData.data.set(pixels);
|
||||
|
|
53
packages/client/src/components/MkManyAnnouncements.vue
Normal file
53
packages/client/src/components/MkManyAnnouncements.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<MkModal ref="modal" :z-priority="'middle'" @closed="$emit('closed')">
|
||||
<div :class="$style.root">
|
||||
<p :class="$style.title">
|
||||
{{ i18n.ts.youHaveUnreadAnnouncements }}
|
||||
</p>
|
||||
<MkButton
|
||||
:class="$style.gotIt"
|
||||
primary
|
||||
full
|
||||
@click="checkAnnouncements()"
|
||||
>{{ i18n.ts.gotIt }}</MkButton
|
||||
>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from "vue";
|
||||
import MkModal from "@/components/MkModal.vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import { i18n } from "@/i18n";
|
||||
import * as os from "@/os";
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
const checkAnnouncements = () => {
|
||||
modal.value.close();
|
||||
location.href = "/announcements";
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
background: var(--panel);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gotIt {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="media" :class="{ mini: plyrMini }">
|
||||
<div class="media" v-size="{ max: [350] }">
|
||||
<button v-if="hide" class="hidden" @click="hide = false">
|
||||
<ImgWithBlurhash
|
||||
:hash="media.blurhash"
|
||||
|
@ -80,7 +80,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, onMounted } from "vue";
|
||||
import { watch, ref } from "vue";
|
||||
import VuePlyr from "vue-plyr";
|
||||
import "vue-plyr/dist/vue-plyr.css";
|
||||
import type * as misskey from "calckey-js";
|
||||
|
@ -98,7 +98,6 @@ const props = defineProps<{
|
|||
let hide = $ref(true);
|
||||
|
||||
const plyr = ref();
|
||||
const plyrMini = ref(false);
|
||||
|
||||
const url =
|
||||
props.raw || defaultStore.state.loadRawImages
|
||||
|
@ -130,17 +129,6 @@ watch(
|
|||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.media.type.startsWith("video")) {
|
||||
plyrMini.value = plyr.value.player.media.scrollWidth < 300;
|
||||
if (plyrMini.value) {
|
||||
plyr.value.player.on("play", () => {
|
||||
plyr.value.player.fullscreen.enter();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -229,16 +217,50 @@ onMounted(() => {
|
|||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
&.mini {
|
||||
:deep(.plyr__controls) {
|
||||
contain: strict;
|
||||
height: 24px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
:deep(.plyr__volume) {
|
||||
display: flex;
|
||||
min-width: max-content;
|
||||
width: 110px;
|
||||
transition: width 0.2s cubic-bezier(0, 0, 0, 1);
|
||||
[data-plyr="volume"] {
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
transition:
|
||||
margin 0.3s,
|
||||
opacity 0.2s 0.2s;
|
||||
}
|
||||
&:not(:hover):not(:focus-within) {
|
||||
width: 0px;
|
||||
transition: width 0.2s;
|
||||
[data-plyr="volume"] {
|
||||
margin-inline: 0px;
|
||||
opacity: 0;
|
||||
transition:
|
||||
margin 0.3s,
|
||||
opacity 0.1s;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.max-width_350px {
|
||||
:deep(.plyr:not(:fullscreen)) {
|
||||
min-width: unset !important;
|
||||
.plyr__control--overlaid,
|
||||
.plyr__progress__container,
|
||||
.plyr__volume,
|
||||
[data-plyr="fullscreen"] {
|
||||
[data-plyr="download"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
:deep(.plyr__time) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -869,6 +869,9 @@ defineExpose({
|
|||
margin: 0;
|
||||
padding: 8px;
|
||||
opacity: 0.7;
|
||||
&:disabled {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
flex-grow: 1;
|
||||
max-width: 3.5em;
|
||||
width: max-content;
|
||||
|
|
|
@ -477,6 +477,9 @@ function noteClick(e) {
|
|||
margin: 0;
|
||||
padding: 8px;
|
||||
opacity: 0.7;
|
||||
&:disabled {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
flex-grow: 1;
|
||||
max-width: 3.5em;
|
||||
width: max-content;
|
||||
|
|
|
@ -10,8 +10,13 @@
|
|||
<i class="ph-repeat ph-bold ph-lg"></i>
|
||||
<p v-if="count > 0 && !detailedView" class="count">{{ count }}</p>
|
||||
</button>
|
||||
<button v-else class="eddddedb _button">
|
||||
<i class="ph-prohibit ph-bold ph-lg"></i>
|
||||
<button
|
||||
v-else
|
||||
class="eddddedb _button"
|
||||
disabled="true"
|
||||
v-tooltip.noDelay.bottom="i18n.ts.disabled"
|
||||
>
|
||||
<i class="ph-repeat ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<MkLoading mini />
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 :title="title || undefined">{{ title || url }}</h1>
|
||||
<h3 :title="title || undefined">{{ title || url }}</h3>
|
||||
<p :title="description">
|
||||
<span>
|
||||
<span :title="sitename || undefined">
|
||||
|
@ -196,6 +196,7 @@ onUnmounted(() => {
|
|||
> a {
|
||||
display: flex;
|
||||
transition: background 0.2s;
|
||||
text-decoration: none;
|
||||
> div:first-child:not(:last-child) {
|
||||
position: relative;
|
||||
width: 90px;
|
||||
|
@ -240,7 +241,7 @@ onUnmounted(() => {
|
|||
width: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
h1,
|
||||
h3,
|
||||
p {
|
||||
display: block;
|
||||
margin: 0;
|
||||
|
@ -248,10 +249,13 @@ onUnmounted(() => {
|
|||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
h1 {
|
||||
h3 {
|
||||
font-size: 1em;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 0.2em;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: transparent;
|
||||
transition: text-decoration-color 0.2s;
|
||||
}
|
||||
p {
|
||||
margin-bottom: -0.5em;
|
||||
|
@ -277,8 +281,8 @@ onUnmounted(() => {
|
|||
&:focus,
|
||||
&:focus-within {
|
||||
background: var(--panelHighlight);
|
||||
h1 {
|
||||
text-decoration: underline;
|
||||
h3 {
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,11 +75,12 @@ const target = self ? null : "_blank";
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.url {
|
||||
white-space: nowrap;
|
||||
max-width: 80%;
|
||||
display: inline-block;
|
||||
overflow: clip;
|
||||
text-overflow: ellipsis;
|
||||
text-decoration: none !important;
|
||||
> span {
|
||||
text-decoration: underline var(--fgTransparent);
|
||||
text-decoration-thickness: 1px;
|
||||
transition: text-decoration-color 0.2s;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
padding-left: 2px;
|
||||
|
@ -109,5 +110,9 @@ const target = self ? null : "_blank";
|
|||
> .hash {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&:hover span {
|
||||
text-decoration-color: var(--link);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -36,7 +36,7 @@ import { version, ui, lang, host } from "@/config";
|
|||
import { applyTheme } from "@/scripts/theme";
|
||||
import { isDeviceDarkmode } from "@/scripts/is-device-darkmode";
|
||||
import { i18n } from "@/i18n";
|
||||
import { confirm, alert, post, popup, toast } from "@/os";
|
||||
import { confirm, alert, post, popup, toast, api } from "@/os";
|
||||
import { stream } from "@/stream";
|
||||
import * as sound from "@/scripts/sound";
|
||||
import { $i, refreshAccount, login, updateAccount, signout } from "@/account";
|
||||
|
@ -272,6 +272,42 @@ function checkForSplash() {
|
|||
}
|
||||
}
|
||||
|
||||
if (
|
||||
$i &&
|
||||
defaultStore.state.tutorial === -1 &&
|
||||
!["/announcements", "/announcements/"].includes(window.location.pathname)
|
||||
) {
|
||||
api("announcements", { withUnreads: true, limit: 10 })
|
||||
.then((announcements) => {
|
||||
const unreadAnnouncements = announcements.filter((item) => {
|
||||
return !item.isRead;
|
||||
});
|
||||
if (unreadAnnouncements.length > 3) {
|
||||
popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkManyAnnouncements.vue"),
|
||||
),
|
||||
{},
|
||||
{},
|
||||
"closed",
|
||||
);
|
||||
} else {
|
||||
unreadAnnouncements.forEach((item) => {
|
||||
if (item.showPopup)
|
||||
popup(
|
||||
defineAsyncComponent(
|
||||
() => import("@/components/MkAnnouncement.vue"),
|
||||
),
|
||||
{ announcement: item },
|
||||
{},
|
||||
"closed",
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
}
|
||||
|
||||
// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
|
||||
watch(
|
||||
defaultStore.reactiveState.darkMode,
|
||||
|
|
|
@ -163,7 +163,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import XEmojis from "./about.emojis.vue";
|
||||
import XFederation from "./about.federation.vue";
|
||||
|
|
|
@ -157,7 +157,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import MkSwitch from "@/components/form/switch.vue";
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
:display-back-button="true"
|
||||
/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="ztgjmzrw">
|
||||
<div :class="$style.root">
|
||||
<section
|
||||
v-for="announcement in announcements"
|
||||
class="_card _gap announcements"
|
||||
|
@ -22,6 +22,17 @@
|
|||
<MkInput v-model="announcement.imageUrl">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkSwitch
|
||||
v-model="announcement.showPopup"
|
||||
class="_formBlock"
|
||||
>{{ i18n.ts.showPopup }}</MkSwitch
|
||||
>
|
||||
<MkSwitch
|
||||
v-if="announcement.showPopup"
|
||||
v-model="announcement.isGoodNews"
|
||||
class="_formBlock"
|
||||
>{{ i18n.ts.showWithSparkles }}</MkSwitch
|
||||
>
|
||||
<p v-if="announcement.reads">
|
||||
{{
|
||||
i18n.t("nUsersRead", { n: announcement.reads })
|
||||
|
@ -57,6 +68,7 @@
|
|||
import {} from "vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import MkInput from "@/components/form/input.vue";
|
||||
import MkSwitch from "@/components/form/switch.vue";
|
||||
import MkTextarea from "@/components/form/textarea.vue";
|
||||
import * as os from "@/os";
|
||||
import { i18n } from "@/i18n";
|
||||
|
@ -74,6 +86,8 @@ function add() {
|
|||
title: "",
|
||||
text: "",
|
||||
imageUrl: null,
|
||||
showPopup: false,
|
||||
isGoodNews: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -137,8 +151,8 @@ definePageMetadata({
|
|||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ztgjmzrw {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
margin: var(--margin);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
|
||||
<FormInput
|
||||
v-model="objectStorageSecretKey"
|
||||
type="password"
|
||||
class="_formBlock"
|
||||
>
|
||||
<template #prefix
|
||||
|
|
|
@ -469,6 +469,7 @@ let enableIdenticonGeneration: boolean = $ref(false);
|
|||
|
||||
async function init() {
|
||||
const meta = await os.api("admin/meta");
|
||||
if (!meta) throw new Error("No meta");
|
||||
name = meta.name;
|
||||
description = meta.description;
|
||||
tosUrl = meta.tosUrl;
|
||||
|
|
|
@ -15,8 +15,13 @@
|
|||
class="_card announcement"
|
||||
>
|
||||
<div class="_title">
|
||||
<span v-if="$i && !announcement.isRead">🆕 </span
|
||||
>{{ announcement.title }}
|
||||
<span v-if="$i && !announcement.isRead">🆕 </span>
|
||||
<h3>{{ announcement.title }}</h3>
|
||||
<MkTime :time="announcement.createdAt" />
|
||||
<div v-if="announcement.updatedAt">
|
||||
{{ i18n.ts.updatedAt }}:
|
||||
<MkTime :time="announcement.createdAt" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<Mfm :text="announcement.text" />
|
||||
|
@ -76,6 +81,10 @@ definePageMetadata({
|
|||
margin-bottom: var(--margin);
|
||||
}
|
||||
|
||||
> ._title {
|
||||
padding: 14px 32px !important;
|
||||
}
|
||||
|
||||
> ._content {
|
||||
> img {
|
||||
display: block;
|
||||
|
|
|
@ -112,7 +112,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, defineComponent, inject, watch } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import MkChannelPreview from "@/components/MkChannelPreview.vue";
|
||||
import MkChannelList from "@/components/MkChannelList.vue";
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onMounted } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import XFeatured from "./explore.featured.vue";
|
||||
import XUsers from "./explore.users.vue";
|
||||
|
|
|
@ -92,9 +92,9 @@
|
|||
>
|
||||
<div class="vfpdbgtk">
|
||||
<MkGalleryPostPreview
|
||||
v-for="post in items"
|
||||
:key="post.id"
|
||||
:post="post"
|
||||
v-for="mypost in items"
|
||||
:key="mypost.id"
|
||||
:post="mypost"
|
||||
class="post"
|
||||
/>
|
||||
</div>
|
||||
|
@ -107,7 +107,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, watch, onMounted } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import MkFolder from "@/components/MkFolder.vue";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
|
|
|
@ -338,7 +338,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import type * as calckey from "calckey-js";
|
||||
import MkChart from "@/components/MkChart.vue";
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, markRaw, onMounted, onUnmounted, watch } from "vue";
|
||||
import * as Acct from "calckey-js/built/acct";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import MkChatPreview from "@/components/MkChatPreview.vue";
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import { notificationTypes } from "calckey-js";
|
||||
import XNotifications from "@/components/MkNotifications.vue";
|
||||
|
|
|
@ -66,10 +66,10 @@
|
|||
:pagination="myPagesPagination"
|
||||
>
|
||||
<MkPagePreview
|
||||
v-for="page in items"
|
||||
:key="page.id"
|
||||
v-for="mypage in items"
|
||||
:key="mypage.id"
|
||||
class="ckltabjg"
|
||||
:page="page"
|
||||
:page="mypage"
|
||||
/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
@ -81,7 +81,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onMounted } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import MkPagePreview from "@/components/MkPagePreview.vue";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onMounted } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import XNotes from "@/components/MkNotes.vue";
|
||||
import XUserList from "@/components/MkUserList.vue";
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onMounted } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import XNotes from "@/components/MkNotes.vue";
|
||||
import XUserList from "@/components/MkUserList.vue";
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from "vue";
|
||||
import { Virtual } from "swiper";
|
||||
import { Virtual } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import XTutorial from "@/components/MkTutorialDialog.vue";
|
||||
import XTimeline from "@/components/MkTimeline.vue";
|
||||
|
|
|
@ -26,6 +26,13 @@
|
|||
class="banner"
|
||||
:style="{
|
||||
backgroundImage: `url('${user.bannerUrl}')`,
|
||||
'--backgroundImageStatic':
|
||||
defaultStore.state.useBlurEffect &&
|
||||
user.bannerUrl
|
||||
? `url('${getStaticImageUrl(
|
||||
user.bannerUrl,
|
||||
)}')`
|
||||
: null,
|
||||
}"
|
||||
></div>
|
||||
<div class="fade"></div>
|
||||
|
@ -384,8 +391,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 +522,7 @@ onUnmounted(() => {
|
|||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--blur, inherit);
|
||||
background: var(--backgroundImageStatic);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
pointer-events: none;
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import { getBlurHashAverageColor } from "fast-blurhash";
|
||||
|
||||
function rgbToHex(rgb: number[]): string {
|
||||
return `#${rgb
|
||||
.map((x) => {
|
||||
const hex = x.toString(16);
|
||||
return hex.length === 1 ? `0${hex}` : hex;
|
||||
})
|
||||
.join("")}`;
|
||||
}
|
||||
|
||||
export function extractAvgColorFromBlurhash(hash: string) {
|
||||
return typeof hash === "string"
|
||||
? `#${[...hash.slice(2, 6)]
|
||||
.map((x) =>
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".indexOf(
|
||||
x,
|
||||
),
|
||||
)
|
||||
.reduce((a, c) => a * 83 + c, 0)
|
||||
.toString(16)
|
||||
.padStart(6, "0")}`
|
||||
? rgbToHex(getBlurHashAverageColor(hash))
|
||||
: undefined;
|
||||
}
|
||||
|
|
|
@ -145,6 +145,12 @@ a {
|
|||
cursor: pointer;
|
||||
color: inherit;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: transparent;
|
||||
transition: text-decoration-color 0.2s;
|
||||
&:hover {
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
// i {
|
||||
|
@ -229,6 +235,7 @@ hr {
|
|||
font-size: 1em;
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&,
|
||||
* {
|
||||
|
@ -650,20 +657,26 @@ hr {
|
|||
._link {
|
||||
position: relative;
|
||||
color: var(--link);
|
||||
text-underline-offset: 0.2em;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0%;
|
||||
border-bottom: 2px solid var(--link);
|
||||
transition: 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
width: 100%;
|
||||
}
|
||||
// &::before,
|
||||
// &::after {
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// bottom: 0;
|
||||
// left: 0;
|
||||
// width: 0%;
|
||||
// border-bottom: 1px solid currentColor;
|
||||
// transition: 0.3s ease-in-out;
|
||||
// }
|
||||
// &::before {
|
||||
// width: 100%;
|
||||
// opacity: 0.4;
|
||||
// }
|
||||
// &:hover:after,
|
||||
// &:focus:after {
|
||||
// width: 100%;
|
||||
// }
|
||||
}
|
||||
|
||||
._caption {
|
||||
|
|
|
@ -66,9 +66,6 @@
|
|||
<span
|
||||
v-if="navbarItemDef[item].indicated"
|
||||
class="indicator"
|
||||
:class="{
|
||||
animateIndicator: $store.state.animation,
|
||||
}"
|
||||
><i class="icon ph-circle ph-fill"></i
|
||||
></span>
|
||||
</component>
|
||||
|
@ -91,7 +88,6 @@
|
|||
updateAvailable
|
||||
"
|
||||
class="indicator"
|
||||
:class="{ animateIndicator: $store.state.animation }"
|
||||
></span
|
||||
><i class="icon ph-door ph-bold ph-fw ph-lg"></i
|
||||
><span class="text">{{ i18n.ts.controlPanel }}</span>
|
||||
|
@ -106,10 +102,7 @@
|
|||
class="icon ph-dots-three-outline ph-bold ph-fw ph-lg"
|
||||
></i
|
||||
><span class="text">{{ i18n.ts.more }}</span>
|
||||
<span
|
||||
v-if="otherMenuItemIndicated"
|
||||
class="indicator"
|
||||
:class="{ animateIndicator: $store.state.animation }"
|
||||
<span v-if="otherMenuItemIndicated" class="indicator"
|
||||
><i class="icon ph-circle ph-fill"></i
|
||||
></span>
|
||||
</button>
|
||||
|
@ -426,9 +419,6 @@ function more(ev: MouseEvent) {
|
|||
left: 20px;
|
||||
color: var(--navIndicator);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
> .animateIndicator {
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
|
@ -612,9 +602,6 @@ function more(ev: MouseEvent) {
|
|||
left: 24px;
|
||||
color: var(--navIndicator);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
> .animateIndicator {
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
|
|
|
@ -490,6 +490,7 @@ console.log(mainRouter.currentRoute.value.name);
|
|||
background: var(--bg);
|
||||
border-radius: calc((2.85rem / 2) + 5px);
|
||||
opacity: 1;
|
||||
z-index: -3;
|
||||
}
|
||||
> ._button:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
|
|
81
packages/megalodon/package.json
Normal file
81
packages/megalodon/package.json
Normal file
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"name": "megalodon",
|
||||
"private": true,
|
||||
"main": "./lib/src/index.js",
|
||||
"typings": "./lib/src/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p ./",
|
||||
"lint": "eslint --ext .js,.ts src",
|
||||
"doc": "typedoc --out ../docs ./src",
|
||||
"test": "NODE_ENV=test jest -u --maxWorkers=3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^@/(.+)": "<rootDir>/src/$1",
|
||||
"^~/(.+)": "<rootDir>/$1"
|
||||
},
|
||||
"testMatch": [
|
||||
"**/test/**/*.spec.ts"
|
||||
],
|
||||
"preset": "ts-jest/presets/default",
|
||||
"transform": {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest"
|
||||
},
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"tsconfig": "tsconfig.json"
|
||||
}
|
||||
},
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/oauth": "^0.9.0",
|
||||
"@types/ws": "^8.5.4",
|
||||
"axios": "1.2.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"form-data": "^4.0.0",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"oauth": "^0.10.0",
|
||||
"object-assign-deep": "^0.4.0",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"socks-proxy-agent": "^7.0.0",
|
||||
"typescript": "4.9.4",
|
||||
"uuid": "^9.0.0",
|
||||
"ws": "8.12.0",
|
||||
"async-lock": "1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/core-js": "^2.5.0",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/object-assign-deep": "^0.4.0",
|
||||
"@types/parse-link-header": "^2.0.0",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/node": "18.11.18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"@types/async-lock": "1.4.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-node": "^11.0.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"jest": "^29.4.0",
|
||||
"jest-worker": "^29.4.0",
|
||||
"lodash": "^4.17.14",
|
||||
"prettier": "^2.8.3",
|
||||
"ts-jest": "^29.0.5",
|
||||
"typedoc": "^0.23.24"
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "test"
|
||||
}
|
||||
}
|
1
packages/megalodon/src/axios.d.ts
vendored
Normal file
1
packages/megalodon/src/axios.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module 'axios/lib/adapters/http'
|
13
packages/megalodon/src/cancel.ts
Normal file
13
packages/megalodon/src/cancel.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export class RequestCanceledError extends Error {
|
||||
public isCancel: boolean
|
||||
|
||||
constructor(msg: string) {
|
||||
super(msg)
|
||||
this.isCancel = true
|
||||
Object.setPrototypeOf(this, RequestCanceledError)
|
||||
}
|
||||
}
|
||||
|
||||
export const isCancel = (value: any): boolean => {
|
||||
return value && value.isCancel
|
||||
}
|
3
packages/megalodon/src/converter.ts
Normal file
3
packages/megalodon/src/converter.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import MisskeyAPI from "./misskey/api_client";
|
||||
|
||||
export default MisskeyAPI.Converter
|
3
packages/megalodon/src/default.ts
Normal file
3
packages/megalodon/src/default.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const NO_REDIRECT = 'urn:ietf:wg:oauth:2.0:oob'
|
||||
export const DEFAULT_SCOPE = ['read', 'write', 'follow']
|
||||
export const DEFAULT_UA = 'megalodon'
|
27
packages/megalodon/src/entities/account.ts
Normal file
27
packages/megalodon/src/entities/account.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/// <reference path="emoji.ts" />
|
||||
/// <reference path="source.ts" />
|
||||
/// <reference path="field.ts" />
|
||||
namespace Entity {
|
||||
export type Account = {
|
||||
id: string
|
||||
username: string
|
||||
acct: string
|
||||
display_name: string
|
||||
locked: boolean
|
||||
created_at: string
|
||||
followers_count: number
|
||||
following_count: number
|
||||
statuses_count: number
|
||||
note: string
|
||||
url: string
|
||||
avatar: string
|
||||
avatar_static: string
|
||||
header: string
|
||||
header_static: string
|
||||
emojis: Array<Emoji>
|
||||
moved: Account | null
|
||||
fields: Array<Field>
|
||||
bot: boolean | null
|
||||
source?: Source
|
||||
}
|
||||
}
|
8
packages/megalodon/src/entities/activity.ts
Normal file
8
packages/megalodon/src/entities/activity.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Entity {
|
||||
export type Activity = {
|
||||
week: string
|
||||
statuses: string
|
||||
logins: string
|
||||
registrations: string
|
||||
}
|
||||
}
|
34
packages/megalodon/src/entities/announcement.ts
Normal file
34
packages/megalodon/src/entities/announcement.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/// <reference path="tag.ts" />
|
||||
/// <reference path="emoji.ts" />
|
||||
/// <reference path="reaction.ts" />
|
||||
|
||||
namespace Entity {
|
||||
export type Announcement = {
|
||||
id: string
|
||||
content: string
|
||||
starts_at: string | null
|
||||
ends_at: string | null
|
||||
published: boolean
|
||||
all_day: boolean
|
||||
published_at: string
|
||||
updated_at: string
|
||||
read?: boolean
|
||||
mentions: Array<AnnouncementAccount>
|
||||
statuses: Array<AnnouncementStatus>
|
||||
tags: Array<Tag>
|
||||
emojis: Array<Emoji>
|
||||
reactions: Array<Reaction>
|
||||
}
|
||||
|
||||
export type AnnouncementAccount = {
|
||||
id: string
|
||||
username: string
|
||||
url: string
|
||||
acct: string
|
||||
}
|
||||
|
||||
export type AnnouncementStatus = {
|
||||
id: string
|
||||
url: string
|
||||
}
|
||||
}
|
7
packages/megalodon/src/entities/application.ts
Normal file
7
packages/megalodon/src/entities/application.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace Entity {
|
||||
export type Application = {
|
||||
name: string
|
||||
website?: string | null
|
||||
vapid_key?: string | null
|
||||
}
|
||||
}
|
14
packages/megalodon/src/entities/async_attachment.ts
Normal file
14
packages/megalodon/src/entities/async_attachment.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/// <reference path="attachment.ts" />
|
||||
namespace Entity {
|
||||
export type AsyncAttachment = {
|
||||
id: string
|
||||
type: 'unknown' | 'image' | 'gifv' | 'video' | 'audio'
|
||||
url: string | null
|
||||
remote_url: string | null
|
||||
preview_url: string
|
||||
text_url: string | null
|
||||
meta: Meta | null
|
||||
description: string | null
|
||||
blurhash: string | null
|
||||
}
|
||||
}
|
49
packages/megalodon/src/entities/attachment.ts
Normal file
49
packages/megalodon/src/entities/attachment.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
namespace Entity {
|
||||
export type Sub = {
|
||||
// For Image, Gifv, and Video
|
||||
width?: number
|
||||
height?: number
|
||||
size?: string
|
||||
aspect?: number
|
||||
|
||||
// For Gifv and Video
|
||||
frame_rate?: string
|
||||
|
||||
// For Audio, Gifv, and Video
|
||||
duration?: number
|
||||
bitrate?: number
|
||||
}
|
||||
|
||||
export type Focus = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export type Meta = {
|
||||
original?: Sub
|
||||
small?: Sub
|
||||
focus?: Focus
|
||||
length?: string
|
||||
duration?: number
|
||||
fps?: number
|
||||
size?: string
|
||||
width?: number
|
||||
height?: number
|
||||
aspect?: number
|
||||
audio_encode?: string
|
||||
audio_bitrate?: string
|
||||
audio_channel?: string
|
||||
}
|
||||
|
||||
export type Attachment = {
|
||||
id: string
|
||||
type: 'unknown' | 'image' | 'gifv' | 'video' | 'audio'
|
||||
url: string
|
||||
remote_url: string | null
|
||||
preview_url: string | null
|
||||
text_url: string | null
|
||||
meta: Meta | null
|
||||
description: string | null
|
||||
blurhash: string | null
|
||||
}
|
||||
}
|
16
packages/megalodon/src/entities/card.ts
Normal file
16
packages/megalodon/src/entities/card.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
namespace Entity {
|
||||
export type Card = {
|
||||
url: string
|
||||
title: string
|
||||
description: string
|
||||
type: 'link' | 'photo' | 'video' | 'rich'
|
||||
image?: string
|
||||
author_name?: string
|
||||
author_url?: string
|
||||
provider_name?: string
|
||||
provider_url?: string
|
||||
html?: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
}
|
8
packages/megalodon/src/entities/context.ts
Normal file
8
packages/megalodon/src/entities/context.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/// <reference path="status.ts" />
|
||||
|
||||
namespace Entity {
|
||||
export type Context = {
|
||||
ancestors: Array<Status>
|
||||
descendants: Array<Status>
|
||||
}
|
||||
}
|
11
packages/megalodon/src/entities/conversation.ts
Normal file
11
packages/megalodon/src/entities/conversation.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/// <reference path="account.ts" />
|
||||
/// <reference path="status.ts" />
|
||||
|
||||
namespace Entity {
|
||||
export type Conversation = {
|
||||
id: string
|
||||
accounts: Array<Account>
|
||||
last_status: Status | null
|
||||
unread: boolean
|
||||
}
|
||||
}
|
9
packages/megalodon/src/entities/emoji.ts
Normal file
9
packages/megalodon/src/entities/emoji.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Entity {
|
||||
export type Emoji = {
|
||||
shortcode: string
|
||||
static_url: string
|
||||
url: string
|
||||
visible_in_picker: boolean
|
||||
category: string
|
||||
}
|
||||
}
|
8
packages/megalodon/src/entities/featured_tag.ts
Normal file
8
packages/megalodon/src/entities/featured_tag.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Entity {
|
||||
export type FeaturedTag = {
|
||||
id: string
|
||||
name: string
|
||||
statuses_count: number
|
||||
last_status_at: string
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue