Merge branch 'develop' of https://codeberg.org/calckey/calckey into notifications

This commit is contained in:
Freeplay 2023-05-01 16:50:21 -04:00
commit f759476a4a
126 changed files with 3366 additions and 1234 deletions

View File

@ -1,7 +1,8 @@
pipeline: pipeline:
testCommit: testCommit:
image: node:latest image: node:alpine
commands: commands:
- apk add --no-cache cargo python3 make g++
- cp .config/ci.yml .config/default.yml - cp .config/ci.yml .config/default.yml
- corepack enable - corepack enable
- corepack prepare pnpm@latest --activate - corepack prepare pnpm@latest --activate

View File

@ -27,9 +27,9 @@
- Notable differences: - Notable differences:
- Improved UI/UX (especially on mobile) - Improved UI/UX (especially on mobile)
- Improved notifications - Improved notifications
- Fediverse account migration
- Improved instance security - Improved instance security
- Improved accessibility - Improved accessibility
- Improved threads
- Recommended Instances timeline - Recommended Instances timeline
- OCR image captioning - OCR image captioning
- New and improved Groups - New and improved Groups

File diff suppressed because it is too large Load Diff

View File

@ -68,8 +68,8 @@ import: "Import"
export: "Export" export: "Export"
files: "Files" files: "Files"
download: "Download" download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Posts\ driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? It\
\ with this file attached will also be deleted." \ will be removed from all posts that contain it as an attachment."
unfollowConfirm: "Are you sure that you want to unfollow {name}?" unfollowConfirm: "Are you sure that you want to unfollow {name}?"
exportRequested: "You've requested an export. This may take a while. It will be added\ exportRequested: "You've requested an export. This may take a while. It will be added\
\ to your Drive once completed." \ to your Drive once completed."
@ -197,6 +197,7 @@ perHour: "Per Hour"
perDay: "Per Day" perDay: "Per Day"
stopActivityDelivery: "Stop sending activities" stopActivityDelivery: "Stop sending activities"
blockThisInstance: "Block this instance" blockThisInstance: "Block this instance"
silenceThisInstance: "Silence this instance"
operations: "Operations" operations: "Operations"
software: "Software" software: "Software"
version: "Version" version: "Version"
@ -218,10 +219,13 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote
blockedInstances: "Blocked Instances" blockedInstances: "Blocked Instances"
blockedInstancesDescription: "List the hostnames of the instances that you want to\ blockedInstancesDescription: "List the hostnames of the instances that you want to\
\ block. Listed instances will no longer be able to communicate with this instance." \ block. Listed instances will no longer be able to communicate with this instance."
silencedInstances: "Silenced Instances"
silencedInstancesDescription: "List the hostnames of the instances that you want to\
\ silence. Accounts in the listed instances are treated as \"Silenced\", can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked instances."
hiddenTags: "Hidden Hashtags" hiddenTags: "Hidden Hashtags"
hiddenTagsDescription: "List the hashtags (without the #) of the hashtags you wish\ hiddenTagsDescription: "List the hashtags (without the #) of the hashtags you wish\
\ to hide from trending and explore. Hidden hashtags are still discoverable via\ \ to hide from trending and explore. Hidden hashtags are still discoverable via\
\ other means." \ other means. Blocked instances are not affected even if listed here."
muteAndBlock: "Mutes and Blocks" muteAndBlock: "Mutes and Blocks"
mutedUsers: "Muted users" mutedUsers: "Muted users"
blockedUsers: "Blocked users" blockedUsers: "Blocked users"
@ -240,6 +244,7 @@ noCustomEmojis: "There are no emoji"
noJobs: "There are no jobs" noJobs: "There are no jobs"
federating: "Federating" federating: "Federating"
blocked: "Blocked" blocked: "Blocked"
silenced: "Silenced"
suspended: "Suspended" suspended: "Suspended"
all: "All" all: "All"
subscribing: "Subscribing" subscribing: "Subscribing"
@ -829,7 +834,7 @@ active: "Active"
offline: "Offline" offline: "Offline"
notRecommended: "Not recommended" notRecommended: "Not recommended"
botProtection: "Bot Protection" botProtection: "Bot Protection"
instanceBlocking: "Blocked Instances" instanceBlocking: "Federation Block/Silence"
selectAccount: "Select account" selectAccount: "Select account"
switchAccount: "Switch account" switchAccount: "Switch account"
enabled: "Enabled" enabled: "Enabled"
@ -1042,7 +1047,7 @@ moveFromLabel: "Account you're moving from:"
moveFromDescription: "This will set an alias of your old account so that you can move\ moveFromDescription: "This will set an alias of your old account so that you can move\
\ from that account to this current one. Do this BEFORE moving from your older account.\ \ from that account to this current one. Do this BEFORE moving from your older account.\
\ Please enter the tag of the account formatted like @person@instance.com" \ Please enter the tag of the account formatted like @person@instance.com"
migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}?\ migrationConfirm: "Are you absolutely sure you want to migrate your account to {account}?\
\ Once you do this, you won't be able to reverse it, and you won't be able to use\ \ Once you do this, you won't be able to reverse it, and you won't be able to use\
\ your account normally again.\nAlso, please ensure that you've set this current\ \ your account normally again.\nAlso, please ensure that you've set this current\
\ account as the account you're moving from." \ account as the account you're moving from."
@ -1197,7 +1202,7 @@ _mfm:
inlineMath: "Math (Inline)" inlineMath: "Math (Inline)"
inlineMathDescription: "Display math formulas (KaTeX) in-line" inlineMathDescription: "Display math formulas (KaTeX) in-line"
blockMath: "Math (Block)" blockMath: "Math (Block)"
blockMathDescription: "Display multi-line math formulas (KaTeX) in a block" blockMathDescription: "Display math formulas (KaTeX) in a block"
quote: "Quote" quote: "Quote"
quoteDescription: "Displays content as a quote." quoteDescription: "Displays content as a quote."
emoji: "Custom Emoji" emoji: "Custom Emoji"
@ -1237,6 +1242,14 @@ _mfm:
sparkleDescription: "Gives content a sparkling particle effect." sparkleDescription: "Gives content a sparkling particle effect."
rotate: "Rotate" rotate: "Rotate"
rotateDescription: "Turns content by a specified angle." rotateDescription: "Turns content by a specified angle."
position: "Position"
positionDescription: "Move content by a specified amount."
scale: "Scale"
scaleDescription: "Scale content by a specified amount."
foreground: "Foreground color"
foregroundDescription: "Change the foreground color of text."
background: "Background color"
backgroundDescription: "Change the background color of text."
plain: "Plain" plain: "Plain"
plainDescription: "Deactivates the effects of all MFM contained within this MFM\ plainDescription: "Deactivates the effects of all MFM contained within this MFM\
\ effect." \ effect."

223
locales/fi.yml Normal file
View File

@ -0,0 +1,223 @@
username: Käyttäjänimi
fetchingAsApObject: Hae Fedeversestä
gotIt: Selvä!
cancel: Peruuta
enterUsername: Anna käyttäjänimi
renotedBy: Buustannut {user}
noNotes: Ei lähetyksiä
noNotifications: Ei ilmoituksia
instance: Instanssi
settings: Asetukset
basicSettings: Perusasetukset
otherSettings: Muut asetukset
openInWindow: Avaa ikkunaan
profile: Profiili
timeline: Aikajana
noAccountDescription: Käyttäjä ei ole vielä kirjoittanut kuvaustaan vielä.
login: Kirjaudu sisään
loggingIn: Kirjautuu sisään
logout: Kirjaudu ulos
uploading: Tallentaa ylös...
save: Tallenna
favorites: Kirjanmerkit
unfavorite: Poista kirjanmerkeistä
favorited: Lisätty kirjanmerkkeihin.
alreadyFavorited: Lisätty jo kirjanmerkkeihin.
cantFavorite: Ei voitu lisätä kirjanmerkkeihin.
pin: Kiinnitä profiiliin
unpin: Irroita profiilista
delete: Poista
forgotPassword: Unohtunut salasana
search: Etsi
notifications: Ilmoitukset
password: Salasana
ok: OK
noThankYou: Ei kiitos
signup: Rekisteröidy
users: Käyttäjät
addUser: Lisää käyttäjä
addInstance: Lisää instanssi
favorite: Lisää kirjanmerkkeihin
copyContent: Kopioi sisältö
deleteAndEdit: Poista ja muokkaa
copyLink: Kopioi linkki
makeFollowManuallyApprove: Seuraajapyyntö vaatii hyväksymistä
follow: Seuraa
pinned: Kiinnitä profiiliin
followRequestPending: Seuraajapyyntö odottaa
you: Sinä
unrenote: Peruuta buustaus
reaction: Reaktiot
reactionSettingDescription2: Vedä uudelleenjärjestelläksesi, napsauta poistaaksesi,
paina "+" lisätäksesi.
attachCancel: Poista liite
enterFileName: Anna tiedostonimi
mute: Hiljennä
unmute: Poista hiljennys
headlineMisskey: Avoimen lähdekoodin, hajautettu sosiaalisen median alusta, joka on
ikuisesti ilmainen! 🚀
monthAndDay: '{day}/{month}'
deleteAndEditConfirm: Oletko varma, että haluat poistaa tämän lähetyksen ja muokata
sitä? Menetät kaikki reaktiot, buustaukset ja vastaukset lähetyksestäsi.
addToList: Lisää listaan
sendMessage: Lähetä viesti
reply: Vastaa
loadMore: Lataa enemmän
showMore: Näytä enemmän
receiveFollowRequest: Seuraajapyyntö vastaanotettu
followRequestAccepted: Seuraajapyyntö hyväksytty
mentions: Maininnat
importAndExport: Tuo/Vie Tietosisältö
import: Tuo
export: Vie
files: Tiedostot
download: Lataa
unfollowConfirm: Oletko varma, ettet halua seurata enää käyttäjää {name}?
noLists: Sinulla ei ole listoja
note: Lähetys
notes: Lähetykset
following: Seuraa
createList: Luo lista
manageLists: Hallitse listoja
error: Virhe
somethingHappened: On tapahtunut virhe
retry: Yritä uudelleen
pageLoadError: Virhe ladattaessa sivua.
serverIsDead: Tämä palvelin ei vastaa. Yritä hetken kuluttua uudelleen.
youShouldUpgradeClient: Nähdäksesi tämän sivun, virkistä päivittääksesi asiakasohjelmasi.
privacy: Tietosuoja
defaultNoteVisibility: Oletusnäkyvyys
followRequest: Seuraajapyyntö
followRequests: Seuraajapyynnöt
unfollow: Poista seuraaminen
enterEmoji: Syötä emoji
renote: Buustaa
renoted: Buustattu.
cantRenote: Tätä lähetystä ei voi buustata.
cantReRenote: Buustausta ei voi buustata.
quote: Lainaus
pinnedNote: Lukittu lähetys
clickToShow: Napsauta nähdäksesi
sensitive: Herkkää sisältöä (NSFW)
add: Lisää
enableEmojiReactions: Ota käyttöön emoji-reaktiot
showEmojisInReactionNotifications: Näytä emojit reaktioilmoituksissa
reactionSetting: Reaktiot näytettäväksi reaktiovalitsimessa
rememberNoteVisibility: Muista lähetyksen näkyvyysasetukset
markAsSensitive: Merkitse herkäksi sisällöksi (NSFW)
unmarkAsSensitive: Poista merkintä herkkää sisältöä (NSFW)
renoteMute: Hiljennä buustit
renoteUnmute: Poista buustien hiljennys
block: Estä
unblock: Poista esto
unsuspend: Poista keskeytys
suspend: Keskeytys
blockConfirm: Oletko varma, että haluat estää tämän tilin?
unblockConfirm: Oletko varma, että haluat poistaa tämän tilin eston?
selectAntenna: Valitse antenni
selectWidget: Valitse vimpain
editWidgets: Muokkaa vimpaimia
editWidgetsExit: Valmis
emoji: Emoji
emojis: Emojit
emojiName: Emojin nimi
emojiUrl: Emojin URL-linkki
cacheRemoteFiles: Taltioi etätiedostot välimuistiin
flagAsBot: Merkitse tili botiksi
flagAsBotDescription: Ota tämä vaihtoehto käyttöön, jos tätä tiliä ohjaa ohjelma.
Jos se on käytössä, se toimii lippuna muille kehittäjille, jotta estetään loputtomat
vuorovaikutusketjut muiden bottien kanssa ja säädetään Calckeyn sisäiset järjestelmät
käsittelemään tätä tiliä botina.
flagAsCat: Oletko kissa? 🐱
flagAsCatDescription: Saat kissan korvat ja puhut kuin kissa!
flagSpeakAsCat: Puhu kuin kissa
flagShowTimelineReplies: Näytä vastaukset aikajanalla
addAccount: Lisää tili
loginFailed: Kirjautuminen epäonnistui
showOnRemote: Katsele etäinstanssilla
general: Yleistä
accountMoved: 'Käyttäjä on muuttanut uuteen tiliin:'
wallpaper: Taustakuva
setWallpaper: Aseta taustakuva
searchWith: 'Etsi: {q}'
youHaveNoLists: Sinulla ei ole listoja
followConfirm: Oletko varma, että haluat seurata käyttäjää {name}?
host: Isäntä
selectUser: Valitse käyttäjä
annotation: Kommentit
registeredAt: Rekisteröity
latestRequestReceivedAt: Viimeisin pyyntö vastaanotettu
latestRequestSentAt: Viimeisin pyyntö lähetetty
storageUsage: Tallennustilan käyttö
charts: Kaaviot
stopActivityDelivery: Lopeta toimintojen lähettäminen
blockThisInstance: Estä tämä instanssi
operations: Toiminnot
metadata: Metatieto
monitor: Seuranta
jobQueue: Työjono
cpuAndMemory: Prosessori ja muisti
network: Verkko
disk: Levy
clearCachedFiles: Tyhjennä välimuisti
clearCachedFilesConfirm: Oletko varma, että haluat tyhjentää kaikki välimuistiin tallennetut
etätiedostot?
blockedInstances: Estetyt instanssit
hiddenTags: Piilotetut asiatunnisteet
mention: Maininta
copyUsername: Kopioi käyttäjänimi
searchUser: Etsi käyttäjää
showLess: Sulje
youGotNewFollower: seurasi sinua
directNotes: Yksityisviestit
driveFileDeleteConfirm: Oletko varma, että haluat poistaa tiedoston " {name}"? Lähetykset,
jotka sisältyvät tiedostoon, poistuvat myös.
importRequested: Olet pyytänyt viemistä. Tämä voi viedä hetken.
exportRequested: Olet pyytänyt tuomista. Tämä voi viedä hetken. Se lisätään asemaan
kun tuonti valmistuu.
lists: Listat
followers: Seuraajat
followsYou: Seuraa sinua
pageLoadErrorDescription: Tämä yleensä johtuu verkkovirheistä tai selaimen välimuistista.
Kokeile tyhjentämällä välimuisti ja yritä sitten hetken kuluttua uudelleen.
enterListName: Anna listalle nimi
withNFiles: '{n} tiedosto(t)'
instanceInfo: Instanssin tiedot
clearQueue: Tyhjennä jono
suspendConfirm: Oletko varma, että haluat keskeyttää tämän tilin?
unsuspendConfirm: Oletko varma, että haluat poistaa tämän tilin keskeytyksen?
selectList: Valitse lista
customEmojis: Kustomoitu Emoji
addEmoji: Lisää
settingGuide: Suositellut asetukset
cacheRemoteFilesDescription: Kun tämä asetus ei ole käytössä, etätiedostot on ladattu
suoraan etäinstanssilta. Asetuksen poistaminen käytöstä vähentää tallennustilan
käyttöä, mutta lisää verkkoliikennettä kun pienoiskuvat eivät muodostu.
flagSpeakAsCatDescription: Lähetyksesi nyanifioidaan, kun olet kissatilassa
flagShowTimelineRepliesDescription: Näyttää käyttäjien vastaukset muiden käyttäjien
lähetyksiin aikajanalla, jos se on päällä.
autoAcceptFollowed: Automaattisesti hyväksy seuraamispyynnöt käyttäjiltä, joita seuraat
perHour: Tunnissa
removeWallpaper: Poista taustakuva
recipient: Vastaanottaja(t)
federation: Federaatio
software: Ohjelmisto
proxyAccount: Proxy-tili
proxyAccountDescription: Välitystili (Proxy-tili) on tili, joka toimii käyttäjien
etäseuraajana tietyin edellytyksin. Kun käyttäjä esimerkiksi lisää etäkäyttäjän
luetteloon, etäkäyttäjän toimintaa ei toimiteta instanssiin, jos yksikään paikallinen
käyttäjä ei seuraa kyseistä käyttäjää, joten välitystili seuraa sen sijaan.
latestStatus: Viimeisin tila
selectInstance: Valitse instanssi
instances: Instanssit
perDay: Päivässä
version: Versio
statistics: Tilastot
clearQueueConfirmTitle: Oletko varma, että haluat tyhjentää jonon?
introMisskey: Tervetuloa! Calckey on avoimen lähdekoodin, hajautettu sosiaalisen median
alusta, joka on ikuisesti ilmainen! 🚀
clearQueueConfirmText: Mitkään välittämättömät lähetykset, jotka ovat jonossa, eivät
federoidu. Yleensä tätä toimintoa ei tarvita.
blockedInstancesDescription: Lista instanssien isäntänimistä, jotka haluat estää.
Listatut instanssit eivät kykene kommunikoimaan enää tämän instanssin kanssa.
_lang_: Suomi

View File

@ -183,6 +183,7 @@ perHour: "1時間ごと"
perDay: "1日ごと" perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送を停止" stopActivityDelivery: "アクティビティの配送を停止"
blockThisInstance: "このインスタンスをブロック" blockThisInstance: "このインスタンスをブロック"
silenceThisInstance: "このインスタンスをサイレンス"
operations: "操作" operations: "操作"
software: "ソフトウェア" software: "ソフトウェア"
version: "バージョン" version: "バージョン"
@ -202,6 +203,8 @@ clearCachedFiles: "キャッシュをクリア"
clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?" clearCachedFilesConfirm: "キャッシュされたリモートファイルをすべて削除しますか?"
blockedInstances: "ブロックしたインスタンス" blockedInstances: "ブロックしたインスタンス"
blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。" blockedInstancesDescription: "ブロックしたいインスタンスのホストを改行で区切って設定します。ブロックされたインスタンスは、このインスタンスとやり取りできなくなります。"
silencedInstances: "サイレンスしたインスタンス"
silencedInstancesDescription: "サイレンスしたいインスタンスのホストを改行で区切って設定します。サイレンスされたインスタンスに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。"
muteAndBlock: "ミュートとブロック" muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー" mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー" blockedUsers: "ブロックしたユーザー"
@ -220,6 +223,7 @@ noCustomEmojis: "絵文字はありません"
noJobs: "ジョブはありません" noJobs: "ジョブはありません"
federating: "連合中" federating: "連合中"
blocked: "ブロック中" blocked: "ブロック中"
silenced: "サイレンス中"
suspended: "配信停止" suspended: "配信停止"
all: "全て" all: "全て"
subscribing: "購読中" subscribing: "購読中"
@ -768,7 +772,7 @@ active: "アクティブ"
offline: "オフライン" offline: "オフライン"
notRecommended: "非推奨" notRecommended: "非推奨"
botProtection: "Botプロテクション" botProtection: "Botプロテクション"
instanceBlocking: "インスタンスブロック" instanceBlocking: "連合ブロック・サイレンス"
selectAccount: "アカウントを選択" selectAccount: "アカウントを選択"
switchAccount: "アカウントを切り替え" switchAccount: "アカウントを切り替え"
enabled: "有効" enabled: "有効"
@ -1079,7 +1083,7 @@ _mfm:
inlineMath: "数式(インライン)" inlineMath: "数式(インライン)"
inlineMathDescription: "数式(KaTeX)をインラインで表示します。" inlineMathDescription: "数式(KaTeX)をインラインで表示します。"
blockMath: "数式(ブロック)" blockMath: "数式(ブロック)"
blockMathDescription: "複数行の数式(KaTeX)をブロックで表示します。" blockMathDescription: "数式(KaTeX)をブロックで表示します。"
quote: "引用" quote: "引用"
quoteDescription: "内容が引用であることを示せます。" quoteDescription: "内容が引用であることを示せます。"
emoji: "カスタム絵文字" emoji: "カスタム絵文字"
@ -1120,6 +1124,7 @@ _mfm:
rotateDescription: "指定した角度で回転させます。" rotateDescription: "指定した角度で回転させます。"
plain: "プレーン" plain: "プレーン"
plainDescription: "内側の構文を全て無効にします。" plainDescription: "内側の構文を全て無効にします。"
position: 位置
_instanceTicker: _instanceTicker:
none: "表示しない" none: "表示しない"
remote: "リモートユーザーに表示" remote: "リモートユーザーに表示"
@ -1128,7 +1133,7 @@ _serverDisconnectedBehavior:
reload: "自動でリロード" reload: "自動でリロード"
dialog: "ダイアログで警告" dialog: "ダイアログで警告"
quiet: "控えめに警告" quiet: "控えめに警告"
nothing: "何も起こらない" nothing: "何もない"
_channel: _channel:
create: "チャンネルを作成" create: "チャンネルを作成"
edit: "チャンネルを編集" edit: "チャンネルを編集"

View File

@ -986,7 +986,7 @@ _registry:
createKey: "Новый ключ" createKey: "Новый ключ"
_aboutMisskey: _aboutMisskey:
about: "Calckey это форк Misskey, сделанный ThatOneCalculator, разработка которого\ about: "Calckey это форк Misskey, сделанный ThatOneCalculator, разработка которого\
\ начал с 2022." \ началась с 2022."
contributors: "Основные соавторы" contributors: "Основные соавторы"
allContributors: "Все соавторы" allContributors: "Все соавторы"
source: "Исходный код" source: "Исходный код"

View File

@ -1009,9 +1009,9 @@ _mfm:
blockCode: "代码(块)" blockCode: "代码(块)"
blockCodeDescription: "语法高亮显示整块程序代码。" blockCodeDescription: "语法高亮显示整块程序代码。"
inlineMath: "数学公式(内嵌)" inlineMath: "数学公式(内嵌)"
inlineMathDescription: "显示内嵌的KaTex公式。" inlineMathDescription: "显示内嵌的KaTeX公式。"
blockMath: "数学公式(块)" blockMath: "数学公式(块)"
blockMathDescription: "显示整块的多行KaTex数学公式。" blockMathDescription: "显示整块的KaTeX数学公式。"
quote: "引用" quote: "引用"
quoteDescription: "可以用来表示引用的内容。" quoteDescription: "可以用来表示引用的内容。"
emoji: "自定义表情符号" emoji: "自定义表情符号"

View File

@ -64,7 +64,7 @@ import: "匯入"
export: "匯出" export: "匯出"
files: "檔案" files: "檔案"
download: "下載" download: "下載"
driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。\n" driveFileDeleteConfirm: "確定要刪除檔案「{name}」嗎?使用此附件的貼文也會跟著消失。"
unfollowConfirm: "確定要取消追隨{name}嗎?" unfollowConfirm: "確定要取消追隨{name}嗎?"
exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。" exportRequested: "已請求匯出。這可能會花一點時間。結束後檔案將會被放到雲端裡。"
importRequested: "已請求匯入。這可能會花一點時間" importRequested: "已請求匯入。這可能會花一點時間"
@ -291,7 +291,7 @@ emptyDrive: "雲端硬碟為空"
emptyFolder: "資料夾為空" emptyFolder: "資料夾為空"
unableToDelete: "無法刪除" unableToDelete: "無法刪除"
inputNewFileName: "輸入檔案名稱" inputNewFileName: "輸入檔案名稱"
inputNewDescription: "請輸入新標題 " inputNewDescription: "請輸入新標題"
inputNewFolderName: "輸入新資料夾的名稱" inputNewFolderName: "輸入新資料夾的名稱"
circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件夾。" circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件夾。"
hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。" hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。"
@ -324,7 +324,7 @@ yearX: "{year}年"
pages: "頁面" pages: "頁面"
integration: "整合" integration: "整合"
connectService: "己連結" connectService: "己連結"
disconnectService: "己斷開 " disconnectService: "己斷開"
enableLocalTimeline: "開啟本地時間軸" enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "啟用公開時間軸" enableGlobalTimeline: "啟用公開時間軸"
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。" disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和協調人仍可以繼續使用,以方便您。"
@ -336,7 +336,7 @@ driveCapacityPerRemoteAccount: "每個非本地用戶的雲端容量"
inMb: "以Mbps為單位" inMb: "以Mbps為單位"
iconUrl: "圖像URL" iconUrl: "圖像URL"
bannerUrl: "橫幅圖像URL" bannerUrl: "橫幅圖像URL"
backgroundImageUrl: "背景圖片的來源網址 " backgroundImageUrl: "背景圖片的來源網址"
basicInfo: "基本資訊" basicInfo: "基本資訊"
pinnedUsers: "置頂用戶" pinnedUsers: "置頂用戶"
pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的使用者。" pinnedUsersDescription: "在「發現」頁面中使用換行標記想要置頂的使用者。"
@ -490,7 +490,7 @@ useObjectStorage: "使用Object Storage"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "Base URL"
objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理请指定其URL例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”" objectStorageBaseUrlDesc: "引用時的URL。如果您使用的是CDN或反向代理请指定其URL例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”"
objectStorageBucket: "儲存空間Bucket" objectStorageBucket: "儲存空間Bucket"
objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。 " objectStorageBucketDesc: "請指定您正在使用的服務的存儲桶名稱。"
objectStoragePrefix: "前綴" objectStoragePrefix: "前綴"
objectStoragePrefixDesc: "它存儲在此前綴目錄下。" objectStoragePrefixDesc: "它存儲在此前綴目錄下。"
objectStorageEndpoint: "端點Endpoint" objectStorageEndpoint: "端點Endpoint"
@ -560,8 +560,8 @@ disablePlayer: "關閉播放器"
expandTweet: "展開推文" expandTweet: "展開推文"
themeEditor: "主題編輯器" themeEditor: "主題編輯器"
description: "描述" description: "描述"
describeFile: "添加標題 " describeFile: "添加標題"
enterFileDescription: "輸入標題 " enterFileDescription: "輸入標題"
author: "作者" author: "作者"
leaveConfirm: "有未保存的更改。要放棄嗎?" leaveConfirm: "有未保存的更改。要放棄嗎?"
manage: "管理" manage: "管理"
@ -865,7 +865,7 @@ driveCapOverrideLabel: "更改這個使用者的雲端硬碟容量上限"
driveCapOverrideCaption: "如果指定0以下的值就會被取消。" driveCapOverrideCaption: "如果指定0以下的值就會被取消。"
requireAdminForView: "必須以管理者帳號登入才可以檢視。" requireAdminForView: "必須以管理者帳號登入才可以檢視。"
isSystemAccount: "由系統自動建立與管理的帳號。" isSystemAccount: "由系統自動建立與管理的帳號。"
typeToConfirm: "要執行這項操作,請輸入 {x} " typeToConfirm: "要執行這項操作,請輸入 {x}"
deleteAccount: "刪除帳號" deleteAccount: "刪除帳號"
document: "文件" document: "文件"
numberOfPageCache: "快取頁面數" numberOfPageCache: "快取頁面數"
@ -876,7 +876,7 @@ statusbar: "狀態列"
pleaseSelect: "請選擇" pleaseSelect: "請選擇"
reverse: "翻轉" reverse: "翻轉"
colored: "彩色" colored: "彩色"
refreshInterval: "更新間隔" refreshInterval: "更新間隔 "
label: "標籤" label: "標籤"
type: "類型" type: "類型"
speed: "速度" speed: "速度"
@ -895,7 +895,7 @@ activeEmailValidationDescription: "積極地驗證用戶的電子郵件地址,
navbar: "導覽列" navbar: "導覽列"
shuffle: "隨機" shuffle: "隨機"
account: "帳戶" account: "帳戶"
move: "移動 " move: "移動"
customKaTeXMacro: "自定義 KaTeX 宏" customKaTeXMacro: "自定義 KaTeX 宏"
customKaTeXMacroDescription: "使用宏來輕鬆的輸入數學表達式吧!宏的用法與 LaTeX 中的命令定義相同。你可以使用 \\newcommand{\\\ customKaTeXMacroDescription: "使用宏來輕鬆的輸入數學表達式吧!宏的用法與 LaTeX 中的命令定義相同。你可以使用 \\newcommand{\\\
name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 來輸入數學表達式。舉個例子,\\\ name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 來輸入數學表達式。舉個例子,\\\
@ -933,11 +933,11 @@ _accountDelete:
inProgress: "正在刪除" inProgress: "正在刪除"
_ad: _ad:
back: "返回" back: "返回"
reduceFrequencyOfThisAd: "降低此廣告的頻率 " reduceFrequencyOfThisAd: "降低此廣告的頻率"
_forgotPassword: _forgotPassword:
enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。" enterEmail: "請輸入您的帳戶註冊的電子郵件地址。 密碼重置連結將被發送到該電子郵件地址。"
ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。 " ifNoEmail: "如果您還沒有註冊您的電子郵件地址,請聯繫管理員。"
contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。 " contactAdmin: "此實例不支持電子郵件,請聯繫您的管理員重置您的密碼。"
_gallery: _gallery:
my: "我的貼文" my: "我的貼文"
liked: "喜歡的貼文" liked: "喜歡的貼文"
@ -1000,7 +1000,7 @@ _mfm:
url: "URL" url: "URL"
urlDescription: "可以展示URL位址。" urlDescription: "可以展示URL位址。"
link: "鏈接" link: "鏈接"
linkDescription: "您可以將特定範圍的文章與 URL 相關聯。 " linkDescription: "您可以將特定範圍的文章與 URL 相關聯。"
bold: "粗體" bold: "粗體"
boldDescription: "可以將文字顯示为粗體来強調。" boldDescription: "可以將文字顯示为粗體来強調。"
small: "縮小" small: "縮小"
@ -1012,9 +1012,9 @@ _mfm:
blockCode: "程式碼(區塊)" blockCode: "程式碼(區塊)"
blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。" blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。"
inlineMath: "數學公式(內嵌)" inlineMath: "數學公式(內嵌)"
inlineMathDescription: "顯示內嵌的KaTex數學公式。" inlineMathDescription: "顯示內嵌的KaTeX數學公式。"
blockMath: "數學公式(方塊)" blockMath: "數學公式(方塊)"
blockMathDescription: "以區塊顯示複數行的KaTex數學式。" blockMathDescription: "以區塊顯示KaTeX數學式。"
quote: "引用" quote: "引用"
quoteDescription: "可以用來表示引用的内容。" quoteDescription: "可以用來表示引用的内容。"
emoji: "自訂表情符號" emoji: "自訂表情符號"
@ -1805,3 +1805,6 @@ migration: 遷移
homeTimeline: 主頁時間軸 homeTimeline: 主頁時間軸
swipeOnDesktop: 允許在桌面上進行手機式滑動 swipeOnDesktop: 允許在桌面上進行手機式滑動
logoImageUrl: 圖標網址 logoImageUrl: 圖標網址
addInstance: 增加一個實例
noInstances: 沒有實例
flagSpeakAsCat: 像貓一樣地說話

View File

@ -1,6 +1,6 @@
{ {
"name": "calckey", "name": "calckey",
"version": "13.2.0-dev38", "version": "13.2.0-dev41",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",
@ -40,6 +40,8 @@
"@bull-board/ui": "^4.10.2", "@bull-board/ui": "^4.10.2",
"@napi-rs/cli": "^2.15.0", "@napi-rs/cli": "^2.15.0",
"@tensorflow/tfjs": "^3.21.0", "@tensorflow/tfjs": "^3.21.0",
"focus-trap": "^7.2.0",
"focus-trap-vue": "^4.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,23 @@
export class LibreTranslate1682777547198 {
name = "LibreTranslate1682777547198";
async up(queryRunner) {
await queryRunner.query(`
ALTER TABLE "meta"
ADD "libreTranslateApiUrl" character varying(512)
`);
await queryRunner.query(`
ALTER TABLE "meta"
ADD "libreTranslateApiKey" character varying(128)
`);
}
async down(queryRunner) {
await queryRunner.query(`
ALTER TABLE "meta" DROP COLUMN "libreTranslateApiKey"
`);
await queryRunner.query(`
ALTER TABLE "meta" DROP COLUMN "libreTranslateApiUrl"
`);
}
}

View File

@ -0,0 +1,165 @@
export class InstanceSilence1682891890317 {
name = "InstanceSilence1682891890317";
async up(queryRunner) {
await queryRunner.query(
`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "fk_7f4e851a35d81b64dda28eee0"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_renote_muting_createdAt"`,
);
await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`);
await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`);
await queryRunner.query(
`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`,
);
await queryRunner.query(
`ALTER TABLE "meta" DROP COLUMN "enableGuestTimeline"`,
);
await queryRunner.query(
`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`,
);
await queryRunner.query(
`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the notification was read.'`,
);
await queryRunner.query(
`COMMENT ON COLUMN "meta"."defaultReaction" IS NULL`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "secureMode" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "privateMode" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-calckey}'`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey'`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://codeberg.org/calckey/calckey/issues/new'`,
);
await queryRunner.query(
`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`,
);
await queryRunner.query(
`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`,
);
await queryRunner.query(
`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`,
);
await queryRunner.query(
`ALTER TABLE "page" ALTER COLUMN "isPublic" DROP DEFAULT`,
);
await queryRunner.query(
`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `,
);
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId") `,
);
await queryRunner.query(
`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
async down(queryRunner) {
await queryRunner.query(
`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`,
);
await queryRunner.query(
`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`,
);
await queryRunner.query(
`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`,
);
await queryRunner.query(
`ALTER TABLE "page" ALTER COLUMN "isPublic" SET DEFAULT true`,
);
await queryRunner.query(
`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`,
);
await queryRunner.query(
`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`,
);
await queryRunner.query(
`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "feedbackUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey/issues/new'`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "repositoryUrl" SET DEFAULT 'https://github.com/misskey-dev/misskey'`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "pinnedPages" SET DEFAULT '{/featured,/channels,/explore,/pages,/about-misskey}'`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "allowedHosts" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "privateMode" DROP NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "meta" ALTER COLUMN "secureMode" DROP NOT NULL`,
);
await queryRunner.query(
`COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`,
);
await queryRunner.query(
`COMMENT ON COLUMN "notification"."isRead" IS 'Whether the Notification is read.'`,
);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
await queryRunner.query(
`ALTER TABLE "meta" ADD "enableGuestTimeline" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `,
);
await queryRunner.query(
`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "fk_7f4e851a35d81b64dda28eee0" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
}

View File

@ -26,7 +26,7 @@
"@bull-board/api": "^4.6.4", "@bull-board/api": "^4.6.4",
"@bull-board/koa": "^4.6.4", "@bull-board/koa": "^4.6.4",
"@bull-board/ui": "^4.6.4", "@bull-board/ui": "^4.6.4",
"@calckey/megalodon": "5.1.24", "@calckey/megalodon": "5.2.0",
"@discordapp/twemoji": "14.0.2", "@discordapp/twemoji": "14.0.2",
"@elastic/elasticsearch": "7.17.0", "@elastic/elasticsearch": "7.17.0",
"@koa/cors": "3.4.3", "@koa/cors": "3.4.3",

View File

@ -89,6 +89,11 @@ export type Source = {
authKey?: string; authKey?: string;
isPro?: boolean; isPro?: boolean;
}; };
libreTranslate: {
managed?: boolean;
apiUrl?: string;
apiKey?: string;
};
email: { email: {
managed?: boolean; managed?: boolean;
address?: string; address?: string;

View File

@ -18,3 +18,21 @@ export async function shouldBlockInstance(
(blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`), (blockedHost) => host === blockedHost || host.endsWith(`.${blockedHost}`),
); );
} }
/**
* Returns whether a specific host (punycoded) should be limited.
*
* @param host punycoded instance host
* @param meta a resolved Meta table
* @returns whether the given host should be limited
*/
export async function shouldSilenceInstance(
host: Instance["host"],
meta?: Meta,
): Promise<boolean> {
const { silencedHosts } = meta ?? (await fetchMeta());
return silencedHosts.some(
(silencedHost) =>
host === silencedHost || host.endsWith(`.${silencedHost}`),
);
}

View File

@ -97,6 +97,11 @@ export class Meta {
}) })
public blockedHosts: string[]; public blockedHosts: string[];
@Column('varchar', {
length: 256, array: true, default: '{}',
})
public silencedHosts: string[];
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })
@ -386,6 +391,18 @@ export class Meta {
}) })
public deeplIsPro: boolean; public deeplIsPro: boolean;
@Column('varchar', {
length: 512,
nullable: true,
})
public libreTranslateApiUrl: string | null;
@Column('varchar', {
length: 128,
nullable: true,
})
public libreTranslateApiKey: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 512,
nullable: true, nullable: true,

View File

@ -1,12 +1,13 @@
import { db } from "@/db/postgre.js"; import { db } from "@/db/postgre.js";
import { Instance } from "@/models/entities/instance.js"; import { Instance } from "@/models/entities/instance.js";
import type { Packed } from "@/misc/schema.js"; import type { Packed } from "@/misc/schema.js";
import { fetchMeta } from "@/misc/fetch-meta.js"; import {
import { shouldBlockInstance } from "@/misc/should-block-instance.js"; shouldBlockInstance,
shouldSilenceInstance,
} from "@/misc/should-block-instance.js";
export const InstanceRepository = db.getRepository(Instance).extend({ export const InstanceRepository = db.getRepository(Instance).extend({
async pack(instance: Instance): Promise<Packed<"FederationInstance">> { async pack(instance: Instance): Promise<Packed<"FederationInstance">> {
const meta = await fetchMeta();
return { return {
id: instance.id, id: instance.id,
caughtAt: instance.caughtAt.toISOString(), caughtAt: instance.caughtAt.toISOString(),
@ -22,6 +23,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({
isNotResponding: instance.isNotResponding, isNotResponding: instance.isNotResponding,
isSuspended: instance.isSuspended, isSuspended: instance.isSuspended,
isBlocked: await shouldBlockInstance(instance.host), isBlocked: await shouldBlockInstance(instance.host),
isSilenced: await shouldSilenceInstance(instance.host),
softwareName: instance.softwareName, softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion, softwareVersion: instance.softwareVersion,
openRegistrations: instance.openRegistrations, openRegistrations: instance.openRegistrations,

View File

@ -68,6 +68,11 @@ export const packedFederationInstanceSchema = {
optional: false, optional: false,
nullable: false, nullable: false,
}, },
isSilenced: {
type: "boolean",
optional: false,
nullable: false,
},
softwareName: { softwareName: {
type: "string", type: "string",
optional: false, optional: false,

View File

@ -10,7 +10,7 @@ import { renderPerson } from "@/remote/activitypub/renderer/person.js";
import renderEmoji from "@/remote/activitypub/renderer/emoji.js"; import renderEmoji from "@/remote/activitypub/renderer/emoji.js";
import { inbox as processInbox } from "@/queue/index.js"; import { inbox as processInbox } from "@/queue/index.js";
import { isSelfHost, toPuny } from "@/misc/convert-host.js"; import { isSelfHost, toPuny } from "@/misc/convert-host.js";
import { Notes, Users, Emojis, NoteReactions } from "@/models/index.js"; import { Notes, Users, Emojis, NoteReactions, FollowRequests } from "@/models/index.js";
import type { ILocalUser, User } from "@/models/entities/user.js"; import type { ILocalUser, User } from "@/models/entities/user.js";
import { renderLike } from "@/remote/activitypub/renderer/like.js"; import { renderLike } from "@/remote/activitypub/renderer/like.js";
import { getUserKeypair } from "@/misc/keypair-store.js"; import { getUserKeypair } from "@/misc/keypair-store.js";
@ -330,7 +330,7 @@ router.get("/likes/:like", async (ctx) => {
}); });
// follow // follow
router.get("/follows/:follower/:followee", async (ctx) => { router.get("/follows/:follower/:followee", async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req); const verify = await checkFetch(ctx.req);
if (verify !== 200) { if (verify !== 200) {
ctx.status = verify; ctx.status = verify;
@ -365,4 +365,47 @@ router.get("/follows/:follower/:followee", async (ctx) => {
setResponseType(ctx); setResponseType(ctx);
}); });
// follow request
router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify !== 200) {
ctx.status = verify;
return;
}
const followRequest = await FollowRequests.findOneBy({
id: ctx.params.followRequestId,
});
if (followRequest == null) {
ctx.status = 404;
return;
}
const [follower, followee] = await Promise.all([
Users.findOneBy({
id: followRequest.followerId,
host: IsNull(),
}),
Users.findOneBy({
id: followRequest.followeeId,
host: Not(IsNull()),
}),
]);
if (follower == null || followee == null) {
ctx.status = 404;
return;
}
const meta = await fetchMeta();
if (meta.secureMode || meta.privateMode) {
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
} else {
ctx.set("Cache-Control", "public, max-age=180");
}
ctx.body = renderActivity(renderFollow(follower, followee));
setResponseType(ctx);
});
export default router; export default router;

View File

@ -30,6 +30,17 @@ export default define(meta, paramDef, async (ps, me) => {
set.deeplIsPro = config.deepl.isPro; set.deeplIsPro = config.deepl.isPro;
} }
} }
if (
config.libreTranslate.managed != null &&
config.libreTranslate.managed === true
) {
if (typeof config.libreTranslate.apiUrl === "string") {
set.libreTranslateApiUrl = config.libreTranslate.apiUrl;
}
if (typeof config.libreTranslate.apiKey === "string") {
set.libreTranslateApiKey = config.libreTranslate.apiKey;
}
}
if (config.email.managed != null && config.email.managed === true) { if (config.email.managed != null && config.email.managed === true) {
set.enableEmail = true; set.enableEmail = true;
if (typeof config.email.address === "string") { if (typeof config.email.address === "string") {

View File

@ -259,6 +259,16 @@ export const meta = {
nullable: false, nullable: false,
}, },
}, },
silencedHosts: {
type: "array",
optional: true,
nullable: false,
items: {
type: "string",
optional: false,
nullable: false,
},
},
allowedHosts: { allowedHosts: {
type: "array", type: "array",
optional: true, optional: true,
@ -512,7 +522,8 @@ export default define(meta, paramDef, async (ps, me) => {
enableGithubIntegration: instance.enableGithubIntegration, enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration, enableDiscordIntegration: instance.enableDiscordIntegration,
enableServiceWorker: instance.enableServiceWorker, enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null, translatorAvailable:
instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
pinnedPages: instance.pinnedPages, pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId, pinnedClipId: instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteFiles: instance.cacheRemoteFiles,
@ -523,6 +534,7 @@ export default define(meta, paramDef, async (ps, me) => {
customSplashIcons: instance.customSplashIcons, customSplashIcons: instance.customSplashIcons,
hiddenTags: instance.hiddenTags, hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts, blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts,
allowedHosts: instance.allowedHosts, allowedHosts: instance.allowedHosts,
privateMode: instance.privateMode, privateMode: instance.privateMode,
secureMode: instance.secureMode, secureMode: instance.secureMode,
@ -564,6 +576,8 @@ export default define(meta, paramDef, async (ps, me) => {
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle, objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
deeplAuthKey: instance.deeplAuthKey, deeplAuthKey: instance.deeplAuthKey,
deeplIsPro: instance.deeplIsPro, deeplIsPro: instance.deeplIsPro,
libreTranslateApiUrl: instance.libreTranslateApiUrl,
libreTranslateApiKey: instance.libreTranslateApiKey,
enableIpLogging: instance.enableIpLogging, enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation, enableActiveEmailValidation: instance.enableActiveEmailValidation,
}; };

View File

@ -61,6 +61,13 @@ export const paramDef = {
type: "string", type: "string",
}, },
}, },
silencedHosts: {
type: "array",
nullable: true,
items: {
type: "string",
},
},
allowedHosts: { allowedHosts: {
type: "array", type: "array",
nullable: true, nullable: true,
@ -124,6 +131,8 @@ export const paramDef = {
summalyProxy: { type: "string", nullable: true }, summalyProxy: { type: "string", nullable: true },
deeplAuthKey: { type: "string", nullable: true }, deeplAuthKey: { type: "string", nullable: true },
deeplIsPro: { type: "boolean" }, deeplIsPro: { type: "boolean" },
libreTranslateApiUrl: { type: "string", nullable: true },
libreTranslateApiKey: { type: "string", nullable: true },
enableTwitterIntegration: { type: "boolean" }, enableTwitterIntegration: { type: "boolean" },
twitterConsumerKey: { type: "string", nullable: true }, twitterConsumerKey: { type: "string", nullable: true },
twitterConsumerSecret: { type: "string", nullable: true }, twitterConsumerSecret: { type: "string", nullable: true },
@ -217,6 +226,15 @@ export default define(meta, paramDef, async (ps, me) => {
}); });
} }
if (Array.isArray(ps.silencedHosts)) {
let lastValue = "";
set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
const lv = lastValue;
lastValue = h;
return h !== "" && h !== lv;
});
}
if (ps.themeColor !== undefined) { if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor; set.themeColor = ps.themeColor;
} }
@ -515,6 +533,22 @@ export default define(meta, paramDef, async (ps, me) => {
set.deeplIsPro = ps.deeplIsPro; set.deeplIsPro = ps.deeplIsPro;
} }
if (ps.libreTranslateApiUrl !== undefined) {
if (ps.libreTranslateApiUrl === "") {
set.libreTranslateApiUrl = null;
} else {
set.libreTranslateApiUrl = ps.libreTranslateApiUrl;
}
}
if (ps.libreTranslateApiKey !== undefined) {
if (ps.libreTranslateApiKey === "") {
set.libreTranslateApiKey = null;
} else {
set.libreTranslateApiKey = ps.libreTranslateApiKey;
}
}
if (ps.enableIpLogging !== undefined) { if (ps.enableIpLogging !== undefined) {
set.enableIpLogging = ps.enableIpLogging; set.enableIpLogging = ps.enableIpLogging;
} }

View File

@ -34,6 +34,7 @@ export const paramDef = {
notResponding: { type: "boolean", nullable: true }, notResponding: { type: "boolean", nullable: true },
suspended: { type: "boolean", nullable: true }, suspended: { type: "boolean", nullable: true },
federating: { type: "boolean", nullable: true }, federating: { type: "boolean", nullable: true },
silenced: { type: "boolean", nullable: true },
subscribing: { type: "boolean", nullable: true }, subscribing: { type: "boolean", nullable: true },
publishing: { type: "boolean", nullable: true }, publishing: { type: "boolean", nullable: true },
limit: { type: "integer", minimum: 1, maximum: 100, default: 30 }, limit: { type: "integer", minimum: 1, maximum: 100, default: 30 },
@ -115,6 +116,22 @@ export default define(meta, paramDef, async (ps, me) => {
} }
} }
if (typeof ps.silenced === "boolean") {
const meta = await fetchMeta(true);
if (ps.silenced) {
if (meta.silencedHosts.length === 0) {
return [];
}
query.andWhere("instance.host IN (:...silences)", {
silences: meta.silencedHosts,
});
} else if (meta.silencedHosts.length > 0) {
query.andWhere("instance.host NOT IN (:...silences)", {
silences: meta.silencedHosts,
});
}
}
if (typeof ps.notResponding === "boolean") { if (typeof ps.notResponding === "boolean") {
if (ps.notResponding) { if (ps.notResponding) {
query.andWhere("instance.isNotResponding = TRUE"); query.andWhere("instance.isNotResponding = TRUE");

View File

@ -482,7 +482,8 @@ export default define(meta, paramDef, async (ps, me) => {
enableServiceWorker: instance.enableServiceWorker, enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null, translatorAvailable:
instance.deeplAuthKey != null || instance.libreTranslateApiUrl != null,
defaultReaction: instance.defaultReaction, defaultReaction: instance.defaultReaction,
...(ps.detail ...(ps.detail

View File

@ -51,15 +51,54 @@ export default define(meta, paramDef, async (ps, user) => {
const instance = await fetchMeta(); const instance = await fetchMeta();
if (instance.deeplAuthKey == null) { if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
return 204; // TODO: 良い感じのエラー返す return 204; // TODO: 良い感じのエラー返す
} }
let targetLang = ps.targetLang; let targetLang = ps.targetLang;
if (targetLang.includes("-")) targetLang = targetLang.split("-")[0]; if (targetLang.includes("-")) targetLang = targetLang.split("-")[0];
if (instance.libreTranslateApiUrl != null) {
const jsonBody = {
q: note.text,
source: "auto",
target: targetLang,
format: "text",
api_key: instance.libreTranslateApiKey ?? "",
};
const url = new URL(instance.libreTranslateApiUrl);
if (url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1);
}
if (!url.pathname.endsWith("/translate")) {
url.pathname += "/translate";
}
const res = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(jsonBody),
agent: getAgentByUrl,
});
const json = (await res.json()) as {
detectedLanguage?: {
confidence: number;
language: string;
};
translatedText: string;
};
return {
sourceLang: json.detectedLanguage?.language,
text: json.translatedText,
};
}
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append("auth_key", instance.deeplAuthKey); params.append("auth_key", instance.deeplAuthKey ?? "");
params.append("text", note.text); params.append("text", note.text);
params.append("target_lang", targetLang); params.append("target_lang", targetLang);

View File

@ -29,6 +29,7 @@ import {
convertId, convertId,
IdConvertType as IdType, IdConvertType as IdType,
} from "../../../native-utils/built/index.js"; } from "../../../native-utils/built/index.js";
import { convertAttachment } from "./mastodon/converters.js";
// re-export native rust id conversion (function and enum) // re-export native rust id conversion (function and enum)
export { IdType, convertId }; export { IdType, convertId };
@ -93,7 +94,7 @@ mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => {
return; return;
} }
const data = await client.uploadMedia(multipartData); const data = await client.uploadMedia(multipartData);
ctx.body = data.data; ctx.body = convertAttachment(data.data as Entity.Attachment);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -112,7 +113,7 @@ mastoFileRouter.post("/v2/media", upload.single("file"), async (ctx) => {
return; return;
} }
const data = await client.uploadMedia(multipartData); const data = await client.uploadMedia(multipartData);
ctx.body = data.data; ctx.body = convertAttachment(data.data as Entity.Attachment);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;

View File

@ -8,6 +8,8 @@ import { apiTimelineMastodon } from "./endpoints/timeline.js";
import { apiNotificationsMastodon } from "./endpoints/notifications.js"; import { apiNotificationsMastodon } from "./endpoints/notifications.js";
import { apiSearchMastodon } from "./endpoints/search.js"; import { apiSearchMastodon } from "./endpoints/search.js";
import { getInstance } from "./endpoints/meta.js"; import { getInstance } from "./endpoints/meta.js";
import { convertAnnouncement, convertFilter } from "./converters.js";
import { convertId, IdType } from "../index.js";
export function getClient( export function getClient(
BASE_URL: string, BASE_URL: string,
@ -68,7 +70,7 @@ export function apiMastodonCompatible(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getInstanceAnnouncements(); const data = await client.getInstanceAnnouncements();
ctx.body = data.data; ctx.body = data.data.map(announcement => convertAnnouncement(announcement));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -83,7 +85,9 @@ export function apiMastodonCompatible(router: Router): void {
const accessTokens = ctx.request.headers.authorization; const accessTokens = ctx.request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.dismissInstanceAnnouncement(ctx.params.id); const data = await client.dismissInstanceAnnouncement(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
@ -100,7 +104,7 @@ export function apiMastodonCompatible(router: Router): void {
// displayed without being logged in // displayed without being logged in
try { try {
const data = await client.getFilters(); const data = await client.getFilters();
ctx.body = data.data; ctx.body = data.data.map(filter => convertFilter(filter));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;

View File

@ -0,0 +1,44 @@
import { Entity } from "@calckey/megalodon";
import { convertId, IdType } from "../index.js";
function simpleConvert(data: any) {
data.id = convertId(data.id, IdType.MastodonId);
return data;
}
export function convertAccount(account: Entity.Account) { return simpleConvert(account); }
export function convertAnnouncement(announcement: Entity.Announcement) { return simpleConvert(announcement); }
export function convertAttachment(attachment: Entity.Attachment) { return simpleConvert(attachment); }
export function convertFilter(filter: Entity.Filter) { return simpleConvert(filter); }
export function convertList(list: Entity.List) { return simpleConvert(list); }
export function convertNotification(notification: Entity.Notification) {
notification.account = convertAccount(notification.account);
notification.id = convertId(notification.id, IdType.MastodonId);
if (notification.status)
notification.status = convertStatus(notification.status);
return notification;
}
export function convertPoll(poll: Entity.Poll) { return simpleConvert(poll); }
export function convertRelationship(relationship: Entity.Relationship) { return simpleConvert(relationship); }
export function convertStatus(status: Entity.Status) {
status.account = convertAccount(status.account);
status.id = convertId(status.id, IdType.MastodonId);
if (status.in_reply_to_account_id)
status.in_reply_to_account_id = convertId(status.in_reply_to_account_id, IdType.MastodonId);
if (status.in_reply_to_id)
status.in_reply_to_id = convertId(status.in_reply_to_id, IdType.MastodonId);
status.media_attachments = status.media_attachments.map(attachment => convertAttachment(attachment));
status.mentions = status.mentions.map(mention => ({
...mention,
id: convertId(mention.id, IdType.MastodonId),
}));
if (status.poll)
status.poll = convertPoll(status.poll);
if (status.reblog)
status.reblog = convertStatus(status.reblog);
return status;
}

View File

@ -3,8 +3,9 @@ import { resolveUser } from "@/remote/resolve-user.js";
import Router from "@koa/router"; import Router from "@koa/router";
import { FindOptionsWhere, IsNull } from "typeorm"; import { FindOptionsWhere, IsNull } from "typeorm";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import { argsToBools, limitToInt } from "./timeline.js"; import { argsToBools, convertTimelinesArgsId, limitToInt } from "./timeline.js";
import { convertId, IdType } from "../../index.js"; import { convertId, IdType } from "../../index.js";
import { convertAccount, convertList, convertRelationship, convertStatus } from "../converters.js";
const relationshipModel = { const relationshipModel = {
id: "", id: "",
@ -62,9 +63,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.updateCredentials( const data = await client.updateCredentials(
(ctx.request as any).body as any, (ctx.request as any).body as any,
); );
let resp = data.data; ctx.body = convertAccount(data.data);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -81,9 +80,7 @@ export function apiAccountMastodon(router: Router): void {
(ctx.request.query as any).acct, (ctx.request.query as any).acct,
"accounts", "accounts",
); );
let resp = data.data.accounts[0]; ctx.body = convertAccount(data.data.accounts[0]);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -91,255 +88,6 @@ export function apiAccountMastodon(router: Router): void {
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });
router.get<{ Params: { id: string } }>("/v1/accounts/:id", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const calcId = convertId(ctx.params.id, IdType.CalckeyId);
const data = await client.getAccount(calcId);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} 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/statuses",
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.getAccountStatuses(
convertId(ctx.params.id, IdType.CalckeyId),
argsToBools(limitToInt(ctx.query as any)),
);
let resp = data.data;
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
resp[statIdx].in_reply_to_account_id = resp[statIdx]
.in_reply_to_account_id
? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
: null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
: null;
let mentions = resp[statIdx].mentions;
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(
mentions[mtnIdx].id,
IdType.MastodonId,
);
}
}
ctx.body = resp;
} 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) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccountFollowers(
convertId(ctx.params.id, IdType.CalckeyId),
limitToInt(ctx.query as any),
);
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} 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/following",
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.getAccountFollowing(
convertId(ctx.params.id, IdType.CalckeyId),
limitToInt(ctx.query as any),
);
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} 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/lists",
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.getAccountLists(ctx.params.id);
ctx.body = data.data;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/follow",
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.followAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let acct = data.data;
acct.following = true;
acct.id = convertId(acct.id, IdType.MastodonId);
ctx.body = acct;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/unfollow",
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.unfollowAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let acct = data.data;
acct.id = convertId(acct.id, IdType.MastodonId);
acct.following = false;
ctx.body = acct;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/block",
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.blockAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/unblock",
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.unblockAccount(
convertId(ctx.params.id, IdType.MastodonId),
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/mute",
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.muteAccount(
convertId(ctx.params.id, IdType.CalckeyId),
(ctx.request as any).body as any,
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/unmute",
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.unmuteAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.get("/v1/accounts/relationships", async (ctx) => { router.get("/v1/accounts/relationships", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
@ -364,11 +112,7 @@ export function apiAccountMastodon(router: Router): void {
} }
const data = await client.getRelationships(reqIds); const data = await client.getRelationships(reqIds);
let resp = data.data; ctx.body = data.data.map(relationship => convertRelationship(relationship));
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
let data = e.response.data; let data = e.response.data;
@ -378,33 +122,228 @@ export function apiAccountMastodon(router: Router): void {
ctx.body = data; ctx.body = data;
} }
}); });
router.get<{ Params: { id: string } }>("/v1/accounts/:id", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const calcId = convertId(ctx.params.id, IdType.CalckeyId);
const data = await client.getAccount(calcId);
ctx.body = convertAccount(data.data);
} 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/statuses",
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.getAccountStatuses(
convertId(ctx.params.id, IdType.CalckeyId),
convertTimelinesArgsId(argsToBools(limitToInt(ctx.query as any))),
);
ctx.body = data.data.map(status => convertStatus(status));
} 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) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccountFollowers(
convertId(ctx.params.id, IdType.CalckeyId),
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
ctx.body = data.data.map(account => convertAccount(account));
} 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/following",
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.getAccountFollowing(
convertId(ctx.params.id, IdType.CalckeyId),
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
ctx.body = data.data.map(account => convertAccount(account));
} 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/lists",
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.getAccountLists(
convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = data.data.map(list => convertList(list));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/follow",
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.followAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let acct = convertRelationship(data.data);
acct.following = true;
ctx.body = acct;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/unfollow",
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.unfollowAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let acct = convertRelationship(data.data);
acct.following = false;
ctx.body = acct;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/block",
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.blockAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/unblock",
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.unblockAccount(
convertId(ctx.params.id, IdType.MastodonId),
);
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/mute",
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.muteAccount(
convertId(ctx.params.id, IdType.CalckeyId),
(ctx.request as any).body as any,
);
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/unmute",
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.unmuteAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertRelationship(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) => { router.get("/v1/bookmarks", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = (await client.getBookmarks( const data = (await client.getBookmarks(
limitToInt(ctx.query as any), convertTimelinesArgsId(limitToInt(ctx.query as any)),
)) as any; ));
let resp = data.data; ctx.body = data.data.map(status => convertStatus(status));
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
resp[statIdx].in_reply_to_account_id = resp[statIdx]
.in_reply_to_account_id
? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
: null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
: null;
let mentions = resp[statIdx].mentions;
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(
mentions[mtnIdx].id,
IdType.MastodonId,
);
}
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -417,26 +356,8 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getFavourites(limitToInt(ctx.query as any)); const data = await client.getFavourites(convertTimelinesArgsId(limitToInt(ctx.query as any)));
let resp = data.data; ctx.body = data.data.map(status => convertStatus(status));
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
resp[statIdx].in_reply_to_account_id = resp[statIdx]
.in_reply_to_account_id
? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
: null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
: null;
let mentions = resp[statIdx].mentions;
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(
mentions[mtnIdx].id,
IdType.MastodonId,
);
}
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -449,12 +370,8 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getMutes(limitToInt(ctx.query as any)); const data = await client.getMutes(convertTimelinesArgsId(limitToInt(ctx.query as any)));
let resp = data.data; ctx.body = data.data.map(account => convertAccount(account));
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -467,12 +384,8 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getBlocks(limitToInt(ctx.query as any)); const data = await client.getBlocks(convertTimelinesArgsId(limitToInt(ctx.query as any)));
let resp = data.data; ctx.body = data.data.map(account => convertAccount(account));
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -488,11 +401,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.getFollowRequests( const data = await client.getFollowRequests(
((ctx.query as any) || { limit: 20 }).limit, ((ctx.query as any) || { limit: 20 }).limit,
); );
let resp = data.data; ctx.body = data.data.map(account => convertAccount(account));
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -510,9 +419,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.acceptFollowRequest( const data = await client.acceptFollowRequest(
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
); );
let resp = data.data; ctx.body = convertRelationship(data.data);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -531,9 +438,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.rejectFollowRequest( const data = await client.rejectFollowRequest(
convertId(ctx.params.id, IdType.CalckeyId), convertId(ctx.params.id, IdType.CalckeyId),
); );
let resp = data.data; ctx.body = convertRelationship(data.data);
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);

View File

@ -1,6 +1,8 @@
import megalodon, { MegalodonInterface } from "@calckey/megalodon"; import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import Router from "@koa/router"; import Router from "@koa/router";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import { IdType, convertId } from "../../index.js";
import { convertFilter } from "../converters.js";
export function apiFilterMastodon(router: Router): void { export function apiFilterMastodon(router: Router): void {
router.get("/v1/filters", async (ctx) => { router.get("/v1/filters", async (ctx) => {
@ -10,7 +12,7 @@ export function apiFilterMastodon(router: Router): void {
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.getFilters(); const data = await client.getFilters();
ctx.body = data.data; ctx.body = data.data.map(filter => convertFilter(filter));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -24,8 +26,10 @@ export function apiFilterMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.getFilter(ctx.params.id); const data = await client.getFilter(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = convertFilter(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -40,7 +44,7 @@ export function apiFilterMastodon(router: Router): void {
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.createFilter(body.phrase, body.context, body); const data = await client.createFilter(body.phrase, body.context, body);
ctx.body = data.data; ctx.body = convertFilter(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -55,11 +59,11 @@ export function apiFilterMastodon(router: Router): void {
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.updateFilter( const data = await client.updateFilter(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
body.phrase, body.phrase,
body.context, body.context,
); );
ctx.body = data.data; ctx.body = convertFilter(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -73,7 +77,9 @@ export function apiFilterMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.deleteFilter(ctx.params.id); const data = await client.deleteFilter(
convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);

View File

@ -1,8 +1,10 @@
import megalodon, { MegalodonInterface } from "@calckey/megalodon"; import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import Router from "@koa/router"; import Router from "@koa/router";
import { koaBody } from "koa-body"; import { koaBody } from "koa-body";
import { convertId, IdType } from "../../index.js";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import { toTextWithReaction } from "./timeline.js"; import { convertTimelinesArgsId, toTextWithReaction } from "./timeline.js";
import { convertNotification } from "../converters.js";
function toLimitToInt(q: any) { function toLimitToInt(q: any) {
if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10); if (q.limit) if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10);
return q; return q;
@ -15,9 +17,10 @@ export function apiNotificationsMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.getNotifications(toLimitToInt(ctx.query)); const data = await client.getNotifications(convertTimelinesArgsId(toLimitToInt(ctx.query)));
const notfs = data.data; const notfs = data.data;
const ret = notfs.map((n) => { const ret = notfs.map((n) => {
n = convertNotification(n);
if (n.type !== "follow" && n.type !== "follow_request") { if (n.type !== "follow" && n.type !== "follow_request") {
if (n.type === "reaction") n.type = "favourite"; if (n.type === "reaction") n.type = "favourite";
n.status = toTextWithReaction( n.status = toTextWithReaction(
@ -43,8 +46,10 @@ export function apiNotificationsMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const dataRaw = await client.getNotification(ctx.params.id); const dataRaw = await client.getNotification(
const data = dataRaw.data; convertId(ctx.params.id, IdType.CalckeyId)
);
const data = convertNotification(dataRaw.data);
if (data.type !== "follow" && data.type !== "follow_request") { if (data.type !== "follow" && data.type !== "follow_request") {
if (data.type === "reaction") data.type = "favourite"; if (data.type === "reaction") data.type = "favourite";
ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0]; ctx.body = toTextWithReaction([data as any], ctx.request.hostname)[0];
@ -79,7 +84,9 @@ export function apiNotificationsMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const data = await client.dismissNotification(ctx.params.id); const data = await client.dismissNotification(
convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);

View File

@ -3,7 +3,8 @@ import Router from "@koa/router";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import axios from "axios"; import axios from "axios";
import { Converter } from "@calckey/megalodon"; import { Converter } from "@calckey/megalodon";
import { limitToInt } from "./timeline.js"; import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
import { convertAccount, convertStatus } from "../converters.js";
export function apiSearchMastodon(router: Router): void { export function apiSearchMastodon(router: Router): void {
router.get("/v1/search", async (ctx) => { router.get("/v1/search", async (ctx) => {
@ -12,7 +13,7 @@ export function apiSearchMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const body: any = ctx.request.body; const body: any = ctx.request.body;
try { try {
const query: any = limitToInt(ctx.query); const query: any = convertTimelinesArgsId(limitToInt(ctx.query));
const type = query.type || ""; const type = query.type || "";
const data = await client.search(query.q, type, query); const data = await client.search(query.q, type, query);
ctx.body = data.data; ctx.body = data.data;
@ -27,18 +28,18 @@ export function apiSearchMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const query: any = limitToInt(ctx.query); const query: any = convertTimelinesArgsId(limitToInt(ctx.query));
const type = query.type; const type = query.type;
if (type) { if (type) {
const data = await client.search(query.q, type, query); const data = await client.search(query.q, type, query);
ctx.body = data.data; ctx.body = data.data.accounts.map(account => convertAccount(account));
} else { } else {
const acct = await client.search(query.q, "accounts", query); const acct = await client.search(query.q, "accounts", query);
const stat = await client.search(query.q, "statuses", query); const stat = await client.search(query.q, "statuses", query);
const tags = await client.search(query.q, "hashtags", query); const tags = await client.search(query.q, "hashtags", query);
ctx.body = { ctx.body = {
accounts: acct.data.accounts, accounts: acct.data.accounts.map(account => convertAccount(account)),
statuses: stat.data.statuses, statuses: stat.data.statuses.map(status => convertStatus(status)),
hashtags: tags.data.hashtags, hashtags: tags.data.hashtags,
}; };
} }
@ -57,7 +58,7 @@ export function apiSearchMastodon(router: Router): void {
ctx.request.hostname, ctx.request.hostname,
accessTokens, accessTokens,
); );
ctx.body = data; ctx.body = data.map(status => convertStatus(status));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -69,12 +70,16 @@ export function apiSearchMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
try { try {
const query: any = ctx.query; const query: any = ctx.query;
const data = await getFeaturedUser( let data = await getFeaturedUser(
BASE_URL, BASE_URL,
ctx.request.hostname, ctx.request.hostname,
accessTokens, accessTokens,
query.limit || 20, query.limit || 20,
); );
data = data.map(suggestion => {
suggestion.account = convertAccount(suggestion.account);
return suggestion;
});
console.log(data); console.log(data);
ctx.body = data; ctx.body = data;
} catch (e: any) { } catch (e: any) {

View File

@ -4,7 +4,9 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
import axios from "axios"; import axios from "axios";
import querystring from "node:querystring"; import querystring from "node:querystring";
import qs from "qs"; import qs from "qs";
import { limitToInt } from "./timeline.js"; import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
import { convertId, IdType } from "../../index.js";
import { convertAccount, convertAttachment, convertPoll, convertStatus } from "../converters.js";
function normalizeQuery(data: any) { function normalizeQuery(data: any) {
const str = querystring.stringify(data); const str = querystring.stringify(data);
@ -18,6 +20,8 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
let body: any = ctx.request.body; let body: any = ctx.request.body;
if (body.in_reply_to_id)
body.in_reply_to_id = convertId(body.in_reply_to_id, IdType.CalckeyId);
if ( if (
(!body.poll && body["poll[options][]"]) || (!body.poll && body["poll[options][]"]) ||
(!body.media_ids && body["media_ids[]"]) (!body.media_ids && body["media_ids[]"])
@ -54,7 +58,7 @@ export function apiStatusMastodon(router: Router): void {
body.sensitive = body.sensitive =
typeof sensitive === "string" ? sensitive === "true" : sensitive; typeof sensitive === "string" ? sensitive === "true" : sensitive;
const data = await client.postStatus(text, body); const data = await client.postStatus(text, body);
ctx.body = data.data; ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -66,8 +70,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getStatus(ctx.params.id); const data = await client.getStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -79,7 +85,9 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.deleteStatus(ctx.params.id); const data = await client.deleteStatus(
convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e.response.data, request.params.id); console.error(e.response.data, request.params.id);
@ -100,10 +108,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const id = ctx.params.id; const id = convertId(ctx.params.id, IdType.CalckeyId);
const data = await client.getStatusContext( const data = await client.getStatusContext(
id, id,
limitToInt(ctx.query as any), convertTimelinesArgsId(limitToInt(ctx.query as any)),
); );
const status = await client.getStatus(id); const status = await client.getStatus(id);
let reqInstance = axios.create({ let reqInstance = axios.create({
@ -126,6 +134,8 @@ export function apiStatusMastodon(router: Router): void {
text, text,
), ),
); );
data.data.ancestors = data.data.ancestors.map(status => convertStatus(status));
data.data.descendants = data.data.descendants.map(status => convertStatus(status));
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
@ -141,8 +151,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getStatusRebloggedBy(ctx.params.id); const data = await client.getStatusRebloggedBy(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = data.data.map(account => convertAccount(account));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -165,11 +177,11 @@ export function apiStatusMastodon(router: Router): void {
const react = await getFirstReaction(BASE_URL, accessTokens); const react = await getFirstReaction(BASE_URL, accessTokens);
try { try {
const a = (await client.createEmojiReaction( const a = (await client.createEmojiReaction(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
react, react,
)) as any; )) as any;
//const data = await client.favouriteStatus(ctx.params.id) as any; //const data = await client.favouriteStatus(ctx.params.id) as any;
ctx.body = a.data; ctx.body = convertStatus(a.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -186,8 +198,11 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
const react = await getFirstReaction(BASE_URL, accessTokens); const react = await getFirstReaction(BASE_URL, accessTokens);
try { try {
const data = await client.deleteEmojiReaction(ctx.params.id, react); const data = await client.deleteEmojiReaction(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
react
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -203,8 +218,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.reblogStatus(ctx.params.id); const data = await client.reblogStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -220,8 +237,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.unreblogStatus(ctx.params.id); const data = await client.unreblogStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -237,8 +256,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.bookmarkStatus(ctx.params.id); const data = await client.bookmarkStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -254,8 +275,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = (await client.unbookmarkStatus(ctx.params.id)) as any; const data = await client.unbookmarkStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -271,8 +294,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.pinStatus(ctx.params.id); const data = await client.pinStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -288,8 +313,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.unpinStatus(ctx.params.id); const data = await client.unpinStatus(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = convertStatus(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -302,8 +329,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getMedia(ctx.params.id); const data = await client.getMedia(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = convertAttachment(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -316,10 +345,10 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.updateMedia( const data = await client.updateMedia(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
ctx.request.body as any, ctx.request.body as any,
); );
ctx.body = data.data; ctx.body = convertAttachment(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -331,8 +360,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getPoll(ctx.params.id); const data = await client.getPoll(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId)
);
ctx.body = convertPoll(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;
@ -347,10 +378,10 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.votePoll( const data = await client.votePoll(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
(ctx.request.body as any).choices, (ctx.request.body as any).choices,
); );
ctx.body = data.data; ctx.body = convertPoll(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = 401; ctx.status = 401;

View File

@ -4,6 +4,8 @@ import { getClient } from "../ApiMastodonCompatibleService.js";
import { statusModel } from "./status.js"; import { statusModel } from "./status.js";
import Autolinker from "autolinker"; import Autolinker from "autolinker";
import { ParsedUrlQuery } from "querystring"; import { ParsedUrlQuery } from "querystring";
import { convertAccount, convertList, convertStatus } from "../converters.js";
import { convertId, IdType } from "../../index.js";
export function limitToInt(q: ParsedUrlQuery) { export function limitToInt(q: ParsedUrlQuery) {
let object: any = q; let object: any = q;
@ -29,6 +31,16 @@ export function argsToBools(q: ParsedUrlQuery) {
return q; return q;
} }
export function convertTimelinesArgsId(q: ParsedUrlQuery) {
if (typeof q.min_id === "string")
q.min_id = convertId(q.min_id, IdType.CalckeyId);
if (typeof q.max_id === "string")
q.max_id = convertId(q.max_id, IdType.CalckeyId);
if (typeof q.since_id === "string")
q.since_id = convertId(q.since_id, IdType.CalckeyId);
return q;
}
export function toTextWithReaction(status: Entity.Status[], host: string) { export function toTextWithReaction(status: Entity.Status[], host: string) {
return status.map((t) => { return status.map((t) => {
if (!t) return statusModel(null, null, [], "no content"); if (!t) return statusModel(null, null, [], "no content");
@ -97,9 +109,10 @@ export function apiTimelineMastodon(router: Router): void {
try { try {
const query: any = ctx.query; const query: any = ctx.query;
const data = query.local const data = query.local
? await client.getLocalTimeline(argsToBools(limitToInt(query))) ? await client.getLocalTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))))
: await client.getPublicTimeline(argsToBools(limitToInt(query))); : await client.getPublicTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))));
ctx.body = toTextWithReaction(data.data, ctx.hostname); let resp = data.data.map(status => convertStatus(status));
ctx.body = toTextWithReaction(resp, ctx.hostname);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -116,9 +129,10 @@ export function apiTimelineMastodon(router: Router): void {
try { try {
const data = await client.getTagTimeline( const data = await client.getTagTimeline(
ctx.params.hashtag, ctx.params.hashtag,
argsToBools(limitToInt(ctx.query)), convertTimelinesArgsId(argsToBools(limitToInt(ctx.query))),
); );
ctx.body = toTextWithReaction(data.data, ctx.hostname); let resp = data.data.map(status => convertStatus(status));
ctx.body = toTextWithReaction(resp, ctx.hostname);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -132,8 +146,9 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getHomeTimeline(limitToInt(ctx.query)); const data = await client.getHomeTimeline(convertTimelinesArgsId(limitToInt(ctx.query)));
ctx.body = toTextWithReaction(data.data, ctx.hostname); let resp = data.data.map(status => convertStatus(status));
ctx.body = toTextWithReaction(resp, ctx.hostname);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -149,10 +164,11 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getListTimeline( const data = await client.getListTimeline(
ctx.params.listId, convertId(ctx.params.listId, IdType.CalckeyId),
limitToInt(ctx.query), convertTimelinesArgsId(limitToInt(ctx.query)),
); );
ctx.body = toTextWithReaction(data.data, ctx.hostname); let resp = data.data.map(status => convertStatus(status));
ctx.body = toTextWithReaction(resp, ctx.hostname);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -166,7 +182,7 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getConversationTimeline(limitToInt(ctx.query)); const data = await client.getConversationTimeline(convertTimelinesArgsId(limitToInt(ctx.query)));
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
@ -181,7 +197,7 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getLists(); const data = await client.getLists();
ctx.body = data.data; ctx.body = data.data.map(list => convertList(list));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -196,8 +212,10 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getList(ctx.params.id); const data = await client.getList(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertList(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -212,7 +230,7 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.createList((ctx.request.body as any).title); const data = await client.createList((ctx.request.body as any).title);
ctx.body = data.data; ctx.body = convertList(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -227,8 +245,11 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.updateList(ctx.params.id, (ctx.request.body as any).title); const data = await client.updateList(
ctx.body = data.data; convertId(ctx.params.id, IdType.CalckeyId),
(ctx.request.body as any).title
);
ctx.body = convertList(data.data);
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -244,7 +265,9 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.deleteList(ctx.params.id); const data = await client.deleteList(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
@ -262,10 +285,10 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getAccountsInList( const data = await client.getAccountsInList(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
ctx.query as any, convertTimelinesArgsId(ctx.query as any),
); );
ctx.body = data.data; ctx.body = data.data.map(account => convertAccount(account));
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
console.error(e.response.data); console.error(e.response.data);
@ -282,8 +305,8 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.addAccountsToList( const data = await client.addAccountsToList(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
(ctx.query as any).account_ids, (ctx.query.account_ids as string[]).map(id => convertId(id, IdType.CalckeyId)),
); );
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
@ -302,8 +325,8 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.deleteAccountsFromList( const data = await client.deleteAccountsFromList(
ctx.params.id, convertId(ctx.params.id, IdType.CalckeyId),
(ctx.query as any).account_ids, (ctx.query.account_ids as string[]).map(id => convertId(id, IdType.CalckeyId)),
); );
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {

View File

@ -55,7 +55,7 @@ export default async (ctx: Koa.Context) => {
return; return;
} }
const available = await validateEmailForAccount(emailAddress); const { available } = await validateEmailForAccount(emailAddress);
if (!available) { if (!available) {
ctx.status = 400; ctx.status = 400;
return; return;

View File

@ -399,8 +399,10 @@ router.get("/notes/:note", async (ctx, next) => {
visibility: In(["public", "home"]), visibility: In(["public", "home"]),
}); });
try {
if (note) { if (note) {
const _note = await Notes.pack(note); const _note = await Notes.pack(note);
const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
const meta = await fetchMeta(); const meta = await fetchMeta();
await ctx.render("note", { await ctx.render("note", {
@ -421,6 +423,7 @@ router.get("/notes/:note", async (ctx, next) => {
return; return;
} }
} catch {}
await next(); await next();
}); });

View File

@ -6,11 +6,13 @@ import {
NoteThreadMutings, NoteThreadMutings,
UserProfiles, UserProfiles,
Users, Users,
Followings,
} from "@/models/index.js"; } from "@/models/index.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import type { User } from "@/models/entities/user.js"; import type { User } from "@/models/entities/user.js";
import type { Notification } from "@/models/entities/notification.js"; import type { Notification } from "@/models/entities/notification.js";
import { sendEmailNotification } from "./send-email-notification.js"; import { sendEmailNotification } from "./send-email-notification.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
export async function createNotification( export async function createNotification(
notifieeId: User["id"], notifieeId: User["id"],
@ -21,6 +23,26 @@ export async function createNotification(
return null; return null;
} }
if (
data.notifierId &&
["mention", "reply", "renote", "quote", "reaction"].includes(type)
) {
const notifier = await Users.findOneBy({ id: data.notifierId });
// suppress if the notifier does not exist or is silenced.
if (!notifier) return null;
// suppress if the notifier is silenced or in a silenced instance, and not followed by the notifiee.
if (
(notifier.isSilenced ||
(Users.isRemoteUser(notifier) &&
(await shouldSilenceInstance(notifier.host)))) &&
!(await Followings.exist({
where: { followerId: notifieeId, followeeId: data.notifierId },
}))
)
return null;
}
const profile = await UserProfiles.findOneBy({ userId: notifieeId }); const profile = await UserProfiles.findOneBy({ userId: notifieeId });
const isMuted = profile?.mutingNotificationTypes.includes(type); const isMuted = profile?.mutingNotificationTypes.includes(type);

View File

@ -27,6 +27,7 @@ import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js
import type { Packed } from "@/misc/schema.js"; import type { Packed } from "@/misc/schema.js";
import { getActiveWebhooks } from "@/misc/webhook-cache.js"; import { getActiveWebhooks } from "@/misc/webhook-cache.js";
import { webhookDeliver } from "@/queue/index.js"; import { webhookDeliver } from "@/queue/index.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
const logger = new Logger("following/create"); const logger = new Logger("following/create");
@ -226,13 +227,19 @@ export default async function (
}); });
// フォロー対象が鍵アカウントである or // フォロー対象が鍵アカウントである or
// The follower is silenced, or
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or
// The follower is remote, the followee is local, and the follower is in a silenced instance.
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
if ( if (
followee.isLocked || followee.isLocked ||
follower.isSilenced ||
(followeeProfile.carefulBot && follower.isBot) || (followeeProfile.carefulBot && follower.isBot) ||
(Users.isLocalUser(follower) && Users.isRemoteUser(followee)) (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) ||
(Users.isRemoteUser(follower) &&
Users.isLocalUser(followee) &&
(await shouldSilenceInstance(follower.host)))
) { ) {
let autoAccept = false; let autoAccept = false;

View File

@ -6,6 +6,7 @@ import type { User } from "@/models/entities/user.js";
import { Blockings, FollowRequests, Users } from "@/models/index.js"; import { Blockings, FollowRequests, Users } from "@/models/index.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import { createNotification } from "../../create-notification.js"; import { createNotification } from "../../create-notification.js";
import config from "@/config/index.js";
export default async function ( export default async function (
follower: { follower: {
@ -79,7 +80,13 @@ export default async function (
} }
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
const content = renderActivity(renderFollow(follower, followee)); const content = renderActivity(
renderFollow(
follower,
followee,
requestId ?? `${config.url}/follows/${followRequest.id}`,
),
);
deliver(follower, content, followee.inbox); deliver(follower, content, followee.inbox);
} }
} }

View File

@ -39,7 +39,7 @@ import {
} from "@/models/index.js"; } from "@/models/index.js";
import type { DriveFile } from "@/models/entities/drive-file.js"; import type { DriveFile } from "@/models/entities/drive-file.js";
import type { App } from "@/models/entities/app.js"; import type { App } from "@/models/entities/app.js";
import { Not, In } from "typeorm"; import { Not, In, IsNull } from "typeorm";
import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import { import {
@ -66,6 +66,7 @@ import { Cache } from "@/misc/cache.js";
import type { UserProfile } from "@/models/entities/user-profile.js"; import type { UserProfile } from "@/models/entities/user-profile.js";
import { db } from "@/db/postgre.js"; import { db } from "@/db/postgre.js";
import { getActiveWebhooks } from "@/misc/webhook-cache.js"; import { getActiveWebhooks } from "@/misc/webhook-cache.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
const mutedWordsCache = new Cache< const mutedWordsCache = new Cache<
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
@ -166,6 +167,7 @@ export default async (
data: Option, data: Option,
silent = false, silent = false,
) => ) =>
// rome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
new Promise<Note>(async (res, rej) => { new Promise<Note>(async (res, rej) => {
// If you reply outside the channel, match the scope of the target. // If you reply outside the channel, match the scope of the target.
// TODO (I think it's a process that could be done on the client side, but it's server side for now.) // TODO (I think it's a process that could be done on the client side, but it's server side for now.)
@ -203,6 +205,15 @@ export default async (
data.visibility = "home"; data.visibility = "home";
} }
// Enforce home visibility if the user is in a silenced instance.
if (
data.visibility === "public" &&
Users.isRemoteUser(user) &&
(await shouldSilenceInstance(user.host))
) {
data.visibility = "home";
}
// Reject if the target of the renote is a public range other than "Home or Entire". // Reject if the target of the renote is a public range other than "Home or Entire".
if ( if (
data.renote && data.renote &&

View File

@ -118,7 +118,7 @@ export default async (
userId: user.id, userId: user.id,
}); });
// リアクションされたユーザーがローカルユーザーなら通知を作成 // Create notification if the reaction target is a local user.
if (note.userHost === null) { if (note.userHost === null) {
createNotification(note.userId, "reaction", { createNotification(note.userId, "reaction", {
notifierId: user.id, notifierId: user.id,
@ -143,7 +143,7 @@ export default async (
} }
}); });
//#region 配信 //#region deliver
if (Users.isLocalUser(user) && !note.localOnly) { if (Users.isLocalUser(user) && !note.localOnly) {
const content = renderActivity(await renderLike(record, note)); const content = renderActivity(await renderLike(record, note));
const dm = new DeliverManager(user, content); const dm = new DeliverManager(user, content);

View File

@ -1,7 +0,0 @@
node_modules
/built
/coverage
/.eslintrc.js
/jest.config.ts
/test
/test-d

View File

@ -1,65 +0,0 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
parserOptions: {
tsconfigRootDir: __dirname,
project: ["./tsconfig.json"],
},
plugins: ["@typescript-eslint"],
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
rules: {
indent: [
"error",
"tab",
{
SwitchCase: 1,
MemberExpression: "off",
flatTernaryExpressions: true,
ArrayExpression: "first",
ObjectExpression: "first",
},
],
"eol-last": ["error", "always"],
semi: ["error", "always"],
quotes: ["error", "single"],
"comma-dangle": ["error", "always-multiline"],
"keyword-spacing": [
"error",
{
before: true,
after: true,
},
],
"key-spacing": [
"error",
{
beforeColon: false,
afterColon: true,
},
],
"space-infix-ops": ["error"],
"space-before-blocks": ["error", "always"],
"object-curly-spacing": ["error", "always"],
"nonblock-statement-body-position": ["error", "beside"],
eqeqeq: ["error", "always", { null: "ignore" }],
"no-multiple-empty-lines": ["error", { max: 1 }],
"no-multi-spaces": ["error"],
"no-var": ["error"],
"prefer-arrow-callback": ["error"],
"no-throw-literal": ["error"],
"no-param-reassign": ["warn"],
"no-constant-condition": ["warn"],
"no-empty-pattern": ["warn"],
"@typescript-eslint/no-unnecessary-condition": ["error"],
"@typescript-eslint/no-inferrable-types": ["warn"],
"@typescript-eslint/no-non-null-assertion": ["warn"],
"@typescript-eslint/explicit-function-return-type": ["warn"],
"@typescript-eslint/no-misused-promises": [
"error",
{
checksVoidReturn: false,
},
],
"@typescript-eslint/consistent-type-imports": "error",
},
};

View File

@ -9,9 +9,8 @@
"tsd": "tsd", "tsd": "tsd",
"api": "pnpm api-extractor run --local --verbose", "api": "pnpm api-extractor run --local --verbose",
"api-prod": "pnpm api-extractor run --verbose", "api-prod": "pnpm api-extractor run --verbose",
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "pnpm typecheck && pnpm eslint", "lint": "pnpm typecheck && pnpm rome check \"src/*.ts\"",
"jest": "jest --coverage --detectOpenHandles", "jest": "jest --coverage --detectOpenHandles",
"test": "pnpm jest && pnpm tsd" "test": "pnpm jest && pnpm tsd"
}, },

View File

@ -55,6 +55,7 @@ export type Endpoints = {
"admin/get-table-stats": { req: TODO; res: TODO }; "admin/get-table-stats": { req: TODO; res: TODO };
"admin/invite": { req: TODO; res: TODO }; "admin/invite": { req: TODO; res: TODO };
"admin/logs": { req: TODO; res: TODO }; "admin/logs": { req: TODO; res: TODO };
"admin/meta": { req: TODO; res: TODO };
"admin/reset-password": { req: TODO; res: TODO }; "admin/reset-password": { req: TODO; res: TODO };
"admin/resolve-abuse-user-report": { req: TODO; res: TODO }; "admin/resolve-abuse-user-report": { req: TODO; res: TODO };
"admin/resync-chart": { req: TODO; res: TODO }; "admin/resync-chart": { req: TODO; res: TODO };

View File

@ -32,7 +32,7 @@
"autosize": "5.0.2", "autosize": "5.0.2",
"blurhash": "1.1.5", "blurhash": "1.1.5",
"broadcast-channel": "4.19.1", "broadcast-channel": "4.19.1",
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git", "browser-image-resizer": "github:misskey-dev/browser-image-resizer",
"calckey-js": "workspace:*", "calckey-js": "workspace:*",
"chart.js": "4.1.1", "chart.js": "4.1.1",
"chartjs-adapter-date-fns": "2.0.1", "chartjs-adapter-date-fns": "2.0.1",

View File

@ -195,8 +195,7 @@ function onMousedown(evt: MouseEvent): void {
} }
&:focus-visible { &:focus-visible {
outline: solid 2px var(--focus); outline: auto;
outline-offset: 2px;
} }
&.inline { &.inline {

View File

@ -1,5 +1,6 @@
<template> <template>
<button <button
ref="el"
class="_button" class="_button"
:class="{ showLess: modelValue, fade: !modelValue }" :class="{ showLess: modelValue, fade: !modelValue }"
@click.stop="toggle" @click.stop="toggle"
@ -12,7 +13,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed, ref } from "vue";
import { length } from "stringz"; import { length } from "stringz";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { concat } from "@/scripts/array"; import { concat } from "@/scripts/array";
@ -27,6 +28,8 @@ const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void; (ev: "update:modelValue", v: boolean): void;
}>(); }>();
const el = ref<HTMLElement>();
const label = computed(() => { const label = computed(() => {
return concat([ return concat([
props.note.text props.note.text
@ -43,6 +46,14 @@ const label = computed(() => {
const toggle = () => { const toggle = () => {
emit("update:modelValue", !props.modelValue); emit("update:modelValue", !props.modelValue);
}; };
function focus() {
el.value.focus();
}
defineExpose({
focus,
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -62,7 +73,8 @@ const toggle = () => {
} }
} }
} }
&:hover > span { &:hover > span,
&:focus > span {
background: var(--cwFg) !important; background: var(--cwFg) !important;
color: var(--cwBg) !important; color: var(--cwBg) !important;
} }
@ -73,6 +85,7 @@ const toggle = () => {
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
z-index: 2;
> span { > span {
display: inline-block; display: inline-block;
background: var(--panel); background: var(--panel);
@ -81,7 +94,8 @@ const toggle = () => {
border-radius: 999px; border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%); box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
} }
&:hover { &:hover,
&:focus {
> span { > span {
background: var(--panelHighlight); background: var(--panelHighlight);
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div ref="thumbnail" class="zdjebgpv"> <button ref="thumbnail" class="zdjebgpv">
<ImgWithBlurhash <ImgWithBlurhash
v-if="isThumbnailAvailable" v-if="isThumbnailAvailable"
:hash="file.blurhash" :hash="file.blurhash"
@ -36,7 +36,7 @@
v-if="isThumbnailAvailable && is === 'video'" v-if="isThumbnailAvailable && is === 'video'"
class="ph-file-video ph-bold ph-lg icon-sub" class="ph-file-video ph-bold ph-lg icon-sub"
></i> ></i>
</div> </button>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -88,6 +88,9 @@ const isThumbnailAvailable = computed(() => {
background: var(--panel); background: var(--panel);
border-radius: 8px; border-radius: 8px;
overflow: clip; overflow: clip;
border: 0;
padding: 0;
cursor: pointer;
> .icon-sub { > .icon-sub {
position: absolute; position: absolute;

View File

@ -1,8 +1,10 @@
<template> <template>
<FocusTrap v-bind:active="isActive">
<div <div
class="omfetrab" class="omfetrab"
:class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]"
:style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }"
tabindex="-1"
> >
<input <input
ref="search" ref="search"
@ -152,6 +154,7 @@
</button> </button>
</div> </div>
</div> </div>
</FocusTrap>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -171,6 +174,7 @@ import { deviceKind } from "@/scripts/device-kind";
import { emojiCategories, instance } from "@/instance"; import { emojiCategories, instance } from "@/instance";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { FocusTrap } from "focus-trap-vue";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{

View File

@ -20,9 +20,12 @@ export default defineComponent({
}, },
computed: { computed: {
compiledFormula(): any { compiledFormula(): any {
return katex.renderToString(this.formula, { const katexString = katex.renderToString(this.formula, {
throwOnError: false, throwOnError: false,
} as any); } as any);
return this.block
? `<div style="text-align:center">${katexString}</div>`
: katexString;
}, },
}, },
}); });

View File

@ -5,6 +5,7 @@
{ {
yellow: instance.isNotResponding, yellow: instance.isNotResponding,
red: instance.isBlocked, red: instance.isBlocked,
purple: instance.isSilenced,
gray: instance.isSuspended, gray: instance.isSuspended,
}, },
]" ]"
@ -23,13 +24,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as misskey from "calckey-js"; import * as calckey from "calckey-js";
import MkMiniChart from "@/components/MkMiniChart.vue"; import MkMiniChart from "@/components/MkMiniChart.vue";
import * as os from "@/os"; import * as os from "@/os";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
const props = defineProps<{ const props = defineProps<{
instance: misskey.entities.Instance; instance: calckey.entities.Instance;
}>(); }>();
let chartValues = $ref<number[] | null>(null); let chartValues = $ref<number[] | null>(null);
@ -135,6 +136,21 @@ function getInstanceIcon(instance): string {
background-size: 16px 16px; background-size: 16px 16px;
} }
&:global(.purple) {
--c: rgba(196, 0, 255, 0.15);
background-image: linear-gradient(
45deg,
var(--c) 16.67%,
transparent 16.67%,
transparent 50%,
var(--c) 50%,
var(--c) 66.67%,
transparent 66.67%,
transparent 100%
);
background-size: 16px 16px;
}
&:global(.gray) { &:global(.gray) {
--c: var(--bg); --c: var(--bg);
background-image: linear-gradient( background-image: linear-gradient(

View File

@ -139,7 +139,8 @@ function close() {
height: 100px; height: 100px;
border-radius: 10px; border-radius: 10px;
&:hover { &:hover,
&:focus-visible {
color: var(--accent); color: var(--accent);
background: var(--accentedBg); background: var(--accentedBg);
text-decoration: none; text-decoration: none;

View File

@ -138,6 +138,10 @@ watch(
background-position: center; background-position: center;
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
box-sizing: border-box;
&:focus-visible {
border: 2px solid var(--accent);
}
> .gif { > .gif {
background-color: var(--fg); background-color: var(--fg);

View File

@ -1,5 +1,5 @@
<template> <template>
<div ref="el" class="sfhdhdhr"> <div ref="el" class="sfhdhdhr" tabindex="-1">
<MkMenu <MkMenu
ref="menu" ref="menu"
:items="items" :items="items"
@ -23,7 +23,6 @@ import {
} from "vue"; } from "vue";
import MkMenu from "./MkMenu.vue"; import MkMenu from "./MkMenu.vue";
import { MenuItem } from "@/types/menu"; import { MenuItem } from "@/types/menu";
import * as os from "@/os";
const props = defineProps<{ const props = defineProps<{
items: MenuItem[]; items: MenuItem[];

View File

@ -1,8 +1,8 @@
<template> <template>
<div> <FocusTrap :active="false" ref="focusTrap">
<div tabindex="-1">
<div <div
ref="itemsEl" ref="itemsEl"
v-hotkey="keymap"
class="rrevdjwt _popup _shadow" class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }" :class="{ center: align === 'center', asDrawer }"
:style="{ :style="{
@ -14,11 +14,12 @@
<template v-for="(item, i) in items2"> <template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div> <div v-if="item === null" class="divider"></div>
<span v-else-if="item.type === 'label'" class="label item"> <span v-else-if="item.type === 'label'" class="label item">
<span :style="item.textStyle || ''">{{ item.text }}</span> <span :style="item.textStyle || ''">{{
item.text
}}</span>
</span> </span>
<span <span
v-else-if="item.type === 'pending'" v-else-if="item.type === 'pending'"
:tabindex="i"
class="pending item" class="pending item"
> >
<span><MkEllipsis /></span> <span><MkEllipsis /></span>
@ -26,7 +27,6 @@
<MkA <MkA
v-else-if="item.type === 'link'" v-else-if="item.type === 'link'"
:to="item.to" :to="item.to"
:tabindex="i"
class="_button item" class="_button item"
@click.passive="close(true)" @click.passive="close(true)"
@mouseenter.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)"
@ -48,8 +48,11 @@
v-if="item.avatar" v-if="item.avatar"
:user="item.avatar" :user="item.avatar"
class="avatar" class="avatar"
disableLink
/> />
<span :style="item.textStyle || ''">{{ item.text }}</span> <span :style="item.textStyle || ''">{{
item.text
}}</span>
<span v-if="item.indicate" class="indicator" <span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i ><i class="ph-circle ph-fill"></i
></span> ></span>
@ -59,7 +62,6 @@
:href="item.href" :href="item.href"
:target="item.target" :target="item.target"
:download="item.download" :download="item.download"
:tabindex="i"
class="_button item" class="_button item"
@click="close(true)" @click="close(true)"
@mouseenter.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)"
@ -77,14 +79,15 @@
:class="icon" :class="icon"
></i> ></i>
</span> </span>
<span :style="item.textStyle || ''">{{ item.text }}</span> <span :style="item.textStyle || ''">{{
item.text
}}</span>
<span v-if="item.indicate" class="indicator" <span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i ><i class="ph-circle ph-fill"></i
></span> ></span>
</a> </a>
<button <button
v-else-if="item.type === 'user' && !items.hidden" v-else-if="item.type === 'user' && !items.hidden"
:tabindex="i"
class="_button item" class="_button item"
:class="{ active: item.active }" :class="{ active: item.active }"
:disabled="item.active" :disabled="item.active"
@ -92,16 +95,17 @@
@mouseenter.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)" @mouseleave.passive="onItemMouseLeave(item)"
> >
<MkAvatar :user="item.user" class="avatar" /><MkUserName <MkAvatar
:user="item.user" :user="item.user"
/> class="avatar"
disableLink
/><MkUserName :user="item.user" />
<span v-if="item.indicate" class="indicator" <span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i ><i class="ph-circle ph-fill"></i
></span> ></span>
</button> </button>
<span <span
v-else-if="item.type === 'switch'" v-else-if="item.type === 'switch'"
:tabindex="i"
class="item" class="item"
@mouseenter.passive="onItemMouseEnter(item)" @mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)" @mouseleave.passive="onItemMouseLeave(item)"
@ -116,10 +120,10 @@
</span> </span>
<button <button
v-else-if="item.type === 'parent'" v-else-if="item.type === 'parent'"
:tabindex="i"
class="_button item parent" class="_button item parent"
:class="{ childShowing: childShowingItem === item }" :class="{ childShowing: childShowingItem === item }"
@mouseenter="showChildren(item, $event)" @mouseenter="showChildren(item, $event)"
@click="showChildren(item, $event)"
> >
<i <i
v-if="item.icon" v-if="item.icon"
@ -133,14 +137,17 @@
:class="icon" :class="icon"
></i> ></i>
</span> </span>
<span :style="item.textStyle || ''">{{ item.text }}</span> <span :style="item.textStyle || ''">{{
item.text
}}</span>
<span class="caret" <span class="caret"
><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i ><i
class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"
></i
></span> ></span>
</button> </button>
<button <button
v-else-if="!item.hidden" v-else-if="!item.hidden"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
:disabled="item.active" :disabled="item.active"
@ -164,8 +171,11 @@
v-if="item.avatar" v-if="item.avatar"
:user="item.avatar" :user="item.avatar"
class="avatar" class="avatar"
disableLink
/> />
<span :style="item.textStyle || ''">{{ item.text }}</span> <span :style="item.textStyle || ''">{{
item.text
}}</span>
<span v-if="item.indicate" class="indicator" <span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i ><i class="ph-circle ph-fill"></i
></span> ></span>
@ -186,6 +196,7 @@
/> />
</div> </div>
</div> </div>
</FocusTrap>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -206,8 +217,10 @@ import FormSwitch from "@/components/form/switch.vue";
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu"; import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { FocusTrap } from "focus-trap-vue";
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue")); const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
const focusTrap = ref();
const props = defineProps<{ const props = defineProps<{
items: MenuItem[]; items: MenuItem[];
@ -228,12 +241,6 @@ let items2: InnerMenuItem[] = $ref([]);
let child = $ref<InstanceType<typeof XChild>>(); let child = $ref<InstanceType<typeof XChild>>();
let keymap = computed(() => ({
"up|k|shift+tab": focusUp,
"down|j|tab": focusDown,
esc: close,
}));
let childShowingItem = $ref<MenuItem | null>(); let childShowingItem = $ref<MenuItem | null>();
watch( watch(
@ -324,6 +331,8 @@ function focusDown() {
} }
onMounted(() => { onMounted(() => {
focusTrap.value.activate();
if (props.viaKeyboard) { if (props.viaKeyboard) {
nextTick(() => { nextTick(() => {
focusNext(itemsEl.children[0], true, false); focusNext(itemsEl.children[0], true, false);
@ -364,8 +373,7 @@ onBeforeUnmount(() => {
font-size: 0.9em; font-size: 0.9em;
line-height: 20px; line-height: 20px;
text-align: left; text-align: left;
overflow: hidden; outline: none;
text-overflow: ellipsis;
&:before { &:before {
content: ""; content: "";
@ -389,7 +397,8 @@ onBeforeUnmount(() => {
transform: translateY(0em); transform: translateY(0em);
} }
&:not(:disabled):hover { &:not(:disabled):hover,
&:focus-visible {
color: var(--accent); color: var(--accent);
text-decoration: none; text-decoration: none;
@ -397,6 +406,9 @@ onBeforeUnmount(() => {
background: var(--accentedBg); background: var(--accentedBg);
} }
} }
&:focus-visible:before {
outline: auto;
}
&.danger { &.danger {
color: #eb6f92; color: #eb6f92;

View File

@ -14,9 +14,11 @@
:duration="transitionDuration" :duration="transitionDuration"
appear appear
@after-leave="emit('closed')" @after-leave="emit('closed')"
@keyup.esc="emit('click')"
@enter="emit('opening')" @enter="emit('opening')"
@after-enter="onOpened" @after-enter="onOpened"
> >
<FocusTrap v-model:active="isActive">
<div <div
v-show="manualShowing != null ? manualShowing : showing" v-show="manualShowing != null ? manualShowing : showing"
v-hotkey.global="keymap" v-hotkey.global="keymap"
@ -24,17 +26,22 @@
$style.root, $style.root,
{ {
[$style.drawer]: type === 'drawer', [$style.drawer]: type === 'drawer',
[$style.dialog]: type === 'dialog' || type === 'dialog:top', [$style.dialog]:
type === 'dialog' || type === 'dialog:top',
[$style.popup]: type === 'popup', [$style.popup]: type === 'popup',
}, },
]" ]"
:style="{ :style="{
zIndex, zIndex,
pointerEvents: (manualShowing != null ? manualShowing : showing) pointerEvents: (
manualShowing != null ? manualShowing : showing
)
? 'auto' ? 'auto'
: 'none', : 'none',
'--transformOrigin': transformOrigin, '--transformOrigin': transformOrigin,
}" }"
tabindex="-1"
v-focus
> >
<div <div
class="_modalBg data-cy-bg" class="_modalBg data-cy-bg"
@ -62,6 +69,7 @@
<slot :max-height="maxHeight" :type="type"></slot> <slot :max-height="maxHeight" :type="type"></slot>
</div> </div>
</div> </div>
</FocusTrap>
</Transition> </Transition>
</template> </template>
@ -71,6 +79,7 @@ import * as os from "@/os";
import { isTouchUsing } from "@/scripts/touch"; import { isTouchUsing } from "@/scripts/touch";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { deviceKind } from "@/scripts/device-kind"; import { deviceKind } from "@/scripts/device-kind";
import { FocusTrap } from "focus-trap-vue";
function getFixedContainer(el: Element | null): Element | null { function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === "BODY") return null; if (el == null || el.tagName === "BODY") return null;
@ -166,6 +175,7 @@ let transitionDuration = $computed(() =>
let contentClicking = false; let contentClicking = false;
const focusedElement = document.activeElement;
function close(opts: { useSendAnimation?: boolean } = {}) { function close(opts: { useSendAnimation?: boolean } = {}) {
if (opts.useSendAnimation) { if (opts.useSendAnimation) {
useSendAnime = true; useSendAnime = true;
@ -175,10 +185,12 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
if (props.src) props.src.style.pointerEvents = "auto"; if (props.src) props.src.style.pointerEvents = "auto";
showing = false; showing = false;
emit("close"); emit("close");
focusedElement.focus();
} }
function onBgClick() { function onBgClick() {
if (contentClicking) return; if (contentClicking) return;
focusedElement.focus();
emit("click"); emit("click");
} }
@ -481,6 +493,7 @@ defineExpose({
} }
.root { .root {
outline: none;
&.dialog { &.dialog {
> .content { > .content {
position: fixed; position: fixed;

View File

@ -158,6 +158,7 @@ function onContextmenu(ev: MouseEvent) {
flex-direction: column; flex-direction: column;
contain: content; contain: content;
border-radius: var(--radius); border-radius: var(--radius);
margin: auto;
--root-margin: 24px; --root-margin: 24px;

View File

@ -3,8 +3,10 @@
ref="modal" ref="modal"
:prefer-type="'dialog'" :prefer-type="'dialog'"
@click="onBgClick" @click="onBgClick"
@keyup.esc="$emit('close')"
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<FocusTrap v-model:active="isActive">
<div <div
ref="rootEl" ref="rootEl"
class="ebkgoccj" class="ebkgoccj"
@ -19,6 +21,7 @@
: '100%', : '100%',
}" }"
@keydown="onKeydown" @keydown="onKeydown"
tabindex="-1"
> >
<div ref="headerEl" class="header"> <div ref="headerEl" class="header">
<button <button
@ -51,11 +54,13 @@
<slot :width="bodyWidth" :height="bodyHeight"></slot> <slot :width="bodyWidth" :height="bodyHeight"></slot>
</div> </div>
</div> </div>
</FocusTrap>
</MkModal> </MkModal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted } from "vue"; import { onMounted, onUnmounted } from "vue";
import { FocusTrap } from "focus-trap-vue";
import MkModal from "./MkModal.vue"; import MkModal from "./MkModal.vue";
const props = withDefaults( const props = withDefaults(

View File

@ -84,6 +84,7 @@
:detailedView="detailedView" :detailedView="detailedView"
:parentId="appearNote.parentId" :parentId="appearNote.parentId"
@push="(e) => router.push(notePage(e))" @push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()"
></MkSubNoteContent> ></MkSubNoteContent>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
@ -117,7 +118,7 @@
<MkTime :time="appearNote.createdAt" mode="absolute" /> <MkTime :time="appearNote.createdAt" mode="absolute" />
</MkA> </MkA>
</div> </div>
<footer ref="el" class="footer" @click.stop> <footer ref="footerEl" class="footer" @click.stop tabindex="-1">
<XReactionsViewer <XReactionsViewer
v-if="enableEmojiReactions" v-if="enableEmojiReactions"
ref="reactionsViewer" ref="reactionsViewer"
@ -278,6 +279,7 @@ const isRenote =
note.poll == null; note.poll == null;
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
@ -298,8 +300,8 @@ const keymap = {
r: () => reply(true), r: () => reply(true),
"e|a|plus": () => react(true), "e|a|plus": () => react(true),
q: () => renoteButton.value.renote(true), q: () => renoteButton.value.renote(true),
"up|k|shift+tab": focusBefore, "up|k": focusBefore,
"down|j|tab": focusAfter, "down|j": focusAfter,
esc: blur, esc: blur,
"m|o": () => menu(true), "m|o": () => menu(true),
s: () => showContent.value !== showContent.value, s: () => showContent.value !== showContent.value,

View File

@ -1,6 +1,6 @@
<template> <template>
<div v-size="{ min: [350, 500] }" class="fefdfafb"> <div v-size="{ min: [350, 500] }" class="fefdfafb">
<MkAvatar class="avatar" :user="$i" /> <MkAvatar class="avatar" :user="$i" disableLink />
<div class="main"> <div class="main">
<div class="header"> <div class="header">
<MkUserName :user="$i" /> <MkUserName :user="$i" />

View File

@ -26,6 +26,7 @@
:note="note" :note="note"
:parentId="appearNote.parentId" :parentId="appearNote.parentId"
:conversation="conversation" :conversation="conversation"
@focusfooter="footerEl.focus()"
/> />
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini /> <MkLoading v-if="translating" mini />
@ -46,7 +47,7 @@
</div> </div>
</div> </div>
</div> </div>
<footer class="footer" @click.stop> <footer ref="footerEl" class="footer" @click.stop tabindex="-1">
<XReactionsViewer <XReactionsViewer
v-if="enableEmojiReactions" v-if="enableEmojiReactions"
ref="reactionsViewer" ref="reactionsViewer"
@ -212,6 +213,7 @@ const isRenote =
note.poll == null; note.poll == null;
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>(); const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();

View File

@ -7,6 +7,8 @@
:transparent-bg="true" :transparent-bg="true"
@click="modal.close()" @click="modal.close()"
@closed="emit('closed')" @closed="emit('closed')"
tabindex="-1"
v-focus
> >
<MkMenu <MkMenu
:items="items" :items="items"

View File

@ -55,7 +55,7 @@
:class="{ active: showPreview }" :class="{ active: showPreview }"
@click="showPreview = !showPreview" @click="showPreview = !showPreview"
> >
<i class="ph-file-code ph-bold ph-lg"></i> <i class="ph-binoculars ph-bold ph-lg"></i>
</button> </button>
<button <button
class="submit _buttonGradate" class="submit _buttonGradate"
@ -462,15 +462,21 @@ if (
props.reply && props.reply &&
["home", "followers", "specified"].includes(props.reply.visibility) ["home", "followers", "specified"].includes(props.reply.visibility)
) { ) {
if (props.reply.visibility === 'home' && visibility === 'followers') {
visibility = 'followers';
} else if (['home', 'followers'].includes(props.reply.visibility) && visibility === 'specified') {
visibility = 'specified';
} else {
visibility = props.reply.visibility; visibility = props.reply.visibility;
if (props.reply.visibility === "specified") { }
os.api("users/show", { if (visibility === 'specified') {
userIds: props.reply.visibleUserIds.filter( if (props.reply.visibleUserIds) {
(uid) => uid !== $i.id && uid !== props.reply.userId os.api('users/show', {
), userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId),
}).then((users) => { }).then(users => {
users.forEach(pushVisibleUser); users.forEach(pushVisibleUser);
}); });
}
if (props.reply.userId !== $i.id) { if (props.reply.userId !== $i.id) {
os.api("users/show", { userId: props.reply.userId }).then( os.api("users/show", { userId: props.reply.userId }).then(

View File

@ -154,22 +154,22 @@ export default defineComponent({
? i18n.ts.unmarkAsSensitive ? i18n.ts.unmarkAsSensitive
: i18n.ts.markAsSensitive, : i18n.ts.markAsSensitive,
icon: file.isSensitive icon: file.isSensitive
? "ph-eye-slash ph-bold ph-lg" ? "ph-eye ph-bold ph-lg"
: "ph-eye ph-bold ph-lg", : "ph-eye-slash ph-bold ph-lg",
action: () => { action: () => {
this.toggleSensitive(file); this.toggleSensitive(file);
}, },
}, },
{ {
text: i18n.ts.describeFile, text: i18n.ts.describeFile,
icon: "ph-cursor-text ph-bold ph-lg", icon: "ph-subtitles ph-bold ph-lg",
action: () => { action: () => {
this.describe(file); this.describe(file);
}, },
}, },
{ {
text: i18n.ts.attachCancel, text: i18n.ts.attachCancel,
icon: "ph-circle-wavy-warning ph-bold ph-lg", icon: "ph-x ph-bold ph-lg",
action: () => { action: () => {
this.detachMedia(file.id); this.detachMedia(file.id);
}, },
@ -198,7 +198,6 @@ export default defineComponent({
height: 64px; height: 64px;
margin-right: 4px; margin-right: 4px;
border-radius: 4px; border-radius: 4px;
overflow: hidden;
cursor: move; cursor: move;
&:hover > .remove { &:hover > .remove {

View File

@ -1,68 +0,0 @@
<template>
<button
v-if="modelValue"
class="fade _button"
@click.stop="toggle"
>
<span>{{ i18n.ts.showMore }}</span>
</button>
<button
v-if="!modelValue"
class="showLess _button"
@click.stop="toggle"
>
<span>{{ i18n.ts.showLess }}</span>
</button>
</template>
<script lang="ts" setup>
import { i18n } from "@/i18n";
const props = defineProps<{
modelValue: boolean;
}>();
const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void;
}>();
const toggle = () => {
emit("update:modelValue", !props.modelValue);
};
</script>
<style lang="scss" scoped>
.fade {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
> span {
display: inline-block;
background: var(--panel);
padding: 0.4em 1em;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
}
}
.showLess {
width: 100%;
margin-top: 1em;
position: sticky;
bottom: var(--stickyBottom);
> span {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 0 7px 7px var(--bg);
}
}
</style>

View File

@ -35,7 +35,20 @@
class="content" class="content"
:class="{ collapsed, isLong, showContent: note.cw && !showContent }" :class="{ collapsed, isLong, showContent: note.cw && !showContent }"
> >
<div class="body"> <XCwButton
ref="cwButton"
v-if="note.cw && !showContent"
v-model="showContent"
:note="note"
v-on:keydown="focusFooter"
/>
<div
class="body"
v-bind="{
'aria-label': !showContent ? '' : null,
tabindex: !showContent ? '-1' : null,
}"
>
<span v-if="note.deletedAt" style="opacity: 0.5" <span v-if="note.deletedAt" style="opacity: 0.5"
>({{ i18n.ts.deleted }})</span >({{ i18n.ts.deleted }})</span
> >
@ -96,22 +109,33 @@
<XNoteSimple :note="note.renote" /> <XNoteSimple :note="note.renote" />
</div> </div>
</template> </template>
<div
v-if="note.cw && !showContent"
tabindex="0"
v-on:focus="cwButton?.focus()"
></div>
</div> </div>
<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton> <XShowMoreButton
<XCwButton v-if="note.cw" v-model="showContent" :note="note" /> v-if="isLong"
v-model="collapsed"
></XShowMoreButton>
<XCwButton
v-if="note.cw && showContent"
v-model="showContent"
:note="note"
/>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue"; import { ref } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import * as mfm from "mfm-js"; import * as mfm from "mfm-js";
import XNoteSimple from "@/components/MkNoteSimple.vue"; import XNoteSimple from "@/components/MkNoteSimple.vue";
import XMediaList from "@/components/MkMediaList.vue"; import XMediaList from "@/components/MkMediaList.vue";
import XPoll from "@/components/MkPoll.vue"; import XPoll from "@/components/MkPoll.vue";
import MkUrlPreview from "@/components/MkUrlPreview.vue"; import MkUrlPreview from "@/components/MkUrlPreview.vue";
import XShowMoreButton from "./MkShowMoreButton.vue";
import XCwButton from "@/components/MkCwButton.vue"; import XCwButton from "@/components/MkCwButton.vue";
import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm"; import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -126,20 +150,27 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "push", v): void; (ev: "push", v): void;
(ev: "focusfooter"): void;
}>(); }>();
const cwButton = ref<HTMLElement>();
const isLong = const isLong =
!props.detailedView && !props.detailedView &&
props.note.cw == null && props.note.cw == null &&
props.note.text != null && props.note.text != null &&
(props.note.text.split("\n").length > 9 || props.note.text.length > 500); (props.note.text.split("\n").length > 9 || props.note.text.length > 500);
const collapsed = $ref(props.note.cw == null && isLong); const collapsed = $ref(props.note.cw == null && isLong);
const urls = props.note.text const urls = props.note.text
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5) ? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)
: null; : null;
let showContent = $ref(false); let showContent = $ref(false);
function focusFooter(ev) {
if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
emit("focusfooter");
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -231,6 +262,9 @@ let showContent = $ref(false);
margin-top: -50px; margin-top: -50px;
padding-top: 50px; padding-top: 50px;
overflow: hidden; overflow: hidden;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
} }
&.collapsed > .body { &.collapsed > .body {
box-sizing: border-box; box-sizing: border-box;
@ -253,6 +287,43 @@ let showContent = $ref(false);
top: 40px; top: 40px;
} }
} }
:deep(.fade) {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
> span {
display: inline-block;
background: var(--panel);
padding: 0.4em 1em;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
}
}
}
:deep(.showLess) {
width: 100%;
margin-top: 1em;
position: sticky;
bottom: var(--stickyBottom);
> span {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 0 7px 7px var(--bg);
}
} }
} }
} }

View File

@ -9,7 +9,6 @@
v-if="item.type === 'a'" v-if="item.type === 'a'"
:href="item.href" :href="item.href"
:target="item.target" :target="item.target"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
> >
@ -22,7 +21,6 @@
</a> </a>
<button <button
v-else-if="item.type === 'button'" v-else-if="item.type === 'button'"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
:disabled="item.active" :disabled="item.active"
@ -38,7 +36,6 @@
<MkA <MkA
v-else v-else
:to="item.to" :to="item.to"
:tabindex="i"
class="_button item" class="_button item"
:class="{ danger: item.danger, active: item.active }" :class="{ danger: item.danger, active: item.active }"
> >
@ -99,7 +96,8 @@ export default defineComponent({
font-size: 0.9em; font-size: 0.9em;
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
&:hover { &:hover,
&:focus-visible {
text-decoration: none; text-decoration: none;
background: var(--panelHighlight); background: var(--panelHighlight);
} }

View File

@ -46,7 +46,10 @@
/></MkA> /></MkA>
<p class="username"><MkAcct :user="user" /></p> <p class="username"><MkAcct :user="user" /></p>
</div> </div>
<div class="description" :class="{ collapsed: isLong && collapsed }"> <div
class="description"
:class="{ collapsed: isLong && collapsed }"
>
<Mfm <Mfm
v-if="user.description" v-if="user.description"
:text="user.description" :text="user.description"
@ -55,7 +58,20 @@
:custom-emojis="user.emojis" :custom-emojis="user.emojis"
/> />
</div> </div>
<XShowMoreButton v-if="isLong" v-model="collapsed"></XShowMoreButton> <button
v-if="isLong && collapsed"
class="fade _button"
@click.stop="collapsed = false"
>
<span>{{ i18n.ts.showMore }}</span>
</button>
<button
v-if="isLong && !collapsed"
class="showLess _button"
@click.stop="collapsed = true"
>
<span>{{ i18n.ts.showLess }}</span>
</button>
<div v-if="user.fields.length > 0" class="fields"> <div v-if="user.fields.length > 0" class="fields">
<dl <dl
v-for="(field, i) in user.fields" v-for="(field, i) in user.fields"
@ -115,7 +131,6 @@ import * as Acct from "calckey-js/built/acct";
import type * as misskey from "calckey-js"; import type * as misskey from "calckey-js";
import MkFollowButton from "@/components/MkFollowButton.vue"; import MkFollowButton from "@/components/MkFollowButton.vue";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
import XShowMoreButton from "./MkShowMoreButton.vue";
import * as os from "@/os"; import * as os from "@/os";
import { $i } from "@/account"; import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -137,14 +152,15 @@ let user = $ref<misskey.entities.UserDetailed | null>(null);
let top = $ref(0); let top = $ref(0);
let left = $ref(0); let left = $ref(0);
let isLong = $ref(false); let isLong = $ref(false);
let collapsed = $ref(!isLong); let collapsed = $ref(!isLong);
onMounted(() => { onMounted(() => {
if (typeof props.q === "object") { if (typeof props.q === "object") {
user = props.q; user = props.q;
isLong = (user.description.split("\n").length > 9 || user.description.length > 400); isLong =
user.description.split("\n").length > 9 ||
user.description.length > 400;
} else { } else {
const query = props.q.startsWith("@") const query = props.q.startsWith("@")
? Acct.parse(props.q.substr(1)) ? Acct.parse(props.q.substr(1))
@ -153,11 +169,12 @@ onMounted(() => {
os.api("users/show", query).then((res) => { os.api("users/show", query).then((res) => {
if (!props.showing) return; if (!props.showing) return;
user = res; user = res;
isLong = (user.description.split("\n").length > 9 || user.description.length > 400); isLong =
user.description.split("\n").length > 9 ||
user.description.length > 400;
}); });
} }
const rect = props.source.getBoundingClientRect(); const rect = props.source.getBoundingClientRect();
const x = const x =
rect.left + props.source.offsetWidth / 2 - 300 / 2 + window.pageXOffset; rect.left + props.source.offsetWidth / 2 - 300 / 2 + window.pageXOffset;
@ -301,7 +318,7 @@ onMounted(() => {
> .fields { > .fields {
padding: 0 16px; padding: 0 16px;
font-size: .8em; font-size: 0.8em;
margin-top: 1em; margin-top: 1em;
> .field { > .field {

View File

@ -46,6 +46,7 @@
:user="user" :user="user"
class="avatar" class="avatar"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<MkUserName :user="user" class="name" /> <MkUserName :user="user" class="name" />
@ -73,6 +74,7 @@
:user="user" :user="user"
class="avatar" class="avatar"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<MkUserName :user="user" class="name" /> <MkUserName :user="user" class="name" />

View File

@ -7,7 +7,7 @@
> >
<div class="beaffaef"> <div class="beaffaef">
<div v-for="u in users" :key="u.id" class="user"> <div v-for="u in users" :key="u.id" class="user">
<MkAvatar class="avatar" :user="u" /> <MkAvatar class="avatar" :user="u" disableLink />
<MkUserName class="name" :user="u" :nowrap="true" /> <MkUserName class="name" :user="u" :nowrap="true" />
</div> </div>
<div v-if="users.length < count" class="omitted"> <div v-if="users.length < count" class="omitted">

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="vjoppmmu"> <div class="vjoppmmu">
<template v-if="edit"> <template v-if="edit">
<header> <header tabindex="-1" v-focus>
<MkSelect <MkSelect
v-model="widgetAdderSelected" v-model="widgetAdderSelected"
style="margin-bottom: var(--margin)" style="margin-bottom: var(--margin)"

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="dwzlatin" :class="{ opened }"> <div class="dwzlatin" :class="{ opened }">
<div class="header _button" @click="toggle"> <button class="header _button" @click="toggle">
<span class="icon"><slot name="icon"></slot></span> <span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot name="label"></slot></span> <span class="text"><slot name="label"></slot></span>
<span class="right"> <span class="right">
@ -8,7 +8,7 @@
<i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i> <i v-if="opened" class="ph-caret-up ph-bold ph-lg icon"></i>
<i v-else class="ph-caret-down ph-bold ph-lg icon"></i> <i v-else class="ph-caret-down ph-bold ph-lg icon"></i>
</span> </span>
</div> </button>
<KeepAlive> <KeepAlive>
<div v-if="openedAtLeastOnce" v-show="opened" class="body"> <div v-if="openedAtLeastOnce" v-show="opened" class="body">
<MkSpacer :margin-min="14" :margin-max="22"> <MkSpacer :margin-min="14" :margin-max="22">

View File

@ -66,6 +66,9 @@ function toggle(): void {
&:hover { &:hover {
border-color: var(--inputBorderHover) !important; border-color: var(--inputBorderHover) !important;
} }
&:focus-within {
outline: auto;
}
&.checked { &.checked {
background-color: var(--accentedBg) !important; background-color: var(--accentedBg) !important;

View File

@ -99,6 +99,9 @@ const toggle = () => {
border-color: var(--inputBorderHover) !important; border-color: var(--inputBorderHover) !important;
} }
} }
&:focus-within > .button {
outline: auto;
}
> .label { > .label {
margin-left: 12px; margin-left: 12px;

View File

@ -19,6 +19,7 @@
class="avatar" class="avatar"
:user="$i" :user="$i"
:disable-preview="true" :disable-preview="true"
disableLink
/> />
</div> </div>
<template v-if="metadata"> <template v-if="metadata">
@ -33,6 +34,7 @@
:user="metadata.avatar" :user="metadata.avatar"
:disable-preview="true" :disable-preview="true"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<i <i
v-else-if="metadata.icon && !narrow" v-else-if="metadata.icon && !narrow"

View File

@ -5,6 +5,9 @@
:is="currentPageComponent" :is="currentPageComponent"
:key="key" :key="key"
v-bind="Object.fromEntries(currentPageProps)" v-bind="Object.fromEntries(currentPageProps)"
tabindex="-1"
v-focus
style="outline: none"
/> />
<template #fallback> <template #fallback>

View File

@ -0,0 +1,3 @@
export default {
mounted: (el) => el.focus()
}

View File

@ -11,6 +11,7 @@ import anim from "./anim";
import clickAnime from "./click-anime"; import clickAnime from "./click-anime";
import panel from "./panel"; import panel from "./panel";
import adaptiveBorder from "./adaptive-border"; import adaptiveBorder from "./adaptive-border";
import focus from "./focus";
export default function (app: App) { export default function (app: App) {
app.directive("userPreview", userPreview); app.directive("userPreview", userPreview);
@ -25,4 +26,5 @@ export default function (app: App) {
app.directive("click-anime", clickAnime); app.directive("click-anime", clickAnime);
app.directive("panel", panel); app.directive("panel", panel);
app.directive("adaptive-border", adaptiveBorder); app.directive("adaptive-border", adaptiveBorder);
app.directive("focus", focus);
} }

View File

@ -76,23 +76,32 @@ export default {
ev.preventDefault(); ev.preventDefault();
}); });
el.addEventListener( function showTooltip() {
start,
() => {
window.clearTimeout(self.showTimer); window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer); window.clearTimeout(self.hideTimer);
self.showTimer = window.setTimeout(self.show, delay); self.showTimer = window.setTimeout(self.show, delay);
}, }
function hideTooltip() {
window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer);
self.hideTimer = window.setTimeout(self.close, delay);
}
el.addEventListener(
start, showTooltip,
{ passive: true },
);
el.addEventListener(
"focusin", showTooltip,
{ passive: true }, { passive: true },
); );
el.addEventListener( el.addEventListener(
end, end, hideTooltip,
() => { { passive: true },
window.clearTimeout(self.showTimer); );
window.clearTimeout(self.hideTimer); el.addEventListener(
self.hideTimer = window.setTimeout(self.close, delay); "focusout", hideTooltip,
},
{ passive: true }, { passive: true },
); );

View File

@ -18,6 +18,7 @@
<option value="publishing">{{ i18n.ts.publishing }}</option> <option value="publishing">{{ i18n.ts.publishing }}</option>
<option value="suspended">{{ i18n.ts.suspended }}</option> <option value="suspended">{{ i18n.ts.suspended }}</option>
<option value="blocked">{{ i18n.ts.blocked }}</option> <option value="blocked">{{ i18n.ts.blocked }}</option>
<option value="silenced">{{ i18n.ts.silenced }}</option>
<option value="notResponding"> <option value="notResponding">
{{ i18n.ts.notResponding }} {{ i18n.ts.notResponding }}
</option> </option>
@ -105,13 +106,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
import MkButton from "@/components/MkButton.vue";
import MkInput from "@/components/form/input.vue"; import MkInput from "@/components/form/input.vue";
import MkSelect from "@/components/form/select.vue"; import MkSelect from "@/components/form/select.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import MkInstanceCardMini from "@/components/MkInstanceCardMini.vue"; import MkInstanceCardMini from "@/components/MkInstanceCardMini.vue";
import FormSplit from "@/components/form/split.vue"; import FormSplit from "@/components/form/split.vue";
import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
let host = $ref(""); let host = $ref("");
@ -134,6 +133,8 @@ const pagination = {
? { suspended: true } ? { suspended: true }
: state === "blocked" : state === "blocked"
? { blocked: true } ? { blocked: true }
: state === "silenced"
? { silenced: true }
: state === "notResponding" : state === "notResponding"
? { notResponding: true } ? { notResponding: true }
: {}), : {}),
@ -143,6 +144,7 @@ const pagination = {
function getStatus(instance) { function getStatus(instance) {
if (instance.isSuspended) return "Suspended"; if (instance.isSuspended) return "Suspended";
if (instance.isBlocked) return "Blocked"; if (instance.isBlocked) return "Blocked";
if (instance.isSilenced) return "Silenced";
if (instance.isNotResponding) return "Error"; if (instance.isNotResponding) return "Error";
return "Alive"; return "Alive";
} }

View File

@ -313,10 +313,8 @@ onUnmounted(() => {
font-weight: normal; font-weight: normal;
opacity: 0.7; opacity: 0.7;
&:hover { &:hover,
opacity: 1; &:focus-visible,
}
&.active { &.active {
opacity: 1; opacity: 1;
} }

View File

@ -3,7 +3,6 @@
<MkStickyContainer> <MkStickyContainer>
<template #header <template #header
><MkPageHeader ><MkPageHeader
v-model:tab="tab"
:actions="headerActions" :actions="headerActions"
:tabs="headerTabs" :tabs="headerTabs"
:display-back-button="true" :display-back-button="true"

View File

@ -7,13 +7,31 @@
:display-back-button="true" :display-back-button="true"
/></template> /></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<MkTab v-model="tab" class="_formBlock">
<option value="block">{{ i18n.ts.blockedInstances }}</option>
<option value="silence">{{ i18n.ts.silencedInstances }}</option>
</MkTab>
<FormSuspense :p="init"> <FormSuspense :p="init">
<FormTextarea v-model="blockedHosts" class="_formBlock"> <FormTextarea
v-if="tab === 'block'"
v-model="blockedHosts"
class="_formBlock"
>
<span>{{ i18n.ts.blockedInstances }}</span> <span>{{ i18n.ts.blockedInstances }}</span>
<template #caption>{{ <template #caption>{{
i18n.ts.blockedInstancesDescription i18n.ts.blockedInstancesDescription
}}</template> }}</template>
</FormTextarea> </FormTextarea>
<FormTextarea
v-else-if="tab === 'silence'"
v-model="silencedHosts"
class="_formBlock"
>
<span>{{ i18n.ts.silencedInstances }}</span>
<template #caption>{{
i18n.ts.silencedInstancesDescription
}}</template>
</FormTextarea>
<FormButton primary class="_formBlock" @click="save" <FormButton primary class="_formBlock" @click="save"
><i class="ph-floppy-disk-back ph-bold ph-lg"></i> ><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
@ -29,21 +47,28 @@ import {} from "vue";
import FormButton from "@/components/MkButton.vue"; import FormButton from "@/components/MkButton.vue";
import FormTextarea from "@/components/form/textarea.vue"; import FormTextarea from "@/components/form/textarea.vue";
import FormSuspense from "@/components/form/suspense.vue"; import FormSuspense from "@/components/form/suspense.vue";
import MkTab from "@/components/MkTab.vue";
import * as os from "@/os"; import * as os from "@/os";
import { fetchInstance } from "@/instance"; import { fetchInstance } from "@/instance";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
let blockedHosts: string = $ref(""); let blockedHosts: string = $ref("");
let silencedHosts: string = $ref("");
let tab = $ref("block");
async function init() { async function init() {
const meta = await os.api("admin/meta"); const meta = await os.api("admin/meta");
if (meta) {
blockedHosts = meta.blockedHosts.join("\n"); blockedHosts = meta.blockedHosts.join("\n");
silencedHosts = meta.silencedHosts.join("\n");
}
} }
function save() { function save() {
os.apiWithDialog("admin/update-meta", { os.apiWithDialog("admin/update-meta", {
blockedHosts: blockedHosts.split("\n").map((h) => h.trim()) || [], blockedHosts: blockedHosts.split("\n").map((h) => h.trim()) || [],
silencedHosts: silencedHosts.split("\n").map((h) => h.trim()) || [],
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance();
}); });

View File

@ -12,7 +12,12 @@
class="user" class="user"
:to="`/user-info/${user.id}`" :to="`/user-info/${user.id}`"
> >
<MkAvatar :user="user" class="avatar" indicator /> <MkAvatar
:user="user"
class="avatar"
indicator
disableLink
/>
</MkA> </MkA>
</div> </div>
</Transition> </Transition>

View File

@ -371,6 +371,34 @@
<template #label>Pro account</template> <template #label>Pro account</template>
</FormSwitch> </FormSwitch>
</FormSection> </FormSection>
<FormSection>
<template #label>Libre Translate</template>
<FormInput
v-model="libreTranslateApiUrl"
class="_formBlock"
>
<template #prefix
><i class="ph-link ph-bold ph-lg"></i
></template>
<template #label
>Libre Translate API URL</template
>
</FormInput>
<FormInput
v-model="libreTranslateApiKey"
class="_formBlock"
>
<template #prefix
><i class="ph-key ph-bold ph-lg"></i
></template>
<template #label
>Libre Translate API Key</template
>
</FormInput>
</FormSection>
</div> </div>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer>
@ -422,6 +450,8 @@ let swPublicKey: any = $ref(null);
let swPrivateKey: any = $ref(null); let swPrivateKey: any = $ref(null);
let deeplAuthKey: string = $ref(""); let deeplAuthKey: string = $ref("");
let deeplIsPro: boolean = $ref(false); let deeplIsPro: boolean = $ref(false);
let libreTranslateApiUrl: string = $ref("");
let libreTranslateApiKey: string = $ref("");
let defaultReaction: string = $ref(""); let defaultReaction: string = $ref("");
let defaultReactionCustom: string = $ref(""); let defaultReactionCustom: string = $ref("");
@ -456,6 +486,8 @@ async function init() {
swPrivateKey = meta.swPrivateKey; swPrivateKey = meta.swPrivateKey;
deeplAuthKey = meta.deeplAuthKey; deeplAuthKey = meta.deeplAuthKey;
deeplIsPro = meta.deeplIsPro; deeplIsPro = meta.deeplIsPro;
libreTranslateApiUrl = meta.libreTranslateApiUrl;
libreTranslateApiKey = meta.libreTranslateApiKey;
defaultReaction = ["⭐", "👍", "❤️"].includes(meta.defaultReaction) defaultReaction = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
? meta.defaultReaction ? meta.defaultReaction
: "custom"; : "custom";
@ -498,6 +530,8 @@ function save() {
swPrivateKey, swPrivateKey,
deeplAuthKey, deeplAuthKey,
deeplIsPro, deeplIsPro,
libreTranslateApiUrl,
libreTranslateApiKey,
defaultReaction, defaultReaction,
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance();

View File

@ -23,6 +23,7 @@
class="avatar" class="avatar"
:user="req.follower" :user="req.follower"
:show-indicator="true" :show-indicator="true"
disableLink
/> />
<div class="body"> <div class="body">
<div class="name"> <div class="name">

View File

@ -98,6 +98,14 @@
@update:modelValue="toggleBlock" @update:modelValue="toggleBlock"
>{{ i18n.ts.blockThisInstance }}</FormSwitch >{{ i18n.ts.blockThisInstance }}</FormSwitch
> >
<FormSwitch
v-model="isSilenced"
class="_formBlock"
@update:modelValue="toggleSilence"
>{{
i18n.ts.silenceThisInstance
}}</FormSwitch
>
</FormSuspense> </FormSuspense>
<MkButton @click="refreshMetadata" <MkButton @click="refreshMetadata"
><i ><i
@ -329,7 +337,7 @@
import { watch } from "vue"; import { watch } from "vue";
import { Virtual } from "swiper"; import { Virtual } from "swiper";
import { Swiper, SwiperSlide } from "swiper/vue"; import { Swiper, SwiperSlide } from "swiper/vue";
import type * as misskey from "calckey-js"; import type * as calckey from "calckey-js";
import MkChart from "@/components/MkChart.vue"; import MkChart from "@/components/MkChart.vue";
import MkObjectView from "@/components/MkObjectView.vue"; import MkObjectView from "@/components/MkObjectView.vue";
import FormLink from "@/components/form/link.vue"; import FormLink from "@/components/form/link.vue";
@ -352,11 +360,13 @@ import "swiper/scss";
import "swiper/scss/virtual"; import "swiper/scss/virtual";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
type AugmentedInstanceMetadata = misskey.entities.DetailedInstanceMetadata & { type AugmentedInstanceMetadata = calckey.entities.DetailedInstanceMetadata & {
blockedHosts: string[]; blockedHosts: string[];
silencedHosts: string[];
}; };
type AugmentedInstance = misskey.entities.Instance & { type AugmentedInstance = calckey.entities.Instance & {
isBlocked: boolean; isBlocked: boolean;
isSilenced: boolean;
}; };
const props = defineProps<{ const props = defineProps<{
@ -373,6 +383,7 @@ let meta = $ref<AugmentedInstanceMetadata | null>(null);
let instance = $ref<AugmentedInstance | null>(null); let instance = $ref<AugmentedInstance | null>(null);
let suspended = $ref(false); let suspended = $ref(false);
let isBlocked = $ref(false); let isBlocked = $ref(false);
let isSilenced = $ref(false);
let faviconUrl = $ref(null); let faviconUrl = $ref(null);
const usersPagination = { const usersPagination = {
@ -386,16 +397,14 @@ const usersPagination = {
offsetMode: true, offsetMode: true,
}; };
async function init() {
meta = await os.api("admin/meta");
}
async function fetch() { async function fetch() {
meta = (await os.api("admin/meta")) as AugmentedInstanceMetadata;
instance = (await os.api("federation/show-instance", { instance = (await os.api("federation/show-instance", {
host: props.host, host: props.host,
})) as AugmentedInstance; })) as AugmentedInstance;
suspended = instance.isSuspended; suspended = instance.isSuspended;
isBlocked = instance.isBlocked; isBlocked = instance.isBlocked;
isSilenced = instance.isSilenced;
faviconUrl = faviconUrl =
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ?? getProxiedImageUrlNullable(instance.faviconUrl, "preview") ??
getProxiedImageUrlNullable(instance.iconUrl, "preview"); getProxiedImageUrlNullable(instance.iconUrl, "preview");
@ -417,6 +426,22 @@ async function toggleBlock() {
}); });
} }
async function toggleSilence() {
if (meta == null) return;
if (!instance) {
throw new Error(`Instance info not loaded`);
}
let silencedHosts: string[];
if (isSilenced) {
silencedHosts = meta.silencedHosts.concat([instance.host]);
} else {
silencedHosts = meta.silencedHosts.filter((x) => x !== instance!.host);
}
await os.api("admin/update-meta", {
silencedHosts,
});
}
async function toggleSuspend(v) { async function toggleSuspend(v) {
await os.api("admin/federation/update-instance", { await os.api("admin/federation/update-instance", {
host: instance.host, host: instance.host,

View File

@ -2,7 +2,7 @@
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader /></template> <template #header><MkPageHeader /></template>
<MkSpacer :content-max="800"> <MkSpacer :content-max="800">
<div class="mwysmxbg"> <div :class="$style.root">
<div>{{ i18n.ts._mfm.intro }}</div> <div>{{ i18n.ts._mfm.intro }}</div>
<br /> <br />
<div class="section _block"> <div class="section _block">
@ -137,6 +137,18 @@
</div> </div>
</div> </div>
</div> </div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.blockMath }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.blockMathDescription }}</p>
<div class="preview">
<Mfm :text="preview_blockMath" />
<MkTextarea v-model="preview_blockMath"
><template #label>MFM</template></MkTextarea
>
</div>
</div>
</div>
<!-- deprecated <!-- deprecated
<div class="section _block"> <div class="section _block">
<div class="title">{{ i18n.ts._mfm.search }}</div> <div class="title">{{ i18n.ts._mfm.search }}</div>
@ -341,6 +353,54 @@
</div> </div>
</div> </div>
</div> </div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.position }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.positionDescription }}</p>
<div class="preview">
<Mfm :text="preview_position" />
<MkTextarea v-model="preview_position"
><span>MFM</span></MkTextarea
>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.scale }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.scaleDescription }}</p>
<div class="preview">
<Mfm :text="preview_scale" />
<MkTextarea v-model="preview_scale"
><span>MFM</span></MkTextarea
>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.foreground }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.foregroundDescription }}</p>
<div class="preview">
<Mfm :text="preview_fg" />
<MkTextarea v-model="preview_fg"
><span>MFM</span></MkTextarea
>
</div>
</div>
</div>
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.background }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.backgroundDescription }}</p>
<div class="preview">
<Mfm :text="preview_bg" />
<MkTextarea v-model="preview_bg"
><span>MFM</span></MkTextarea
>
</div>
</div>
</div>
<div class="section _block"> <div class="section _block">
<div class="title">{{ i18n.ts._mfm.plain }}</div> <div class="title">{{ i18n.ts._mfm.plain }}</div>
<div class="content"> <div class="content">
@ -379,8 +439,11 @@ let preview_blockCode = $ref(
'```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```' '```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```'
); );
let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)"); let preview_inlineMath = $ref("\\(x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\)");
let preview_blockMath = $ref("\\[x= \\frac{-b' \\pm \\sqrt{(b')^2-ac}}{a}\\]");
let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`); let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
let preview_search = $ref(`${i18n.ts._mfm.dummy} 検索`); let preview_search = $ref(
`${i18n.ts._mfm.dummy} [search]\n${i18n.ts._mfm.dummy} [検索]\n${i18n.ts._mfm.dummy} 検索`
);
let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]"); let preview_jelly = $ref("$[jelly 🍮] $[jelly.speed=5s 🍮]");
let preview_tada = $ref("$[tada 🍮] $[tada.speed=5s 🍮]"); let preview_tada = $ref("$[tada 🍮] $[tada.speed=5s 🍮]");
let preview_jump = $ref("$[jump 🍮] $[jump.speed=5s 🍮]"); let preview_jump = $ref("$[jump 🍮] $[jump.speed=5s 🍮]");
@ -402,7 +465,17 @@ let preview_x4 = $ref("$[x4 🍮]");
let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`); let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
let preview_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]"); let preview_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]");
let preview_sparkle = $ref("$[sparkle 🍮]"); let preview_sparkle = $ref("$[sparkle 🍮]");
let preview_rotate = $ref("$[rotate 🍮]"); let preview_rotate = $ref(
"$[rotate 🍮]\n$[rotate.deg=45 🍮]\n$[rotate.x,deg=45 Hello, world!]"
);
let preview_position = $ref(
"$[position.y=-1 Positioning]\n$[position.x=-1 Positioning]"
);
let preview_scale = $ref(
"$[scale.x=1.3 Scaling]\n$[scale.x=1.3,y=2 Scaling]\n$[scale.y=0.3 Tiny scaling]"
);
let preview_fg = $ref("$[fg.color=ff0000 Text color]");
let preview_bg = $ref("$[bg.color=ff0000 Background color]");
let preview_plain = $ref( let preview_plain = $ref(
"<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>" "<plain>**bold** @mention #hashtag `code` $[x2 🍮]</plain>"
); );
@ -413,8 +486,8 @@ definePageMetadata({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.mwysmxbg { .root {
background: var(--bg); background: var(--bg);
> .section { > .section {

View File

@ -6,14 +6,14 @@
{{ i18n.ts.addAccount }}</FormButton {{ i18n.ts.addAccount }}</FormButton
> >
<div <button
v-for="account in accounts" v-for="account in accounts"
:key="account.id" :key="account.id"
class="_panel _button lcjjdxlm" class="_panel _button lcjjdxlm"
@click="menu(account, $event)" @click="menu(account, $event)"
> >
<div class="avatar"> <div class="avatar">
<MkAvatar :user="account" class="avatar" /> <MkAvatar :user="account" class="avatar" disableLink />
</div> </div>
<div class="body"> <div class="body">
<div class="name"> <div class="name">
@ -23,7 +23,7 @@
<MkAcct :user="account" /> <MkAcct :user="account" />
</div> </div>
</div> </div>
</div> </button>
</FormSuspense> </FormSuspense>
</div> </div>
</template> </template>
@ -158,6 +158,8 @@ definePageMetadata({
.lcjjdxlm { .lcjjdxlm {
display: flex; display: flex;
padding: 16px; padding: 16px;
width: 100%;
text-align: unset;
> .avatar { > .avatar {
display: block; display: block;

View File

@ -308,6 +308,7 @@ function showMenu(ev) {
height: 32px; height: 32px;
border-radius: 8px; border-radius: 8px;
font-size: 18px; font-size: 18px;
z-index: 2;
} }
> .fg { > .fg {

View File

@ -204,10 +204,6 @@ hr {
pointer-events: none; pointer-events: none;
} }
&:focus-visible {
outline: none;
}
&:disabled { &:disabled {
opacity: 0.5; opacity: 0.5;
cursor: default; cursor: default;

Some files were not shown because too many files have changed in this diff Show More