From 44c6bc7dd0c796a2c28136ab5574c39a8941c20c Mon Sep 17 00:00:00 2001 From: cutestnekoaqua Date: Sun, 5 Feb 2023 12:38:40 +0100 Subject: [PATCH 001/146] fixing a git merge error. --- packages/backend/src/misc/reaction-lib.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts index 5fd47aab7..babebfb62 100644 --- a/packages/backend/src/misc/reaction-lib.ts +++ b/packages/backend/src/misc/reaction-lib.ts @@ -71,6 +71,12 @@ export async function toDbReaction( if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; // Convert old heart to new if (reaction === "♥️") return "❤️"; + // Allow unicode reactions + const match = emojiRegex.exec(reaction); + if (match) { + const unicode = match[0]; + return unicode; + } const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); if (custom) { From 2ae58c1c629dcd6d2d0e8f8f48d5dc80e26923fb Mon Sep 17 00:00:00 2001 From: cutestnekoaqua Date: Sun, 5 Feb 2023 12:39:57 +0100 Subject: [PATCH 002/146] fix: docker tags --- .woodpecker/dockerHubTag.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.woodpecker/dockerHubTag.yml b/.woodpecker/dockerHubTag.yml index 4294e1a58..5543ae234 100644 --- a/.woodpecker/dockerHubTag.yml +++ b/.woodpecker/dockerHubTag.yml @@ -16,6 +16,3 @@ pipeline: # Push new version when version tag is created event: tag tag: v* - -depends_on: - - prSecurityCheck From 3ecc69185c9f01334d8c0047f37eed78d7f740eb Mon Sep 17 00:00:00 2001 From: Cleo Date: Thu, 9 Feb 2023 22:59:05 +0000 Subject: [PATCH 003/146] Beta Docker build (#9589) Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9589 --- .woodpecker/dockerHubRelease.yml | 2 +- .woodpecker/dockerHubReleaseCandidate.yml | 15 +++++++++++++++ .woodpecker/testDocker.yml | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .woodpecker/dockerHubReleaseCandidate.yml diff --git a/.woodpecker/dockerHubRelease.yml b/.woodpecker/dockerHubRelease.yml index e995b3fd0..bcb6df490 100644 --- a/.woodpecker/dockerHubRelease.yml +++ b/.woodpecker/dockerHubRelease.yml @@ -12,4 +12,4 @@ pipeline: # Secret 'docker_password' needs to be set in the CI settings from_secret: docker_password -branch: main +branches: main diff --git a/.woodpecker/dockerHubReleaseCandidate.yml b/.woodpecker/dockerHubReleaseCandidate.yml new file mode 100644 index 000000000..9b1a84316 --- /dev/null +++ b/.woodpecker/dockerHubReleaseCandidate.yml @@ -0,0 +1,15 @@ +pipeline: + publish-docker-latest: + image: plugins/kaniko + settings: + repo: thatonecalculator/calckey + tags: rc + dockerfile: Dockerfile + username: + # Secret 'docker_username' needs to be set in the CI settings + from_secret: docker_username + password: + # Secret 'docker_password' needs to be set in the CI settings + from_secret: docker_password + +branches: beta diff --git a/.woodpecker/testDocker.yml b/.woodpecker/testDocker.yml index aef58c9ad..e3fdf0eb9 100644 --- a/.woodpecker/testDocker.yml +++ b/.woodpecker/testDocker.yml @@ -8,4 +8,4 @@ pipeline: no_push: true branches: - include: [ main, develop ] + include: [ main, develop, beta ] From 69089016d551c5a39a89a1ddc7c6a057bff1daab Mon Sep 17 00:00:00 2001 From: Cleo Date: Fri, 10 Feb 2023 07:00:47 +0000 Subject: [PATCH 004/146] Meow --- .woodpecker/dockerHubReleaseCandidate.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.woodpecker/dockerHubReleaseCandidate.yml b/.woodpecker/dockerHubReleaseCandidate.yml index 9b1a84316..48bd39525 100644 --- a/.woodpecker/dockerHubReleaseCandidate.yml +++ b/.woodpecker/dockerHubReleaseCandidate.yml @@ -11,5 +11,4 @@ pipeline: password: # Secret 'docker_password' needs to be set in the CI settings from_secret: docker_password - branches: beta From a6e1669d608a1518be6e5b8c48b97770dcd6c33e Mon Sep 17 00:00:00 2001 From: Cleo Date: Fri, 10 Feb 2023 07:10:42 +0000 Subject: [PATCH 005/146] Nya --- cypress/e2e/widgets.cy.js | 1 - 1 file changed, 1 deletion(-) diff --git a/cypress/e2e/widgets.cy.js b/cypress/e2e/widgets.cy.js index db35a60b5..9eea010bd 100644 --- a/cypress/e2e/widgets.cy.js +++ b/cypress/e2e/widgets.cy.js @@ -2,7 +2,6 @@ describe('After user signed in', () => { beforeEach(() => { cy.resetState(); cy.viewport('macbook-16'); - // インスタンス初期セットアップ cy.registerUser('admin', 'pass', true); From 234315dacd415567e0c7381b601bcfb94857c3d9 Mon Sep 17 00:00:00 2001 From: Cleo Date: Fri, 10 Feb 2023 10:04:32 +0000 Subject: [PATCH 006/146] Fix build --- packages/backend/src/misc/reaction-lib.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts index 41036489a..babebfb62 100644 --- a/packages/backend/src/misc/reaction-lib.ts +++ b/packages/backend/src/misc/reaction-lib.ts @@ -78,13 +78,6 @@ export async function toDbReaction( return unicode; } - // Allow unicode reactions - const match = emojiRegex.exec(reaction); - if (match) { - const unicode = match[0]; - return unicode; - } - const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/); if (custom) { const name = custom[1]; From 424fdd4f267d1f8630588a2d540e3102f0c3ba45 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Sat, 11 Feb 2023 06:02:08 +0000 Subject: [PATCH 007/146] Upload files to 'packages/backend/assets' --- packages/backend/assets/favicon.svg | Bin 0 -> 4285 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/backend/assets/favicon.svg diff --git a/packages/backend/assets/favicon.svg b/packages/backend/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..a77c6e22bbd6a1a0142e667bf938e57ee5aa97ba GIT binary patch literal 4285 zcmb7I+m74F5q-~BG-L$pMZ{*`FN$O!fs@6)B*3nLJO(t`nqfu~1(IXU%-82sla%CL zC)sFVh}DA>M!)oCb~@n!Q8*LCwUz=!U+uhSm;6F;S~{P^i(xl9&tC!+5yj!xU>+80#C|7t1ley$!6%AxCbt9SeSS+UPu-=uyS1ycAdqaA2!$JgCTaAof_ zF+6q`@-;kGO?TO?)ZHA7-|d61h2>2?{xQ{0ssG$nJ=A$W+oa)X38Rtp2chDihn{r2 zuMR`H^Yzws$E;>6ycgzfrUp{V3GSka_kq>OLkh+1{o+e|LI%a9ZN^7TJIehU?>zaK z+K0z+w~87O{c;kAePatj_UpCB>ZLulZwcw#d#a!N9&{In>NSCxsbb4a>M@N~Q;k)o zaxtXKjDvINZ}Zc~ztU3rZ0maSkFI}WG+uq~o)PrCG*3761{HLy#t-cgEKy1P0Tr<= zZxgd8_`=xudQRC_pQie|>iS--O?`w$&Z+!)Y!8PY$@3~lpJSRFQFpnHZOb{%Dei4~ zm&UeSvdsA(+Ec3fANs0kfzX7)>te3h!#we+uTBG{wp;bxSdHm@6yGl(4P$p^-F`nL zz+rz6QDLB1losoH-vn?)y%H0U`7pj7&=QNHsrn}rFv_E7PZUZ)EAq|_d#9ZBPW*Aj zh@k(r79`BMZ@-Eq{wf$Py$SnUF#fj(<89;B{}If#{2JQ~ca!!*MuHxsnZn*qFOmm< zv1sH?Xp^j2v9kj4?X-4E?MhW{6OcIW5d{Y zPpKfR|JAj4|MrwE(VE9mIjKLODL|Ukt8#eunkV8rSK}kY@R1cp@}Q+!d(UNLH7hg^ zF6gyj1x9>OYst0sffbRXSIM>E!sn>Dmu{`Nx5~mz9$h;woWk1KoVGB_h1GE_xG>R2 zhDC4-OKKBX$R5ZPQsPAo2XxbjN zaX45RE7!&J%_KKcd!(kgQwk5USVRYOqQ{I1N@1N;_G)2J z*^s>{=N-jll!r2CnT9>@%AR758IB#^v+$-7WcKZ@=H#7;yiqv_gB%7GC_9{>S@uXf z^9hh(MnBJcX&ob!jKRqZN0A5bkpFDIF%S+cEJ($Wa1-CgSopNu0yE;@I7irs0Mb}7 z^K(UfJqZM1Ya7hk<48E76TnmC9wGt0P(>hdP*_ZtDzn;h zAlB3is4s3zX?%9}p05ZaXxk1z;?qzP4Ik%3CmoY!}zKrxVpv=o-Q3x_lze4<`s zJxSOpq6zb$dM*@P=2C(BBNHV?!Ch@fRZN^K$EAfYa21^;(orrx672A&EpSJWN`QTj zTmYm-9m|ZKGbDLje2kX)*yxNfSda01?qn-;GOr+B+$5D2=$Kq92`x(Q(Gp4d!we!4bCQyVFKa>F@{ky4juS_Asnglt`N|< zA(+gU$~}yPkbf{Zazib3VwWuNhF*Z`BgW7JbCBo=xloA+8kn6!*yx`$U~LGQQ`8|X zK?{Hg$;q5L$zfvoiUmLd6nQQBGcq?3=pz6T!7r5@@;}gv2)`=nrG@@YiNGlmKyS+5 z8CD207cGhybw^4fo6Zd3Drm|Pg2qOYAl(S=*5!VLb7?XvuxX$poC-}ul)eYDsD#;4 zJPME3A6`y68la3|HA(=L=QPMe3xiyc2!JI-(O&5d1yKPbycQw3#qwMS6fN+coDv); zW9XZFTi|h93FnXq+_{inm)R}(!#j@-o$CD-M7H1l=bhx#o!(O4{inOfd?T5E3DK_? z+&bv?AvLxv|6zcePNK{Huv^vBXQ+qU!~BS$>5pdeg2lYMd6f^#{Xai!$==d$k`Mm| D`KnNH literal 0 HcmV?d00001 From a1592dad907736c3afe1f8d44f9a9188d0348da3 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Sat, 11 Feb 2023 06:10:15 +0000 Subject: [PATCH 008/146] Delete 'packages/backend/assets/favicon.svg' --- packages/backend/assets/favicon.svg | Bin 4285 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/backend/assets/favicon.svg diff --git a/packages/backend/assets/favicon.svg b/packages/backend/assets/favicon.svg deleted file mode 100644 index a77c6e22bbd6a1a0142e667bf938e57ee5aa97ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4285 zcmb7I+m74F5q-~BG-L$pMZ{*`FN$O!fs@6)B*3nLJO(t`nqfu~1(IXU%-82sla%CL zC)sFVh}DA>M!)oCb~@n!Q8*LCwUz=!U+uhSm;6F;S~{P^i(xl9&tC!+5yj!xU>+80#C|7t1ley$!6%AxCbt9SeSS+UPu-=uyS1ycAdqaA2!$JgCTaAof_ zF+6q`@-;kGO?TO?)ZHA7-|d61h2>2?{xQ{0ssG$nJ=A$W+oa)X38Rtp2chDihn{r2 zuMR`H^Yzws$E;>6ycgzfrUp{V3GSka_kq>OLkh+1{o+e|LI%a9ZN^7TJIehU?>zaK z+K0z+w~87O{c;kAePatj_UpCB>ZLulZwcw#d#a!N9&{In>NSCxsbb4a>M@N~Q;k)o zaxtXKjDvINZ}Zc~ztU3rZ0maSkFI}WG+uq~o)PrCG*3761{HLy#t-cgEKy1P0Tr<= zZxgd8_`=xudQRC_pQie|>iS--O?`w$&Z+!)Y!8PY$@3~lpJSRFQFpnHZOb{%Dei4~ zm&UeSvdsA(+Ec3fANs0kfzX7)>te3h!#we+uTBG{wp;bxSdHm@6yGl(4P$p^-F`nL zz+rz6QDLB1losoH-vn?)y%H0U`7pj7&=QNHsrn}rFv_E7PZUZ)EAq|_d#9ZBPW*Aj zh@k(r79`BMZ@-Eq{wf$Py$SnUF#fj(<89;B{}If#{2JQ~ca!!*MuHxsnZn*qFOmm< zv1sH?Xp^j2v9kj4?X-4E?MhW{6OcIW5d{Y zPpKfR|JAj4|MrwE(VE9mIjKLODL|Ukt8#eunkV8rSK}kY@R1cp@}Q+!d(UNLH7hg^ zF6gyj1x9>OYst0sffbRXSIM>E!sn>Dmu{`Nx5~mz9$h;woWk1KoVGB_h1GE_xG>R2 zhDC4-OKKBX$R5ZPQsPAo2XxbjN zaX45RE7!&J%_KKcd!(kgQwk5USVRYOqQ{I1N@1N;_G)2J z*^s>{=N-jll!r2CnT9>@%AR758IB#^v+$-7WcKZ@=H#7;yiqv_gB%7GC_9{>S@uXf z^9hh(MnBJcX&ob!jKRqZN0A5bkpFDIF%S+cEJ($Wa1-CgSopNu0yE;@I7irs0Mb}7 z^K(UfJqZM1Ya7hk<48E76TnmC9wGt0P(>hdP*_ZtDzn;h zAlB3is4s3zX?%9}p05ZaXxk1z;?qzP4Ik%3CmoY!}zKrxVpv=o-Q3x_lze4<`s zJxSOpq6zb$dM*@P=2C(BBNHV?!Ch@fRZN^K$EAfYa21^;(orrx672A&EpSJWN`QTj zTmYm-9m|ZKGbDLje2kX)*yxNfSda01?qn-;GOr+B+$5D2=$Kq92`x(Q(Gp4d!we!4bCQyVFKa>F@{ky4juS_Asnglt`N|< zA(+gU$~}yPkbf{Zazib3VwWuNhF*Z`BgW7JbCBo=xloA+8kn6!*yx`$U~LGQQ`8|X zK?{Hg$;q5L$zfvoiUmLd6nQQBGcq?3=pz6T!7r5@@;}gv2)`=nrG@@YiNGlmKyS+5 z8CD207cGhybw^4fo6Zd3Drm|Pg2qOYAl(S=*5!VLb7?XvuxX$poC-}ul)eYDsD#;4 zJPME3A6`y68la3|HA(=L=QPMe3xiyc2!JI-(O&5d1yKPbycQw3#qwMS6fN+coDv); zW9XZFTi|h93FnXq+_{inm)R}(!#j@-o$CD-M7H1l=bhx#o!(O4{inOfd?T5E3DK_? z+&bv?AvLxv|6zcePNK{Huv^vBXQ+qU!~BS$>5pdeg2lYMd6f^#{Xai!$==d$k`Mm| D`KnNH From 00bcaeaa47ef48d1fd13f190b21a6ceda619235a Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Mon, 13 Feb 2023 03:35:21 +0000 Subject: [PATCH 009/146] Update 'CALCKEY.md' --- CALCKEY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CALCKEY.md b/CALCKEY.md index 015232f70..1186277e2 100644 --- a/CALCKEY.md +++ b/CALCKEY.md @@ -108,6 +108,7 @@ - Allows custom emoji - Fix lint errors - Use Rome instead of ESLint +- Keyboard accesibility - MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1) - [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996) - [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056) From 94b9db451ec9121e1494adbc7e0ca92bfa2f2231 Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Mon, 20 Feb 2023 12:20:47 -0800 Subject: [PATCH 010/146] update readme --- README.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index bc4141a8e..fd6b14c25 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ If you have access to a server that supports one of the sources below, I recomme ### 🐋 Docker -[How to run Calckey with Docker](./docker-README.md). +[How to run Calckey with Docker](./docs/docker.md). ## 🧑‍💻 Dependencies @@ -97,9 +97,10 @@ If you have access to a server that supports one of the sources below, I recomme ```sh git clone --depth 1 https://codeberg.org/calckey/calckey.git cd calckey/ -# git checkout main # if you want only stable versions ``` +By default, you're on the development branch. Run `git checkout beta` or `git checkout main` to switch to the Beta/Main branches. + ## 📩 Install dependencies ```sh @@ -123,6 +124,8 @@ psql postgres -c "create database calckey with encoding = 'UTF8';" - To add custom CSS for all users, edit `./custom/assets/instance.css`. - To add static assets (such as images for the splash screen), place them in the `./custom/assets/` directory. They'll then be available on `https://yourinstance.tld/static-assets/filename.ext`. - To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`) +- To add custom error images, place them in the `./custom/assets/badges` directory, replacing the files already there. +- To add custom sounds, place only mp3 files in the `./custom/assets/sounds` directory. - To update custom assets without rebuilding, just run `pnpm run gulp`. ## 🧑‍🔬 Configuring a new instance @@ -133,12 +136,7 @@ psql postgres -c "create database calckey with encoding = 'UTF8';" ## 🚚 Migrating from Misskey to Calckey -> ⚠️ Because of their changes, migrating from Foundkey is not supported. - -```sh -cp ../misskey/.config/default.yml ./.config/default.yml # replace `../misskey/` with misskey path, add `docker.env` if you use Docker -cp -r ../misskey/files . -``` +For migrating from Misskey v13, Misskey v12, and Foundkey, read [this document](https://codeberg.org/calckey/calckey/src/branch/develop/docs/migrate.md). ## 🍀 NGINX From c48c42850c6b08eb4345815e43087f2261ddeeec Mon Sep 17 00:00:00 2001 From: Freeplay Date: Thu, 23 Feb 2023 20:50:58 -0500 Subject: [PATCH 011/146] fix not being able to click around there are new posts button --- packages/client/src/pages/timeline.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue index 3646e8d86..9357050c7 100644 --- a/packages/client/src/pages/timeline.vue +++ b/packages/client/src/pages/timeline.vue @@ -315,12 +315,14 @@ onMounted(() => { top: calc(var(--stickyTop, 0px) + 16px); z-index: 1000; width: 100%; + pointer-events: none; > button { display: block; margin: var(--margin) auto 0 auto; padding: 8px 16px; border-radius: 32px; + pointer-events: all; } } From 2f419ad7278fd23d9afe0261e569c9f251af4e00 Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Fri, 17 Feb 2023 00:01:22 -0800 Subject: [PATCH 012/146] perf: :zap: emoji lib performance fix --- packages/backend/src/misc/reaction-lib.ts | 87 +++++++++++------------ 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/packages/backend/src/misc/reaction-lib.ts b/packages/backend/src/misc/reaction-lib.ts index a61c0a119..7d78904bb 100644 --- a/packages/backend/src/misc/reaction-lib.ts +++ b/packages/backend/src/misc/reaction-lib.ts @@ -4,73 +4,68 @@ import { Emojis } from "@/models/index.js"; import { toPunyNullable } from "./convert-host.js"; import { IsNull } from "typeorm"; -const legacies: Record = { - like: "👍", - love: "❤️", // ここに記述する場合は異体字セレクタを入れない <- not that good because modern browsers just display it as the red heart so just convert it to it to not end up with two seperate reactions of "the same emoji" for the user - laugh: "😆", - hmm: "🤔", - surprise: "😮", - congrats: "🎉", - angry: "💢", - confused: "😥", - rip: "😇", - pudding: "🍮", - star: "⭐", -}; +const legacies = new Map([ + ['like', '👍'], + ['love', '❤️'], + ['laugh', '😆'], + ['hmm', '🤔'], + ['surprise', '😮'], + ['congrats', '🎉'], + ['angry', '💢'], + ['confused', '😥'], + ['rip', '😇'], + ['pudding', '🍮'], + ['star', '⭐'], +]); -export async function getFallbackReaction(): Promise { +export async function getFallbackReaction() { const meta = await fetchMeta(); return meta.defaultReaction; } export function convertLegacyReactions(reactions: Record) { - const _reactions = {} as Record; + const _reactions = new Map(); + const decodedReactions = new Map(); - for (const reaction of Object.keys(reactions)) { + for (const reaction in reactions) { if (reactions[reaction] <= 0) continue; - if (Object.keys(legacies).includes(reaction)) { - if (_reactions[legacies[reaction]]) { - _reactions[legacies[reaction]] += reactions[reaction]; - } else { - _reactions[legacies[reaction]] = reactions[reaction]; - } - } else if (reaction === "♥️") { - if (_reactions["❤️"]) { - _reactions["❤️"] += reactions[reaction]; - } else { - _reactions["❤️"] = reactions[reaction]; - } + let decodedReaction; + if (decodedReactions.has(reaction)) { + decodedReaction = decodedReactions.get(reaction); } else { - if (_reactions[reaction]) { - _reactions[reaction] += reactions[reaction]; - } else { - _reactions[reaction] = reactions[reaction]; - } + decodedReaction = decodeReaction(reaction); + decodedReactions.set(reaction, decodedReaction); + } + + let emoji = legacies.get(decodedReaction.reaction); + if (emoji) { + _reactions.set(emoji, (_reactions.get(emoji) || 0) + reactions[reaction]); + } else { + _reactions.set(reaction, (_reactions.get(reaction) || 0) + reactions[reaction]); } } - const _reactions2 = {} as Record; - - for (const reaction of Object.keys(_reactions)) { - _reactions2[decodeReaction(reaction).reaction] = _reactions[reaction]; + const _reactions2 = new Map(); + for (const [reaction, count] of _reactions) { + const decodedReaction = decodedReactions.get(reaction); + _reactions2.set(decodedReaction.reaction, count); } - return _reactions2; + return Object.fromEntries(_reactions2); } export async function toDbReaction( reaction?: string | null, reacterHost?: string | null, ): Promise { - if (reaction == null) return await getFallbackReaction(); + if (!reaction) return await getFallbackReaction(); reacterHost = toPunyNullable(reacterHost); // Convert string-type reactions to unicode - if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; - // Convert old heart to new - if (reaction === "♥️") return "❤️"; + const emoji = legacies.get(reaction) || (reaction === "♥️" ? "❤️" : null); + if (emoji) return emoji; // Allow unicode reactions const match = emojiRegex.exec(reaction); @@ -83,7 +78,7 @@ export async function toDbReaction( if (custom) { const name = custom[1]; const emoji = await Emojis.findOneBy({ - host: reacterHost ?? IsNull(), + host: reacterHost || IsNull(), name, }); @@ -132,7 +127,7 @@ export function decodeReaction(str: string): DecodedReaction { } export function convertLegacyReaction(reaction: string): string { - reaction = decodeReaction(reaction).reaction; - if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; - return reaction; + const decoded = decodeReaction(reaction).reaction; + if (legacies.has(decoded)) return legacies.get(decoded)!; + return decoded; } From 456eb4b62746c62cdf505872cd1b7a501dc0a9c9 Mon Sep 17 00:00:00 2001 From: Free Date: Mon, 13 Feb 2023 21:14:06 +0000 Subject: [PATCH 013/146] New MkPageHeader --- .../src/components/global/MkPageHeader.vue | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/packages/client/src/components/global/MkPageHeader.vue b/packages/client/src/components/global/MkPageHeader.vue index 464a965b3..5eaa5e4fe 100644 --- a/packages/client/src/components/global/MkPageHeader.vue +++ b/packages/client/src/components/global/MkPageHeader.vue @@ -1,35 +1,35 @@ diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue index 115211ede..d6bf3b37b 100644 --- a/packages/client/src/components/MkPostForm.vue +++ b/packages/client/src/components/MkPostForm.vue @@ -3,7 +3,7 @@ v-size="{ max: [310, 500] }" class="gafaadew" :class="{ modal, _popup: modal }" - :aria-label="i18n.ts.blocks.post" + :aria-label="i18n.ts._pages.blocks.post" @dragover.stop="onDragover" @dragenter="onDragenter" @dragleave="onDragleave" diff --git a/packages/client/src/components/global/MkPageHeader.vue b/packages/client/src/components/global/MkPageHeader.vue index 67747e620..9d8676795 100644 --- a/packages/client/src/components/global/MkPageHeader.vue +++ b/packages/client/src/components/global/MkPageHeader.vue @@ -7,12 +7,17 @@ :style="{ background: bg }" @click="onClick" > - + > + +
{ display: none; } - > .button { + > .button/*, @at-root .backButton*/ { /* I don't know how to get this to work */ display: flex; align-items: center; justify-content: center; diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue index 67e940fa1..1e9384bd1 100644 --- a/packages/client/src/pages/settings/theme.vue +++ b/packages/client/src/pages/settings/theme.vue @@ -296,7 +296,6 @@ definePageMetadata({ > .toggleWrapper { display: inline-block; text-align: left; - overflow: clip; padding: 0 100px; vertical-align: bottom; @@ -304,6 +303,10 @@ definePageMetadata({ position: absolute; left: -99em; } + + &:focus-within > .toggle { + outline: auto; + } } .toggle { @@ -506,7 +509,6 @@ definePageMetadata({ } } } - > .sync { padding: 14px 16px; border-top: solid 0.5px var(--divider); From 1f6b1a7c322bdf24d4db9a600023bc69a6329244 Mon Sep 17 00:00:00 2001 From: Lily Cohen Date: Wed, 10 May 2023 16:07:45 -0700 Subject: [PATCH 104/146] adding calckey helm chart --- .gitignore | 1 + chart/.helmignore | 23 ++ chart/Chart.yaml | 39 ++- chart/README.md | 83 +++++++ chart/files/default.yml | 162 ------------ chart/templates/ConfigMap.yml | 8 - chart/templates/Deployment.yml | 47 ---- chart/templates/NOTES.txt | 22 ++ chart/templates/Service.yml | 14 -- chart/templates/_helpers.tpl | 276 ++++++++++++++++++++- chart/templates/deployment.yaml | 78 ++++++ chart/templates/hpa.yaml | 28 +++ chart/templates/ingress.yaml | 61 +++++ chart/templates/secret-config.yaml | 9 + chart/templates/service.yaml | 15 ++ chart/templates/serviceaccount.yaml | 12 + chart/templates/tests/test-connection.yaml | 15 ++ chart/values.yaml | 158 ++++++++++++ chart/values.yml | 3 - kubernetes-README.md | 45 ++++ 20 files changed, 853 insertions(+), 246 deletions(-) create mode 100644 chart/.helmignore create mode 100644 chart/README.md delete mode 100644 chart/files/default.yml delete mode 100644 chart/templates/ConfigMap.yml delete mode 100644 chart/templates/Deployment.yml create mode 100644 chart/templates/NOTES.txt delete mode 100644 chart/templates/Service.yml create mode 100644 chart/templates/deployment.yaml create mode 100644 chart/templates/hpa.yaml create mode 100644 chart/templates/ingress.yaml create mode 100644 chart/templates/secret-config.yaml create mode 100644 chart/templates/service.yaml create mode 100644 chart/templates/serviceaccount.yaml create mode 100644 chart/templates/tests/test-connection.yaml create mode 100644 chart/values.yaml delete mode 100644 chart/values.yml create mode 100644 kubernetes-README.md diff --git a/.gitignore b/.gitignore index 135bf9660..2613bba00 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ coverage /.config/* !/.config/example.yml !/.config/docker_example.env +!/.config/helm_values_example.yml #docker dev config /dev/docker-compose.yml diff --git a/chart/.helmignore b/chart/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 8f31cf7fb..dfd476dad 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -1,3 +1,38 @@ apiVersion: v2 -name: misskey -version: 0.0.0 +name: calckey +description: A fun, new, open way to experience social media https://calckey.org + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "rc" + +dependencies: + - name: elasticsearch + version: 19.0.1 + repository: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami + condition: elasticsearch.enabled + - name: postgresql + version: 11.1.3 + repository: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami + condition: postgresql.enabled + - name: redis + version: 16.13.2 + repository: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami + condition: redis.enabled diff --git a/chart/README.md b/chart/README.md new file mode 100644 index 000000000..a04b3d29a --- /dev/null +++ b/chart/README.md @@ -0,0 +1,83 @@ +# calckey + +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: rc](https://img.shields.io/badge/AppVersion-rc-informational?style=flat-square) + +A fun, new, open way to experience social media https://calckey.org + +## Requirements + +| Repository | Name | Version | +|------------|------|---------| +| https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami | elasticsearch | 19.0.1 | +| https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami | postgresql | 11.1.3 | +| https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami | redis | 16.13.2 | + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | | +| autoscaling.enabled | bool | `false` | | +| autoscaling.maxReplicas | int | `100` | | +| autoscaling.minReplicas | int | `1` | | +| autoscaling.targetCPUUtilizationPercentage | int | `80` | | +| calckey.allowedPrivateNetworks | list | `[]` | If you want to allow calckey to connect to private ips, enter the cidrs here. | +| calckey.domain | string | `"calckey.local"` | | +| calckey.isManagedHosting | bool | `true` | | +| calckey.objectStorage.access_key | string | `""` | | +| calckey.objectStorage.access_secret | string | `""` | | +| calckey.objectStorage.baseUrl | string | `""` | | +| calckey.objectStorage.bucket | string | `""` | | +| calckey.objectStorage.endpoint | string | `""` | | +| calckey.objectStorage.managed | bool | `true` | | +| calckey.objectStorage.prefix | string | `"files"` | | +| calckey.objectStorage.region | string | `""` | | +| calckey.reservedUsernames[0] | string | `"root"` | | +| calckey.reservedUsernames[1] | string | `"admin"` | | +| calckey.reservedUsernames[2] | string | `"administrator"` | | +| calckey.reservedUsernames[3] | string | `"me"` | | +| calckey.reservedUsernames[4] | string | `"system"` | | +| calckey.smtp.from_address | string | `"notifications@example.com"` | | +| calckey.smtp.login | string | `""` | | +| calckey.smtp.managed | bool | `true` | | +| calckey.smtp.password | string | `""` | | +| calckey.smtp.port | int | `587` | | +| calckey.smtp.server | string | `"smtp.mailgun.org"` | | +| calckey.smtp.useImplicitSslTls | bool | `false` | | +| elasticsearch | object | `{"auth":null,"enabled":false,"hostname":"","port":9200,"ssl":false}` | https://github.com/bitnami/charts/tree/master/bitnami/elasticsearch#parameters | +| fullnameOverride | string | `""` | | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.repository | string | `"docker.io/thatonecalculator/calckey"` | | +| image.tag | string | `""` | | +| imagePullSecrets | list | `[]` | | +| ingress.annotations | object | `{}` | | +| ingress.className | string | `""` | | +| ingress.enabled | bool | `false` | | +| ingress.hosts[0].host | string | `"chart-example.local"` | | +| ingress.hosts[0].paths[0].path | string | `"/"` | | +| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | | +| ingress.tls | list | `[]` | | +| nameOverride | string | `""` | | +| nodeSelector | object | `{}` | | +| podAnnotations | object | `{}` | | +| podSecurityContext | object | `{}` | | +| postgresql.auth.database | string | `"calckey_production"` | | +| postgresql.auth.password | string | `""` | | +| postgresql.auth.username | string | `"calckey"` | | +| postgresql.enabled | bool | `true` | disable if you want to use an existing db; in which case the values below must match those of that external postgres instance | +| redis.auth.password | string | `""` | you must set a password; the password generated by the redis chart will be rotated on each upgrade: | +| redis.enabled | bool | `true` | | +| redis.hostname | string | `""` | | +| redis.port | int | `6379` | | +| replicaCount | int | `1` | | +| resources | object | `{}` | | +| securityContext | object | `{}` | | +| service.port | int | `80` | | +| service.type | string | `"ClusterIP"` | | +| serviceAccount.annotations | object | `{}` | | +| serviceAccount.create | bool | `true` | | +| serviceAccount.name | string | `""` | | +| tolerations | list | `[]` | | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/chart/files/default.yml b/chart/files/default.yml deleted file mode 100644 index 91a947f26..000000000 --- a/chart/files/default.yml +++ /dev/null @@ -1,162 +0,0 @@ -#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -# Misskey configuration -#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -# ┌─────┐ -#───┘ URL └───────────────────────────────────────────────────── - -# Final accessible URL seen by a user. -# url: https://example.tld/ - -# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE -# URL SETTINGS AFTER THAT! - -# ┌───────────────────────┐ -#───┘ Port and TLS settings └─────────────────────────────────── - -# -# Misskey supports two deployment options for public. -# - -# Option 1: With Reverse Proxy -# -# +----- https://example.tld/ ------------+ -# +------+ |+-------------+ +----------------+| -# | User | ---> || Proxy (443) | ---> | Misskey (3000) || -# +------+ |+-------------+ +----------------+| -# +---------------------------------------+ -# -# You need to setup reverse proxy. (eg. nginx) -# You do not define 'https' section. - -# Option 2: Standalone -# -# +- https://example.tld/ -+ -# +------+ | +---------------+ | -# | User | ---> | | Misskey (443) | | -# +------+ | +---------------+ | -# +------------------------+ -# -# You need to run Misskey as root. -# You need to set Certificate in 'https' section. - -# To use option 1, uncomment below line. -port: 3000 # A port that your Misskey server should listen. - -# To use option 2, uncomment below lines. -#port: 443 - -#https: -# # path for certification -# key: /etc/letsencrypt/live/example.tld/privkey.pem -# cert: /etc/letsencrypt/live/example.tld/fullchain.pem - -# ┌──────────────────────────┐ -#───┘ PostgreSQL configuration └──────────────────────────────── - -db: - host: localhost - port: 5432 - - # Database name - db: misskey - - # Auth - user: example-misskey-user - pass: example-misskey-pass - - # Whether disable Caching queries - #disableCache: true - - # Extra Connection options - #extra: - # ssl: true - -# ┌─────────────────────┐ -#───┘ Redis configuration └───────────────────────────────────── - -redis: - host: localhost - port: 6379 - #pass: example-pass - #prefix: example-prefix - #db: 1 - -# ┌─────────────────────────────┐ -#───┘ Elasticsearch configuration └───────────────────────────── - -#elasticsearch: -# host: localhost -# port: 9200 -# ssl: false -# user: -# pass: - -# ┌───────────────┐ -#───┘ ID generation └─────────────────────────────────────────── - -# You can select the ID generation method. -# You don't usually need to change this setting, but you can -# change it according to your preferences. - -# Available methods: -# aid ... Short, Millisecond accuracy -# meid ... Similar to ObjectID, Millisecond accuracy -# ulid ... Millisecond accuracy -# objectid ... This is left for backward compatibility - -# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE -# ID SETTINGS AFTER THAT! - -id: "aid" -# ┌─────────────────────┐ -#───┘ Other configuration └───────────────────────────────────── - -# Whether disable HSTS -#disableHsts: true - -# Number of worker processes -#clusterLimit: 1 - -# Job concurrency per worker -# deliverJobConcurrency: 128 -# inboxJobConcurrency: 16 - -# Job rate limiter -# deliverJobPerSec: 128 -# inboxJobPerSec: 16 - -# Job attempts -# deliverJobMaxAttempts: 12 -# inboxJobMaxAttempts: 8 - -# IP address family used for outgoing request (ipv4, ipv6 or dual) -#outgoingAddressFamily: ipv4 - -# Syslog option -#syslog: -# host: localhost -# port: 514 - -# Proxy for HTTP/HTTPS -#proxy: http://127.0.0.1:3128 - -#proxyBypassHosts: [ -# 'example.com', -# '192.0.2.8' -#] - -# Proxy for SMTP/SMTPS -#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT -#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 -#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 - -# Media Proxy -#mediaProxy: https://example.com/proxy - -#allowedPrivateNetworks: [ -# '127.0.0.1/32' -#] - -# Upload or download file size limits (bytes) -#maxFileSize: 262144000 diff --git a/chart/templates/ConfigMap.yml b/chart/templates/ConfigMap.yml deleted file mode 100644 index 37c25e086..000000000 --- a/chart/templates/ConfigMap.yml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "misskey.fullname" . }}-configuration -data: - default.yml: |- - {{ .Files.Get "files/default.yml"|nindent 4 }} - url: {{ .Values.url }} diff --git a/chart/templates/Deployment.yml b/chart/templates/Deployment.yml deleted file mode 100644 index d16aece91..000000000 --- a/chart/templates/Deployment.yml +++ /dev/null @@ -1,47 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "misskey.fullname" . }} - labels: - {{- include "misskey.labels" . | nindent 4 }} -spec: - selector: - matchLabels: - {{- include "misskey.selectorLabels" . | nindent 6 }} - replicas: 1 - template: - metadata: - labels: - {{- include "misskey.selectorLabels" . | nindent 8 }} - spec: - containers: - - name: misskey - image: {{ .Values.image }} - env: - - name: NODE_ENV - value: {{ .Values.environment }} - volumeMounts: - - name: {{ include "misskey.fullname" . }}-configuration - mountPath: /misskey/.config - readOnly: true - ports: - - containerPort: 3000 - - name: postgres - image: postgres:14-alpine - env: - - name: POSTGRES_USER - value: "example-misskey-user" - - name: POSTGRES_PASSWORD - value: "example-misskey-pass" - - name: POSTGRES_DB - value: "misskey" - ports: - - containerPort: 5432 - - name: redis - image: redis:alpine - ports: - - containerPort: 6379 - volumes: - - name: {{ include "misskey.fullname" . }}-configuration - configMap: - name: {{ include "misskey.fullname" . }}-configuration diff --git a/chart/templates/NOTES.txt b/chart/templates/NOTES.txt new file mode 100644 index 000000000..d3e4f2f20 --- /dev/null +++ b/chart/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "calckey.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "calckey.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "calckey.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "calckey.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/chart/templates/Service.yml b/chart/templates/Service.yml deleted file mode 100644 index 320958129..000000000 --- a/chart/templates/Service.yml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "misskey.fullname" . }} - annotations: - dev.okteto.com/auto-ingress: "true" -spec: - type: ClusterIP - ports: - - port: 3000 - protocol: TCP - name: http - selector: - {{- include "misskey.selectorLabels" . | nindent 4 }} diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl index a5a2499f3..00702ec34 100644 --- a/chart/templates/_helpers.tpl +++ b/chart/templates/_helpers.tpl @@ -1,7 +1,7 @@ {{/* Expand the name of the chart. */}} -{{- define "misskey.name" -}} +{{- define "calckey.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} @@ -10,7 +10,7 @@ Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} -{{- define "misskey.fullname" -}} +{{- define "calckey.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} @@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "misskey.chart" -}} +{{- define "calckey.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "misskey.labels" -}} -helm.sh/chart: {{ include "misskey.chart" . }} -{{ include "misskey.selectorLabels" . }} +{{- define "calckey.labels" -}} +helm.sh/chart: {{ include "calckey.chart" . }} +{{ include "calckey.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -45,18 +45,274 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Selector labels */}} -{{- define "misskey.selectorLabels" -}} -app.kubernetes.io/name: {{ include "misskey.name" . }} +{{- define "calckey.selectorLabels" -}} +app.kubernetes.io/name: {{ include "calckey.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} -{{- define "misskey.serviceAccountName" -}} +{{- define "calckey.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} -{{- default (include "misskey.fullname" .) .Values.serviceAccount.name }} +{{- default (include "calckey.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +Create a default fully qualified name for dependent services. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "calckey.elasticsearch.fullname" -}} +{{- printf "%s-%s" .Release.Name "elasticsearch" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "calckey.redis.fullname" -}} +{{- printf "%s-%s" .Release.Name "redis" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "calckey.postgresql.fullname" -}} +{{- printf "%s-%s" .Release.Name "postgresql" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +config/default.yml content +*/}} +{{- define "calckey.configDir.default.yml" -}} +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Calckey configuration +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +# ┌─────┐ +#───┘ URL └───────────────────────────────────────────────────── + +# Final accessible URL seen by a user. +url: "https://{{ .Values.calckey.domain }}/" + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# URL SETTINGS AFTER THAT! + +# ┌───────────────────────┐ +#───┘ Port and TLS settings └─────────────────────────────────── + +# +# Misskey requires a reverse proxy to support HTTPS connections. +# +# +----- https://example.tld/ ------------+ +# +------+ |+-------------+ +----------------+| +# | User | ---> || Proxy (443) | ---> | Misskey (3000) || +# +------+ |+-------------+ +----------------+| +# +---------------------------------------+ +# +# You need to set up a reverse proxy. (e.g. nginx) +# An encrypted connection with HTTPS is highly recommended +# because tokens may be transferred in GET requests. + +# The port that your Misskey server should listen on. +port: 3000 + +# ┌──────────────────────────┐ +#───┘ PostgreSQL configuration └──────────────────────────────── + +db: + {{- if .Values.postgresql.enabled }} + host: {{ template "calckey.postgresql.fullname" . }} + port: '5432' + {{- else }} + host: {{ .Values.postgresql.postgresqlHostname }} + port: {{ .Values.postgresql.postgresqlPort | default "5432" | quote }} + {{- end }} + + # Database name + db: {{ .Values.postgresql.auth.database }} + + # Auth + user: {{ .Values.postgresql.auth.username }} + pass: "{{ .Values.postgresql.auth.password }}" + + # Whether disable Caching queries + #disableCache: true + + # Extra Connection options + #extra: + # ssl: true + +# ┌─────────────────────┐ +#───┘ Redis configuration └───────────────────────────────────── + +redis: + {{- if .Values.redis.enabled }} + host: {{ template "calckey.redis.fullname" . }}-master + {{- else }} + host: {{ required "When the redis chart is disabled .Values.redis.hostname is required" .Values.redis.hostname }} + {{- end }} + port: {{ .Values.redis.port | default "6379" | quote }} + #family: 0 # 0=Both, 4=IPv4, 6=IPv6 + pass: {{ .Values.redis.auth.password | quote }} + #prefix: example-prefix + #db: 1 + +# ┌─────────────────────┐ +#───┘ Sonic configuration └───────────────────────────────────── + +#sonic: +# host: localhost +# port: 1491 +# auth: SecretPassword +# collection: notes +# bucket: default + +# ┌─────────────────────────────┐ +#───┘ Elasticsearch configuration └───────────────────────────── + +{{- if .Values.elasticsearch.enabled }} +elasticsearch: + host: {{ template "mastodon.elasticsearch.fullname" . }}-master-hl + port: 9200 + ssl: false +{{- else if .Values.elasticsearch.hostname }} +elasticsearch: + host: {{ .Values.elasticsearch.hostname | quote }} + port: {{ .Values.elasticsearch.port }} + ssl: {{ .Values.elasticsearch.ssl }} + {{- if .Values.elasticsearch.auth }} + user: {{ .Values.elasticsearch.auth.username | quote }} + pass: {{ .Values.elasticsearch.auth.password | quote }} + {{- end }} +{{- end }} + +# ┌───────────────┐ +#───┘ ID generation └─────────────────────────────────────────── + +# You can select the ID generation method. +# You don't usually need to change this setting, but you can +# change it according to your preferences. + +# Available methods: +# aid ... Short, Millisecond accuracy +# meid ... Similar to ObjectID, Millisecond accuracy +# ulid ... Millisecond accuracy +# objectid ... This is left for backward compatibility + +# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE +# ID SETTINGS AFTER THAT! + +id: 'aid' + +# ┌─────────────────────┐ +#───┘ Other configuration └───────────────────────────────────── + +# Max note length, should be < 8000. +#maxNoteLength: 3000 + +# Maximum lenght of an image caption or file comment (default 1500, max 8192) +#maxCaptionLength: 1500 + +# Reserved usernames that only the administrator can register with +reservedUsernames: +{{ .Values.calckey.reservedUsernames | toYaml }} + +# Whether disable HSTS +#disableHsts: true + +# Number of worker processes +#clusterLimit: 1 + +# Job concurrency per worker +# deliverJobConcurrency: 128 +# inboxJobConcurrency: 16 + +# Job rate limiter +# deliverJobPerSec: 128 +# inboxJobPerSec: 16 + +# Job attempts +# deliverJobMaxAttempts: 12 +# inboxJobMaxAttempts: 8 + +# IP address family used for outgoing request (ipv4, ipv6 or dual) +#outgoingAddressFamily: ipv4 + +# Syslog option +#syslog: +# host: localhost +# port: 514 + +# Proxy for HTTP/HTTPS +#proxy: http://127.0.0.1:3128 + +#proxyBypassHosts: [ +# 'example.com', +# '192.0.2.8' +#] + +# Proxy for SMTP/SMTPS +#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT +#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4 +#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5 + +# Media Proxy +#mediaProxy: https://example.com/proxy + +# Proxy remote files (default: false) +#proxyRemoteFiles: true + +allowedPrivateNetworks: +{{ .Values.calckey.allowedPrivateNetworks | toYaml }} + +# TWA +#twa: +# nameSpace: android_app +# packageName: tld.domain.twa +# sha256CertFingerprints: ['AB:CD:EF'] + +# Upload or download file size limits (bytes) +#maxFileSize: 262144000 + +# Managed hosting settings +# !!!!!!!!!! +# >>>>>> NORMAL SELF-HOSTERS, STAY AWAY! <<<<<< +# >>>>>> YOU DON'T NEED THIS! <<<<<< +# !!!!!!!!!! +# Each category is optional, but if each item in each category is mandatory! +# If you mess this up, that's on you, you've been warned... + +#maxUserSignups: 100 +isManagedHosting: {{ .Values.calckey.isManagedHosting }} +deepl: + managed: false +# authKey: '' +# isPro: false +# +email: + managed: {{ .Values.calckey.smtp.managed }} + address: {{ .Values.calckey.smtp.from_address | quote }} + host: {{ .Values.calckey.smtp.server | quote }} + port: {{ .Values.calckey.smtp.port }} + user: {{ .Values.calckey.smtp.login | quote }} + pass: {{ .Values.calckey.smtp.password | quote }} + useImplicitSslTls: {{ .Values.calckey.smtp.useImplicitSslTls }} +objectStorage: + managed: {{ .Values.calckey.objectStorage.managed }} + baseUrl: {{ .Values.calckey.objectStorage.baseUrl | quote }} + bucket: {{ .Values.calckey.objectStorage.bucket | quote }} + prefix: {{ .Values.calckey.objectStorage.prefix | quote }} + endpoint: {{ .Values.calckey.objectStorage.endpoint | quote }} + region: {{ .Values.calckey.objectStorage.region | quote }} + accessKey: {{ .Values.calckey.objectStorage.access_key | quote }} + secretKey: {{ .Values.calckey.objectStorage.access_secret | quote }} + useSsl: true + connnectOverProxy: false + setPublicReadOnUpload: true + s3ForcePathStyle: true + +# !!!!!!!!!! +# >>>>>> AGAIN, NORMAL SELF-HOSTERS, STAY AWAY! <<<<<< +# >>>>>> YOU DON'T NEED THIS, ABOVE SETTINGS ARE FOR MANAGED HOSTING ONLY! <<<<<< +# !!!!!!!!!! + +# Seriously. Do NOT fill out the above settings if you're self-hosting. +# They're much better off being set from the control panel. +{{- end }} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 000000000..5bcf8851a --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "calckey.fullname" . }} + labels: + {{- include "calckey.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "calckey.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/secret-config: {{ include ( print $.Template.BasePath "/secret-config.yaml" ) . | sha256sum | quote }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "calckey.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "calckey.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + volumes: + - name: config-volume + secret: + secretName: {{ template "calckey.fullname" . }}-config + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: "NODE_ENV" + value: "production" + volumeMounts: + - name: config-volume + mountPath: /calckey/.config + ports: + - name: http + containerPort: 3000 + protocol: TCP + startupProbe: + httpGet: + path: / + port: http + failureThreshold: 30 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/chart/templates/hpa.yaml b/chart/templates/hpa.yaml new file mode 100644 index 000000000..4cdd2b625 --- /dev/null +++ b/chart/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "calckey.fullname" . }} + labels: + {{- include "calckey.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "calckey.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml new file mode 100644 index 000000000..212c40e4b --- /dev/null +++ b/chart/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "calckey.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "calckey.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/chart/templates/secret-config.yaml b/chart/templates/secret-config.yaml new file mode 100644 index 000000000..2dad134c5 --- /dev/null +++ b/chart/templates/secret-config.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "calckey.fullname" . }}-config + labels: + {{- include "calckey.labels" . | nindent 4 }} +type: Opaque +data: + default.yml: {{ include "calckey.configDir.default.yml" . | b64enc }} diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 000000000..d46067a40 --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "calckey.fullname" . }} + labels: + {{- include "calckey.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "calckey.selectorLabels" . | nindent 4 }} diff --git a/chart/templates/serviceaccount.yaml b/chart/templates/serviceaccount.yaml new file mode 100644 index 000000000..f269ad028 --- /dev/null +++ b/chart/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "calckey.serviceAccountName" . }} + labels: + {{- include "calckey.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/chart/templates/tests/test-connection.yaml b/chart/templates/tests/test-connection.yaml new file mode 100644 index 000000000..b8db3d9a1 --- /dev/null +++ b/chart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "calckey.fullname" . }}-test-connection" + labels: + {{- include "calckey.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "calckey.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 000000000..1f8f8c8f7 --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,158 @@ +# Default values for calckey. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: docker.io/thatonecalculator/calckey + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +calckey: + isManagedHosting: true + domain: calckey.local + + smtp: + managed: true + from_address: notifications@example.com + port: 587 + server: smtp.mailgun.org + useImplicitSslTls: false + login: "" + password: "" + + objectStorage: + managed: true + access_key: "" + access_secret: "" + baseUrl: "" # e.g. "https://my-bucket.nyc3.cdn.digitaloceanspaces.com" + bucket: "" # e.g. "my-bucket" + prefix: files + endpoint: "" # e.g. "nyc3.digitaloceanspaces.com:443" + region: "" # e.g. "nyc3" + + # -- If you want to allow calckey to connect to private ips, enter the cidrs here. + allowedPrivateNetworks: [] + # - "10.0.0.0/8" + + reservedUsernames: + - root + - admin + - administrator + - me + - system + +# https://github.com/bitnami/charts/tree/master/bitnami/postgresql#parameters +postgresql: + # -- disable if you want to use an existing db; in which case the values below + # must match those of that external postgres instance + enabled: true + # postgresqlHostname: preexisting-postgresql + # postgresqlPort: 5432 + auth: + database: calckey_production + username: calckey + # you must set a password; the password generated by the postgresql chart will + # be rotated on each upgrade: + # https://github.com/bitnami/charts/tree/master/bitnami/postgresql#upgrade + password: "" + +# https://github.com/bitnami/charts/tree/master/bitnami/redis#parameters +redis: + # disable if you want to use an existing redis instance; in which case the + # values below must match those of that external redis instance + enabled: true + hostname: "" + port: 6379 + auth: + # -- you must set a password; the password generated by the redis chart will be + # rotated on each upgrade: + password: "" + +# -- https://github.com/bitnami/charts/tree/master/bitnami/elasticsearch#parameters +elasticsearch: + # disable if you want to use an existing redis instance; in which case the + # values below must match those of that external elasticsearch instance + enabled: false + hostname: "" + port: 9200 + ssl: false + auth: {} + # username: "" + # password: "" + # @ignored + image: + tag: 7 + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/chart/values.yml b/chart/values.yml deleted file mode 100644 index a7031538a..000000000 --- a/chart/values.yml +++ /dev/null @@ -1,3 +0,0 @@ -url: https://example.tld/ -image: okteto.dev/misskey -environment: production diff --git a/kubernetes-README.md b/kubernetes-README.md new file mode 100644 index 000000000..710d0dee0 --- /dev/null +++ b/kubernetes-README.md @@ -0,0 +1,45 @@ +# Running a Calckey instance with Kubernetes and Helm + +This is a [Helm](https://helm.sh/) chart directory in the root of the project +that you can use to deploy calckey to a Kubernetes cluster + +## Deployment + +1. Copy the example helm values and make your changes: +```shell +cp .config/helm_values_example.yml .config/helm_values.yml +``` + +2. Update helm dependencies: +```shell +cd chart +helm dependency list $dir 2> /dev/null | tail +2 | head -n -1 | awk '{ print "helm repo add " $1 " " $3 }' | while read cmd; do $cmd; done; +cd ../ +``` + +3. Create the calckey helm release (also used to update existing deployment): +```shell +helm upgrade \ + --install \ + --namespace calckey \ + --create-namespace \ + calckey chart/ \ + -f .config/helm_values.yml +``` + +4. Watch your calckey instance spin up: +```shell +kubectl -n calckey get po -w +``` + +5. Initial the admin user and managed config: +```shell +export CALCKEY_USERNAME="my_desired_admin_handle" && \ +export CALCKEY_PASSWORD="myDesiredInitialPassword" && \ +export CALCKEY_HOST="calckey.example.com" && \ +export CALCKEY_TOKEN=$(curl -X POST https://$CALCKEY_HOST/api/admin/accounts/create -H "Content-Type: application/json" -d "{ \"username\":\"$CALCKEY_USERNAME\", \"password\":\"$CALCKEY_PASSWORD\" }" | jq -r '.token') && \ +echo "Save this token: ${CALCKEY_TOKEN}" && \ +curl -X POST -H "Authorization: Bearer $CALCKEY_TOKEN" https://$CALCKEY_HOST/api/admin/accounts/hosted +``` + +6. Enjoy! From a7e361da6c5c47aa7c90c621a6d2e99c0b89fd02 Mon Sep 17 00:00:00 2001 From: Lily Cohen Date: Wed, 10 May 2023 16:17:05 -0700 Subject: [PATCH 105/146] adding example config --- .config/helm_values_example.yml | 82 +++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .config/helm_values_example.yml diff --git a/.config/helm_values_example.yml b/.config/helm_values_example.yml new file mode 100644 index 000000000..b600eb8aa --- /dev/null +++ b/.config/helm_values_example.yml @@ -0,0 +1,82 @@ +replicaCount: 1 + +resources: + requests: + cpu: 0.5 + memory: 512Mi + limits: + cpu: 1 + memory: 1Gi + +calckey: + domain: example.tld + smtp: + from_address: noreply@example.tld + port: 587 + server: smtp.gmail.com + useImplicitSslTls: false + login: me@example.tld + password: CHANGEME + objectStorage: + baseUrl: https://example-bucket.nyc3.cdn.digitaloceanspaces.com + access_key: CHANGEME + access_secret: CHANGEME + bucket: example-bucket + endpoint: nyc3.digitaloceanspaces.com:443 + region: nyc3 + allowedPrivateNetworks: [] + +ingress: + enabled: true + annotations: + cert-manager.io/cluster-issuer: letsencrypt + hosts: + - host: example.tld + paths: + - path: / + pathType: ImplementationSpecific + tls: + - secretName: example-tld-certificate + hosts: + - example.tld + +elasticsearch: + enabled: false + +postgresql: + auth: + password: CHANGEME + postgresPassword: CHANGEME + primary: + persistence: + enabled: true + storageClass: vultr-block-storage + size: 25Gi + resources: + requests: + cpu: 0.25 + memory: 256Mi + limits: + cpu: 0.5 + memory: 512Mi + metrics: + enabled: true + +redis: + auth: + password: CHANGEME + master: + resources: + requests: + cpu: 0.25 + memory: 256Mi + limits: + cpu: 0.5 + memory: 256Mi + persistence: + storageclass: vultr-block-storage + size: 10Gi + replica: + replicaCount: 0 + metrics: + enabled: true From f74cf0606267f1fadbc37a5dd1a270c438c67347 Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Wed, 10 May 2023 16:56:46 -0700 Subject: [PATCH 106/146] docs: fix k8s link --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 09f750667..1732736fe 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,8 @@ If you have access to a server that supports one of the sources below, I recomme ## 🛳️ Containerization -- [🐳 How to run Calckey with Docker](https://codeberg.org/calckey/calckey/src/branch/develop/docs/docker.md). -- [🛞 How to run Calckey with Kubernetes/Helm](https://codeberg.org/calckey/calckey/src/branch/develop/kubernetes.md). +- [🐳 How to run Calckey with Docker](https://codeberg.org/calckey/calckey/src/branch/develop/docs/docker.md) +- [🛞 How to run Calckey with Kubernetes/Helm](https://codeberg.org/calckey/calckey/src/branch/develop/docs/kubernetes.md) ## 🧑‍💻 Dependencies From 19d745b8439b742ecb3cf0bc729cdac9df856e2b Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Wed, 10 May 2023 16:59:47 -0700 Subject: [PATCH 107/146] docs: add opencollective --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1732736fe..ded16ba28 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [![no github badge](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page/) [![status badge](https://ci.codeberg.org/api/badges/calckey/calckey/status.svg)](https://ci.codeberg.org/calckey/calckey) +[![opencollective badge](https://opencollective.com/calckey/tiers/badge.svg)](https://opencollective.com/Calckey) [![liberapay badge](https://img.shields.io/liberapay/receives/ThatOneCalculator?logo=liberapay)](https://liberapay.com/ThatOneCalculator) [![translate-badge](https://hosted.weblate.org/widgets/calckey/-/svg-badge.svg)](https://hosted.weblate.org/engage/calckey/) [![docker badge](https://img.shields.io/docker/pulls/thatonecalculator/calckey?logo=docker)](https://hub.docker.com/r/thatonecalculator/calckey) @@ -46,6 +47,7 @@ # 🥂 Links +- 💸 OpenCollective: - 💸 Liberapay: - Donate publicly to get your name on the Patron list! - 🚢 Flagship instance: From 6b9b4a686610aa19cf675c5abf76f7ef00e83c02 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Thu, 11 May 2023 00:02:38 +0000 Subject: [PATCH 108/146] Merge pull request 'docs: Add Apache2 documentation' (#10078) from warrows/calckey:main into main Reviewed-on: https://codeberg.org/calckey/calckey/pulls/10078 --- README.md | 17 ++++++++++++++--- calckey.apache.conf | 13 +++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 calckey.apache.conf diff --git a/README.md b/README.md index ded16ba28..8466ce834 100644 --- a/README.md +++ b/README.md @@ -168,13 +168,24 @@ In Calckey's directory, fill out the `sonic` section of `.config/default.yml` wi For migrating from Misskey v13, Misskey v12, and Foundkey, read [this document](https://codeberg.org/calckey/calckey/src/branch/develop/docs/migrate.md). -## 🍀 NGINX +## Web proxy + +Choose between NGINX or Apache (we recommend NGINX) + +### 🍀 NGINX - Run `sudo cp ./calckey.nginx.conf /etc/nginx/sites-available/ && cd /etc/nginx/sites-available/` - Edit `calckey.nginx.conf` to reflect your instance properly -- Run `sudo cp ./calckey.nginx.conf ../sites-enabled/` +- Run `sudo ln -s ./calckey.nginx.conf ../sites-enabled/calckey.nginx.conf` - Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service. +### Apache 2 + +- Run `sudo cp ./calckey.apache.conf /etc/apache2/sites-available/ && cd /etc/apache2/sites-available/` +- Edit `calckey.apache.conf` to reflect your instance properly +- Run `sudo a2ensite calckey.apache` to enable the site +- Run `sudo service apache2 restart` to reload apache2 configuration + ## 🚀 Build and launch! @@ -194,7 +205,7 @@ pm2 start "NODE_ENV=production pnpm run start" --name Calckey - When editing the config file, please don't fill out the settings at the bottom. They're designed *only* for managed hosting, not self hosting. Those settings are much better off being set in Calckey's control panel. - Port 3000 (used in the default config) might be already used on your server for something else. To find an open port for Calckey, run `for p in {3000..4000}; do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1`. Replace 3000 with the minimum port and 4000 with the maximum port if you need it. -- I'd recommend you use a S3 Bucket/CDN for Object Storage, especially if you use Docker. +- I'd recommend you use a S3 Bucket/CDN for Object Storage, especially if you use Docker. - I'd ***strongly*** recommend against using CloudFlare, but if you do, make sure to turn code minification off. - For push notifications, run `npx web-push generate-vapid-keys`, then put the public and private keys into Control Panel > General > ServiceWorker. - For translations, make a [DeepL](https://deepl.com) account and generate an API key, then put it into Control Panel > General > DeepL Translation. diff --git a/calckey.apache.conf b/calckey.apache.conf new file mode 100644 index 000000000..b0c69d51d --- /dev/null +++ b/calckey.apache.conf @@ -0,0 +1,13 @@ +# Replace example.tld with your domain + + + ServerName example.tld + # For WebSocket + ProxyPass "/streaming" "ws://127.0.0.1:3000/streaming/" + # Proxy to Node + ProxyPass "/" "http://127.0.0.1:3000/" + ProxyPassReverse "/" "http://127.0.0.1:3000/" + ProxyPreserveHost On + # For files proxy + AllowEncodedSlashes On + \ No newline at end of file From 9ed2173b42b1ddf882dff3858889cfbbc1db26f5 Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Wed, 10 May 2023 17:08:16 -0700 Subject: [PATCH 109/146] docs: cleanup apache --- README.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8466ce834..6577e45a2 100644 --- a/README.md +++ b/README.md @@ -80,17 +80,16 @@ If you have access to a server that supports one of the sources below, I recomme - Install with [nvm](https://github.com/nvm-sh/nvm) - 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12 - 🍱 At least [Redis](https://redis.io/) v6 (v7 recommend) +- Web Proxy (one of the following) + - 🍀 Nginx (recommended) + - 🪶 Apache ### 😗 Optional dependencies - [FFmpeg](https://ffmpeg.org/) for video transcoding -- Full text search (choost one of the following) - - 🦔 [Sonic](https://crates.io/crates/sonic-server) (highly recommended!) +- Full text search (one of the following) + - 🦔 [Sonic](https://crates.io/crates/sonic-server) (recommended) - [ElasticSearch](https://www.elastic.co/elasticsearch/) -- Management (choose one of the following) - - 🛰️ [pm2](https://pm2.io/) - - 🐳 [Docker](https://docker.com) - - Service manager (systemd, openrc, etc) ### 🏗️ Build dependencies @@ -168,18 +167,16 @@ In Calckey's directory, fill out the `sonic` section of `.config/default.yml` wi For migrating from Misskey v13, Misskey v12, and Foundkey, read [this document](https://codeberg.org/calckey/calckey/src/branch/develop/docs/migrate.md). -## Web proxy +## 🌐 Web proxy -Choose between NGINX or Apache (we recommend NGINX) - -### 🍀 NGINX +### 🍀 Nginx (recommended) - Run `sudo cp ./calckey.nginx.conf /etc/nginx/sites-available/ && cd /etc/nginx/sites-available/` - Edit `calckey.nginx.conf` to reflect your instance properly - Run `sudo ln -s ./calckey.nginx.conf ../sites-enabled/calckey.nginx.conf` - Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service. -### Apache 2 +### 🪶 Apache - Run `sudo cp ./calckey.apache.conf /etc/apache2/sites-available/ && cd /etc/apache2/sites-available/` - Edit `calckey.apache.conf` to reflect your instance properly From 6bc07036ac6f2da5b1b2548a9edf74b855ca89e9 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 10 May 2023 10:52:41 +0900 Subject: [PATCH 110/146] =?UTF-8?q?feat:=20=E6=8A=95=E7=A8=BF=E3=81=97?= =?UTF-8?q?=E3=81=9F=E3=82=B3=E3=83=B3=E3=83=86=E3=83=B3=E3=83=84=E3=81=AE?= =?UTF-8?q?AI=E3=81=AB=E3=82=88=E3=82=8B=E5=AD=A6=E7=BF=92=E3=82=92?= =?UTF-8?q?=E8=BB=BD=E6=B8=9B=E3=81=99=E3=82=8B=E3=82=AA=E3=83=97=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: GitHub --- locales/en-US.yml | 3 + locales/ja-JP.yml | 2 + .../1683682889948-prevent-ai-larning.js | 11 + .../src/models/entities/user-profile.ts | 5 + .../backend/src/models/repositories/user.ts | 1 + packages/backend/src/models/schema/user.ts | 5 + .../server/api/endpoints/admin/show-user.ts | 1 + .../src/server/api/endpoints/i/update.ts | 2 + .../backend/src/server/web/views/clip.pug | 2 + .../src/server/web/views/gallery-post.pug | 2 + .../backend/src/server/web/views/note.pug | 2 + .../backend/src/server/web/views/page.pug | 2 + .../backend/src/server/web/views/user.pug | 2 + packages/backend/test/e2e/users.ts | 891 ++++++++++++++++++ packages/calckey-js/src/api.types.ts | 1 + packages/calckey-js/src/entities.ts | 1 + .../client/src/pages/settings/privacy.vue | 6 + 17 files changed, 939 insertions(+) create mode 100644 packages/backend/migration/1683682889948-prevent-ai-larning.js create mode 100644 packages/backend/test/e2e/users.ts diff --git a/locales/en-US.yml b/locales/en-US.yml index e2d5e04dc..bd32c836a 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1083,6 +1083,9 @@ signupsDisabled: "Signups on this server are currently disabled, but you can alw findOtherInstance: "Find another server" apps: "Apps" sendModMail: "Send Moderation Notice" +preventAiLearning: "Prevent AI bot scraping" +preventAiLearningDescription: "Request third-party AI language models not to study content you upload, such as posts and images." + _sensitiveMediaDetection: description: "Reduces the effort of server moderation through automatically recognizing\ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 86c869126..4fc2f42d7 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -973,6 +973,8 @@ customKaTeXMacroDescription: "数式入力を楽にするためのマクロを name}{content} または \\newcommand{\\add}[2]{#1 + #2} のように記述します。後者の例では \\add{3}{foo}\ \ が 3 + foo に展開されます。また、マクロの名前を囲む波括弧を丸括弧 () および角括弧 [] に変更した場合、マクロの引数に使用する括弧が変更されます。マクロの定義は一行に一つのみで、途中で改行はできません。マクロの定義が無効な行は無視されます。文字列を単純に置換する機能のみに対応していて、条件分岐などの高度な構文は使用できません。" enableCustomKaTeXMacro: "カスタムKaTeXマクロを有効にする" +preventAiLearning: "AIによる学習を防止" +preventAiLearningDescription: "投稿したノート、添付した画像などのコンテンツを学習の対象にしないようAIに要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されます。" _sensitiveMediaDetection: description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てられます。サーバーの負荷が少し増えます。" diff --git a/packages/backend/migration/1683682889948-prevent-ai-larning.js b/packages/backend/migration/1683682889948-prevent-ai-larning.js new file mode 100644 index 000000000..8a88fb3bc --- /dev/null +++ b/packages/backend/migration/1683682889948-prevent-ai-larning.js @@ -0,0 +1,11 @@ +export class PreventAiLarning1683682889948 { + name = 'PreventAiLarning1683682889948' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "preventAiLearning" boolean NOT NULL DEFAULT true`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "preventAiLearning"`); + } +} diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts index cc3d23867..a5eca6f48 100644 --- a/packages/backend/src/models/entities/user-profile.ts +++ b/packages/backend/src/models/entities/user-profile.ts @@ -154,6 +154,11 @@ export class UserProfile { }) public noCrawle: boolean; + @Column('boolean', { + default: true, + }) + public preventAiLearning: boolean; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index f6fcdae29..40760e8ee 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -535,6 +535,7 @@ export const UserRepository = db.getRepository(User).extend({ carefulBot: profile!.carefulBot, autoAcceptFollowed: profile!.autoAcceptFollowed, noCrawle: profile!.noCrawle, + preventAiLearning: profile!.preventAiLearning, isExplorable: user.isExplorable, isDeleted: user.isDeleted, hideOnlineStatus: user.hideOnlineStatus, diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts index 80e94fe50..4c840d0ba 100644 --- a/packages/backend/src/models/schema/user.ts +++ b/packages/backend/src/models/schema/user.ts @@ -394,6 +394,11 @@ export const packedMeDetailedOnlySchema = { nullable: true, optional: false, }, + preventAiLearning: { + type: "boolean", + nullable: true, + optional: false, + }, isExplorable: { type: "boolean", nullable: false, diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index e91f07f83..347ebb9cd 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -59,6 +59,7 @@ export default define(meta, paramDef, async (ps, me) => { emailVerified: profile.emailVerified, autoAcceptFollowed: profile.autoAcceptFollowed, noCrawle: profile.noCrawle, + preventAiLearning: profile.preventAiLearning, alwaysMarkNsfw: profile.alwaysMarkNsfw, autoSensitive: profile.autoSensitive, carefulBot: profile.carefulBot, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 56ed64296..fbfedfbc4 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -102,6 +102,7 @@ export const paramDef = { carefulBot: { type: "boolean" }, autoAcceptFollowed: { type: "boolean" }, noCrawle: { type: "boolean" }, + preventAiLearning: { type: "boolean" }, isBot: { type: "boolean" }, isCat: { type: "boolean" }, speakAsCat: { type: "boolean" }, @@ -191,6 +192,7 @@ export default define(meta, paramDef, async (ps, _user, token) => { if (typeof ps.autoAcceptFollowed === "boolean") profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.noCrawle === "boolean") profileUpdates.noCrawle = ps.noCrawle; + if (typeof ps.preventAiLearning === "boolean") profileUpdates.preventAiLearning = ps.preventAiLearning; if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat; if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat; if (typeof ps.injectFeaturedNote === "boolean") diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug index 2432470c1..e40127f13 100644 --- a/packages/backend/src/server/web/views/clip.pug +++ b/packages/backend/src/server/web/views/clip.pug @@ -24,6 +24,8 @@ block meta unless privateMode if profile.noCrawle meta(name='robots' content='noindex') + if profile.preventAiLearning + meta(name='robots' content='noai') meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug index 1b1c2fbfb..4c8fa3098 100644 --- a/packages/backend/src/server/web/views/gallery-post.pug +++ b/packages/backend/src/server/web/views/gallery-post.pug @@ -24,6 +24,8 @@ block meta unless privateMode if user.host || profile.noCrawle meta(name='robots' content='noindex') + if profile.preventAiLearning + meta(name='robots' content='noai') meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index 476d42d9e..3f432bfe9 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -36,6 +36,8 @@ block meta unless privateMode if user.host || isRenote || profile.noCrawle meta(name='robots' content='noindex') + if profile.preventAiLearning + meta(name='robots' content='noai') meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug index 109528213..67479cf81 100644 --- a/packages/backend/src/server/web/views/page.pug +++ b/packages/backend/src/server/web/views/page.pug @@ -24,6 +24,8 @@ block meta unless privateMode if profile.noCrawle meta(name='robots' content='noindex') + if profile.preventAiLearning + meta(name='robots' content='noai') meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug index cc14dedb3..ce233fc10 100644 --- a/packages/backend/src/server/web/views/user.pug +++ b/packages/backend/src/server/web/views/user.pug @@ -23,6 +23,8 @@ block meta unless privateMode if user.host || profile.noCrawle meta(name='robots' content='noindex') + if profile.preventAiLearning + meta(name='robots' content='noai') meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts new file mode 100644 index 000000000..75b0911f9 --- /dev/null +++ b/packages/backend/test/e2e/users.ts @@ -0,0 +1,891 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { inspect } from 'node:util'; +import { DEFAULT_POLICIES } from '@/core/RoleService.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { + signup, + post, + page, + role, + startServer, + api, + successfulApiCall, + failedApiCall, + uploadFile, +} from '../utils.js'; +import type * as misskey from 'misskey-js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('ユーザー', () => { + // エンティティとしてのユーザーを主眼においたテストを記述する + // (Userを返すエンドポイントとUserエンティティを書き換えるエンドポイントをテストする) + + const stripUndefined = (orig: T): Partial => { + return Object.entries({ ...orig }) + .filter(([, value]) => value !== undefined) + .reduce((obj: Partial, [key, value]) => { + obj[key as keyof T] = value; + return obj; + }, {}); + }; + + // BUG misskey-jsとjson-schemaと実際に返ってくるデータが全部違う + type UserLite = misskey.entities.UserLite & { + badgeRoles: any[], + }; + + type UserDetailedNotMe = UserLite & + misskey.entities.UserDetailed & { + roles: any[], + }; + + type MeDetailed = UserDetailedNotMe & + misskey.entities.MeDetailed & { + showTimelineReplies: boolean, + achievements: object[], + loggedInDays: number, + policies: object, + }; + + type User = MeDetailed & { token: string }; + + const show = async (id: string, me = root): Promise => { + return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any; + }; + + // UserLiteのキーが過不足なく入っている? + const userLite = (user: User): Partial => { + return stripUndefined({ + id: user.id, + name: user.name, + username: user.username, + host: user.host, + avatarUrl: user.avatarUrl, + avatarBlurhash: user.avatarBlurhash, + isBot: user.isBot, + isCat: user.isCat, + instance: user.instance, + emojis: user.emojis, + onlineStatus: user.onlineStatus, + badgeRoles: user.badgeRoles, + + // BUG isAdmin/isModeratorはUserLiteではなくMeDetailedOnlyに含まれる。 + isAdmin: undefined, + isModerator: undefined, + }); + }; + + // UserDetailedNotMeのキーが過不足なく入っている? + const userDetailedNotMe = (user: User): Partial => { + return stripUndefined({ + ...userLite(user), + url: user.url, + uri: user.uri, + movedTo: user.movedTo, + alsoKnownAs: user.alsoKnownAs, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + lastFetchedAt: user.lastFetchedAt, + bannerUrl: user.bannerUrl, + bannerBlurhash: user.bannerBlurhash, + isLocked: user.isLocked, + isSilenced: user.isSilenced, + isSuspended: user.isSuspended, + description: user.description, + location: user.location, + birthday: user.birthday, + lang: user.lang, + fields: user.fields, + followersCount: user.followersCount, + followingCount: user.followingCount, + notesCount: user.notesCount, + pinnedNoteIds: user.pinnedNoteIds, + pinnedNotes: user.pinnedNotes, + pinnedPageId: user.pinnedPageId, + pinnedPage: user.pinnedPage, + publicReactions: user.publicReactions, + ffVisibility: user.ffVisibility, + twoFactorEnabled: user.twoFactorEnabled, + usePasswordLessLogin: user.usePasswordLessLogin, + securityKeys: user.securityKeys, + roles: user.roles, + memo: user.memo, + }); + }; + + // Relations関連のキーが過不足なく入っている? + const userDetailedNotMeWithRelations = (user: User): Partial => { + return stripUndefined({ + ...userDetailedNotMe(user), + isFollowing: user.isFollowing ?? false, + isFollowed: user.isFollowed ?? false, + hasPendingFollowRequestFromYou: user.hasPendingFollowRequestFromYou ?? false, + hasPendingFollowRequestToYou: user.hasPendingFollowRequestToYou ?? false, + isBlocking: user.isBlocking ?? false, + isBlocked: user.isBlocked ?? false, + isMuted: user.isMuted ?? false, + isRenoteMuted: user.isRenoteMuted ?? false, + }); + }; + + // MeDetailedのキーが過不足なく入っている? + const meDetailed = (user: User, security = false): Partial => { + return stripUndefined({ + ...userDetailedNotMe(user), + avatarId: user.avatarId, + bannerId: user.bannerId, + isModerator: user.isModerator, + isAdmin: user.isAdmin, + injectFeaturedNote: user.injectFeaturedNote, + receiveAnnouncementEmail: user.receiveAnnouncementEmail, + alwaysMarkNsfw: user.alwaysMarkNsfw, + autoSensitive: user.autoSensitive, + carefulBot: user.carefulBot, + autoAcceptFollowed: user.autoAcceptFollowed, + noCrawle: user.noCrawle, + preventAiLearning: user.preventAiLearning, + isExplorable: user.isExplorable, + isDeleted: user.isDeleted, + hideOnlineStatus: user.hideOnlineStatus, + hasUnreadSpecifiedNotes: user.hasUnreadSpecifiedNotes, + hasUnreadMentions: user.hasUnreadMentions, + hasUnreadAnnouncement: user.hasUnreadAnnouncement, + hasUnreadAntenna: user.hasUnreadAntenna, + hasUnreadChannel: user.hasUnreadChannel, + hasUnreadNotification: user.hasUnreadNotification, + hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest, + mutedWords: user.mutedWords, + mutedInstances: user.mutedInstances, + mutingNotificationTypes: user.mutingNotificationTypes, + emailNotificationTypes: user.emailNotificationTypes, + showTimelineReplies: user.showTimelineReplies, + achievements: user.achievements, + loggedInDays: user.loggedInDays, + policies: user.policies, + ...(security ? { + email: user.email, + emailVerified: user.emailVerified, + securityKeysList: user.securityKeysList, + } : {}), + }); + }; + + let app: INestApplicationContext; + + let root: User; + let alice: User; + let aliceNote: misskey.entities.Note; + let alicePage: misskey.entities.Page; + let aliceList: misskey.entities.UserList; + + let bob: User; + let bobNote: misskey.entities.Note; + + let carol: User; + let dave: User; + let ellen: User; + let frank: User; + + let usersReplying: User[]; + + let userNoNote: User; + let userNotExplorable: User; + let userLocking: User; + let userAdmin: User; + let roleAdmin: any; + let userModerator: User; + let roleModerator: any; + let userRolePublic: User; + let rolePublic: any; + let userRoleBadge: User; + let roleBadge: any; + let userSilenced: User; + let roleSilenced: any; + let userSuspended: User; + let userDeletedBySelf: User; + let userDeletedByAdmin: User; + let userFollowingAlice: User; + let userFollowedByAlice: User; + let userBlockingAlice: User; + let userBlockedByAlice: User; + let userMutingAlice: User; + let userMutedByAlice: User; + let userRnMutingAlice: User; + let userRnMutedByAlice: User; + let userFollowRequesting: User; + let userFollowRequested: User; + + beforeAll(async () => { + app = await startServer(); + }, 1000 * 60 * 2); + + beforeAll(async () => { + root = await signup({ username: 'root' }); + alice = await signup({ username: 'alice' }); + aliceNote = await post(alice, { text: 'test' }) as any; + alicePage = await page(alice); + aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body; + bob = await signup({ username: 'bob' }); + bobNote = await post(bob, { text: 'test' }) as any; + carol = await signup({ username: 'carol' }); + dave = await signup({ username: 'dave' }); + ellen = await signup({ username: 'ellen' }); + frank = await signup({ username: 'frank' }); + + // @alice -> @replyingへのリプライ。Promise.allで一気に作るとtimeoutしてしまうのでreduceで一つ一つawaitする + usersReplying = await [...Array(10)].map((_, i) => i).reduce(async (acc, i) => { + const u = await signup({ username: `replying${i}` }); + for (let j = 0; j < 10 - i; j++) { + const p = await post(u, { text: `test${j}` }); + await post(alice, { text: `@${u.username} test${j}`, replyId: p.id }); + } + + return (await acc).concat(u); + }, Promise.resolve([] as User[])); + + userNoNote = await signup({ username: 'userNoNote' }); + userNotExplorable = await signup({ username: 'userNotExplorable' }); + await post(userNotExplorable, { text: 'test' }); + await api('i/update', { isExplorable: false }, userNotExplorable); + userLocking = await signup({ username: 'userLocking' }); + await post(userLocking, { text: 'test' }); + await api('i/update', { isLocked: true }, userLocking); + userAdmin = await signup({ username: 'userAdmin' }); + roleAdmin = await role(root, { isAdministrator: true, name: 'Admin Role' }); + await api('admin/roles/assign', { userId: userAdmin.id, roleId: roleAdmin.id }, root); + userModerator = await signup({ username: 'userModerator' }); + roleModerator = await role(root, { isModerator: true, name: 'Moderator Role' }); + await api('admin/roles/assign', { userId: userModerator.id, roleId: roleModerator.id }, root); + userRolePublic = await signup({ username: 'userRolePublic' }); + rolePublic = await role(root, { isPublic: true, name: 'Public Role' }); + await api('admin/roles/assign', { userId: userRolePublic.id, roleId: rolePublic.id }, root); + userRoleBadge = await signup({ username: 'userRoleBadge' }); + roleBadge = await role(root, { asBadge: true, name: 'Badge Role' }); + await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root); + userSilenced = await signup({ username: 'userSilenced' }); + await post(userSilenced, { text: 'test' }); + roleSilenced = await role(root, {}, { canPublicNote: { priority: 0, useDefault: false, value: false } }); + await api('admin/roles/assign', { userId: userSilenced.id, roleId: roleSilenced.id }, root); + userSuspended = await signup({ username: 'userSuspended' }); + await post(userSuspended, { text: 'test' }); + await successfulApiCall({ endpoint: 'i/update', parameters: { description: '#user_testuserSuspended' }, user: userSuspended }); + await api('admin/suspend-user', { userId: userSuspended.id }, root); + userDeletedBySelf = await signup({ username: 'userDeletedBySelf', password: 'userDeletedBySelf' }); + await post(userDeletedBySelf, { text: 'test' }); + await api('i/delete-account', { password: 'userDeletedBySelf' }, userDeletedBySelf); + userDeletedByAdmin = await signup({ username: 'userDeletedByAdmin' }); + await post(userDeletedByAdmin, { text: 'test' }); + await api('admin/delete-account', { userId: userDeletedByAdmin.id }, root); + userFollowingAlice = await signup({ username: 'userFollowingAlice' }); + await post(userFollowingAlice, { text: 'test' }); + await api('following/create', { userId: alice.id }, userFollowingAlice); + userFollowedByAlice = await signup({ username: 'userFollowedByAlice' }); + await post(userFollowedByAlice, { text: 'test' }); + await api('following/create', { userId: userFollowedByAlice.id }, alice); + userBlockingAlice = await signup({ username: 'userBlockingAlice' }); + await post(userBlockingAlice, { text: 'test' }); + await api('blocking/create', { userId: alice.id }, userBlockingAlice); + userBlockedByAlice = await signup({ username: 'userBlockedByAlice' }); + await post(userBlockedByAlice, { text: 'test' }); + await api('blocking/create', { userId: userBlockedByAlice.id }, alice); + userMutingAlice = await signup({ username: 'userMutingAlice' }); + await post(userMutingAlice, { text: 'test' }); + await api('mute/create', { userId: alice.id }, userMutingAlice); + userMutedByAlice = await signup({ username: 'userMutedByAlice' }); + await post(userMutedByAlice, { text: 'test' }); + await api('mute/create', { userId: userMutedByAlice.id }, alice); + userRnMutingAlice = await signup({ username: 'userRnMutingAlice' }); + await post(userRnMutingAlice, { text: 'test' }); + await api('renote-mute/create', { userId: alice.id }, userRnMutingAlice); + userRnMutedByAlice = await signup({ username: 'userRnMutedByAlice' }); + await post(userRnMutedByAlice, { text: 'test' }); + await api('renote-mute/create', { userId: userRnMutedByAlice.id }, alice); + userFollowRequesting = await signup({ username: 'userFollowRequesting' }); + await post(userFollowRequesting, { text: 'test' }); + userFollowRequested = userLocking; + await api('following/create', { userId: userFollowRequested.id }, userFollowRequesting); + }, 1000 * 60 * 10); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + alice = { + ...alice, + ...await successfulApiCall({ endpoint: 'i', parameters: {}, user: alice }) as any, + }; + aliceNote = await successfulApiCall({ endpoint: 'notes/show', parameters: { noteId: aliceNote.id }, user: alice }); + }); + + //#region サインアップ(signup) + + test('が作れる。(作りたての状態で自分のユーザー情報が取れる)', async () => { + // SignupApiService.ts + const response = await successfulApiCall({ + endpoint: 'signup', + parameters: { username: 'zoe', password: 'password' }, + user: undefined, + }) as unknown as User; // BUG MeDetailedに足りないキーがある + + // signupの時はtokenが含まれる特別なMeDetailedが返ってくる + assert.match(response.token, /[a-zA-Z0-9]{16}/); + + // UserLite + assert.match(response.id, /[0-9a-z]{10}/); + assert.strictEqual(response.name, null); + assert.strictEqual(response.username, 'zoe'); + assert.strictEqual(response.host, null); + assert.match(response.avatarUrl, /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); + assert.strictEqual(response.avatarBlurhash, null); + assert.strictEqual(response.isBot, false); + assert.strictEqual(response.isCat, false); + assert.strictEqual(response.instance, undefined); + assert.deepStrictEqual(response.emojis, {}); + assert.strictEqual(response.onlineStatus, 'unknown'); + assert.deepStrictEqual(response.badgeRoles, []); + // UserDetailedNotMeOnly + assert.strictEqual(response.url, null); + assert.strictEqual(response.uri, null); + assert.strictEqual(response.movedTo, null); + assert.strictEqual(response.alsoKnownAs, null); + assert.strictEqual(response.createdAt, new Date(response.createdAt).toISOString()); + assert.strictEqual(response.updatedAt, null); + assert.strictEqual(response.lastFetchedAt, null); + assert.strictEqual(response.bannerUrl, null); + assert.strictEqual(response.bannerBlurhash, null); + assert.strictEqual(response.isLocked, false); + assert.strictEqual(response.isSilenced, false); + assert.strictEqual(response.isSuspended, false); + assert.strictEqual(response.description, null); + assert.strictEqual(response.location, null); + assert.strictEqual(response.birthday, null); + assert.strictEqual(response.lang, null); + assert.deepStrictEqual(response.fields, []); + assert.strictEqual(response.followersCount, 0); + assert.strictEqual(response.followingCount, 0); + assert.strictEqual(response.notesCount, 0); + assert.deepStrictEqual(response.pinnedNoteIds, []); + assert.deepStrictEqual(response.pinnedNotes, []); + assert.strictEqual(response.pinnedPageId, null); + assert.strictEqual(response.pinnedPage, null); + assert.strictEqual(response.publicReactions, false); + assert.strictEqual(response.ffVisibility, 'public'); + assert.strictEqual(response.twoFactorEnabled, false); + assert.strictEqual(response.usePasswordLessLogin, false); + assert.strictEqual(response.securityKeys, false); + assert.deepStrictEqual(response.roles, []); + assert.strictEqual(response.memo, null); + + // MeDetailedOnly + assert.strictEqual(response.avatarId, null); + assert.strictEqual(response.bannerId, null); + assert.strictEqual(response.isModerator, false); + assert.strictEqual(response.isAdmin, false); + assert.strictEqual(response.injectFeaturedNote, true); + assert.strictEqual(response.receiveAnnouncementEmail, true); + assert.strictEqual(response.alwaysMarkNsfw, false); + assert.strictEqual(response.autoSensitive, false); + assert.strictEqual(response.carefulBot, false); + assert.strictEqual(response.autoAcceptFollowed, true); + assert.strictEqual(response.noCrawle, false); + assert.strictEqual(response.preventAiLearning, true); + assert.strictEqual(response.isExplorable, true); + assert.strictEqual(response.isDeleted, false); + assert.strictEqual(response.hideOnlineStatus, false); + assert.strictEqual(response.hasUnreadSpecifiedNotes, false); + assert.strictEqual(response.hasUnreadMentions, false); + assert.strictEqual(response.hasUnreadAnnouncement, false); + assert.strictEqual(response.hasUnreadAntenna, false); + assert.strictEqual(response.hasUnreadChannel, false); + assert.strictEqual(response.hasUnreadNotification, false); + assert.strictEqual(response.hasPendingReceivedFollowRequest, false); + assert.deepStrictEqual(response.mutedWords, []); + assert.deepStrictEqual(response.mutedInstances, []); + assert.deepStrictEqual(response.mutingNotificationTypes, []); + assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']); + assert.strictEqual(response.showTimelineReplies, false); + assert.deepStrictEqual(response.achievements, []); + assert.deepStrictEqual(response.loggedInDays, 0); + assert.deepStrictEqual(response.policies, DEFAULT_POLICIES); + assert.notStrictEqual(response.email, undefined); + assert.strictEqual(response.emailVerified, false); + assert.deepStrictEqual(response.securityKeysList, []); + }); + + //#endregion + //#region 自分の情報(i) + + test('を読み取ることができること(自分)、キーが過不足なく入っていること。', async () => { + const response = await successfulApiCall({ + endpoint: 'i', + parameters: {}, + user: userNoNote, + }); + const expected = meDetailed(userNoNote, true); + expected.loggedInDays = 1; // iはloggedInDaysを更新する + assert.deepStrictEqual(response, expected); + }); + + //#endregion + //#region 自分の情報の更新(i/update) + + test.each([ + { parameters: (): object => ({ name: null }) }, + { parameters: (): object => ({ name: 'x'.repeat(50) }) }, + { parameters: (): object => ({ name: 'x' }) }, + { parameters: (): object => ({ name: 'My name' }) }, + { parameters: (): object => ({ description: null }) }, + { parameters: (): object => ({ description: 'x'.repeat(1500) }) }, + { parameters: (): object => ({ description: 'x' }) }, + { parameters: (): object => ({ description: 'My description' }) }, + { parameters: (): object => ({ location: null }) }, + { parameters: (): object => ({ location: 'x'.repeat(50) }) }, + { parameters: (): object => ({ location: 'x' }) }, + { parameters: (): object => ({ location: 'My location' }) }, + { parameters: (): object => ({ birthday: '0000-00-00' }) }, + { parameters: (): object => ({ birthday: '9999-99-99' }) }, + { parameters: (): object => ({ lang: 'en-US' }) }, + { parameters: (): object => ({ fields: [] }) }, + { parameters: (): object => ({ fields: [{ name: 'x', value: 'x' }] }) }, + { parameters: (): object => ({ fields: [{ name: 'x'.repeat(3000), value: 'x'.repeat(3000) }] }) }, // BUG? fieldには制限がない + { parameters: (): object => ({ fields: Array(16).fill({ name: 'x', value: 'y' }) }) }, + { parameters: (): object => ({ isLocked: true }) }, + { parameters: (): object => ({ isLocked: false }) }, + { parameters: (): object => ({ isExplorable: false }) }, + { parameters: (): object => ({ isExplorable: true }) }, + { parameters: (): object => ({ hideOnlineStatus: true }) }, + { parameters: (): object => ({ hideOnlineStatus: false }) }, + { parameters: (): object => ({ publicReactions: false }) }, + { parameters: (): object => ({ publicReactions: true }) }, + { parameters: (): object => ({ autoAcceptFollowed: true }) }, + { parameters: (): object => ({ autoAcceptFollowed: false }) }, + { parameters: (): object => ({ noCrawle: true }) }, + { parameters: (): object => ({ noCrawle: false }) }, + { parameters: (): object => ({ preventAiLearning: false }) }, + { parameters: (): object => ({ preventAiLearning: true }) }, + { parameters: (): object => ({ isBot: true }) }, + { parameters: (): object => ({ isBot: false }) }, + { parameters: (): object => ({ isCat: true }) }, + { parameters: (): object => ({ isCat: false }) }, + { parameters: (): object => ({ showTimelineReplies: true }) }, + { parameters: (): object => ({ showTimelineReplies: false }) }, + { parameters: (): object => ({ injectFeaturedNote: true }) }, + { parameters: (): object => ({ injectFeaturedNote: false }) }, + { parameters: (): object => ({ receiveAnnouncementEmail: true }) }, + { parameters: (): object => ({ receiveAnnouncementEmail: false }) }, + { parameters: (): object => ({ alwaysMarkNsfw: true }) }, + { parameters: (): object => ({ alwaysMarkNsfw: false }) }, + { parameters: (): object => ({ autoSensitive: true }) }, + { parameters: (): object => ({ autoSensitive: false }) }, + { parameters: (): object => ({ ffVisibility: 'private' }) }, + { parameters: (): object => ({ ffVisibility: 'followers' }) }, + { parameters: (): object => ({ ffVisibility: 'public' }) }, + { parameters: (): object => ({ mutedWords: Array(19).fill(['xxxxx']) }) }, + { parameters: (): object => ({ mutedWords: [['x'.repeat(194)]] }) }, + { parameters: (): object => ({ mutedWords: [] }) }, + { parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) }, + { parameters: (): object => ({ mutedInstances: [] }) }, + { parameters: (): object => ({ mutingNotificationTypes: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) }, + { parameters: (): object => ({ mutingNotificationTypes: [] }) }, + { parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) }, + { parameters: (): object => ({ emailNotificationTypes: [] }) }, + ] as const)('を書き換えることができる($#)', async ({ parameters }) => { + const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters(), user: alice }); + const expected = { ...meDetailed(alice, true), ...parameters() }; + assert.deepStrictEqual(response, expected, inspect(parameters())); + }); + + test('を書き換えることができる(Avatar)', async () => { + const aliceFile = (await uploadFile(alice)).body; + const parameters = { avatarId: aliceFile.id }; + const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); + assert.match(response.avatarUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); + assert.match(response.avatarBlurhash ?? '.', /[ -~]{54}/); + const expected = { + ...meDetailed(alice, true), + avatarId: aliceFile.id, + avatarBlurhash: response.avatarBlurhash, + avatarUrl: response.avatarUrl, + }; + assert.deepStrictEqual(response, expected, inspect(parameters)); + + const parameters2 = { avatarId: null }; + const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice }); + const expected2 = { + ...meDetailed(alice, true), + avatarId: null, + avatarBlurhash: null, + avatarUrl: alice.avatarUrl, // 解除した場合、identiconになる + }; + assert.deepStrictEqual(response2, expected2, inspect(parameters)); + }); + + test('を書き換えることができる(Banner)', async () => { + const aliceFile = (await uploadFile(alice)).body; + const parameters = { bannerId: aliceFile.id }; + const response = await successfulApiCall({ endpoint: 'i/update', parameters: parameters, user: alice }); + assert.match(response.bannerUrl ?? '.', /^[-a-zA-Z0-9@:%._\+~#&?=\/]+$/); + assert.match(response.bannerBlurhash ?? '.', /[ -~]{54}/); + const expected = { + ...meDetailed(alice, true), + bannerId: aliceFile.id, + bannerBlurhash: response.bannerBlurhash, + bannerUrl: response.bannerUrl, + }; + assert.deepStrictEqual(response, expected, inspect(parameters)); + + const parameters2 = { bannerId: null }; + const response2 = await successfulApiCall({ endpoint: 'i/update', parameters: parameters2, user: alice }); + const expected2 = { + ...meDetailed(alice, true), + bannerId: null, + bannerBlurhash: null, + bannerUrl: null, + }; + assert.deepStrictEqual(response2, expected2, inspect(parameters)); + }); + + //#endregion + //#region 自分の情報の更新(i/pin, i/unpin) + + test('を書き換えることができる(ピン止めノート)', async () => { + const parameters = { noteId: aliceNote.id }; + const response = await successfulApiCall({ endpoint: 'i/pin', parameters, user: alice }); + const expected = { ...meDetailed(alice, false), pinnedNoteIds: [aliceNote.id], pinnedNotes: [aliceNote] }; + assert.deepStrictEqual(response, expected); + + const response2 = await successfulApiCall({ endpoint: 'i/unpin', parameters, user: alice }); + const expected2 = meDetailed(alice, false); + assert.deepStrictEqual(response2, expected2); + }); + + //#endregion + //#region メモの更新(users/update-memo) + + test.each([ + { label: '最大長', memo: 'x'.repeat(2048) }, + { label: '空文字', memo: '', expects: null }, + { label: 'null', memo: null }, + ])('を書き換えることができる(メモを$labelに)', async ({ memo, expects }) => { + const expected = { ...await show(bob.id, alice), memo: expects === undefined ? memo : expects }; + const parameters = { userId: bob.id, memo }; + await successfulApiCall({ endpoint: 'users/update-memo', parameters, user: alice }); + const response = await show(bob.id, alice); + assert.deepStrictEqual(response, expected); + }); + + //#endregion + //#region ユーザー(users) + + test.each([ + { label: 'ID昇順', parameters: { limit: 5 }, selector: (u: UserLite): string => u.id }, + { label: 'フォロワー昇順', parameters: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: 'フォロワー降順', parameters: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: '登録日時昇順', parameters: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '登録日時降順', parameters: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '投稿日時昇順', parameters: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, + { label: '投稿日時降順', parameters: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, + ] as const)('をリスト形式で取得することができる($label)', async ({ parameters, selector }) => { + const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice }); + + // 結果の並びを事前にアサートするのは困難なので返ってきたidに対応するユーザーが返っており、ソート順が正しいことだけを検証する + const users = await Promise.all(response.map(u => show(u.id, alice))); + const expected = users.sort((x, y) => { + const index = (selector(x) < selector(y)) ? -1 : (selector(x) > selector(y)) ? 1 : 0; + return index * (parameters.sort?.startsWith('+') ? -1 : 1); + }); + assert.deepStrictEqual(response, expected); + }); + test.each([ + { label: '「見つけやすくする」がOFFのユーザーが含まれない', user: (): User => userNotExplorable, excluded: true }, + { label: 'ミュートユーザーが含まれない', user: (): User => userMutedByAlice, excluded: true }, + { label: 'ブロックされているユーザーが含まれない', user: (): User => userBlockedByAlice, excluded: true }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice, excluded: true }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, + ] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => { + const parameters = { limit: 100 }; + const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice }); + const expected = (excluded ?? false) ? [] : [await show(user().id, alice)]; + assert.deepStrictEqual(response.filter((u) => u.id === user().id), expected); + }); + test.todo('をリスト形式で取得することができる(リモート, hostname指定)'); + test.todo('をリスト形式で取得することができる(pagenation)'); + + //#endregion + //#region ユーザー情報(users/show) + + test.each([ + { label: 'ID指定で自分自身を', parameters: (): object => ({ userId: alice.id }), user: (): User => alice, type: meDetailed }, + { label: 'ID指定で他人を', parameters: (): object => ({ userId: alice.id }), user: (): User => bob, type: userDetailedNotMeWithRelations }, + { label: 'ID指定かつ未認証', parameters: (): object => ({ userId: alice.id }), user: undefined, type: userDetailedNotMe }, + { label: '@指定で自分自身を', parameters: (): object => ({ username: alice.username }), user: (): User => alice, type: meDetailed }, + { label: '@指定で他人を', parameters: (): object => ({ username: alice.username }), user: (): User => bob, type: userDetailedNotMeWithRelations }, + { label: '@指定かつ未認証', parameters: (): object => ({ username: alice.username }), user: undefined, type: userDetailedNotMe }, + ] as const)('を取得することができる($label)', async ({ parameters, user, type }) => { + const response = await successfulApiCall({ endpoint: 'users/show', parameters: parameters(), user: user?.() }); + const expected = type(alice); + assert.deepStrictEqual(response, expected); + }); + test.each([ + { label: 'Administratorになっている', user: (): User => userAdmin, me: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin }, + { label: '自分以外から見たときはAdministratorか判定できない', user: (): User => userAdmin, selector: (user: User): unknown => user.isAdmin, expected: (): undefined => undefined }, + { label: 'Moderatorになっている', user: (): User => userModerator, me: (): User => userModerator, selector: (user: User): unknown => user.isModerator }, + { label: '自分以外から見たときはModeratorか判定できない', user: (): User => userModerator, selector: (user: User): unknown => user.isModerator, expected: (): undefined => undefined }, + { label: 'サイレンスになっている', user: (): User => userSilenced, selector: (user: User): unknown => user.isSilenced }, + //{ label: 'サスペンドになっている', user: (): User => userSuspended, selector: (user: User): unknown => user.isSuspended }, + { label: '削除済みになっている', user: (): User => userDeletedBySelf, me: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted }, + { label: '自分以外から見たときは削除済みか判定できない', user: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined }, + { label: '削除済み(byAdmin)になっている', user: (): User => userDeletedByAdmin, me: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted }, + { label: '自分以外から見たときは削除済み(byAdmin)か判定できない', user: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined }, + { label: 'フォロー中になっている', user: (): User => userFollowedByAlice, selector: (user: User): unknown => user.isFollowing }, + { label: 'フォローされている', user: (): User => userFollowingAlice, selector: (user: User): unknown => user.isFollowed }, + { label: 'ブロック中になっている', user: (): User => userBlockedByAlice, selector: (user: User): unknown => user.isBlocking }, + { label: 'ブロックされている', user: (): User => userBlockingAlice, selector: (user: User): unknown => user.isBlocked }, + { label: 'ミュート中になっている', user: (): User => userMutedByAlice, selector: (user: User): unknown => user.isMuted }, + { label: 'リノートミュート中になっている', user: (): User => userRnMutedByAlice, selector: (user: User): unknown => user.isRenoteMuted }, + { label: 'フォローリクエスト中になっている', user: (): User => userFollowRequested, me: (): User => userFollowRequesting, selector: (user: User): unknown => user.hasPendingFollowRequestFromYou }, + { label: 'フォローリクエストされている', user: (): User => userFollowRequesting, me: (): User => userFollowRequested, selector: (user: User): unknown => user.hasPendingFollowRequestToYou }, + ] as const)('を取得することができ、$labelこと', async ({ user, me, selector, expected }) => { + const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: user().id }, user: me?.() ?? alice }); + assert.strictEqual(selector(response), (expected ?? ((): true => true))()); + }); + test('を取得することができ、Publicなロールがセットされていること', async () => { + const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRolePublic.id }, user: alice }); + assert.deepStrictEqual(response.badgeRoles, []); + assert.deepStrictEqual(response.roles, [{ + id: rolePublic.id, + name: rolePublic.name, + color: rolePublic.color, + iconUrl: rolePublic.iconUrl, + description: rolePublic.description, + isModerator: rolePublic.isModerator, + isAdministrator: rolePublic.isAdministrator, + displayOrder: rolePublic.displayOrder, + }]); + }); + test('を取得することができ、バッヂロールがセットされていること', async () => { + const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRoleBadge.id }, user: alice }); + assert.deepStrictEqual(response.badgeRoles, [{ + name: roleBadge.name, + iconUrl: roleBadge.iconUrl, + displayOrder: roleBadge.displayOrder, + }]); + assert.deepStrictEqual(response.roles, []); // バッヂだからといってrolesが取れるとは限らない + }); + test('をID指定のリスト形式で取得することができる(空)', async () => { + const parameters = { userIds: [] }; + const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: alice }); + const expected: [] = []; + assert.deepStrictEqual(response, expected); + }); + test('をID指定のリスト形式で取得することができる', async() => { + const parameters = { userIds: [bob.id, alice.id, carol.id] }; + const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: alice }); + const expected = [ + await successfulApiCall({ endpoint: 'users/show', parameters: { userId: bob.id }, user: alice }), + await successfulApiCall({ endpoint: 'users/show', parameters: { userId: alice.id }, user: alice }), + await successfulApiCall({ endpoint: 'users/show', parameters: { userId: carol.id }, user: alice }), + ]; + assert.deepStrictEqual(response, expected); + }); + test.each([ + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが(モデレーターが見るときは)含まれる', user: (): User => userSuspended, me: (): User => root }, + // BUG サスペンドユーザーを一般ユーザーから見るとrootユーザーが返ってくる + //{ label: 'サスペンドユーザーが(一般ユーザーが見るときは)含まれない', user: (): User => userSuspended, me: (): User => bob, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, + ] as const)('をID指定のリスト形式で取得することができ、結果に$label', async ({ user, me, excluded }) => { + const parameters = { userIds: [user().id] }; + const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: me?.() ?? alice }); + const expected = (excluded ?? false) ? [] : [await show(user().id, me?.() ?? alice)]; + assert.deepStrictEqual(response, expected); + }); + test.todo('をID指定のリスト形式で取得することができる(リモート)'); + + //#endregion + //#region 検索(users/search) + + test('を検索することができる', async () => { + const parameters = { query: 'carol', limit: 10 }; + const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice }); + const expected = [await show(carol.id, alice)]; + assert.deepStrictEqual(response, expected); + }); + test('を検索することができる(UserLite)', async () => { + const parameters = { query: 'carol', detail: false, limit: 10 }; + const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice }); + const expected = [userLite(await show(carol.id, alice))]; + assert.deepStrictEqual(response, expected); + }); + test.each([ + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, + ] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => { + const parameters = { query: user().username, limit: 1 }; + const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice }); + const expected = (excluded ?? false) ? [] : [await show(user().id, alice)]; + assert.deepStrictEqual(response, expected); + }); + test.todo('を検索することができる(リモート)'); + test.todo('を検索することができる(pagenation)'); + + //#endregion + //#region ID指定検索(users/search-by-username-and-host) + + test.each([ + { label: '自分', parameters: { username: 'alice' }, user: (): User[] => [alice] }, + { label: '自分かつusernameが大文字', parameters: { username: 'ALICE' }, user: (): User[] => [alice] }, + { label: 'ローカルのフォロイーでノートなし', parameters: { username: 'userFollowedByAlice' }, user: (): User[] => [userFollowedByAlice] }, + { label: 'ローカルでノートなしは検索に載らない', parameters: { username: 'userNoNote' }, user: (): User[] => [] }, + { label: 'ローカルの他人1', parameters: { username: 'bob' }, user: (): User[] => [bob] }, + { label: 'ローカルの他人2', parameters: { username: 'bob', host: null }, user: (): User[] => [bob] }, + { label: 'ローカルの他人3', parameters: { username: 'bob', host: '.' }, user: (): User[] => [bob] }, + { label: 'ローカル', parameters: { host: null, limit: 1 }, user: (): User[] => [userFollowedByAlice] }, + { label: 'ローカル', parameters: { host: '.', limit: 1 }, user: (): User[] => [userFollowedByAlice] }, + ])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => { + const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice }); + const expected = await Promise.all(user().map(u => show(u.id, alice))); + assert.deepStrictEqual(response, expected); + }); + test.each([ + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, + ] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => { + const parameters = { username: user().username }; + const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice }); + const expected = (excluded ?? false) ? [] : [await show(user().id, alice)]; + assert.deepStrictEqual(response, expected); + }); + test.todo('をID&ホスト指定で検索できる(リモート)'); + + //#endregion + //#region ID指定検索(users/get-frequently-replied-users) + + test('がよくリプライをするユーザーのリストを取得できる', async () => { + const parameters = { userId: alice.id, limit: 5 }; + const response = await successfulApiCall({ endpoint: 'users/get-frequently-replied-users', parameters, user: alice }); + const expected = await Promise.all(usersReplying.slice(0, parameters.limit).map(async (s, i) => ({ + user: await show(s.id, alice), + weight: (usersReplying.length - i) / usersReplying.length, + }))); + assert.deepStrictEqual(response, expected); + }); + test.each([ + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれない', user: (): User => userBlockingAlice, excluded: true }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + //{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, + ] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => { + const replyTo = (await successfulApiCall({ endpoint: 'users/notes', parameters: { userId: user().id }, user: undefined }))[0]; + await post(alice, { text: `@${user().username} test`, replyId: replyTo.id }); + const parameters = { userId: alice.id, limit: 100 }; + const response = await successfulApiCall({ endpoint: 'users/get-frequently-replied-users', parameters, user: alice }); + const expected = (excluded ?? false) ? [] : [await show(user().id, alice)]; + assert.deepStrictEqual(response.map(s => s.user).filter((u) => u.id === user().id), expected); + }); + + //#endregion + //#region ハッシュタグ(hashtags/users) + + test.each([ + { label: 'フォロワー昇順', sort: { sort: '+follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: 'フォロワー降順', sort: { sort: '-follower' }, selector: (u: UserDetailedNotMe): string => String(u.followersCount) }, + { label: '登録日時昇順', sort: { sort: '+createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '登録日時降順', sort: { sort: '-createdAt' }, selector: (u: UserDetailedNotMe): string => u.createdAt }, + { label: '投稿日時昇順', sort: { sort: '+updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, + { label: '投稿日時降順', sort: { sort: '-updatedAt' }, selector: (u: UserDetailedNotMe): string => String(u.updatedAt) }, + ] as const)('をハッシュタグ指定で取得することができる($label)', async ({ sort, selector }) => { + const hashtag = 'test_hashtag'; + await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice }); + const parameters = { tag: hashtag, limit: 5, ...sort }; + const response = await successfulApiCall({ endpoint: 'hashtags/users', parameters, user: alice }); + const users = await Promise.all(response.map(u => show(u.id, alice))); + const expected = users.sort((x, y) => { + const index = (selector(x) < selector(y)) ? -1 : (selector(x) > selector(y)) ? 1 : 0; + return index * (parameters.sort.startsWith('+') ? -1 : 1); + }); + assert.deepStrictEqual(response, expected); + }); + test.each([ + { label: '「見つけやすくする」がOFFのユーザーが含まれる', user: (): User => userNotExplorable }, + { label: 'ミュートユーザーが含まれる', user: (): User => userMutedByAlice }, + { label: 'ブロックされているユーザーが含まれる', user: (): User => userBlockedByAlice }, + { label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice }, + { label: '承認制ユーザーが含まれる', user: (): User => userLocking }, + { label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced }, + { label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true }, + { label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf }, + { label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin }, + ] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user, excluded }) => { + const hashtag = `user_test${user().username}`; + if (user() !== userSuspended) { + // サスペンドユーザーはupdateできない。 + await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: user() }); + } + const parameters = { tag: hashtag, limit: 100, sort: '-follower' } as const; + const response = await successfulApiCall({ endpoint: 'hashtags/users', parameters, user: alice }); + const expected = (excluded ?? false) ? [] : [await show(user().id, alice)]; + assert.deepStrictEqual(response, expected); + }); + test.todo('をハッシュタグ指定で取得することができる(リモート)'); + + //#endregion + //#region オススメユーザー(users/recommendation) + + // BUG users/recommendationは壊れている? > QueryFailedError: missing FROM-clause entry for table "note" + test.skip('のオススメを取得することができる', async () => { + const parameters = {}; + const response = await successfulApiCall({ endpoint: 'users/recommendation', parameters, user: alice }); + const expected = await Promise.all(response.map(u => show(u.id))); + assert.deepStrictEqual(response, expected); + }); + + //#endregion + //#region ピン止めユーザー(pinned-users) + + test('のピン止めユーザーを取得することができる', async () => { + await successfulApiCall({ endpoint: 'admin/update-meta', parameters: { pinnedUsers: [bob.username, `@${carol.username}`] }, user: root }); + const parameters = {} as const; + const response = await successfulApiCall({ endpoint: 'pinned-users', parameters, user: alice }); + const expected = await Promise.all([bob, carol].map(u => show(u.id, alice))); + assert.deepStrictEqual(response, expected); + }); + + //#endregion + + test.todo('を管理人として確認することができる(admin/show-user)'); + test.todo('を管理人として確認することができる(admin/show-users)'); + test.todo('をサーバー向けに取得することができる(federation/users)'); +}); diff --git a/packages/calckey-js/src/api.types.ts b/packages/calckey-js/src/api.types.ts index 478b86721..267f2ba0a 100644 --- a/packages/calckey-js/src/api.types.ts +++ b/packages/calckey-js/src/api.types.ts @@ -687,6 +687,7 @@ export type Endpoints = { carefulBot?: boolean; autoAcceptFollowed?: boolean; noCrawle?: boolean; + preventAiLearning?: boolean; isBot?: boolean; isCat?: boolean; injectFeaturedNote?: boolean; diff --git a/packages/calckey-js/src/entities.ts b/packages/calckey-js/src/entities.ts index bf881df2f..f6bbda728 100644 --- a/packages/calckey-js/src/entities.ts +++ b/packages/calckey-js/src/entities.ts @@ -104,6 +104,7 @@ export type MeDetailed = UserDetailed & { mutedWords: string[][]; mutingNotificationTypes: string[]; noCrawle: boolean; + preventAiLearning: boolean; receiveAnnouncementEmail: boolean; usePasswordLessLogin: boolean; [other: string]: any; diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue index c3ab41a64..f0d926428 100644 --- a/packages/client/src/pages/settings/privacy.vue +++ b/packages/client/src/pages/settings/privacy.vue @@ -60,6 +60,10 @@ {{ i18n.ts.noCrawle }} + + {{ i18n.ts.preventAiLearning }}{{ i18n.ts.beta }} + + Date: Wed, 10 May 2023 10:54:56 +0900 Subject: [PATCH 111/146] =?UTF-8?q?=E5=BF=B5=E3=81=AE=E3=81=9F=E3=82=81noi?= =?UTF-8?q?mageai=E3=82=82=E3=81=A4=E3=81=91=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/web/views/clip.pug | 1 + packages/backend/src/server/web/views/gallery-post.pug | 1 + packages/backend/src/server/web/views/note.pug | 1 + packages/backend/src/server/web/views/page.pug | 1 + packages/backend/src/server/web/views/user.pug | 1 + 5 files changed, 5 insertions(+) diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug index e40127f13..4c29d64d1 100644 --- a/packages/backend/src/server/web/views/clip.pug +++ b/packages/backend/src/server/web/views/clip.pug @@ -26,6 +26,7 @@ block meta meta(name='robots' content='noindex') if profile.preventAiLearning meta(name='robots' content='noai') + meta(name='robots' content='noimageai') meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug index 4c8fa3098..86bbb4769 100644 --- a/packages/backend/src/server/web/views/gallery-post.pug +++ b/packages/backend/src/server/web/views/gallery-post.pug @@ -26,6 +26,7 @@ block meta meta(name='robots' content='noindex') if profile.preventAiLearning meta(name='robots' content='noai') + meta(name='robots' content='noimageai') meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug index 3f432bfe9..ff2a77165 100644 --- a/packages/backend/src/server/web/views/note.pug +++ b/packages/backend/src/server/web/views/note.pug @@ -38,6 +38,7 @@ block meta meta(name='robots' content='noindex') if profile.preventAiLearning meta(name='robots' content='noai') + meta(name='robots' content='noimageai') meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug index 67479cf81..eacaaab98 100644 --- a/packages/backend/src/server/web/views/page.pug +++ b/packages/backend/src/server/web/views/page.pug @@ -26,6 +26,7 @@ block meta meta(name='robots' content='noindex') if profile.preventAiLearning meta(name='robots' content='noai') + meta(name='robots' content='noimageai') meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug index ce233fc10..1cc429156 100644 --- a/packages/backend/src/server/web/views/user.pug +++ b/packages/backend/src/server/web/views/user.pug @@ -25,6 +25,7 @@ block meta meta(name='robots' content='noindex') if profile.preventAiLearning meta(name='robots' content='noai') + meta(name='robots' content='noimageai') meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-id' content=user.id) From 016ee7c621cd47bf4f74f62412307f2701fcfed6 Mon Sep 17 00:00:00 2001 From: jolupa Date: Wed, 10 May 2023 21:15:38 +0000 Subject: [PATCH 112/146] chore: Translated using Weblate (Catalan) Currently translated at 100.0% (1735 of 1735 strings) Translation: Calckey/locales Translate-URL: https://hosted.weblate.org/projects/calckey/locales/ca/ --- locales/ca-ES.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index f4bdad205..21b62dab6 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1599,7 +1599,7 @@ squareAvatars: Mostra avatars quadrats secureModeInfo: Quan es faci una solicitut d'altres instàncies no contestar sense una prova. privateModeInfo: Quan està activat, només les instàncies de la llista blanca es poden - federar amb les vostres instàncies. Totes les notes s'amagaran al públic. + federar amb la vostra instància. Totes les publicacions s'amagaran al públic. useBlurEffect: Utilitzeu efectes de desenfocament a la interfície d'usuari accountDeletionInProgress: La supressió del compte està en curs unmuteThread: Desfés el silenci al fil @@ -2034,3 +2034,4 @@ userSaysSomethingReasonReply: '{name} ha respost a una publicació que conté {r userSaysSomethingReasonRenote: '{name} ha impulsat una publicació que conté {reason}' highlightCw: Ressalta el contingut de les publicacions advertides apps: Aplicacions +sendModMail: Envia avís de moderació From 70de728188fde31965f9da592e9f05104785b47f Mon Sep 17 00:00:00 2001 From: Michael 465537 Date: Thu, 11 May 2023 02:19:11 +0000 Subject: [PATCH 113/146] chore: Translated using Weblate (German) Currently translated at 97.5% (1693 of 1735 strings) Translation: Calckey/locales Translate-URL: https://hosted.weblate.org/projects/calckey/locales/de/ --- locales/de-DE.yml | 102 +++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/locales/de-DE.yml b/locales/de-DE.yml index bf3605205..800d2677b 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -270,7 +270,7 @@ upload: "Hochladen" keepOriginalUploading: "Originalbild speichern" keepOriginalUploadingDescription: "Speichert das Originalbild so, wie es ist. Ist\ \ dies deaktiviert, wird eine Version zum Anzeigen im Internet generiert." -fromDrive: "Aus Drive" +fromDrive: "Aus dem Cloud-Drive" fromUrl: "Von einer URL" uploadFromUrl: "Von einer URL hochladen" uploadFromUrlDescription: "URL der hochzuladenden Datei" @@ -301,7 +301,7 @@ dark: "Dunkel" lightThemes: "Helle Farbschemata" darkThemes: "Dunkle Farbschemata" syncDeviceDarkMode: "Einstellung deines Geräts übernehmen" -drive: "Drive" +drive: "Cloud-Drive" fileName: "Dateiname" selectFile: "Datei auswählen" selectFiles: "Dateien auswählen" @@ -313,7 +313,7 @@ createFolder: "Ordner erstellen" renameFolder: "Ordner umbenennen" deleteFolder: "Ordner löschen" addFile: "Datei hinzufügen" -emptyDrive: "Deine Drive ist leer" +emptyDrive: "Deine Cloud-Drive ist leer" emptyFolder: "Dieser Ordner ist leer" unableToDelete: "Nicht löschbar" inputNewFileName: "Gib einen neuen Dateinamen ein" @@ -349,18 +349,18 @@ today: "Heute" dayX: "{day}" monthX: "{month}" yearX: "{year}" -pages: "Seiten" +pages: "Nutzer-Seiten" integration: "Integration" connectService: "Verbinden" disconnectService: "Trennen" -enableLocalTimeline: "Lokale Timeline aktivieren" -enableGlobalTimeline: "Globale Timeline aktivieren" +enableLocalTimeline: "Local-Timeline aktivieren" +enableGlobalTimeline: "Global-Timeline aktivieren" disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle\ \ Timelines, auch wenn diese deaktiviert sind." registration: "Registrieren" enableRegistration: "Registration neuer Nutzer erlauben" invite: "Einladen" -driveCapacityPerLocalAccount: "Drive-Kapazität pro lokalem Nutzerkonto" +driveCapacityPerLocalAccount: "Cloud-Drive-Kapazität pro lokalem Nutzerkonto" driveCapacityPerRemoteAccount: "Laufwerkskapazität pro Remote-Nutzer" inMb: "In Megabytes" iconUrl: "Icon-URL (favicon etc)" @@ -370,8 +370,8 @@ basicInfo: "Grundlegende Informationen" pinnedUsers: "Angeheftete Nutzer" pinnedUsersDescription: "Gib durch Leerzeichen getrennte Nutzer an, die an die \"\ Erkunden\"-Seite angeheftet werden sollen." -pinnedPages: "Angeheftete Seiten" -pinnedPagesDescription: "Geben Sie die Pfade der Seiten, getrennt durch Zeilenumbrüche,\ +pinnedPages: "Angeheftete Nutzer-Seiten" +pinnedPagesDescription: "Geben Sie die Pfade der Nutzer-Seiten, getrennt durch Zeilenumbrüche,\ \ ein, die Sie an die oberste Startseite dieses Servers anheften möchten." pinnedClipId: "ID des anzuheftenden Clips" pinnedNotes: "Angeheftete Beiträge" @@ -733,10 +733,10 @@ pollVotesCount: "Anzahl gesendeter Antworten auf Umfragen" pollVotedCount: "Anzahl erhaltener Antworten auf Umfragen" yes: "Ja" no: "Nein" -driveFilesCount: "Anzahl der Dateien in Drive" -driveUsage: "Drive-Auslastung" +driveFilesCount: "Anzahl der Dateien in Cloud-Drive" +driveUsage: "Cloud-Drive-Auslastung" noCrawle: "Crawler-Indexierung ablehnen" -noCrawleDescription: "Suchmaschinen bitten, die eigene Profilseite, Beiträge, Seiten\ +noCrawleDescription: "Suchmaschinen bitten, die eigene Profilseite, Beiträge, Nutzer-Seiten\ \ usw. nicht zu indexieren." lockedAccountInfo: "Auch wenn du Follow-Anfragen auf manuelle Bestätigung setzt, wird\ \ jeder deiner Posts öffentlich sichtbar sein, sofern du ihre Sichtbarkeit nicht\ @@ -749,8 +749,8 @@ verificationEmailSent: "Eine Bestätigungsmail wurde an deine Email-Adresse vers notSet: "Nicht konfiguriert" emailVerified: "Email-Adresse bestätigt" noteFavoritesCount: "Anzahl der favorisierten Beiträge" -pageLikesCount: "Anzahl an als \"Gefällt mir\" markierter Seiten" -pageLikedCount: "Anzahl erhaltener \"Gefällt mir\" auf Seiten" +pageLikesCount: "Anzahl an als \"Gefällt mir\" markierter Nutzer-Seiten" +pageLikedCount: "Anzahl erhaltener \"Gefällt mir\" auf Nutzer-Seiten" contact: "Kontakt" useSystemFont: "Standardschriftart des Systems verwenden" clips: "Clips" @@ -847,8 +847,8 @@ switch: "Wechseln" noMaintainerInformationWarning: "Betreiberinformationen sind nicht konfiguriert." noBotProtectionWarning: "Schutz vor Bots ist nicht konfiguriert." configure: "Konfigurieren" -postToGallery: "Neuen Galeriebeitrag erstellen" -gallery: "Galerie" +postToGallery: "Erstelle einen neuen Bilder-Galerie-Beitrag" +gallery: "Bilder-Galerie" recentPosts: "Neue Beiträge" popularPosts: "Beliebte Beiträge" shareWithNote: "Mit Beitrag teilen" @@ -947,7 +947,7 @@ noEmailServerWarning: "Es ist kein Email-Server konfiguriert." thereIsUnresolvedAbuseReportWarning: "Es liegen ungelöste Meldungen vor." recommended: "Empfehlung" check: "Kontrolle" -driveCapOverrideLabel: "Die Drive-Kapazität dieses Nutzers verändern" +driveCapOverrideLabel: "Die Cloud-Drive-Kapazität dieses Nutzers verändern" driveCapOverrideCaption: "Gib einen Wert von 0 oder weniger ein, um die Kapazität\ \ auf den Standard zurückzusetzen." requireAdminForView: "Melde dich mit einem Administratorkonto an, um dies einzusehen." @@ -978,7 +978,7 @@ failedToUpload: "Hochladen fehlgeschlagen" cannotUploadBecauseInappropriate: "Diese Datei kann nicht hochgeladen werden, da Anteile\ \ der Datei als möglicherweise NSFW festgestellt wurden." cannotUploadBecauseNoFreeSpace: "Die Datei konnte nicht hochgeladen werden, da dein\ - \ Drive-Speicherplatz aufgebraucht ist." + \ Cloud-Drive-Speicherplatz aufgebraucht ist." beta: "Beta" enableAutoSensitive: "NSFW-Automarkierung" enableAutoSensitiveDescription: "Erlaubt, wo möglich, die automatische Erkennung und\ @@ -1044,7 +1044,7 @@ _forgotPassword: \ Kontaktiere bitte den Server-Administrator, um dein Passwort zurücksetzen zu\ \ lassen." _gallery: - my: "Meine Galerie" + my: "Meine Bilder-Galerie" liked: "Mit \"Gefällt mir\" markierte Beiträge" like: "Gefällt mir" unlike: "\"Gefällt mir\" entfernen" @@ -1286,7 +1286,7 @@ _theme: buttonHoverBg: "Hintergrund von Schaltflächen (Mouseover)" inputBorder: "Rahmen von Eingabefeldern" listItemHoverBg: "Hintergrund von Listeneinträgen (Mouseover)" - driveFolderBg: "Hintergrund von Drive-Ordnern" + driveFolderBg: "Hintergrund von Cloud-Drive-Ordnern" wallpaperOverlay: "Hintergrundbild-Overlay" badge: "Wappen" messageBg: "Hintergrund von Chats" @@ -1325,26 +1325,26 @@ _tutorial: \ erkennen, ob sie deine Beiträge sehen oder dir folgen wollen." step3_1: "Jetzt ist es an der Zeit, einigen Leuten zu folgen!" step3_2: "Deine Home- und Social-Timeline basiert darauf, wem du folgst, also folge\ - \ für den Anfang ein paar Accounts.\nKlicke das Plus Symbol oben links in einem\ - \ Profil um es zu folgen." + \ für den Anfang ein paar Nutzerkonten.\nKlicke das Plus Symbol oben links in\ + \ einem Profil um ihm zu folgen." step4_1: "Wir bringen dich nach draußen." step4_2: "Für Ihren ersten Beitrag machen einige Leute gerne einen {introduction}-Beitrag\ \ oder ein einfaches \"Hallo Welt!\"" step5_1: "Timelines, Timelines überall!" step5_2: "Dein Server hat {timelines} verschiedene Timelines aktiviert." - step5_3: "Die Home Timeline {icon} ist die Timeline, in der du die Beiträge der\ + step5_3: "Die {icon} Home-Timeline ist die Timeline, in der du die Beiträge der\ \ Nutzerkonten sehen kannst, denen du folgst und von jedem anderen auf diesem\ - \ Server. Solltest du bevorzugen, dass deine Home Timeline nur Beiträge von den\ + \ Server. Solltest du bevorzugen, dass deine Home-Timeline nur Beiträge von den\ \ Nutzerkonten enthält, denen du folgst, kannst du das ganz einfach in den Einstellungen\ \ ändern!" - step5_4: "In der lokalen {Icon} Timeline kannst du die Beiträge aller anderen Mitglieder\ + step5_4: "In der {Icon} Local-Timeline kannst du die Beiträge aller anderen Mitglieder\ \ dieses Servers sehen." - step5_5: "Die Timeline \"Sozial\" {icon} zeigt dir ausschließlich Beiträge von Nutzerkonten\ + step5_5: "Die {icon} Social-Timeline zeigt dir ausschließlich Beiträge von Nutzerkonten\ \ denen Du folgst." - step5_6: "In der Timeline \"Empfehlungen\" {icon} kannst du Beiträge von Servern\ - \ sehen, die dir von den Server-Administratoren empfohlen/vorgeschlagen werden." - step5_7: "In der globalen Timeline {icon} können Sie Beiträge von jedem anderen\ - \ verbundenen Server sehen." + step5_6: "In der {icon} Empfehlungen-Timeline kannst du Beiträge von Servern sehen,\ + \ die dir von den Server-Administratoren empfohlen/vorgeschlagen werden." + step5_7: "In der {icon} Global-Timeline können Sie Beiträge von jedem anderen verbundenen\ + \ Server im fediverse sehen." step6_1: "Also, was ist das hier?" step6_2: "Schön, mit Deiner Anmeldung zu Calckey bist Du gleichzeitig einem Portal\ \ zum Fediverse beigetreten, einem Netzwerk mit Tausenden von verbundenen Servern\ @@ -1373,8 +1373,8 @@ _permissions: "write:account": "Deine Nutzerkontoinformationen bearbeiten" "read:blocks": "Die Liste deiner blockierten Nutzer lesen" "write:blocks": "Die Liste deiner blockierten Nutzer bearbeiten" - "read:drive": "Deine Drive-Dateien und Ordner lesen" - "write:drive": "Deine Drive-Dateien und Ordner bearbeiten oder löschen" + "read:drive": "Deine Cloud-Drive-Dateien und Ordner lesen" + "write:drive": "Deine Cloud-Drive-Dateien und Ordner bearbeiten oder löschen" "read:favorites": "Deine Lesezeichen-Liste lesen" "write:favorites": "Deine Lesezeichen-Liste bearbeiten" "read:following": "Die Liste der Nutzer, denen du folgst, lesen" @@ -1389,19 +1389,19 @@ _permissions: "read:reactions": "Reaktionen lesen" "write:reactions": "Reaktionen bedienen" "write:votes": "Umfragen bedienen" - "read:pages": "Deine Seiten lesen" - "write:pages": "Deine Seiten bearbeiten oder löschen" - "read:page-likes": "Liste der Seiten, die mir gefallen, lesen" - "write:page-likes": "Liste der Seiten, die mir gefallen, bearbeiten" + "read:pages": "Deine Nutzer-Seiten lesen" + "write:pages": "Deine Nutzer-Seiten bearbeiten oder löschen" + "read:page-likes": "Liste der Nutzer-Seiten, die mir gefallen, lesen" + "write:page-likes": "Liste der Nutzer-Seiten, die mir gefallen, bearbeiten" "read:user-groups": "Nutzergruppen lesen" "write:user-groups": "Nutzergruppen bearbeiten oder löschen" "read:channels": "Channels lesen" "write:channels": "Channels bedienen" - "read:gallery": "Beiträge deiner Galerie lesen" - "write:gallery": "Deine Galerie bearbeiten" - "read:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Galerie-Beiträge\ + "read:gallery": "Beiträge deiner Bilder-Galerie lesen" + "write:gallery": "Deine Bilder-Galerie bearbeiten" + "read:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Bilder-Galerie-Beiträge\ \ lesen" - "write:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Galerie-Beiträge\ + "write:gallery-likes": "Liste deiner mit \"Gefällt mir\" markierten Bilder-Galerie-Beiträge\ \ bearbeiten" _auth: shareAccess: "Möchtest du „{name}“ authorisieren, auf dieses Nutzerkonto zugreifen\ @@ -1485,7 +1485,7 @@ _visibility: public: "Öffentlich" publicDescription: "Dein Beitrag wird global für alle Nutzer sichtbar sein" home: "nicht aufgelistet" - homeDescription: "Beitrag nur auf der Home Timeline anzeigen" + homeDescription: "Beitrag nur auf der Home-Timeline anzeigen" followers: "Follower" followersDescription: "Nur für Follower sichtbar" specified: "Direkt" @@ -1551,10 +1551,10 @@ _instanceCharts: files: "Unterschied in der Anzahl an Dateien" filesTotal: "Gesamtanzahl an Dateien" _timelines: - home: "Home Timeline" - local: "Lokale Timeline" - social: "Sozial" - global: "Global" + home: "Home-Timeline" + local: "Local-Timeline" + social: "Social-Timeline" + global: "Global-Timeline" recommended: Empfehlungen _pages: newPage: "Seite erstellen" @@ -1572,18 +1572,18 @@ _pages: viewPage: "Seite anschauen" like: "Gefällt mir" unlike: "\"Gefällt mir\" entfernen" - my: "Meine Seiten" - liked: "Seiten, die mir gefallen" + my: "Meine Nutzer-Seiten" + liked: "Nutzer-Seiten, die mir gefallen" featured: "Beliebt" inspector: "Inspektor" contents: "Inhalte" content: "Seitenblock" variables: "Variablen" title: "Titel" - url: "Seiten-URL" + url: "Nutzer-Seiten-URL" summary: "Zusammenfassung" alignCenter: "Zentrieren" - hideTitleWhenPinned: "Seitentitel wenn angeheftet ausblenden" + hideTitleWhenPinned: "Nutzer-Seitentitel wenn angeheftet ausblenden" font: "Schriftart" fontSerif: "Serif" fontSansSerif: "sans-serif" @@ -1957,7 +1957,7 @@ pushNotificationNotSupported: Dein Browser oder der Server unterstützt keine Pu pushNotification: Push-Benachrichtigungen subscribePushNotification: Push-Benachrichtigungen aktivieren showLocalPosts: 'Zeige lokale Beiträge in:' -homeTimeline: Home Timeline +homeTimeline: Home-Timeline cannotUploadBecauseExceedsFileSizeLimit: Die Datei konnte nicht hochgeladen werden, da sie die maximal zulässige Größe überschreitet moveFromLabel: 'Nutzerkonto von dem Sie umziehen:' @@ -1979,7 +1979,7 @@ swipeOnDesktop: Am Desktop PC das Wischen wie bei mobilen Geräten zulassen enterSendsMessage: Drücken sie zum Senden des Beitrages die Eingabetaste (Strg-Taste ausgeschaltet) showUpdates: Zeigt ein Popup an, wenn Calckey aktualisiert wird. -socialTimeline: Social Timeline +socialTimeline: Social-Timeline moveFrom: Bisheriges Nutzerkonto zu diesem Nutzerkonto umziehen _messaging: groups: Gruppen From 37fea1136bc880959972018dedd4ef2b0555b58a Mon Sep 17 00:00:00 2001 From: ThatOneCalculator Date: Wed, 10 May 2023 23:30:48 -0700 Subject: [PATCH 114/146] calckey.org --- CODE_OF_CONDUCT.md | 2 +- packages/backend/src/server/nodeinfo.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 44e1e220c..143c63d29 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -62,7 +62,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at @thatonecalculator on Codeberg, -`@thatonecalculator@stop.voring.me` or `@t1c@i.calckey.cloud` on the Fediverse, +`@kainoa@calckey.social` on the Fediverse, or kainoa@t1c.dev via email. All complaints will be reviewed and investigated promptly and fairly. diff --git a/packages/backend/src/server/nodeinfo.ts b/packages/backend/src/server/nodeinfo.ts index 8563573d4..2a0e1981a 100644 --- a/packages/backend/src/server/nodeinfo.ts +++ b/packages/backend/src/server/nodeinfo.ts @@ -53,7 +53,7 @@ const nodeinfo2 = async () => { name: "calckey", version: config.version, repository: meta.repositoryUrl, - homepage: "https://calckey.cloud", + homepage: "https://calckey.org/", }, protocols: ["activitypub"], services: { From a719b386e7dafa6d6cb4e6abd952db288fb0bfd7 Mon Sep 17 00:00:00 2001 From: naskya Date: Thu, 11 May 2023 20:10:19 +0900 Subject: [PATCH 115/146] Add local only boost option --- .../client/src/components/MkRenoteButton.vue | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue index 090321003..18da67ea2 100644 --- a/packages/client/src/components/MkRenoteButton.vue +++ b/packages/client/src/components/MkRenoteButton.vue @@ -179,6 +179,42 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => { }); } + if (canRenote) { + buttonActions.push({ + text: `${i18n.ts.renote} (${i18n.ts.local})`, + icon: "ph-hand-first ph-bold ph-lg", + danger: false, + action: () => { + os.api("notes/create", + props.note.visibility === "specified" + ? { + renoteId: props.note.id, + visibility: props.note.visibility, + visibleUserIds: props.note.visibleUserIds, + localOnly: true, + } + : { + renoteId: props.note.id, + visibility: props.note.visibility, + localOnly: true, + } + }); + const el = + ev && + ((ev.currentTarget ?? ev.target) as + | HTMLElement + | null + | undefined); + if (el) { + const rect = el.getBoundingClientRect(); + const x = rect.left + el.offsetWidth / 2; + const y = rect.top + el.offsetHeight / 2; + os.popup(Ripple, { x, y }, {}, "end"); + } + }, + }); + } + if (!defaultStore.state.seperateRenoteQuote) { buttonActions.push({ text: i18n.ts.quote, From 59010e200464f76cd044d8c472192bf21acf59bd Mon Sep 17 00:00:00 2001 From: naskya Date: Thu, 11 May 2023 20:28:32 +0900 Subject: [PATCH 116/146] Fix silly typo, format --- .../client/src/components/MkRenoteButton.vue | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue index 18da67ea2..b86ca535b 100644 --- a/packages/client/src/components/MkRenoteButton.vue +++ b/packages/client/src/components/MkRenoteButton.vue @@ -182,23 +182,24 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => { if (canRenote) { buttonActions.push({ text: `${i18n.ts.renote} (${i18n.ts.local})`, - icon: "ph-hand-first ph-bold ph-lg", + icon: "ph-hand-fist ph-bold ph-lg", danger: false, action: () => { - os.api("notes/create", + os.api( + "notes/create", props.note.visibility === "specified" - ? { - renoteId: props.note.id, - visibility: props.note.visibility, - visibleUserIds: props.note.visibleUserIds, - localOnly: true, - } - : { - renoteId: props.note.id, - visibility: props.note.visibility, - localOnly: true, - } - }); + ? { + renoteId: props.note.id, + visibility: props.note.visibility, + visibleUserIds: props.note.visibleUserIds, + localOnly: true, + } + : { + renoteId: props.note.id, + visibility: props.note.visibility, + localOnly: true, + } + ); const el = ev && ((ev.currentTarget ?? ev.target) as From 0e07a2773da2e5605e95a4229eecfdd44f196c6f Mon Sep 17 00:00:00 2001 From: Pyrox Date: Thu, 11 May 2023 09:25:08 -0400 Subject: [PATCH 117/146] docs: Add configuration for Caddy --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6577e45a2..79bd8f86c 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ If you have access to a server that supports one of the sources below, I recomme - Web Proxy (one of the following) - 🍀 Nginx (recommended) - 🪶 Apache + - 🦦 Caddy ### 😗 Optional dependencies @@ -183,7 +184,15 @@ For migrating from Misskey v13, Misskey v12, and Foundkey, read [this document]( - Run `sudo a2ensite calckey.apache` to enable the site - Run `sudo service apache2 restart` to reload apache2 configuration - +### 🦦 Caddy + +- Add the following block to your `Caddyfile`, replacing `example.tld` with your own domain: +```caddy +example.tld { + reverse_proxy http://127.0.0.1:3000 +} +``` +- Reload your caddy configuration ## 🚀 Build and launch! From 91345ceed17fee16488fd8b17ed1c0f64b041231 Mon Sep 17 00:00:00 2001 From: Freeplay Date: Thu, 11 May 2023 18:34:44 -0400 Subject: [PATCH 118/146] Replace classic view widgets w/ the one from default view --- packages/client/src/ui/classic.vue | 5 +- packages/client/src/ui/classic.widgets.vue | 127 --------------------- 2 files changed, 2 insertions(+), 130 deletions(-) delete mode 100644 packages/client/src/ui/classic.widgets.vue diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue index 266effd9a..2c7f12aca 100644 --- a/packages/client/src/ui/classic.vue +++ b/packages/client/src/ui/classic.vue @@ -72,7 +72,7 @@ import { import { defaultStore } from "@/store"; import { i18n } from "@/i18n"; const XHeaderMenu = defineAsyncComponent(() => import("./classic.header.vue")); -const XWidgets = defineAsyncComponent(() => import("./classic.widgets.vue")); +const XWidgets = defineAsyncComponent(() => import("./universal.widgets.vue")); const DESKTOP_THRESHOLD = 1100; @@ -101,7 +101,7 @@ provide("shouldSpacerMin", true); function attachSticky(el) { const sticky = new StickySidebar( el, - defaultStore.state.menuDisplay === "top" ? 0 : 16, + defaultStore.state.menuDisplay === 0, defaultStore.state.menuDisplay === "top" ? 60 : 0 ); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す window.addEventListener( @@ -282,7 +282,6 @@ onMounted(() => { > .widgets { //--panelBorder: none; width: 300px; - margin-top: 16px; @media (max-width: $widgets-hide-threshold) { display: none; diff --git a/packages/client/src/ui/classic.widgets.vue b/packages/client/src/ui/classic.widgets.vue deleted file mode 100644 index 2c7f16ece..000000000 --- a/packages/client/src/ui/classic.widgets.vue +++ /dev/null @@ -1,127 +0,0 @@ - - - - - From 547848d663229401f43d50c096a78f29f3540187 Mon Sep 17 00:00:00 2001 From: Freeplay Date: Thu, 11 May 2023 19:46:42 -0400 Subject: [PATCH 119/146] Replace classic navbar w/ new --- packages/client/src/ui/_common_/navbar.vue | 17 +++--- packages/client/src/ui/classic.vue | 61 ++++++++++++++++++---- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue index 4ff10845f..31c4a40c3 100644 --- a/packages/client/src/ui/_common_/navbar.vue +++ b/packages/client/src/ui/_common_/navbar.vue @@ -1,5 +1,5 @@ @@ -128,13 +139,16 @@ import { ref } from "vue"; import * as misskey from "calckey-js"; import * as mfm from "mfm-js"; +import * as os from "@/os"; import XNoteSimple from "@/components/MkNoteSimple.vue"; import XMediaList from "@/components/MkMediaList.vue"; import XPoll from "@/components/MkPoll.vue"; import MkUrlPreview from "@/components/MkUrlPreview.vue"; import XShowMoreButton from "@/components/MkShowMoreButton.vue"; import XCwButton from "@/components/MkCwButton.vue"; +import MkButton from "@/components/MkButton.vue"; import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm"; +import { extractMfmWithAnimation } from "@/scripts/extract-mfm"; import { i18n } from "@/i18n"; const props = defineProps<{ @@ -164,6 +178,26 @@ const urls = props.note.text let showContent = $ref(false); +const mfms = props.note.text ? extractMfmWithAnimation(mfm.parse(props.note.text)) : null; + +const hasMfm = $ref(mfms.length > 0); + +let disableMfm = $ref(hasMfm); + +async function toggleMfm() { + if (disableMfm) { + const { canceled } = await os.confirm({ + type: "warning", + text: i18n.ts._mfm.warn, + }); + if (canceled) return; + + disableMfm = false; + } else { + disableMfm = true; + } +} + function focusFooter(ev) { if (ev.key == "Tab" && !ev.getModifierState("Shift")) { emit("focusfooter"); @@ -195,6 +229,12 @@ function focusFooter(ev) { margin-right: 8px; } } + +.mfm-warning { + button { + padding: 1em; + } +} .wrmlmaau { .content { overflow-wrap: break-word; @@ -286,6 +326,11 @@ function focusFooter(ev) { } } } + + &.disableAnim :deep(*) { + animation: none !important; + transition: none !important; + } } } diff --git a/packages/client/src/scripts/extract-mfm.ts b/packages/client/src/scripts/extract-mfm.ts new file mode 100644 index 000000000..88b1bb63f --- /dev/null +++ b/packages/client/src/scripts/extract-mfm.ts @@ -0,0 +1,16 @@ +import * as mfm from "mfm-js"; + +const animatedMfm = ["tada", "jelly", "twitch", "shake", "spin", "jump", "bounce", "rainbow"]; + +export function extractMfmWithAnimation( + nodes: mfm.MfmNode[], +): string[] { + const mfmNodes = mfm.extract(nodes, (node) => { + return ( + node.type === "fn" && animatedMfm.indexOf(node.props.name) > -1 + ); + }); + const mfms = mfmNodes.map((x) => x.props.fn); + + return mfms; +} From 62e0ded409c84f55d37b39df797b41033f41500b Mon Sep 17 00:00:00 2001 From: Pyrox Date: Thu, 11 May 2023 09:11:28 -0400 Subject: [PATCH 133/146] flake: Cleanup devenv on clean, add helper scripts, and run dev server on `devenv up` Also adds a new config example for use with the devenv scripts, as well as a developer's guide for setting up the Nix environment. This could also have steps for speific distros, such as what packages to install, and specific notes. --- .config/devenv.yml | 38 ++++++++++++++++++++++++++++++++++++++ .gitignore | 1 + docs/development.md | 22 ++++++++++++++++++++++ flake.nix | 14 ++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 .config/devenv.yml create mode 100644 docs/development.md diff --git a/.config/devenv.yml b/.config/devenv.yml new file mode 100644 index 000000000..6c60f338a --- /dev/null +++ b/.config/devenv.yml @@ -0,0 +1,38 @@ +url: http://localhost:3000 +port: 3000 + +db: + host: 127.0.0.1 + port: 5432 + + db: calckey + + user: calckey + pass: calckey + +redis: + host: localhost + port: 6379 + family: 4 +#sonic: +# host: localhost +# port: 1491 +# auth: SecretPassword +# collection: notes +# bucket: default + +#elasticsearch: +# host: localhost +# port: 9200 +# ssl: false +# user: +# pass: + +id: 'aid' + +reservedUsernames: + - root + - admin + - administrator + - me + - system diff --git a/.gitignore b/.gitignore index 1fadce431..63ee4f35f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ coverage # config /.config/* !/.config/example.yml +!/.config/devenv.yml !/.config/docker_example.env !/.config/helm_values_example.yml diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 000000000..41d1b3469 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,22 @@ +# 🌎 Calckey Developer Docs + +## Nix Dev Environment +The Calckey repo comes with a Nix-based shell environment to help make development as easy as possible! + +Please note, however, that this environment will not work on Windows outside of a WSL2 environment. + +### Prerequisites + +- Installed the [Nix Package Manager](https://nixos.org/download.html) +- Installed [direnv](https://direnv.net/docs/installation.html) and added its hook to your shell. + +Once the repo is cloned to your computer, follow these next few steps inside the Calckey folder: + +- Run `direnv allow`. This will build the environment and install all needed tools. +- Run `install-deps`, then `prepare-config`, to install the node dependencies and prepare the needed config files. +- In a second terminal, run `devenv up`. This will spawn a **Redis** server, a **Postgres** server, and the **Calckey** server in dev mode. +- Once you see the Calckey banner printed in your second terminal, run `migrate` in the first. +- Once migrations finish, open http://localhost:3000 in your web browser. +- You should now see the admin user creation screen! + +Note: When you want to restart a dev server, all you need to do is run `devenv up`, no other steps are necessary. diff --git a/flake.nix b/flake.nix index fd9098f0d..73d8fe02f 100644 --- a/flake.nix +++ b/flake.nix @@ -32,6 +32,8 @@ # Add additional packages to our environment packages = [ pkgs.nodePackages.pnpm + + pkgs.python3 ]; # No need to warn on a new version, we'll update as needed. devenv.warnOnNewVersion = false; @@ -43,6 +45,18 @@ # Enable stable Rust for the backend languages.rust.enable = true; languages.rust.version = "stable"; + processes = { + dev-server.exec = "pnpm run dev"; + }; + scripts = { + build.exec = "pnpm run build"; + clean.exec = "pnpm run clean"; + clear-state.exec = "rm -rf .devenv/state/redis .devenv/state/postgres"; + format.exec = "pnpm run format"; + install-deps.exec = "pnpm install"; + migrate.exec = "pnpm run migrate"; + prepare-config.exec = "cp .config/devenv.yml .config/default.yml"; + }; services = { postgres = { enable = true; From a5f9dfd84a5a3c920b2652a67050c02f30f38c10 Mon Sep 17 00:00:00 2001 From: Freeplay Date: Fri, 12 May 2023 20:05:33 -0400 Subject: [PATCH 134/146] Settings option --- locales/en-US.yml | 1 + .../src/components/MkSubNoteContent.vue | 7 +++- .../client/src/components/form/section.vue | 7 ++-- packages/client/src/components/mfm.ts | 41 ++++--------------- .../client/src/pages/settings/general.vue | 6 +-- 5 files changed, 21 insertions(+), 41 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 5558e538d..a60c6beaf 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1196,6 +1196,7 @@ _mfm: play: "Play MFM" stop: "Stop MFM" warn: "MFM may contain rapidly moving or flashy animations" + alwaysPlay: "Always autoplay all MFM" cheatSheet: "MFM Cheatsheet" intro: "MFM is a markup language used on Misskey, Calckey, Akkoma, and more that\ \ can be used in many places. Here you can view a list of all available MFM syntax." diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue index 773f7f26e..a3c52deb0 100644 --- a/packages/client/src/components/MkSubNoteContent.vue +++ b/packages/client/src/components/MkSubNoteContent.vue @@ -121,7 +121,7 @@ > @@ -185,8 +185,6 @@ const hasMfm = $ref(mfms.length > 0); let disableMfm = $ref(hasMfm && defaultStore.state.animatedMfm); -console.log(disableMfm + " " + props.note.id + " " + defaultStore.state.animatedMfm); - async function toggleMfm() { if (disableMfm) { const { canceled } = await os.confirm({ @@ -335,5 +333,8 @@ function focusFooter(ev) { transition: none !important; } } + > :deep(button) { + margin-top: 10px; + } } From f0fe5fcf6c658e97476f4a07a5ba8d847747d917 Mon Sep 17 00:00:00 2001 From: Freeplay Date: Fri, 12 May 2023 21:19:56 -0400 Subject: [PATCH 136/146] Add reduced motion & autoplay MFM toggles to welcome popup --- .../src/components/MkTutorialDialog.vue | 60 +++++++++++++------ .../client/src/pages/settings/general.vue | 10 +++- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/packages/client/src/components/MkTutorialDialog.vue b/packages/client/src/components/MkTutorialDialog.vue index 8fecc4429..2d5dc4519 100644 --- a/packages/client/src/components/MkTutorialDialog.vue +++ b/packages/client/src/components/MkTutorialDialog.vue @@ -41,16 +41,26 @@ {{ i18n.ts.next }}
-

- - {{ i18n.ts._tutorial.title }} -

-
+
+

+ + {{ i18n.ts._tutorial.title }} +

{{ i18n.ts._tutorial.step1_1 }}

{{ i18n.ts._tutorial.step1_2 }}
-
-
+ {{ i18n.ts._mfm.alwaysPlay }} + + + + {{ i18n.ts.reduceUiAnimation }} + + +

-
-
+
{{ i18n.ts.next }} -
-
+

-
-
+
-
-
+
{{ i18n.ts.pwa }} -
+
@@ -196,7 +206,7 @@