Merge branch 'develop' into beta

This commit is contained in:
ThatOneCalculator 2023-05-01 21:06:11 -07:00
commit ef37a58743
91 changed files with 2855 additions and 531 deletions

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -68,8 +68,8 @@ import: "Import"
export: "Export"
files: "Files"
download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Posts\
\ with this file attached will also be deleted."
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? It\
\ will be removed from all posts that contain it as an attachment."
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\
\ to your Drive once completed."
@ -197,6 +197,7 @@ perHour: "Per Hour"
perDay: "Per Day"
stopActivityDelivery: "Stop sending activities"
blockThisInstance: "Block this instance"
silenceThisInstance: "Silence this instance"
operations: "Operations"
software: "Software"
version: "Version"
@ -218,10 +219,13 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote
blockedInstances: "Blocked Instances"
blockedInstancesDescription: "List the hostnames of the instances that you want to\
\ 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"
hiddenTagsDescription: "List the hashtags (without the #) of the hashtags you wish\
\ 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"
mutedUsers: "Muted users"
blockedUsers: "Blocked users"
@ -240,6 +244,7 @@ noCustomEmojis: "There are no emoji"
noJobs: "There are no jobs"
federating: "Federating"
blocked: "Blocked"
silenced: "Silenced"
suspended: "Suspended"
all: "All"
subscribing: "Subscribing"
@ -829,7 +834,7 @@ active: "Active"
offline: "Offline"
notRecommended: "Not recommended"
botProtection: "Bot Protection"
instanceBlocking: "Blocked Instances"
instanceBlocking: "Federation Block/Silence"
selectAccount: "Select account"
switchAccount: "Switch account"
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\
\ 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"
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\
\ your account normally again.\nAlso, please ensure that you've set this current\
\ account as the account you're moving from."
@ -1197,7 +1202,7 @@ _mfm:
inlineMath: "Math (Inline)"
inlineMathDescription: "Display math formulas (KaTeX) in-line"
blockMath: "Math (Block)"
blockMathDescription: "Display multi-line math formulas (KaTeX) in a block"
blockMathDescription: "Display math formulas (KaTeX) in a block"
quote: "Quote"
quoteDescription: "Displays content as a quote."
emoji: "Custom Emoji"

View File

@ -1,9 +1,10 @@
_lang_: "Suomi"
username: Käyttäjänimi
fetchingAsApObject: Hae Fedeversestä
gotIt: Selvä!
cancel: Peruuta
enterUsername: Anna käyttäjänimi
renotedBy: Buustannut {käyttäjä}
renotedBy: Buustannut {user}
noNotes: Ei lähetyksiä
noNotifications: Ei ilmoituksia
instance: Instanssi
@ -41,3 +42,182 @@ 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.

View File

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

View File

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

View File

@ -1012,9 +1012,9 @@ _mfm:
blockCode: "程式碼(區塊)"
blockCodeDescription: "在區塊中用高亮度顯示,例如複數行的程式碼語法。"
inlineMath: "數學公式(內嵌)"
inlineMathDescription: "顯示內嵌的KaTex數學公式。"
inlineMathDescription: "顯示內嵌的KaTeX數學公式。"
blockMath: "數學公式(方塊)"
blockMathDescription: "以區塊顯示複數行的KaTex數學式。"
blockMathDescription: "以區塊顯示KaTeX數學式。"
quote: "引用"
quoteDescription: "可以用來表示引用的内容。"
emoji: "自訂表情符號"

View File

@ -1,6 +1,6 @@
{
"name": "calckey",
"version": "13.2.0-beta9h",
"version": "14.0.0-rc",
"codename": "aqua",
"repository": {
"type": "git",

BIN
packages/backend/assets/favicon.ico (Stored with Git LFS)

Binary file not shown.

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,13 @@
export class InstanceSilence1682891890317 {
name = "InstanceSilence1682891890317";
async up(queryRunner) {
await queryRunner.query(
`ALTER TABLE "meta" ADD "silencedHosts" character varying(256) array NOT NULL DEFAULT '{}'`,
);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "silencedHosts"`);
}
}

View File

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

View File

@ -18,3 +18,21 @@ export async function shouldBlockInstance(
(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[];
@Column('varchar', {
length: 256, array: true, default: '{}',
})
public silencedHosts: string[];
@Column('boolean', {
default: false,
})
@ -386,6 +391,18 @@ export class Meta {
})
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', {
length: 512,
nullable: true,

View File

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

View File

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

View File

@ -10,7 +10,13 @@ import { renderPerson } from "@/remote/activitypub/renderer/person.js";
import renderEmoji from "@/remote/activitypub/renderer/emoji.js";
import { inbox as processInbox } from "@/queue/index.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 { renderLike } from "@/remote/activitypub/renderer/like.js";
import { getUserKeypair } from "@/misc/keypair-store.js";
@ -330,22 +336,68 @@ router.get("/likes/:like", async (ctx) => {
});
// follow
router.get("/follows/:follower/:followee", async (ctx) => {
router.get(
"/follows/:follower/:followee",
async (ctx: Router.RouterContext) => {
const verify = await checkFetch(ctx.req);
if (verify !== 200) {
ctx.status = verify;
return;
}
// This may be used before the follow is completed, so we do not
// check if the following exists.
const [follower, followee] = await Promise.all([
Users.findOneBy({
id: ctx.params.follower,
host: IsNull(),
}),
Users.findOneBy({
id: ctx.params.followee,
host: Not(IsNull()),
}),
]);
if (follower == null || followee == null) {
ctx.status = 404;
return;
}
ctx.body = renderActivity(renderFollow(follower, followee));
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");
}
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;
}
// This may be used before the follow is completed, so we do not
// check if the following exists.
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: ctx.params.follower,
id: followRequest.followerId,
host: IsNull(),
}),
Users.findOneBy({
id: ctx.params.followee,
id: followRequest.followeeId,
host: Not(IsNull()),
}),
]);
@ -355,13 +407,13 @@ router.get("/follows/:follower/:followee", async (ctx) => {
return;
}
ctx.body = renderActivity(renderFollow(follower, followee));
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);
});

View File

@ -30,6 +30,17 @@ export default define(meta, paramDef, async (ps, me) => {
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) {
set.enableEmail = true;
if (typeof config.email.address === "string") {

View File

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

View File

@ -61,6 +61,13 @@ export const paramDef = {
type: "string",
},
},
silencedHosts: {
type: "array",
nullable: true,
items: {
type: "string",
},
},
allowedHosts: {
type: "array",
nullable: true,
@ -124,6 +131,8 @@ export const paramDef = {
summalyProxy: { type: "string", nullable: true },
deeplAuthKey: { type: "string", nullable: true },
deeplIsPro: { type: "boolean" },
libreTranslateApiUrl: { type: "string", nullable: true },
libreTranslateApiKey: { type: "string", nullable: true },
enableTwitterIntegration: { type: "boolean" },
twitterConsumerKey: { 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) {
set.themeColor = ps.themeColor;
}
@ -515,6 +533,22 @@ export default define(meta, paramDef, async (ps, me) => {
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) {
set.enableIpLogging = ps.enableIpLogging;
}

View File

@ -34,6 +34,7 @@ export const paramDef = {
notResponding: { type: "boolean", nullable: true },
suspended: { type: "boolean", nullable: true },
federating: { type: "boolean", nullable: true },
silenced: { type: "boolean", nullable: true },
subscribing: { type: "boolean", nullable: true },
publishing: { type: "boolean", nullable: true },
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 (ps.notResponding) {
query.andWhere("instance.isNotResponding = TRUE");

View File

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

View File

@ -51,15 +51,54 @@ export default define(meta, paramDef, async (ps, user) => {
const instance = await fetchMeta();
if (instance.deeplAuthKey == null) {
if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
return 204; // TODO: 良い感じのエラー返す
}
let targetLang = ps.targetLang;
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();
params.append("auth_key", instance.deeplAuthKey);
params.append("auth_key", instance.deeplAuthKey ?? "");
params.append("text", note.text);
params.append("target_lang", targetLang);

View File

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

View File

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

View File

@ -0,0 +1,61 @@
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,14 @@ import { resolveUser } from "@/remote/resolve-user.js";
import Router from "@koa/router";
import { FindOptionsWhere, IsNull } from "typeorm";
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 {
convertAccount,
convertList,
convertRelationship,
convertStatus,
} from "../converters.js";
const relationshipModel = {
id: "",
@ -62,9 +68,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.updateCredentials(
(ctx.request as any).body as any,
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertAccount(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -81,9 +85,7 @@ export function apiAccountMastodon(router: Router): void {
(ctx.request.query as any).acct,
"accounts",
);
let resp = data.data.accounts[0];
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertAccount(data.data.accounts[0]);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -115,11 +117,9 @@ export function apiAccountMastodon(router: Router): void {
}
const data = await client.getRelationships(reqIds);
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
ctx.body = data.data.map((relationship) =>
convertRelationship(relationship),
);
} catch (e: any) {
console.error(e);
let data = e.response.data;
@ -136,9 +136,7 @@ export function apiAccountMastodon(router: Router): void {
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;
ctx.body = convertAccount(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -155,27 +153,9 @@ export function apiAccountMastodon(router: Router): void {
try {
const data = await client.getAccountStatuses(
convertId(ctx.params.id, IdType.CalckeyId),
argsToBools(limitToInt(ctx.query as any)),
convertTimelinesArgsId(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;
ctx.body = data.data.map((status) => convertStatus(status));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -193,13 +173,9 @@ export function apiAccountMastodon(router: Router): void {
try {
const data = await client.getAccountFollowers(
convertId(ctx.params.id, IdType.CalckeyId),
limitToInt(ctx.query as any),
convertTimelinesArgsId(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;
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -217,13 +193,9 @@ export function apiAccountMastodon(router: Router): void {
try {
const data = await client.getAccountFollowing(
convertId(ctx.params.id, IdType.CalckeyId),
limitToInt(ctx.query as any),
convertTimelinesArgsId(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;
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -239,8 +211,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccountLists(ctx.params.id);
ctx.body = data.data;
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);
@ -259,9 +233,8 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.followAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let acct = data.data;
let acct = convertRelationship(data.data);
acct.following = true;
acct.id = convertId(acct.id, IdType.MastodonId);
ctx.body = acct;
} catch (e: any) {
console.error(e);
@ -281,8 +254,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.unfollowAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let acct = data.data;
acct.id = convertId(acct.id, IdType.MastodonId);
let acct = convertRelationship(data.data);
acct.following = false;
ctx.body = acct;
} catch (e: any) {
@ -303,9 +275,7 @@ export function apiAccountMastodon(router: Router): void {
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;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -324,9 +294,7 @@ export function apiAccountMastodon(router: Router): void {
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;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -346,9 +314,7 @@ export function apiAccountMastodon(router: Router): void {
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;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -367,9 +333,7 @@ export function apiAccountMastodon(router: Router): void {
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;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -383,28 +347,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = (await client.getBookmarks(
limitToInt(ctx.query as any),
)) 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;
const data = await client.getBookmarks(
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
ctx.body = data.data.map((status) => convertStatus(status));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -417,26 +363,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getFavourites(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;
const data = await client.getFavourites(
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
ctx.body = data.data.map((status) => convertStatus(status));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -449,12 +379,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getMutes(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;
const data = await client.getMutes(
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);
@ -467,12 +395,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getBlocks(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;
const data = await client.getBlocks(
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);
@ -488,11 +414,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.getFollowRequests(
((ctx.query as any) || { limit: 20 }).limit,
);
let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
}
ctx.body = resp;
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -510,9 +432,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.acceptFollowRequest(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -531,9 +451,7 @@ export function apiAccountMastodon(router: Router): void {
const data = await client.rejectFollowRequest(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp;
ctx.body = convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);

View File

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

View File

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

View File

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

View File

@ -4,7 +4,14 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
import axios from "axios";
import querystring from "node:querystring";
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) {
const str = querystring.stringify(data);
@ -18,6 +25,8 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
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 (
(!body.poll && body["poll[options][]"]) ||
(!body.media_ids && body["media_ids[]"])
@ -54,7 +63,7 @@ export function apiStatusMastodon(router: Router): void {
body.sensitive =
typeof sensitive === "string" ? sensitive === "true" : sensitive;
const data = await client.postStatus(text, body);
ctx.body = data.data;
ctx.body = convertStatus(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -66,8 +75,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getStatus(ctx.params.id);
ctx.body = data.data;
const data = await client.getStatus(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -79,7 +90,9 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.deleteStatus(ctx.params.id);
const data = await client.deleteStatus(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data;
} catch (e: any) {
console.error(e.response.data, request.params.id);
@ -100,10 +113,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const id = ctx.params.id;
const id = convertId(ctx.params.id, IdType.CalckeyId);
const data = await client.getStatusContext(
id,
limitToInt(ctx.query as any),
convertTimelinesArgsId(limitToInt(ctx.query as any)),
);
const status = await client.getStatus(id);
let reqInstance = axios.create({
@ -126,6 +139,12 @@ export function apiStatusMastodon(router: Router): void {
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;
} catch (e: any) {
console.error(e);
@ -141,8 +160,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getStatusRebloggedBy(ctx.params.id);
ctx.body = data.data;
const data = await client.getStatusRebloggedBy(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -165,11 +186,11 @@ export function apiStatusMastodon(router: Router): void {
const react = await getFirstReaction(BASE_URL, accessTokens);
try {
const a = (await client.createEmojiReaction(
ctx.params.id,
convertId(ctx.params.id, IdType.CalckeyId),
react,
)) as any;
//const data = await client.favouriteStatus(ctx.params.id) as any;
ctx.body = a.data;
ctx.body = convertStatus(a.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -186,8 +207,11 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
const react = await getFirstReaction(BASE_URL, accessTokens);
try {
const data = await client.deleteEmojiReaction(ctx.params.id, react);
ctx.body = data.data;
const data = await client.deleteEmojiReaction(
convertId(ctx.params.id, IdType.CalckeyId),
react,
);
ctx.body = convertStatus(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -203,8 +227,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.reblogStatus(ctx.params.id);
ctx.body = data.data;
const data = await client.reblogStatus(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -220,8 +246,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.unreblogStatus(ctx.params.id);
ctx.body = data.data;
const data = await client.unreblogStatus(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -237,8 +265,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.bookmarkStatus(ctx.params.id);
ctx.body = data.data;
const data = await client.bookmarkStatus(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -254,8 +284,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = (await client.unbookmarkStatus(ctx.params.id)) as any;
ctx.body = data.data;
const data = await client.unbookmarkStatus(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -271,8 +303,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.pinStatus(ctx.params.id);
ctx.body = data.data;
const data = await client.pinStatus(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -288,8 +322,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.unpinStatus(ctx.params.id);
ctx.body = data.data;
const data = await client.unpinStatus(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertStatus(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -302,8 +338,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getMedia(ctx.params.id);
ctx.body = data.data;
const data = await client.getMedia(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertAttachment(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -316,10 +354,10 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.updateMedia(
ctx.params.id,
convertId(ctx.params.id, IdType.CalckeyId),
ctx.request.body as any,
);
ctx.body = data.data;
ctx.body = convertAttachment(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -331,8 +369,10 @@ export function apiStatusMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getPoll(ctx.params.id);
ctx.body = data.data;
const data = await client.getPoll(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertPoll(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;
@ -347,10 +387,10 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.votePoll(
ctx.params.id,
convertId(ctx.params.id, IdType.CalckeyId),
(ctx.request.body as any).choices,
);
ctx.body = data.data;
ctx.body = convertPoll(data.data);
} catch (e: any) {
console.error(e);
ctx.status = 401;

View File

@ -4,6 +4,8 @@ import { getClient } from "../ApiMastodonCompatibleService.js";
import { statusModel } from "./status.js";
import Autolinker from "autolinker";
import { ParsedUrlQuery } from "querystring";
import { convertAccount, convertList, convertStatus } from "../converters.js";
import { convertId, IdType } from "../../index.js";
export function limitToInt(q: ParsedUrlQuery) {
let object: any = q;
@ -29,6 +31,16 @@ export function argsToBools(q: ParsedUrlQuery) {
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) {
return status.map((t) => {
if (!t) return statusModel(null, null, [], "no content");
@ -97,9 +109,14 @@ export function apiTimelineMastodon(router: Router): void {
try {
const query: any = ctx.query;
const data = query.local
? await client.getLocalTimeline(argsToBools(limitToInt(query)))
: await client.getPublicTimeline(argsToBools(limitToInt(query)));
ctx.body = toTextWithReaction(data.data, ctx.hostname);
? await client.getLocalTimeline(
convertTimelinesArgsId(argsToBools(limitToInt(query))),
)
: await client.getPublicTimeline(
convertTimelinesArgsId(argsToBools(limitToInt(query))),
);
let resp = data.data.map((status) => convertStatus(status));
ctx.body = toTextWithReaction(resp, ctx.hostname);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -116,9 +133,10 @@ export function apiTimelineMastodon(router: Router): void {
try {
const data = await client.getTagTimeline(
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) {
console.error(e);
console.error(e.response.data);
@ -132,8 +150,11 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getHomeTimeline(limitToInt(ctx.query));
ctx.body = toTextWithReaction(data.data, ctx.hostname);
const data = await client.getHomeTimeline(
convertTimelinesArgsId(limitToInt(ctx.query)),
);
let resp = data.data.map((status) => convertStatus(status));
ctx.body = toTextWithReaction(resp, ctx.hostname);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -149,10 +170,11 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getListTimeline(
ctx.params.listId,
limitToInt(ctx.query),
convertId(ctx.params.listId, IdType.CalckeyId),
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) {
console.error(e);
console.error(e.response.data);
@ -166,7 +188,9 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getConversationTimeline(limitToInt(ctx.query));
const data = await client.getConversationTimeline(
convertTimelinesArgsId(limitToInt(ctx.query)),
);
ctx.body = data.data;
} catch (e: any) {
console.error(e);
@ -181,7 +205,7 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getLists();
ctx.body = data.data;
ctx.body = data.data.map((list) => convertList(list));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -196,8 +220,10 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getList(ctx.params.id);
ctx.body = data.data;
const data = await client.getList(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = convertList(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -212,7 +238,7 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.createList((ctx.request.body as any).title);
ctx.body = data.data;
ctx.body = convertList(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -227,8 +253,11 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.updateList(ctx.params.id, (ctx.request.body as any).title);
ctx.body = data.data;
const data = await client.updateList(
convertId(ctx.params.id, IdType.CalckeyId),
(ctx.request.body as any).title,
);
ctx.body = convertList(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -244,7 +273,9 @@ export function apiTimelineMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.deleteList(ctx.params.id);
const data = await client.deleteList(
convertId(ctx.params.id, IdType.CalckeyId),
);
ctx.body = data.data;
} catch (e: any) {
console.error(e);
@ -262,10 +293,10 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccountsInList(
ctx.params.id,
ctx.query as any,
convertId(ctx.params.id, IdType.CalckeyId),
convertTimelinesArgsId(ctx.query as any),
);
ctx.body = data.data;
ctx.body = data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
@ -282,8 +313,10 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.addAccountsToList(
ctx.params.id,
(ctx.query as any).account_ids,
convertId(ctx.params.id, IdType.CalckeyId),
(ctx.query.account_ids as string[]).map((id) =>
convertId(id, IdType.CalckeyId),
),
);
ctx.body = data.data;
} catch (e: any) {
@ -302,8 +335,10 @@ export function apiTimelineMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.deleteAccountsFromList(
ctx.params.id,
(ctx.query as any).account_ids,
convertId(ctx.params.id, IdType.CalckeyId),
(ctx.query.account_ids as string[]).map((id) =>
convertId(id, IdType.CalckeyId),
),
);
ctx.body = data.data;
} catch (e: any) {

View File

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

View File

@ -399,28 +399,33 @@ router.get("/notes/:note", async (ctx, next) => {
visibility: In(["public", "home"]),
});
if (note) {
const _note = await Notes.pack(note);
const profile = await UserProfiles.findOneByOrFail({ userId: note.userId });
const meta = await fetchMeta();
await ctx.render("note", {
note: _note,
profile,
avatarUrl: await Users.getAvatarUrl(
await Users.findOneByOrFail({ id: note.userId }),
),
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note),
instanceName: meta.name || "Calckey",
icon: meta.iconUrl,
privateMode: meta.privateMode,
themeColor: meta.themeColor,
});
try {
if (note) {
const _note = await Notes.pack(note);
ctx.set("Cache-Control", "public, max-age=15");
const profile = await UserProfiles.findOneByOrFail({
userId: note.userId,
});
const meta = await fetchMeta();
await ctx.render("note", {
note: _note,
profile,
avatarUrl: await Users.getAvatarUrl(
await Users.findOneByOrFail({ id: note.userId }),
),
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note),
instanceName: meta.name || "Calckey",
icon: meta.iconUrl,
privateMode: meta.privateMode,
themeColor: meta.themeColor,
});
return;
}
ctx.set("Cache-Control", "public, max-age=15");
return;
}
} catch {}
await next();
});

View File

@ -6,11 +6,13 @@ import {
NoteThreadMutings,
UserProfiles,
Users,
Followings,
} from "@/models/index.js";
import { genId } from "@/misc/gen-id.js";
import type { User } from "@/models/entities/user.js";
import type { Notification } from "@/models/entities/notification.js";
import { sendEmailNotification } from "./send-email-notification.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
export async function createNotification(
notifieeId: User["id"],
@ -21,6 +23,26 @@ export async function createNotification(
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 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 { getActiveWebhooks } from "@/misc/webhook-cache.js";
import { webhookDeliver } from "@/queue/index.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
const logger = new Logger("following/create");
@ -226,13 +227,19 @@ export default async function (
});
// フォロー対象が鍵アカウントである or
// The follower is silenced, or
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or
// The follower is remote, the followee is local, and the follower is in a silenced instance.
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
if (
followee.isLocked ||
follower.isSilenced ||
(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;

View File

@ -6,6 +6,7 @@ import type { User } from "@/models/entities/user.js";
import { Blockings, FollowRequests, Users } from "@/models/index.js";
import { genId } from "@/misc/gen-id.js";
import { createNotification } from "../../create-notification.js";
import config from "@/config/index.js";
export default async function (
follower: {
@ -79,7 +80,13 @@ export default async function (
}
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);
}
}

View File

@ -39,7 +39,7 @@ import {
} from "@/models/index.js";
import type { DriveFile } from "@/models/entities/drive-file.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 { genId } from "@/misc/gen-id.js";
import {
@ -66,6 +66,7 @@ import { Cache } from "@/misc/cache.js";
import type { UserProfile } from "@/models/entities/user-profile.js";
import { db } from "@/db/postgre.js";
import { getActiveWebhooks } from "@/misc/webhook-cache.js";
import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
const mutedWordsCache = new Cache<
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
@ -166,6 +167,7 @@ export default async (
data: Option,
silent = false,
) =>
// rome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
new Promise<Note>(async (res, rej) => {
// 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.)
@ -203,6 +205,15 @@ export default async (
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".
if (
data.renote &&

View File

@ -118,7 +118,7 @@ export default async (
userId: user.id,
});
// リアクションされたユーザーがローカルユーザーなら通知を作成
// Create notification if the reaction target is a local user.
if (note.userHost === null) {
createNotification(note.userId, "reaction", {
notifierId: user.id,
@ -143,7 +143,7 @@ export default async (
}
});
//#region 配信
//#region deliver
if (Users.isLocalUser(user) && !note.localOnly) {
const content = renderActivity(await renderLike(record, note));
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",
"api": "pnpm api-extractor run --local --verbose",
"api-prod": "pnpm api-extractor run --verbose",
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx",
"typecheck": "tsc --noEmit",
"lint": "pnpm typecheck && pnpm eslint",
"lint": "pnpm typecheck && pnpm rome check \"src/*.ts\"",
"jest": "jest --coverage --detectOpenHandles",
"test": "pnpm jest && pnpm tsd"
},

View File

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

View File

@ -32,7 +32,7 @@
"autosize": "5.0.2",
"blurhash": "1.1.5",
"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:*",
"chart.js": "4.1.1",
"chartjs-adapter-date-fns": "2.0.1",

View File

@ -28,7 +28,7 @@ const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void;
}>();
const el = ref<HTMLElement>();
const el = ref<HTMLElement>();
const label = computed(() => {
return concat([
@ -52,7 +52,7 @@ function focus() {
}
defineExpose({
focus
focus,
});
</script>
@ -73,9 +73,46 @@ defineExpose({
}
}
}
&:hover > span, &:focus > span {
&:hover > span,
&:focus > span {
background: var(--cwFg) !important;
color: var(--cwBg) !important;
}
&.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

@ -174,7 +174,7 @@ import { deviceKind } from "@/scripts/device-kind";
import { emojiCategories, instance } from "@/instance";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import { FocusTrap } from 'focus-trap-vue';
import { FocusTrap } from "focus-trap-vue";
const props = withDefaults(
defineProps<{

View File

@ -5,6 +5,7 @@
{
yellow: instance.isNotResponding,
red: instance.isBlocked,
purple: instance.isSilenced,
gray: instance.isSuspended,
},
]"
@ -23,13 +24,13 @@
</template>
<script lang="ts" setup>
import * as misskey from "calckey-js";
import * as calckey from "calckey-js";
import MkMiniChart from "@/components/MkMiniChart.vue";
import * as os from "@/os";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
const props = defineProps<{
instance: misskey.entities.Instance;
instance: calckey.entities.Instance;
}>();
let chartValues = $ref<number[] | null>(null);
@ -135,6 +136,21 @@ function getInstanceIcon(instance): string {
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) {
--c: var(--bg);
background-image: linear-gradient(

View File

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

View File

@ -1,14 +1,14 @@
<template>
<div ref="el" class="sfhdhdhr" tabindex="-1">
<MkMenu
ref="menu"
:items="items"
:align="align"
:width="width"
:as-drawer="false"
@close="onChildClosed"
/>
</div>
<MkMenu
ref="menu"
:items="items"
:align="align"
:width="width"
:as-drawer="false"
@close="onChildClosed"
/>
</div>
</template>
<script lang="ts" setup>

View File

@ -1,6 +1,6 @@
<template>
<FocusTrap v-bind:active="isActive">
<div tabindex="-1" v-focus>
<FocusTrap :active="false" ref="focusTrap">
<div tabindex="-1">
<div
ref="itemsEl"
class="rrevdjwt _popup _shadow"
@ -14,7 +14,9 @@
<template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div>
<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
v-else-if="item.type === 'pending'"
@ -48,7 +50,9 @@
class="avatar"
disableLink
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span :style="item.textStyle || ''">{{
item.text
}}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
@ -75,7 +79,9 @@
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span :style="item.textStyle || ''">{{
item.text
}}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
@ -89,9 +95,11 @@
@mouseenter.passive="onItemMouseEnter(item)"
@mouseleave.passive="onItemMouseLeave(item)"
>
<MkAvatar :user="item.user" class="avatar" disableLink /><MkUserName
<MkAvatar
:user="item.user"
/>
class="avatar"
disableLink
/><MkUserName :user="item.user" />
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
@ -129,9 +137,13 @@
:class="icon"
></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span :style="item.textStyle || ''">{{
item.text
}}</span>
<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>
</button>
<button
@ -161,7 +173,9 @@
class="avatar"
disableLink
/>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span :style="item.textStyle || ''">{{
item.text
}}</span>
<span v-if="item.indicate" class="indicator"
><i class="ph-circle ph-fill"></i
></span>
@ -203,9 +217,10 @@ import FormSwitch from "@/components/form/switch.vue";
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from "@/types/menu";
import * as os from "@/os";
import { i18n } from "@/i18n";
import { FocusTrap } from 'focus-trap-vue';
import { FocusTrap } from "focus-trap-vue";
const XChild = defineAsyncComponent(() => import("./MkMenu.child.vue"));
const focusTrap = ref();
const props = defineProps<{
items: MenuItem[];
@ -316,6 +331,8 @@ function focusDown() {
}
onMounted(() => {
focusTrap.value.activate();
if (props.viaKeyboard) {
nextTick(() => {
focusNext(itemsEl.children[0], true, false);
@ -380,7 +397,8 @@ onBeforeUnmount(() => {
transform: translateY(0em);
}
&:not(:disabled):hover, &:focus-visible {
&:not(:disabled):hover,
&:focus-visible {
color: var(--accent);
text-decoration: none;

View File

@ -26,13 +26,16 @@
$style.root,
{
[$style.drawer]: type === 'drawer',
[$style.dialog]: type === 'dialog' || type === 'dialog:top',
[$style.dialog]:
type === 'dialog' || type === 'dialog:top',
[$style.popup]: type === 'popup',
},
]"
:style="{
zIndex,
pointerEvents: (manualShowing != null ? manualShowing : showing)
pointerEvents: (
manualShowing != null ? manualShowing : showing
)
? 'auto'
: 'none',
'--transformOrigin': transformOrigin,
@ -76,7 +79,7 @@ import * as os from "@/os";
import { isTouchUsing } from "@/scripts/touch";
import { defaultStore } from "@/store";
import { deviceKind } from "@/scripts/device-kind";
import { FocusTrap } from 'focus-trap-vue';
import { FocusTrap } from "focus-trap-vue";
function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === "BODY") return null;

View File

@ -60,7 +60,7 @@
<script lang="ts" setup>
import { onMounted, onUnmounted } from "vue";
import { FocusTrap } from 'focus-trap-vue';
import { FocusTrap } from "focus-trap-vue";
import MkModal from "./MkModal.vue";
const props = withDefaults(

View File

@ -279,7 +279,7 @@ const isRenote =
note.poll == null;
const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();

View File

@ -462,15 +462,26 @@ if (
props.reply &&
["home", "followers", "specified"].includes(props.reply.visibility)
) {
visibility = props.reply.visibility;
if (props.reply.visibility === "specified") {
os.api("users/show", {
userIds: props.reply.visibleUserIds.filter(
(uid) => uid !== $i.id && uid !== props.reply.userId
),
}).then((users) => {
users.forEach(pushVisibleUser);
});
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;
}
if (visibility === "specified") {
if (props.reply.visibleUserIds) {
os.api("users/show", {
userIds: props.reply.visibleUserIds.filter(
(uid) => uid !== $i.id && uid !== props.reply.userId
),
}).then((users) => {
users.forEach(pushVisibleUser);
});
}
if (props.reply.userId !== $i.id) {
os.api("users/show", { userId: props.reply.userId }).then(

View File

@ -0,0 +1,60 @@
<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,10 +35,19 @@
class="content"
:class="{ collapsed, isLong, showContent: note.cw && !showContent }"
>
<XCwButton ref="cwButton" v-if="note.cw && !showContent" v-model="showContent" :note="note" v-on:keydown="focusFooter" />
<div
<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 }"
v-bind="{
'aria-label': !showContent ? '' : null,
tabindex: !showContent ? '-1' : null,
}"
>
<span v-if="note.deletedAt" style="opacity: 0.5"
>({{ i18n.ts.deleted }})</span
@ -106,27 +115,17 @@
v-on:focus="cwButton?.focus()"
></div>
</div>
<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>
<XShowMoreButton
v-if="isLong"
v-model="collapsed"
></XShowMoreButton>
<XCwButton v-if="note.cw" v-model="showContent" :note="note" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { ref } from "vue";
import * as misskey from "calckey-js";
import * as mfm from "mfm-js";
import XNoteSimple from "@/components/MkNoteSimple.vue";
@ -150,7 +149,7 @@ const emit = defineEmits<{
(ev: "focusfooter"): void;
}>();
const cwButton = ref<HTMLElement>();
const cwButton = ref<HTMLElement>();
const isLong =
!props.detailedView &&
props.note.cw == null &&
@ -163,7 +162,6 @@ const urls = props.note.text
let showContent = $ref(false);
function focusFooter(ev) {
if (ev.key == "Tab" && !ev.getModifierState("Shift")) {
emit("focusfooter");

View File

@ -96,7 +96,8 @@ export default defineComponent({
font-size: 0.9em;
margin-bottom: 0.3rem;
&:hover, &:focus-visible {
&:hover,
&:focus-visible {
text-decoration: none;
background: var(--panelHighlight);
}

View File

@ -46,7 +46,10 @@
/></MkA>
<p class="username"><MkAcct :user="user" /></p>
</div>
<div class="description" :class="{ collapsed: isLong && collapsed }">
<div
class="description"
:class="{ collapsed: isLong && collapsed }"
>
<Mfm
v-if="user.description"
:text="user.description"
@ -55,20 +58,10 @@
:custom-emojis="user.emojis"
/>
</div>
<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>
<XShowMoreButton
v-if="isLong"
v-model="collapsed"
></XShowMoreButton>
<div v-if="user.fields.length > 0" class="fields">
<dl
v-for="(field, i) in user.fields"
@ -149,14 +142,15 @@ let user = $ref<misskey.entities.UserDetailed | null>(null);
let top = $ref(0);
let left = $ref(0);
let isLong = $ref(false);
let collapsed = $ref(!isLong);
onMounted(() => {
if (typeof props.q === "object") {
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 {
const query = props.q.startsWith("@")
? Acct.parse(props.q.substr(1))
@ -165,10 +159,11 @@ onMounted(() => {
os.api("users/show", query).then((res) => {
if (!props.showing) return;
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 x =
@ -313,7 +308,7 @@ onMounted(() => {
> .fields {
padding: 0 16px;
font-size: .8em;
font-size: 0.8em;
margin-top: 1em;
> .field {

View File

@ -7,7 +7,7 @@
v-bind="Object.fromEntries(currentPageProps)"
tabindex="-1"
v-focus
style="outline: none;"
style="outline: none"
/>
<template #fallback>

View File

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

View File

@ -87,23 +87,11 @@ export default {
self.hideTimer = window.setTimeout(self.close, delay);
}
el.addEventListener(
start, showTooltip,
{ passive: true },
);
el.addEventListener(
"focusin", showTooltip,
{ passive: true },
);
el.addEventListener(start, showTooltip, { passive: true });
el.addEventListener("focusin", showTooltip, { passive: true });
el.addEventListener(
end, hideTooltip,
{ passive: true },
);
el.addEventListener(
"focusout", hideTooltip,
{ passive: true },
);
el.addEventListener(end, hideTooltip, { passive: true });
el.addEventListener("focusout", hideTooltip, { passive: true });
el.addEventListener("click", () => {
window.clearTimeout(self.showTimer);

View File

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

View File

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

View File

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

View File

@ -7,13 +7,31 @@
:display-back-button="true"
/></template>
<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">
<FormTextarea v-model="blockedHosts" class="_formBlock">
<FormTextarea
v-if="tab === 'block'"
v-model="blockedHosts"
class="_formBlock"
>
<span>{{ i18n.ts.blockedInstances }}</span>
<template #caption>{{
i18n.ts.blockedInstancesDescription
}}</template>
</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"
><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 FormTextarea from "@/components/form/textarea.vue";
import FormSuspense from "@/components/form/suspense.vue";
import MkTab from "@/components/MkTab.vue";
import * as os from "@/os";
import { fetchInstance } from "@/instance";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
let blockedHosts: string = $ref("");
let silencedHosts: string = $ref("");
let tab = $ref("block");
async function init() {
const meta = await os.api("admin/meta");
blockedHosts = meta.blockedHosts.join("\n");
if (meta) {
blockedHosts = meta.blockedHosts.join("\n");
silencedHosts = meta.silencedHosts.join("\n");
}
}
function save() {
os.apiWithDialog("admin/update-meta", {
blockedHosts: blockedHosts.split("\n").map((h) => h.trim()) || [],
silencedHosts: silencedHosts.split("\n").map((h) => h.trim()) || [],
}).then(() => {
fetchInstance();
});

View File

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

View File

@ -371,6 +371,34 @@
<template #label>Pro account</template>
</FormSwitch>
</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>
</FormSuspense>
</MkSpacer>
@ -422,6 +450,8 @@ let swPublicKey: any = $ref(null);
let swPrivateKey: any = $ref(null);
let deeplAuthKey: string = $ref("");
let deeplIsPro: boolean = $ref(false);
let libreTranslateApiUrl: string = $ref("");
let libreTranslateApiKey: string = $ref("");
let defaultReaction: string = $ref("");
let defaultReactionCustom: string = $ref("");
@ -456,6 +486,8 @@ async function init() {
swPrivateKey = meta.swPrivateKey;
deeplAuthKey = meta.deeplAuthKey;
deeplIsPro = meta.deeplIsPro;
libreTranslateApiUrl = meta.libreTranslateApiUrl;
libreTranslateApiKey = meta.libreTranslateApiKey;
defaultReaction = ["⭐", "👍", "❤️"].includes(meta.defaultReaction)
? meta.defaultReaction
: "custom";
@ -498,6 +530,8 @@ function save() {
swPrivateKey,
deeplAuthKey,
deeplIsPro,
libreTranslateApiUrl,
libreTranslateApiKey,
defaultReaction,
}).then(() => {
fetchInstance();

View File

@ -98,6 +98,14 @@
@update:modelValue="toggleBlock"
>{{ i18n.ts.blockThisInstance }}</FormSwitch
>
<FormSwitch
v-model="isSilenced"
class="_formBlock"
@update:modelValue="toggleSilence"
>{{
i18n.ts.silenceThisInstance
}}</FormSwitch
>
</FormSuspense>
<MkButton @click="refreshMetadata"
><i
@ -329,7 +337,7 @@
import { watch } from "vue";
import { Virtual } from "swiper";
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 MkObjectView from "@/components/MkObjectView.vue";
import FormLink from "@/components/form/link.vue";
@ -352,11 +360,13 @@ import "swiper/scss";
import "swiper/scss/virtual";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
type AugmentedInstanceMetadata = misskey.entities.DetailedInstanceMetadata & {
type AugmentedInstanceMetadata = calckey.entities.DetailedInstanceMetadata & {
blockedHosts: string[];
silencedHosts: string[];
};
type AugmentedInstance = misskey.entities.Instance & {
type AugmentedInstance = calckey.entities.Instance & {
isBlocked: boolean;
isSilenced: boolean;
};
const props = defineProps<{
@ -373,6 +383,7 @@ let meta = $ref<AugmentedInstanceMetadata | null>(null);
let instance = $ref<AugmentedInstance | null>(null);
let suspended = $ref(false);
let isBlocked = $ref(false);
let isSilenced = $ref(false);
let faviconUrl = $ref(null);
const usersPagination = {
@ -386,16 +397,14 @@ const usersPagination = {
offsetMode: true,
};
async function init() {
meta = await os.api("admin/meta");
}
async function fetch() {
meta = (await os.api("admin/meta")) as AugmentedInstanceMetadata;
instance = (await os.api("federation/show-instance", {
host: props.host,
})) as AugmentedInstance;
suspended = instance.isSuspended;
isBlocked = instance.isBlocked;
isSilenced = instance.isSilenced;
faviconUrl =
getProxiedImageUrlNullable(instance.faviconUrl, "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) {
await os.api("admin/federation/update-instance", {
host: instance.host,

View File

@ -2,7 +2,7 @@
<MkStickyContainer>
<template #header><MkPageHeader /></template>
<MkSpacer :content-max="800">
<div class="mwysmxbg">
<div :class="$style.root">
<div>{{ i18n.ts._mfm.intro }}</div>
<br />
<div class="section _block">
@ -137,6 +137,18 @@
</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
<div class="section _block">
<div class="title">{{ i18n.ts._mfm.search }}</div>
@ -427,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```'
);
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_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_tada = $ref("$[tada 🍮] $[tada.speed=5s 🍮]");
let preview_jump = $ref("$[jump 🍮] $[jump.speed=5s 🍮]");
@ -450,9 +465,15 @@ let preview_x4 = $ref("$[x4 🍮]");
let preview_blur = $ref(`$[blur ${i18n.ts._mfm.dummy}]`);
let preview_rainbow = $ref("$[rainbow 🍮] $[rainbow.speed=5s 🍮]");
let preview_sparkle = $ref("$[sparkle 🍮]");
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_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(
@ -465,8 +486,8 @@ definePageMetadata({
});
</script>
<style lang="scss" scoped>
.mwysmxbg {
<style lang="scss" module>
.root {
background: var(--bg);
> .section {

View File

@ -1,7 +1,7 @@
{
id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea',
base: 'dark',
name: 'Mi Astro Dark',
name: 'Astro Dark',
author: 'syuilo',
props: {
bg: '#232125',

View File

@ -1,7 +1,7 @@
{
id: '504debaf-4912-6a4c-5059-1db08a76b737',
name: 'Mi Botanical Dark',
name: 'Botanical Dark',
author: 'syuilo',
base: 'dark',

View File

@ -1,7 +1,7 @@
{
id: 'ffcd3328-5c57-4ca3-9dac-4580cbf7742f',
base: 'dark',
name: 'Catppuccin frappe',
name: 'Catppuccin Frappe',
props: {
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',

View File

@ -1,7 +1,7 @@
{
id: 'd413f41f-a489-48be-9e20-3532ffbb4363',
base: 'dark',
name: 'Catppuccin mocha',
name: 'Catppuccin Mocha',
props: {
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',

View File

@ -1,7 +1,7 @@
{
id: '679b3b87-a4e9-4789-8696-b56c15cc33b0',
name: 'Mi Cherry Dark',
name: 'Cherry Dark',
author: 'syuilo',
base: 'dark',

View File

@ -1,7 +1,7 @@
{
id: '32a637ef-b47a-4775-bb7b-bacbb823f865',
name: 'Mi Future Dark',
name: 'Future Dark',
author: 'syuilo',
base: 'dark',

View File

@ -1,7 +1,7 @@
{
id: '02816013-8107-440f-877e-865083ffe194',
name: 'Mi Green+Lime Dark',
name: 'Mi Dark',
author: 'syuilo',
base: 'dark',

View File

@ -1,24 +0,0 @@
{
id: 'dc489603-27b5-424a-9b25-1ff6aec9824a',
name: 'Mi Green+Orange Dark',
author: 'syuilo',
base: 'dark',
props: {
accent: '#e97f00',
bg: '#0C1210',
fg: '#dee7e4',
fgHighlighted: '#fff',
fgOnAccent: '#192320',
divider: '#e7fffb24',
panel: '#192320',
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
popup: '#293330',
renote: '@accent',
mentionMe: '#b4e900',
link: '#24d7ce',
},
}

View File

@ -1,7 +1,7 @@
{
id: '66e7e5a9-cd43-42cd-837d-12f47841fa34',
name: 'Mi Ice Dark',
name: 'Ice Dark',
author: 'syuilo',
base: 'dark',

View File

@ -1,7 +1,7 @@
{
id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
name: 'Mi Persimmon Dark',
name: 'Persimmon Dark',
author: 'syuilo',
base: 'dark',

View File

@ -1,7 +1,7 @@
{
id: '7a5bc13b-df8f-4d44-8e94-4452f0c634bb',
base: 'dark',
name: 'Mi U0 Dark',
name: 'U0 Dark',
props: {
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',

View File

@ -1,7 +1,7 @@
{
id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
name: 'Mi Apricot Light',
name: 'Apricot Light',
author: 'syuilo',
base: 'light',

View File

@ -0,0 +1,94 @@
{
id: "169661d2-5a17-4dfc-b71b-9938cbbbed3e",
base: "light",
name: "Catppuccin Latte",
props: {
X2: ":darken<2<@panel",
X3: "rgba(255, 255, 255, 0.05)",
X4: "rgba(255, 255, 255, 0.1)",
X5: "rgba(255, 255, 255, 0.05)",
X6: "rgba(255, 255, 255, 0.15)",
X7: "rgba(255, 255, 255, 0.05)",
X8: ":lighten<5<@accent",
X9: ":darken<5<@accent",
bg: "#dce0e8",
fg: "#4c4f69",
X10: ":alpha<0.4<@accent",
X11: "rgba(0, 0, 0, 0.3)",
X12: "rgba(255, 255, 255, 0.1)",
X13: "rgba(255, 255, 255, 0.15)",
X14: ":alpha<0.5<@navBg",
X15: ":alpha<0<@panel",
X16: ":alpha<0.7<@panel",
X17: ":alpha<0.8<@bg",
cwBg: "#bcc0cc",
cwFg: "#5c5f77",
link: "#1e66f5",
warn: "#fe640b",
badge: "#1e66f5",
error: "#d20f39",
focus: ":alpha<0.3<@accent",
navBg: "@panel",
navFg: "@fg",
panel: ":lighten<3<@bg",
popup: ":lighten<3<@panel",
accent: "#8839ef",
header: ":alpha<0.7<@panel",
infoBg: "#ccd0da",
infoFg: "#6c6f85",
renote: "#1e66f5",
shadow: "rgba(0, 0, 0, 0.3)",
divider: "rgba(255, 255, 255, 0.1)",
hashtag: "#209fb5",
mention: "@accent",
modalBg: "rgba(0, 0, 0, 0.5)",
success: "#40a02b",
buttonBg: "rgba(255, 255, 255, 0.05)",
switchBg: "rgba(255, 255, 255, 0.15)",
acrylicBg: ":alpha<0.5<@bg",
cwHoverBg: "#acb0be",
indicator: "@accent",
mentionMe: "@mention",
messageBg: "@bg",
navActive: "@accent",
accentedBg: ":alpha<0.15<@accent",
codeNumber: "#40a02b",
codeString: "#fe640b",
fgOnAccent: "#eff1f5",
infoWarnBg: "#ccd0da",
infoWarnFg: "#5c5f77",
navHoverFg: ":lighten<17<@fg",
swutchOnBg: "@accentedBg",
swutchOnFg: "@accent",
codeBoolean: "@accent",
dateLabelFg: "@fg",
deckDivider: "#9ca0b0",
inputBorder: "rgba(255, 255, 255, 0.1)",
panelBorder: "solid 1px var(--divider)",
swutchOffBg: "rgba(255, 255, 255, 0.1)",
swutchOffFg: "@fg",
accentDarken: ":darken<10<@accent",
acrylicPanel: ":alpha<0.5<@panel",
navIndicator: "@indicator",
windowHeader: ":alpha<0.85<@panel",
accentLighten: ":lighten<10<@accent",
buttonHoverBg: "rgba(255, 255, 255, 0.1)",
driveFolderBg: ":alpha<0.3<@accent",
fgHighlighted: ":lighten<3<@fg",
fgTransparent: ":alpha<0.5<@fg",
panelHeaderBg: ":lighten<3<@panel",
panelHeaderFg: "@fg",
buttonGradateA: "@accent",
buttonGradateB: ":hue<20<@accent",
htmlThemeColor: "@bg",
panelHighlight: ":lighten<3<@panel",
listItemHoverBg: "rgba(255, 255, 255, 0.03)",
scrollbarHandle: "rgba(255, 255, 255, 0.2)",
inputBorderHover: "rgba(255, 255, 255, 0.2)",
wallpaperOverlay: "rgba(0, 0, 0, 0.5)",
fgTransparentWeak: ":alpha<0.75<@fg",
panelHeaderDivider: "rgba(0, 0, 0, 0)",
scrollbarHandleHover: "rgba(255, 255, 255, 0.4)",
},
author: "somebody ¯_(ツ)_/¯",
}

View File

@ -1,7 +1,7 @@
{
id: 'ac168876-f737-4074-a3fc-a370c732ef48',
name: 'Mi Cherry Light',
name: 'Cherry Light',
author: 'syuilo',
base: 'light',

View File

@ -1,7 +1,7 @@
{
id: '6ed80faa-74f0-42c2-98e4-a64d9e138eab',
name: 'Mi Coffee Light',
name: 'Coffee Light',
author: 'syuilo',
base: 'light',

View File

@ -1,7 +1,7 @@
{
id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96',
name: 'Mi Rainy Light',
name: 'Rainy Light',
author: 'syuilo',
base: 'light',

View File

@ -1,7 +1,7 @@
{
id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c',
name: 'Mi Sushi Light',
name: 'Sushi Light',
author: 'syuilo',
base: 'light',

View File

@ -1,7 +1,7 @@
{
id: 'e2c940b5-6e9a-4c03-b738-261c720c426d',
base: 'light',
name: 'Mi U0 Light',
name: 'U0 Light',
props: {
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',

View File

@ -1,7 +1,7 @@
{
id: '6128c2a9-5c54-43fe-a47d-17942356470b',
name: 'Mi Vivid Light',
name: 'Vivid Light',
author: 'syuilo',
base: 'light',

View File

@ -440,7 +440,7 @@ function more(ev: MouseEvent) {
color: var(--navActive);
}
&:hover,
&:hover,
&:focus-within,
&.active {
color: var(--accent);

View File

@ -26,6 +26,9 @@
"@jovikowi@calckey.social",
"@padraig@calckey.social",
"@pancakes@cats.city",
"@theresmiling@calckey.social",
"@AlderForrest@calckey.social",
"@kristian@calckey.social",
"Interkosmos Link"
]
}

View File

@ -24,7 +24,7 @@ importers:
version: 7.2.0
focus-trap-vue:
specifier: ^4.0.1
version: 4.0.1(focus-trap@7.2.0)(vue@3.2.45)
version: 4.0.1(focus-trap@7.2.0)(vue@3.2.47)
js-yaml:
specifier: 4.1.0
version: 4.1.0
@ -726,8 +726,8 @@ importers:
specifier: 4.19.1
version: 4.19.1
browser-image-resizer:
specifier: https://github.com/misskey-dev/browser-image-resizer.git
version: github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c
specifier: github:misskey-dev/browser-image-resizer
version: github.com/misskey-dev/browser-image-resizer/56f504427ad7f6500e141a6d9f3aee42023d7f3e
calckey-js:
specifier: workspace:*
version: link:../calckey-js
@ -1071,6 +1071,11 @@ packages:
resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==}
engines: {node: '>=6.9.0'}
/@babel/helper-string-parser@7.21.5:
resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==}
engines: {node: '>=6.9.0'}
dev: false
/@babel/helper-validator-identifier@7.19.1:
resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
engines: {node: '>=6.9.0'}
@ -1114,6 +1119,14 @@ packages:
'@babel/types': 7.21.4
dev: true
/@babel/parser@7.21.5:
resolution: {integrity: sha512-J+IxH2IsxV4HbnTrSWgMAQj0UEo61hDA4Ny8h8PCX0MLXiibqHbqIOVneqdocemSBc22VpBKxt4J6FQzy9HarQ==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.21.5
dev: false
/@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.4):
resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==}
peerDependencies:
@ -1300,6 +1313,15 @@ packages:
to-fast-properties: 2.0.0
dev: true
/@babel/types@7.21.5:
resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-string-parser': 7.21.5
'@babel/helper-validator-identifier': 7.19.1
to-fast-properties: 2.0.0
dev: false
/@bcoe/v8-coverage@0.2.3:
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
dev: true
@ -3809,12 +3831,30 @@ packages:
'@vue/shared': 3.2.45
estree-walker: 2.0.2
source-map: 0.6.1
dev: true
/@vue/compiler-core@3.2.47:
resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==}
dependencies:
'@babel/parser': 7.21.5
'@vue/shared': 3.2.47
estree-walker: 2.0.2
source-map: 0.6.1
dev: false
/@vue/compiler-dom@3.2.45:
resolution: {integrity: sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==}
dependencies:
'@vue/compiler-core': 3.2.45
'@vue/shared': 3.2.45
dev: true
/@vue/compiler-dom@3.2.47:
resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==}
dependencies:
'@vue/compiler-core': 3.2.47
'@vue/shared': 3.2.47
dev: false
/@vue/compiler-sfc@2.7.14:
resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
@ -3837,12 +3877,36 @@ packages:
magic-string: 0.25.9
postcss: 8.4.21
source-map: 0.6.1
dev: true
/@vue/compiler-sfc@3.2.47:
resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==}
dependencies:
'@babel/parser': 7.21.5
'@vue/compiler-core': 3.2.47
'@vue/compiler-dom': 3.2.47
'@vue/compiler-ssr': 3.2.47
'@vue/reactivity-transform': 3.2.47
'@vue/shared': 3.2.47
estree-walker: 2.0.2
magic-string: 0.25.9
postcss: 8.4.23
source-map: 0.6.1
dev: false
/@vue/compiler-ssr@3.2.45:
resolution: {integrity: sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==}
dependencies:
'@vue/compiler-dom': 3.2.45
'@vue/shared': 3.2.45
dev: true
/@vue/compiler-ssr@3.2.47:
resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==}
dependencies:
'@vue/compiler-dom': 3.2.47
'@vue/shared': 3.2.47
dev: false
/@vue/reactivity-transform@3.2.45:
resolution: {integrity: sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==}
@ -3852,17 +3916,43 @@ packages:
'@vue/shared': 3.2.45
estree-walker: 2.0.2
magic-string: 0.25.9
dev: true
/@vue/reactivity-transform@3.2.47:
resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==}
dependencies:
'@babel/parser': 7.21.5
'@vue/compiler-core': 3.2.47
'@vue/shared': 3.2.47
estree-walker: 2.0.2
magic-string: 0.25.9
dev: false
/@vue/reactivity@3.2.45:
resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==}
dependencies:
'@vue/shared': 3.2.45
dev: true
/@vue/reactivity@3.2.47:
resolution: {integrity: sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==}
dependencies:
'@vue/shared': 3.2.47
dev: false
/@vue/runtime-core@3.2.45:
resolution: {integrity: sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A==}
dependencies:
'@vue/reactivity': 3.2.45
'@vue/shared': 3.2.45
dev: true
/@vue/runtime-core@3.2.47:
resolution: {integrity: sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==}
dependencies:
'@vue/reactivity': 3.2.47
'@vue/shared': 3.2.47
dev: false
/@vue/runtime-dom@3.2.45:
resolution: {integrity: sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA==}
@ -3870,6 +3960,15 @@ packages:
'@vue/runtime-core': 3.2.45
'@vue/shared': 3.2.45
csstype: 2.6.21
dev: true
/@vue/runtime-dom@3.2.47:
resolution: {integrity: sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==}
dependencies:
'@vue/runtime-core': 3.2.47
'@vue/shared': 3.2.47
csstype: 2.6.21
dev: false
/@vue/server-renderer@3.2.45(vue@3.2.45):
resolution: {integrity: sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g==}
@ -3879,9 +3978,25 @@ packages:
'@vue/compiler-ssr': 3.2.45
'@vue/shared': 3.2.45
vue: 3.2.45
dev: true
/@vue/server-renderer@3.2.47(vue@3.2.47):
resolution: {integrity: sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==}
peerDependencies:
vue: 3.2.47
dependencies:
'@vue/compiler-ssr': 3.2.47
'@vue/shared': 3.2.47
vue: 3.2.47
dev: false
/@vue/shared@3.2.45:
resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
dev: true
/@vue/shared@3.2.47:
resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==}
dev: false
/@webassemblyjs/ast@1.11.1:
resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==}
@ -7439,14 +7554,14 @@ packages:
readable-stream: 2.3.7
dev: true
/focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.45):
/focus-trap-vue@4.0.1(focus-trap@7.2.0)(vue@3.2.47):
resolution: {integrity: sha512-2iqOeoSvgq7Um6aL+255a/wXPskj6waLq2oKCa4gOnMORPo15JX7wN6J5bl1SMhMlTlkHXGSrQ9uJPJLPZDl5w==}
peerDependencies:
focus-trap: ^7.0.0
vue: ^3.0.0
dependencies:
focus-trap: 7.2.0
vue: 3.2.45
vue: 3.2.47
dev: false
/focus-trap@7.2.0:
@ -7606,7 +7721,7 @@ packages:
resolution: {integrity: sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==}
engines: {node: '>= 0.10'}
dependencies:
graceful-fs: 4.2.11
graceful-fs: 4.2.10
through2: 2.0.5
dev: true
@ -9695,7 +9810,7 @@ packages:
/jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
optionalDependencies:
graceful-fs: 4.2.11
graceful-fs: 4.2.10
/jsonfile@5.0.0:
resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==}
@ -9709,7 +9824,7 @@ packages:
dependencies:
universalify: 2.0.0
optionalDependencies:
graceful-fs: 4.2.11
graceful-fs: 4.2.10
dev: true
/jsonld@6.0.0:
@ -10832,6 +10947,12 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/nanoid@3.3.6:
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
dev: false
/nanomatch@1.2.13:
resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==}
engines: {node: '>=0.10.0'}
@ -11953,6 +12074,15 @@ packages:
picocolors: 1.0.0
source-map-js: 1.0.2
/postcss@8.4.23:
resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.6
picocolors: 1.0.0
source-map-js: 1.0.2
dev: false
/postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
@ -14675,7 +14805,7 @@ packages:
dependencies:
append-buffer: 1.0.2
convert-source-map: 1.9.0
graceful-fs: 4.2.11
graceful-fs: 4.2.10
normalize-path: 2.1.1
now-and-later: 2.0.1
remove-bom-buffer: 3.0.0
@ -14788,6 +14918,17 @@ packages:
'@vue/runtime-dom': 3.2.45
'@vue/server-renderer': 3.2.45(vue@3.2.45)
'@vue/shared': 3.2.45
dev: true
/vue@3.2.47:
resolution: {integrity: sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==}
dependencies:
'@vue/compiler-dom': 3.2.47
'@vue/compiler-sfc': 3.2.47
'@vue/runtime-dom': 3.2.47
'@vue/server-renderer': 3.2.47(vue@3.2.47)
'@vue/shared': 3.2.47
dev: false
/vuedraggable@4.1.0(vue@3.2.45):
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
@ -15402,10 +15543,10 @@ packages:
resolution: {integrity: sha512-+MLeeUcLTlnzVo5xDn9+LVN9oX4esvgZ7qfZczBN+YVUvZBafIrPPVyG2WdjMWU2Qkb2ZAh2M8lpqf1wIoGqJQ==}
dev: false
github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c:
resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/0380d12c8e736788ea7f4e6e985175521ea7b23c}
github.com/misskey-dev/browser-image-resizer/56f504427ad7f6500e141a6d9f3aee42023d7f3e:
resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/56f504427ad7f6500e141a6d9f3aee42023d7f3e}
name: browser-image-resizer
version: 2.2.1-misskey.3
version: 2.2.1-misskey.4
dev: true
github.com/sampotts/plyr/d434c9af16e641400aaee93188594208d88f2658: