diff --git a/.config/docker_example.env b/.config/docker_example.env index 7a0261524..fdd7e3108 100644 --- a/.config/docker_example.env +++ b/.config/docker_example.env @@ -1,4 +1,4 @@ # db settings -POSTGRES_PASSWORD=example-misskey-pass -POSTGRES_USER=example-misskey-user -POSTGRES_DB=misskey +POSTGRES_PASSWORD=example-calckey-pass +POSTGRES_USER=example-calckey-user +POSTGRES_DB=calckey diff --git a/CALCKEY.md b/CALCKEY.md index 4a85c43b4..dfcb191e4 100644 --- a/CALCKEY.md +++ b/CALCKEY.md @@ -9,7 +9,6 @@ - User "choices" (recommended users) like Mastodon and Soapbox - Option to publicize instance blocks - Fully revamp non-logged-in screen -- Remote follow button - Personal notes for all accounts - Non-nyaify cat mode - Timeline filters @@ -21,8 +20,8 @@ ## Work in progress - Better Messaging UI - - Videos can be played in DMs -- Make your password hasn't been pwned +- Better API Documentation +- Remote follow button - Admin custom CSS - Add back time machine (jump to date) - Improve accesibility score @@ -86,7 +85,12 @@ - Link hover effect - Replace all `$ts` with i18n - AVIF support +- Page drafts +- Patron list +- Animations respect reduced motion - Obliteration of Ai-chan +- Undo renote button inside original note +- 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) - [OAuth bearer token authentication](https://github.com/misskey-dev/misskey/pull/9021) diff --git a/README.md b/README.md index 828e92b6f..8e4b03be9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@
- + Calckey logo -**๐ŸŒŽ **[Calckey](https://stop.voring.me/)** is an open source, decentralized social media platform that's free forever! ๐Ÿš€** +**๐ŸŒŽ **[Calckey](https://i.calckey.cloud/)** is an open source, decentralized social media platform that's free forever! ๐Ÿš€**
@@ -20,6 +20,7 @@ - Improved UI/UX (especially on mobile) - Improved notifications - Improved instance security + - Improved accessibility - Recommended Instances timeline - OCR image captioning - New and improved Groups @@ -34,6 +35,9 @@ # ๐Ÿฅ‚ Links - ๐Ÿ’ธ Liberapay: + - Donate publicly to get your name on the Patron list! +- ๐Ÿšข Flagship instance: +- ๐Ÿ“ฃ Official account: - ๐Ÿ’ Matrix support room: - ๐Ÿ“œ Instance list: - ๐Ÿ“– JoinFediverse Wiki: @@ -45,15 +49,27 @@ This guide will work for both **starting from scratch** and **migrating from Mis ## ๐Ÿ“ฆ Dependencies -- At least ๐Ÿข [NodeJS](https://nodejs.org/en/) v16.15.0 (v18.12.1 recommended) - -> โš ๏ธ NodeJS v19 is not supported as of right now because of [this issue](https://github.com/nodejs/node-gyp/issues/2757). - +- ๐Ÿข At least [NodeJS](https://nodejs.org/en/) v18.12.1 (v19.1.0 recommended) + - 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 recommended) -- ๐Ÿ›ฐ๏ธ (Optional, for non-Docker) [pm2](https://pm2.io/) +### ๐Ÿ˜— Optional dependencies + +- ๐Ÿ“— [FFmpeg](https://ffmpeg.org/) for video transcoding +- ๐Ÿ” [ElasticSearch](https://www.elastic.co/elasticsearch/) for full-text search + - OpenSearch/Sonic are not supported as of right now +- ๐Ÿฅก Management (choose one of the following) + - ๐Ÿ›ฐ๏ธ [pm2](https://pm2.io/) + - ๐Ÿณ [Docker](https://docker.com) + - ๐Ÿ“ Service manager (systemd, openrc, etc) + +### ๐Ÿ—๏ธ Build dependencies + +- ๐Ÿฆฌ C/C++ compiler & build tools + - `build-essential` on Debian/Ubuntu Linux + - `base-devel` on Arch Linux +- ๐Ÿ [Python 3](https://www.python.org/) ## ๐Ÿ‘€ Get folder ready @@ -70,10 +86,19 @@ cd calckey/ corepack enable ``` +## ๐Ÿ˜ Create database + +Assuming you set up PostgreSQL correctly, all you have to run is: + +```sh +psql postgres -c "create database calckey with encoding = 'UTF8';" +``` + ## ๐Ÿ’… Customize -- To add custom CSS for all users, edit `./custom/instance.css`. -- To add static assets (such as images for the splash screen), place them in the `./custom/` directory. They'll then be avaliable on `https://yourinstance.tld/static-assets/filename.ext`. +- 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 avaliable 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 update custom assets without rebuilding, just run `yarn run gulp`. ## ๐Ÿง‘โ€๐Ÿ”ฌ Configuring a new instance @@ -93,7 +118,7 @@ cp -r ../misskey/files . # if you don't use object storage ## ๐Ÿ€ NGINX -- Run `sudo cp ./calckey.nginx.conf /etc/nginx/sites-avaliable/ && cd /etc/nginx/sites-avaliable/` +- 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 nginx -t` to validate that the config is valid, then restart the NGINX service. @@ -102,7 +127,7 @@ cp -r ../misskey/files . # if you don't use object storage ## ๐Ÿš€ Build and launch! -### ๐Ÿข NodeJS +### ๐Ÿข NodeJS + pm2 #### `git pull` and run these steps to update Calckey in the future! @@ -123,15 +148,16 @@ docker up -d ### ๐Ÿณ Docker Compose ```sh -docker compose build +docker-compose build docker-compose run --rm web yarn run init -docker compose up -d +docker-compose up -d ``` ## ๐Ÿ˜‰ Tips & Tricks - 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 $(seq 3000 4000); do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1` +- 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`, the 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/custom/instance.css b/custom/assets/instance.css similarity index 100% rename from custom/instance.css rename to custom/assets/instance.css diff --git a/custom/locales/.gitkeep b/custom/locales/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/gulpfile.js b/gulpfile.js index 0bf4c276d..89a6acb83 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,7 +16,7 @@ gulp.task('copy:backend:views', () => ); gulp.task('copy:backend:custom', () => - gulp.src('./custom/*').pipe(gulp.dest('./packages/backend/assets/')) + gulp.src('./custom/assets/*').pipe(gulp.dest('./packages/backend/assets/')) ); gulp.task('copy:client:fonts', () => @@ -24,7 +24,7 @@ gulp.task('copy:client:fonts', () => ); gulp.task('copy:client:phosphor', () => - gulp.src('./node_modules/phosphor-icons/src/css/*').pipe(gulp.dest('./built/_client_dist_/phosphor/')) + gulp.src('./node_modules/phosphor-icons/src/fonts/*').pipe(gulp.dest('./built/_client_dist_/phosphor/')) ); gulp.task('copy:client:locales', cb => { diff --git a/locales/en-US.yml b/locales/en-US.yml index 1215055d5..aaef40b34 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -32,12 +32,12 @@ uploading: "Uploading..." save: "Save" users: "Users" addUser: "Add a user" -favorite: "Add to favorites" -favorites: "Favorites" -unfavorite: "Remove from favorites" -favorited: "Added to favorites." -alreadyFavorited: "Already added to favorites." -cantFavorite: "Couldn't add to favorites." +favorite: "Add to bookmarks" +favorites: "Bookmarks" +unfavorite: "Remove from bookmarks" +favorited: "Added to bookmarks." +alreadyFavorited: "Already added to bookmarks." +cantFavorite: "Couldn't add to bookmarks." pin: "Pin to profile" unpin: "Unpin from profile" copyContent: "Copy contents" @@ -160,7 +160,7 @@ proxyAccount: "Proxy account" proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead." host: "Host" selectUser: "Select a user" -recipient: "Recipient" +recipient: "Recipient(s)" annotation: "Comments" federation: "Federation" instances: "Instances" @@ -680,7 +680,7 @@ disableShowingAnimatedImages: "Don't play animated images" verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification." notSet: "Not set" emailVerified: "Email has been verified" -noteFavoritesCount: "Number of favorite notes" +noteFavoritesCount: "Number of bookmarked notes" pageLikesCount: "Number of liked Pages" pageLikedCount: "Number of received Page likes" contact: "Contact" @@ -771,8 +771,8 @@ noBotProtectionWarning: "Bot protection is not configured." configure: "Configure" postToGallery: "Create new gallery post" gallery: "Gallery" -recentPosts: "Recent posts" -popularPosts: "Popular posts" +recentPosts: "Recent pages" +popularPosts: "Popular pages" shareWithNote: "Share with note" ads: "Advertisements" expiration: "Deadline" @@ -1002,9 +1002,9 @@ _aboutMisskey: allContributors: "All contributors" source: "Source code" translation: "Translate Misskey" - donate: "Donate to Misskey" + donate: "Donate to Calckey" morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! ๐Ÿฅฐ" - patrons: "Misskey patrons" + patrons: "Calckey patrons" _nsfw: respect: "Hide NSFW media" ignore: "Don't hide NSFW media" @@ -1095,7 +1095,7 @@ _channel: usersCount: "{n} Participants" notesCount: "{n} Notes" _messaging: - dms: "DMs" + dms: "Private" groups: "Groups" _menuDisplay: sideFull: "Side" @@ -1228,7 +1228,7 @@ _tutorial: step5_3: "The Home {icon} timeline is where you can see posts from your followers." step5_4: "The Local {icon} timeline is where you can see posts from everyone else on this instance." step5_5: "The Recommended {icon} timeline is where you can see posts from instances the admins recommend." - step5_6: "The Social {icon} timeline is where you can see posts from friends of your followers." + step5_6: "The Social {icon} timeline is your home + local." step5_7: "The Global {icon} timeline is where you can see posts from every other connected instance." step6_1: "So, what is this place?" step6_2: "Well, you didn't just join Calckey. You joined a portal to the Fediverse, an interconnected network of thousands of servers, called \"instances\"." @@ -1251,8 +1251,8 @@ _permissions: "write:blocks": "Edit your list of blocked users" "read:drive": "Access your Drive files and folders" "write:drive": "Edit or delete your Drive files and folders" - "read:favorites": "View your list of favorites" - "write:favorites": "Edit your list of favorites" + "read:favorites": "View your list of bookmarks" + "write:favorites": "Edit your list of bookmarks" "read:following": "View information on who you follow" "write:following": "Follow or unfollow other accounts" "read:messaging": "View your chats" @@ -1265,10 +1265,10 @@ _permissions: "read:reactions": "View your reactions" "write:reactions": "Edit your reactions" "write:votes": "Vote on a poll" - "read:pages": "View your pages" - "write:pages": "Edit or delete your pages" - "read:page-likes": "View your likes on pages" - "write:page-likes": "Edit your likes on pages" + "read:pages": "View your page" + "write:pages": "Edit or delete your page" + "read:page-likes": "View your likes on page" + "write:page-likes": "Edit your likes on page" "read:user-groups": "View your user groups" "write:user-groups": "Edit or delete your user groups" "read:channels": "View your channels" @@ -1442,7 +1442,7 @@ _pages: liked: "Liked Pages" featured: "Popular" inspector: "Inspector" - contents: "Contents" + contents: "Content" content: "Page block" variables: "Variables" title: "Title" diff --git a/locales/index.js b/locales/index.js index 92cd9b467..7399bb5a1 100644 --- a/locales/index.js +++ b/locales/index.js @@ -4,6 +4,8 @@ const fs = require('fs'); const yaml = require('js-yaml'); +let languages = [] +let languages_custom = [] const merge = (...args) => args.reduce((a, c) => ({ ...a, @@ -13,33 +15,20 @@ const merge = (...args) => args.reduce((a, c) => ({ .reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {}) }), {}); -const languages = [ - 'ar-SA', - 'cs-CZ', - 'da-DK', - 'de-DE', - 'en-US', - 'es-ES', - 'fr-FR', - 'id-ID', - 'it-IT', - 'ja-JP', - 'ja-KS', - 'kab-KAB', - 'kn-IN', - 'ko-KR', - 'nl-NL', - 'no-NO', - 'pl-PL', - 'pt-PT', - 'ru-RU', - 'sk-SK', - 'ug-CN', - 'uk-UA', - 'vi-VN', - 'zh-CN', - 'zh-TW', -]; + +fs.readdirSync(__dirname).forEach((file) => { + if (file.includes('.yml')){ + file = file.slice(0, file.indexOf('.')) + languages.push(file); + } +}) + +fs.readdirSync(__dirname + '/../custom/locales').forEach((file) => { + if (file.includes('.yml')){ + file = file.slice(0, file.indexOf('.')) + languages_custom.push(file); + } +}) const primaries = { 'en': 'US', @@ -51,6 +40,8 @@ const primaries = { const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {}); +const locales_custom = languages_custom.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/../custom/locales/${c}.yml`, 'utf-8'))) || {}, a), {}); +Object.assign(locales, locales_custom) module.exports = Object.entries(locales) .reduce((a, [k ,v]) => (a[k] = (() => { diff --git a/package.json b/package.json index 83b4cec1b..7e0d15401 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "calckey", - "version": "12.119.0-calc.14.6", + "version": "12.119.0-calc.18", "codename": "aqua", "repository": { "type": "git", "url": "https://codeberg.org/thatonecalculator/calckey.git" }, - "packageManager": "yarn@3.2.4", + "packageManager": "yarn@3.3.0", "workspaces": [ "packages/client", "packages/backend", @@ -42,7 +42,7 @@ "@bull-board/api": "^4.6.4", "@bull-board/ui": "^4.6.4", "@tensorflow/tfjs": "^3.21.0", - "eslint": "^8.27.0", + "eslint": "^8.28.0", "execa": "5.1.1", "gulp": "4.0.2", "gulp-cssnano": "2.1.3", diff --git a/packages/backend/migration/1668828368510PageDraft.js b/packages/backend/migration/1668828368510PageDraft.js new file mode 100644 index 000000000..4a6818912 --- /dev/null +++ b/packages/backend/migration/1668828368510PageDraft.js @@ -0,0 +1,8 @@ +export class Page1668828368510 { + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "page" ADD "isPublic" boolean NOT NULL DEFAULT true`); + } + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "isPublic"`); + } +} diff --git a/packages/backend/migration/1668831378728FixCalckeyAgain.js b/packages/backend/migration/1668831378728FixCalckeyAgain.js new file mode 100644 index 000000000..d5e67a48c --- /dev/null +++ b/packages/backend/migration/1668831378728FixCalckeyAgain.js @@ -0,0 +1,11 @@ +export class FixCalckeyAgain1668831378728 { + name = 'FixCalckeyAgain1668831378728' + + async up(queryRunner) { + await queryRunner.query(`UPDATE "meta" SET "useStarForReactionFallback" = TRUE`); + } + + async down(queryRunner) { + await queryRunner.query(`UPDATE "meta" SET "useStarForReactionFallback" = FALSE`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index c7532bed3..2f7a11984 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -4,8 +4,8 @@ "private": true, "type": "module", "scripts": { - "start": "node --experimental-json-modules ./built/index.js", - "start:test": "NODE_ENV=test node --experimental-json-modules ./built/index.js", + "start": "node ./built/index.js", + "start:test": "NODE_ENV=test node ./built/index.js", "migrate": "typeorm migration:run -d ormconfig.js", "build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "watch": "node watch.mjs", @@ -36,11 +36,11 @@ "archiver": "5.3.1", "autobind-decorator": "2.4.0", "autwh": "0.1.0", - "aws-sdk": "2.1255.0", + "aws-sdk": "2.1258.0", "bcryptjs": "2.4.3", "blurhash": "1.1.5", "bull": "4.10.1", - "cacheable-lookup": "6.1.0", + "cacheable-lookup": "7.0.0", "cbor": "8.1.0", "chalk": "5.1.2", "chalk-template": "0.4.0", @@ -83,7 +83,7 @@ "node-fetch": "3.3.0", "nodemailer": "6.8.0", "nsfwjs": "2.4.2", - "oauth": "^0.9.15", + "oauth": "^0.10.0", "os-utils": "0.0.14", "parse5": "7.1.1", "pg": "8.8.0", @@ -111,7 +111,7 @@ "stringz": "2.1.0", "summaly": "2.7.0", "syslog-pro": "1.0.0", - "systeminformation": "5.12.14", + "systeminformation": "5.13.5", "tesseract.js": "^3.0.3", "tinycolor2": "1.4.2", "tmp": "0.2.1", @@ -130,7 +130,7 @@ "xev": "3.0.2" }, "devDependencies": { - "@redocly/openapi-core": "1.0.0-beta.112", + "@redocly/openapi-core": "1.0.0-beta.114", "@types/bcryptjs": "2.4.2", "@types/bull": "3.15.9", "@types/cbor": "6.0.0", @@ -165,7 +165,7 @@ "@types/rename": "1.0.4", "@types/sanitize-html": "2.6.2", "@types/semver": "7.3.13", - "@types/sharp": "0.30.5", + "@types/sharp": "0.31.0", "@types/sinonjs__fake-timers": "8.1.2", "@types/speakeasy": "2.0.7", "@types/tinycolor2": "1.4.3", @@ -177,7 +177,7 @@ "@typescript-eslint/eslint-plugin": "5.43.0", "@typescript-eslint/parser": "5.43.0", "cross-env": "7.0.3", - "eslint": "8.27.0", + "eslint": "8.28.0", "eslint-plugin-import": "2.26.0", "execa": "6.1.0", "typescript": "4.9.3" diff --git a/packages/backend/src/misc/clone.ts b/packages/backend/src/misc/clone.ts new file mode 100644 index 000000000..16fad2412 --- /dev/null +++ b/packages/backend/src/misc/clone.ts @@ -0,0 +1,18 @@ +// structredCloneใŒ้…ใ„ใŸใ‚ +// SEE: http://var.blog.jp/archives/86038606.html + +type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[]; + +export function deepClone(x: T): T { + if (typeof x === 'object') { + if (x === null) return x; + if (Array.isArray(x)) return x.map(deepClone) as T; + const obj = {} as Record; + for (const [k, v] of Object.entries(x)) { + obj[k] = deepClone(v); + } + return obj as T; + } else { + return x; + } +} diff --git a/packages/backend/src/models/entities/page.ts b/packages/backend/src/models/entities/page.ts index baad3a36f..fac59479e 100644 --- a/packages/backend/src/models/entities/page.ts +++ b/packages/backend/src/models/entities/page.ts @@ -40,6 +40,9 @@ export class Page { @Column('boolean') public alignCenter: boolean; + @Column('boolean') + public isPublic: boolean; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index 4366b02cc..a01fd86c6 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -9,6 +9,8 @@ import { query, appendQuery } from '@/prelude/url.js'; import { Meta } from '@/models/entities/meta.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; import { Users, DriveFolders } from '../index.js'; +import { deepClone } from '@/misc/clone.js'; + type PackOptions = { detail?: boolean, @@ -29,9 +31,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({ getPublicProperties(file: DriveFile): DriveFile['properties'] { if (file.properties.orientation != null) { - // TODO - //const properties = structuredClone(file.properties); - const properties = JSON.parse(JSON.stringify(file.properties)); + const properties = deepClone(file.properties); if (file.properties.orientation >= 5) { [properties.width, properties.height] = [properties.height, properties.width]; } diff --git a/packages/backend/src/models/repositories/notification.ts b/packages/backend/src/models/repositories/notification.ts index 42b47ab15..efa3e860b 100644 --- a/packages/backend/src/models/repositories/notification.ts +++ b/packages/backend/src/models/repositories/notification.ts @@ -1,14 +1,14 @@ import { In, Repository } from 'typeorm'; -import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js'; import { Notification } from '@/models/entities/notification.js'; import { awaitAll } from '@/prelude/await-all.js'; -import { Packed } from '@/misc/schema.js'; -import { Note } from '@/models/entities/note.js'; -import { NoteReaction } from '@/models/entities/note-reaction.js'; -import { User } from '@/models/entities/user.js'; +import type { Packed } from '@/misc/schema.js'; +import type { Note } from '@/models/entities/note.js'; +import type { NoteReaction } from '@/models/entities/note-reaction.js'; +import type { User } from '@/models/entities/user.js'; import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js'; import { notificationTypes } from '@/types.js'; import { db } from '@/db/postgre.js'; +import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js'; export const NotificationRepository = db.getRepository(Notification).extend({ async pack( @@ -17,7 +17,7 @@ export const NotificationRepository = db.getRepository(Notification).extend({ _hintForEachNotes_?: { myReactions: Map; }; - } + }, ): Promise> { const notification = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({ id: notification.appAccessTokenId }) : null; @@ -86,7 +86,7 @@ export const NotificationRepository = db.getRepository(Notification).extend({ async packMany( notifications: Notification[], - meId: User['id'] + meId: User['id'], ) { if (notifications.length === 0) return []; @@ -106,10 +106,15 @@ export const NotificationRepository = db.getRepository(Notification).extend({ await prefetchEmojis(aggregateNoteEmojis(notes)); - return await Promise.all(notifications.map(x => this.pack(x, { - _hintForEachNotes_: { - myReactions: myReactionsMap, - }, - }))); + const results = await Promise.all(notifications + .map(x => + this.pack(x, { + _hintForEachNotes_: { + myReactions: myReactionsMap, + }, + }).catch(e => null), + ), + ); + return results.filter(x => x != null); }, }); diff --git a/packages/backend/src/models/repositories/page.ts b/packages/backend/src/models/repositories/page.ts index 092b26b39..65b54f8b8 100644 --- a/packages/backend/src/models/repositories/page.ts +++ b/packages/backend/src/models/repositories/page.ts @@ -65,6 +65,7 @@ export const PageRepository = db.getRepository(Page).extend({ content: page.content, variables: page.variables, title: page.title, + isPublic: page.isPublic, name: page.name, summary: page.summary, hideTitleWhenPinned: page.hideTitleWhenPinned, diff --git a/packages/backend/src/models/schema/page.ts b/packages/backend/src/models/schema/page.ts index 55ba3ce7f..19074947b 100644 --- a/packages/backend/src/models/schema/page.ts +++ b/packages/backend/src/models/schema/page.ts @@ -47,5 +47,9 @@ export const packedPageSchema = { ref: 'UserLite', optional: false, nullable: false, }, + isPublic: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/remote/activitypub/kernel/update/index.ts b/packages/backend/src/remote/activitypub/kernel/update/index.ts index 9e8a81bb3..022be0ad8 100644 --- a/packages/backend/src/remote/activitypub/kernel/update/index.ts +++ b/packages/backend/src/remote/activitypub/kernel/update/index.ts @@ -26,7 +26,7 @@ export default async (actor: CacheableRemoteUser, activity: IUpdate): Promise console.log(e)); + await updateQuestion(object, resolver).catch(e => console.log(e)); return `ok: Question updated`; } else { return `skip: Unknown type: ${getApType(object)}`; diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 0b3654489..2719f50d2 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -272,7 +272,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise logger.error(err)); + await updateFeatured(user!.id, resolver).catch(err => logger.error(err)); return user!; } @@ -386,7 +386,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), }); - await updateFeatured(exist.id).catch(err => logger.error(err)); + await updateFeatured(exist.id, resolver).catch(err => logger.error(err)); } /** @@ -464,14 +464,14 @@ export function analyzeAttachments(attachments: IObject | IObject[] | undefined) return { fields, services }; } -export async function updateFeatured(userId: User['id']) { +export async function updateFeatured(userId: User['id'], resolver?: Resolver) { const user = await Users.findOneByOrFail({ id: userId }); if (!Users.isRemoteUser(user)) return; if (!user.featured) return; logger.info(`Updating the featured: ${user.uri}`); - const resolver = new Resolver(); + if (resolver == null) resolver = new Resolver(); // Resolve to (Ordered)Collection Object const collection = await resolver.resolveCollection(user.featured); diff --git a/packages/backend/src/remote/activitypub/models/question.ts b/packages/backend/src/remote/activitypub/models/question.ts index f0321fdf2..94a50d4f7 100644 --- a/packages/backend/src/remote/activitypub/models/question.ts +++ b/packages/backend/src/remote/activitypub/models/question.ts @@ -40,7 +40,7 @@ export async function extractPollFromQuestion(source: string | IObject, resolver * @param uri URI of AP Question object * @returns true if updated */ -export async function updateQuestion(value: any) { +export async function updateQuestion(value: any, resolver?: Resolver) { const uri = typeof value === 'string' ? value : value.id; // URIใŒใ“ใฎใ‚ตใƒผใƒใƒผใ‚’ๆŒ‡ใ—ใฆใ„ใ‚‹ใชใ‚‰ใ‚นใ‚ญใƒƒใƒ— @@ -55,7 +55,7 @@ export async function updateQuestion(value: any) { //#endregion // resolve new Question object - const resolver = new Resolver(); + if (resolver == null) resolver = new Resolver(); const question = await resolver.resolve(value) as IQuestion; apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index 5c9d44292..94227e4db 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -19,9 +19,11 @@ import renderFollow from '@/remote/activitypub/renderer/follow.js'; export default class Resolver { private history: Set; private user?: ILocalUser; + private recursionLimit?: number; - constructor() { + constructor(recursionLimit = 100) { this.history = new Set(); + this.recursionLimit = recursionLimit; } public getHistory(): string[] { @@ -59,7 +61,9 @@ export default class Resolver { if (this.history.has(value)) { throw new Error('cannot resolve already resolved one'); } - + if (this.recursionLimit && this.history.size > this.recursionLimit) { + throw new Error('hit recursion limit'); + } this.history.add(value); const host = extractDbHost(value); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 29cd19026..d662d53af 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -275,6 +275,7 @@ import * as ep___pinnedUsers from './endpoints/pinned-users.js'; import * as ep___customMOTD from './endpoints/custom-motd.js'; import * as ep___customSplashIcons from './endpoints/custom-splash-icons.js'; import * as ep___latestVersion from './endpoints/latest-version.js'; +import * as ep___patrons from './endpoints/patrons.js'; import * as ep___promo_read from './endpoints/promo/read.js'; import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; import * as ep___resetDb from './endpoints/reset-db.js'; @@ -599,6 +600,7 @@ const eps = [ ['custom-motd', ep___customMOTD], ['custom-splash-icons', ep___customSplashIcons], ['latest-version', ep___latestVersion], + ['patrons', ep___patrons], ['promo/read', ep___promo_read], ['request-reset-password', ep___requestResetPassword], ['reset-db', ep___resetDb], diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index e7b78fb38..063faf6ec 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -53,6 +53,7 @@ export const paramDef = { eyeCatchingImageId: { type: 'string', format: 'misskey:id', nullable: true }, font: { type: 'string', enum: ['serif', 'sans-serif'], default: 'sans-serif' }, alignCenter: { type: 'boolean', default: false }, + isPublic: { type: 'boolean', default: true }, hideTitleWhenPinned: { type: 'boolean', default: false }, }, required: ['title', 'name', 'content', 'variables', 'script'], @@ -97,6 +98,7 @@ export default define(meta, paramDef, async (ps, user) => { alignCenter: ps.alignCenter, hideTitleWhenPinned: ps.hideTitleWhenPinned, font: ps.font, + isPublic: ps.isPublic, })).then(x => Pages.findOneByOrFail(x.identifiers[0])); return await Pages.pack(page); diff --git a/packages/backend/src/server/api/endpoints/pages/show.ts b/packages/backend/src/server/api/endpoints/pages/show.ts index 54ae43deb..384975af7 100644 --- a/packages/backend/src/server/api/endpoints/pages/show.ts +++ b/packages/backend/src/server/api/endpoints/pages/show.ts @@ -67,5 +67,9 @@ export default define(meta, paramDef, async (ps, user) => { throw new ApiError(meta.errors.noSuchPage); } + if (!page.isPublic && (user == null || (page.userId !== user.id))) { + throw new ApiError(meta.errors.noSuchPage); + } + return await Pages.pack(page, user); }); diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index 8230ea09b..585e9e73e 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -60,6 +60,7 @@ export const paramDef = { font: { type: 'string', enum: ['serif', 'sans-serif'] }, alignCenter: { type: 'boolean' }, hideTitleWhenPinned: { type: 'boolean' }, + isPublic: { type: 'boolean' }, }, required: ['pageId', 'title', 'name', 'content', 'variables', 'script'], } as const; @@ -104,6 +105,7 @@ export default define(meta, paramDef, async (ps, user) => { content: ps.content, variables: ps.variables, script: ps.script, + isPublic: ps.isPublic, alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, font: ps.font === undefined ? page.font : ps.font, diff --git a/packages/backend/src/server/api/endpoints/patrons.ts b/packages/backend/src/server/api/endpoints/patrons.ts new file mode 100644 index 000000000..de6632fb9 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/patrons.ts @@ -0,0 +1,27 @@ +import define from '../define.js'; + +export const meta = { + tags: ['meta'], + description: 'Get list of Calckey patrons from Codeberg', + + requireCredential: false, + requireCredentialPrivateMode: false, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async () => { + let patrons; + await fetch('https://codeberg.org/thatonecalculator/calckey/raw/branch/develop/patrons.json') + .then((response) => response.json()) + .then((data) => { + patrons = data['patrons']; + }); + + return patrons; +}); diff --git a/packages/backend/src/server/api/endpoints/users/pages.ts b/packages/backend/src/server/api/endpoints/users/pages.ts index e1d876e6b..b76071264 100644 --- a/packages/backend/src/server/api/endpoints/users/pages.ts +++ b/packages/backend/src/server/api/endpoints/users/pages.ts @@ -34,7 +34,8 @@ export const paramDef = { export default define(meta, paramDef, async (ps, user) => { const query = makePaginationQuery(Pages.createQueryBuilder('page'), ps.sinceId, ps.untilId) .andWhere('page.userId = :userId', { userId: ps.userId }) - .andWhere('page.visibility = \'public\''); + .andWhere('page.visibility = \'public\'') + .andWhere('page.isPublic = true'); const pages = await query .take(ps.limit) diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 00769d100..18473d79b 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -9,7 +9,7 @@ export function genOpenapiSpec() { info: { version: 'v1', - title: 'Misskey API', + title: 'Calckey API', 'x-logo': { url: '/static-assets/api-doc.png' }, }, diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index c81506384..446df1554 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -232,8 +232,43 @@ const getFeed = async (acct: string) => { return user && await packFeed(user); }; +// As the /@user[.json|.rss|.atom]/sub endpoint is complicated, we will use a regex to switch between them. +const reUser = new RegExp(`^/@(?[^/]+?)(?:\.(?json|rss|atom))?(?:/(?[^/]+))?$`); +router.get(reUser, async (ctx, next) => { + const groups = reUser.exec(ctx.originalUrl)?.groups; + if (!groups) { + await next(); + return; + } + + ctx.params = groups; + + console.log(ctx, ctx.params) + if (groups.feed) { + if (groups.sub) { + await next(); + return; + } + + switch (groups.feed) { + case 'json': + await jsonFeed(ctx, next); + break; + case 'rss': + await rssFeed(ctx, next); + break; + case 'atom': + await atomFeed(ctx, next); + break; + } + return; + } + + await userPage(ctx, next); +}); + // Atom -router.get('/@:user.atom', async ctx => { +const atomFeed: Router.Middleware = async ctx => { const feed = await getFeed(ctx.params.user); if (feed) { @@ -242,10 +277,10 @@ router.get('/@:user.atom', async ctx => { } else { ctx.status = 404; } -}); +}; // RSS -router.get('/@:user.rss', async ctx => { +const rssFeed: Router.Middleware = async ctx => { const feed = await getFeed(ctx.params.user); if (feed) { @@ -254,10 +289,10 @@ router.get('/@:user.rss', async ctx => { } else { ctx.status = 404; } -}); +}; // JSON -router.get('/@:user.json', async ctx => { +const jsonFeed: Router.Middleware = async ctx => { const feed = await getFeed(ctx.params.user); if (feed) { @@ -266,43 +301,47 @@ router.get('/@:user.json', async ctx => { } else { ctx.status = 404; } -}); +}; //#region SSR (for crawlers) // User -router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { - const { username, host } = Acct.parse(ctx.params.user); +const userPage: Router.Middleware = async (ctx, next) => { + const userParam = ctx.params.user; + const subParam = ctx.params.sub; + const { username, host } = Acct.parse(userParam); + const user = await Users.findOneBy({ usernameLower: username.toLowerCase(), host: host ?? IsNull(), isSuspended: false, }); - if (user != null) { - const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); - const meta = await fetchMeta(); - const me = profile.fields - ? profile.fields - .filter(filed => filed.value != null && filed.value.match(/^https?:/)) - .map(field => field.value) - : []; - - await ctx.render('user', { - user, profile, me, - avatarUrl: await Users.getAvatarUrl(user), - sub: ctx.params.sub, - instanceName: meta.name || 'Calckey', - icon: meta.iconUrl, - themeColor: meta.themeColor, - privateMode: meta.privateMode, - }); - ctx.set('Cache-Control', 'public, max-age=15'); - } else { - // ใƒชใƒขใƒผใƒˆใƒฆใƒผใ‚ถใƒผใชใฎใง - // ใƒขใƒ‡ใƒฌใƒผใ‚ฟใŒAPI็ตŒ็”ฑใงๅ‚็…งๅฏ่ƒฝใซใ™ใ‚‹ใŸใ‚ใซ404ใซใฏใ—ใชใ„ + if (user === null) { await next(); + return; } -}); + + const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); + const meta = await fetchMeta(); + const me = profile.fields + ? profile.fields + .filter(filed => filed.value != null && filed.value.match(/^https?:/)) + .map(field => field.value) + : []; + + const userDetail = { + user, profile, me, + avatarUrl: await Users.getAvatarUrl(user), + sub: subParam, + instanceName: meta.name || 'Calckey', + icon: meta.iconUrl, + themeColor: meta.themeColor, + privateMode: meta.privateMode, + }; + + await ctx.render('user', userDetail); + ctx.set('Cache-Control', 'public, max-age=15'); +}; router.get('/users/:user', async ctx => { const user = await Users.findOneBy({ diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index cfdaac600..5072e0ad4 100644 --- a/packages/backend/src/server/web/style.css +++ b/packages/backend/src/server/web/style.css @@ -42,7 +42,7 @@ html { width: 28px; height: 28px; transform: translateY(110px); - display: none !important; + display: none; color: var(--accent); } #splashSpinner > .spinner { @@ -101,6 +101,16 @@ html { } } +@media(prefers-reduced-motion) { + #splashSpinner { + display: block; + } + + #splashIcon { + animation: none; + } +} + #splashText { position: absolute; top: 0; diff --git a/packages/client/package.json b/packages/client/package.json index 00ecb0456..bc49ac021 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -19,8 +19,8 @@ "blurhash": "1.1.5", "broadcast-channel": "4.18.1", "browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git#commit=0380d12c8e736788ea7f4e6e985175521ea7b23c", - "chart.js": "3.9.1", - "chartjs-adapter-date-fns": "2.0.0", + "chart.js": "4.0.1", + "chartjs-adapter-date-fns": "2.0.1", "chartjs-plugin-gradient": "0.5.1", "chartjs-plugin-zoom": "1.2.1", "compare-versions": "5.0.1", @@ -31,7 +31,7 @@ "idb-keyval": "6.2.0", "insert-text-at-cursor": "0.3.0", "json5": "2.2.1", - "katex": "0.15.6", + "katex": "0.16.3", "matter-js": "0.18.0", "mfm-js": "0.23.0", "misskey-js": "0.0.14", @@ -48,7 +48,7 @@ "swiper": "^8.4.4", "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", - "three": "0.144.0", + "three": "0.146.0", "throttle-debounce": "5.0.0", "tinycolor2": "1.4.2", "tsc-alias": "1.7.1", @@ -80,7 +80,7 @@ "@typescript-eslint/parser": "5.43.0", "cross-env": "7.0.3", "cypress": "10.11.0", - "eslint": "8.27.0", + "eslint": "8.28.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-vue": "9.7.0", "rollup": "2.79.1", diff --git a/packages/client/src/components/MkChannelPreview.vue b/packages/client/src/components/MkChannelPreview.vue index 26cff3b21..a970c9ae6 100644 --- a/packages/client/src/components/MkChannelPreview.vue +++ b/packages/client/src/components/MkChannelPreview.vue @@ -81,9 +81,12 @@ const bannerStyle = computed(() => { top: 16px; left: 16px; padding: 12px 16px; - background: rgba(0, 0, 0, 0.7); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + background: rgba(0, 0, 0, 0.2); color: #fff; font-size: 1.2em; + border-radius: 999px; } > .status { @@ -93,7 +96,9 @@ const bannerStyle = computed(() => { right: 16px; padding: 8px 12px; font-size: 80%; - background: rgba(0, 0, 0, 0.7); + -webkit-backdrop-filter: var(--blur, blur(8px)); + backdrop-filter: var(--blur, blur(8px)); + background: rgba(0, 0, 0, 0.2); border-radius: 6px; color: #fff; } diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue index f1dce7284..02568c6ec 100644 --- a/packages/client/src/components/MkContainer.vue +++ b/packages/client/src/components/MkContainer.vue @@ -178,6 +178,7 @@ export default defineComponent({ > ::v-deep(i) { margin-right: 6px; + transform: translateY(0.1em); } &:empty { diff --git a/packages/client/src/components/MkFollowButton.vue b/packages/client/src/components/MkFollowButton.vue index a2ba06faf..6fd5ccb1c 100644 --- a/packages/client/src/components/MkFollowButton.vue +++ b/packages/client/src/components/MkFollowButton.vue @@ -1,5 +1,6 @@ -