diff --git a/.config/example.yml b/.config/example.yml index 02661b7fbf..b7b56f2287 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -72,6 +72,16 @@ redis: # user: # pass: +# ┌─────────────────────┐ +#───┘ Sonic configuration └───────────────────────────────────── + +#sonic: +# host: localhost +# port: 1491 +# auth: SecretPassword +# collection: notes +# bucket: default + # ┌───────────────┐ #───┘ ID generation └─────────────────────────────────────────── diff --git a/.gitignore b/.gitignore index 52139614c2..5e1d4a26d0 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ ormconfig.json packages/backend/assets/instance.css packages/backend/assets/sounds/None.mp3 +!packages/backend/src/db # blender backups *.blend1 diff --git a/CALCKEY.md b/CALCKEY.md index 015232f700..525bccf286 100644 --- a/CALCKEY.md +++ b/CALCKEY.md @@ -3,21 +3,17 @@ ## Planned - Stucture - - [Sonic](https://crates.io/crates/sonic-server) support as an ElasticSearch alternative - [DragonflyDB](https://dragonflydb.io/) support as a Redis alternative - Optionally use [ScyllaDB](https://www.scylladb.com/open-source-nosql-database/) for storing notes - Rewrite backend in Rust and [Axum](https://github.com/tokio-rs/axum) - Function - Federate with note edits - - Admin customizable max note length (100-8000) - User "choices" (recommended users) like Mastodon and Soapbox - Join Reason system like Mastodon/Pleroma - Option to publicize instance blocks - - Backfill remote users - Build flag to remove NSFW/AI stuff - Timeline filters - Filter notifications by user - - Non-nyaify cat mode - Exclude self from antenna - Form - MFM button @@ -37,6 +33,7 @@ - Admin custom CSS - Add back time machine (jump to date) - Improve accesibility +- Non-nyaify cat mode ## Implemented @@ -108,6 +105,14 @@ - Allows custom emoji - Fix lint errors - Use Rome instead of ESLint +- Mastodon API support +- More antenna options +- New dashboard +- Backfill follower counts +- Improved emoji licensing +- Compile time compression +- Sonic search +- Popular color schemes, including Nord, Gruvbox, and Catppuccin - 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) diff --git a/README.md b/README.md index c4fdcf5c65..5512106762 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ - OCR image captioning - New and improved Groups - Better intro tutorial + - Compatibility with Mastodon clients/apps + - Backfill user information + - Sonic search - Many more user and admin settings - [So much more!](./CALCKEY.md) @@ -78,8 +81,9 @@ If you have access to a server that supports one of the sources below, I recomme ### 😗 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 +- Full text search (choost one of the following) + - 🦔 [Sonic](https://crates.io/crates/sonic-server) (highly recommended!) + - [ElasticSearch](https://www.elastic.co/elasticsearch/) - Management (choose one of the following) - 🛰️ [pm2](https://pm2.io/) - 🐳 [Docker](https://docker.com) @@ -119,6 +123,17 @@ Assuming you set up PostgreSQL correctly, all you have to run is: psql postgres -c "create database calckey with encoding = 'UTF8';" ``` +In Calckey's directory, fill out the `db` section of `.config/default.yml` with the correct information, where the `db` key is `calckey`. + +## 🦔 Set up search + +Follow sonic's [installation guide](https://github.com/valeriansaliou/sonic#installation) + +If you use IPv4: in Sonic's directory, edit the `config.cfg` file to change `inet` to `"0.0.0.0:1491"`. + +In Calckey's directory, fill out the `sonic` section of `.config/default.yml` with the correct information. + + ## 💅 Customize - To add custom CSS for all users, edit `./custom/assets/instance.css`. @@ -155,7 +170,8 @@ For migrating from Misskey v13, Misskey v12, and Foundkey, read [this document]( ```sh # git pull -NODE_ENV=production pnpm install && pnpm run build && pnpm run migrate +pnpm install +NODE_ENV=production pnpm run build && pnpm run migrate pm2 start "NODE_ENV=production pnpm run start" --name Calckey ``` diff --git a/locales/en-US.yml b/locales/en-US.yml index 47cbb783c1..8da78d3b8d 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -835,7 +835,7 @@ muteThread: "Mute thread" unmuteThread: "Unmute thread" ffVisibility: "Follows/Followers Visibility" ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you." -continueThread: "View thread continuation" +continueThread: "Continue thread" deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" incorrectPassword: "Incorrect password." voteConfirm: "Confirm your vote for \"{choice}\"?" @@ -935,6 +935,7 @@ moveFromLabel: "Account you're moving from:" moveFromDescription: "This will set an alias of your old account so that you can move from that account to this current one. Do this BEFORE moving from your older account. Please enter the tag of the account formatted like @person@instance.com" migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}? Once you do this, you won't be able to reverse it, and you won't be able to use your account normally again.\nAlso, please ensure that you've set this current account as the account you're moving from." defaultReaction: "Default emoji reaction for outgoing and incoming posts" +license: "License" _sensitiveMediaDetection: description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server." @@ -1400,7 +1401,7 @@ _profile: metadataContent: "Content" changeAvatar: "Change avatar" changeBanner: "Change banner" - locationDescription: "If entered properly, this will display your local time to other users." + locationDescription: "If you enter your city, it will display your local time to other users." _exportOrImport: allNotes: "All posts" followingList: "Followed users" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index fe672c16ef..59bf28d5a6 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -935,6 +935,7 @@ moveFromLabel: "引っ越し元のアカウント:" moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。必ず引っ越しを実行する前に作成してください!引っ越し元のアカウントをこのように入力してください:@person@instance.com" migrationConfirm: "本当にこのアカウントを {account} に引っ越しますか?一度引っ越しを行うと取り消せず、二度とこのアカウントを元の状態で使用することはできません。\nまた、引っ越し先のアカウントでエイリアスを作成したことを確認してください。" defaultReaction: "リモートとローカルの投稿に対するデフォルトの絵文字リアクション" +license: "ライセンス" _sensitiveMediaDetection: description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。" diff --git a/package.json b/package.json index a9f8c8670b..f24c164cd9 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "calckey", - "version": "13.2.0-beta", + "version": "13.2.0-beta2", "codename": "aqua", "repository": { "type": "git", "url": "https://codeberg.org/calckey/calckey.git" }, - "packageManager": "pnpm@7.27.1", + "packageManager": "pnpm@7.29.3", "private": true, "scripts": { "rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp", diff --git a/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js b/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js new file mode 100644 index 0000000000..656a921770 --- /dev/null +++ b/packages/backend/migration/1678945242650-add-props-for-custom-emoji.js @@ -0,0 +1,11 @@ +export class addPropsForCustomEmoji1678945242650 { + name = 'addPropsForCustomEmoji1678945242650' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" ADD "license" character varying(1024)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "license"`); + } +} diff --git a/packages/backend/migration/1679269929000-fix-repo.js b/packages/backend/migration/1679269929000-fix-repo.js new file mode 100644 index 0000000000..6784b3793c --- /dev/null +++ b/packages/backend/migration/1679269929000-fix-repo.js @@ -0,0 +1,13 @@ +export class FixRepo1679269929000 { + name = 'FixRepo1679269929000' + + async up(queryRunner) { + await queryRunner.query(`UPDATE meta SET "repositoryUrl" = 'https://codeberg.org/calckey/calckey'`); + await queryRunner.query(`UPDATE meta SET "feedbackUrl" = 'https://codeberg.org/calckey/calckey/issues'`); + } + + async down(queryRunner) { + await queryRunner.query(`UPDATE meta SET "repositoryUrl" = 'https://codeberg.org/calckey/calckey'`); + await queryRunner.query(`UPDATE meta SET "feedbackUrl" = 'https://codeberg.org/calckey/calckey/issues'`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 910ffd3748..e7bec1a220 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -81,7 +81,7 @@ "koa-send": "5.0.1", "koa-slow": "2.1.0", "koa-views": "7.0.2", - "@calckey/megalodon": "5.1.2", + "@calckey/megalodon": "5.1.21", "mfm-js": "0.23.2", "mime-types": "2.1.35", "multer": "1.4.4-lts.1", @@ -112,6 +112,7 @@ "seedrandom": "^3.0.5", "semver": "7.3.8", "sharp": "0.31.3", + "sonic-channel": "^1.3.1", "speakeasy": "2.0.0", "stringz": "2.1.0", "summaly": "2.7.0", diff --git a/packages/backend/src/config/types.ts b/packages/backend/src/config/types.ts index ed9b0ece08..a7cdc89cf2 100644 --- a/packages/backend/src/config/types.ts +++ b/packages/backend/src/config/types.ts @@ -32,6 +32,13 @@ export type Source = { pass?: string; index?: string; }; + sonic: { + host: string; + port: number; + auth?: string; + collection?: string; + bucket?: string; + }; proxy?: string; proxySmtp?: string; diff --git a/packages/backend/src/db/sonic.ts b/packages/backend/src/db/sonic.ts new file mode 100644 index 0000000000..6c4d28f703 --- /dev/null +++ b/packages/backend/src/db/sonic.ts @@ -0,0 +1,51 @@ +import * as SonicChannel from "sonic-channel"; +import { dbLogger } from "./logger.js"; + +import config from "@/config/index.js"; + +const logger = dbLogger.createSubLogger("sonic", "gray", false); + +logger.info("Connecting to Sonic"); + +const handlers = (type: string): SonicChannel.Handlers => ( + { + connected: () => { + logger.succ(`Connected to Sonic ${type}`); + }, + disconnected: (error) => { + logger.warn(`Disconnected from Sonic ${type}, error: ${error}`); + }, + error: (error) => { + logger.warn(`Sonic ${type} error: ${error}`); + }, + retrying: () => { + logger.info(`Sonic ${type} retrying`); + }, + timeout: () => { + logger.warn(`Sonic ${type} timeout`); + }, + } +) + +const hasConfig = + config.sonic + && ( config.sonic.host + || config.sonic.port + || config.sonic.auth + ) + +const host = hasConfig ? config.sonic.host ?? "localhost" : ""; +const port = hasConfig ? config.sonic.port ?? 1491 : 0; +const auth = hasConfig ? config.sonic.auth ?? "SecretPassword" : ""; +const collection = hasConfig ? config.sonic.collection ?? "main" : ""; +const bucket = hasConfig ? config.sonic.bucket ?? "default" : ""; + +export default hasConfig + ? { + search: new SonicChannel.Search({host, port, auth}).connect(handlers("search")), + ingest: new SonicChannel.Ingest({host, port, auth}).connect(handlers("ingest")), + + collection, + bucket, + } + : null; diff --git a/packages/backend/src/models/entities/emoji.ts b/packages/backend/src/models/entities/emoji.ts index f251de8973..2315686968 100644 --- a/packages/backend/src/models/entities/emoji.ts +++ b/packages/backend/src/models/entities/emoji.ts @@ -55,4 +55,9 @@ export class Emoji { array: true, length: 128, default: '{}', }) public aliases: string[]; + + @Column('varchar', { + length: 1024, nullable: true, + }) + public license: string | null; } diff --git a/packages/backend/src/models/repositories/emoji.ts b/packages/backend/src/models/repositories/emoji.ts index e868fe94fe..6eabfe9558 100644 --- a/packages/backend/src/models/repositories/emoji.ts +++ b/packages/backend/src/models/repositories/emoji.ts @@ -15,6 +15,7 @@ export const EmojiRepository = db.getRepository(Emoji).extend({ host: emoji.host, // || emoji.originalUrl してるのは後方互換性のため url: emoji.publicUrl || emoji.originalUrl, + license: emoji.license, }; }, diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index 882cfdd318..ca284b9237 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -12,7 +12,6 @@ import { Channels, } from "../index.js"; import type { Packed } from "@/misc/schema.js"; -import { nyaize } from "@/misc/nyaize.js"; import { awaitAll } from "@/prelude/await-all.js"; import { convertLegacyReaction, @@ -263,7 +262,7 @@ export const NoteRepository = db.getRepository(Note).extend({ : {}), }); - if (packed.user.isCat && packed.text) { + /* if (packed.user.isCat && packed.text) { const tokens = packed.text ? mfm.parse(packed.text) : []; mfm.inspect(tokens, (node) => { if (node.type === "text") { @@ -272,7 +271,7 @@ export const NoteRepository = db.getRepository(Note).extend({ } }); packed.text = mfm.toString(tokens); - } + } */ return packed; }, diff --git a/packages/backend/src/models/schema/emoji.ts b/packages/backend/src/models/schema/emoji.ts index 1e1dab8689..8994381b31 100644 --- a/packages/backend/src/models/schema/emoji.ts +++ b/packages/backend/src/models/schema/emoji.ts @@ -40,5 +40,10 @@ export const packedEmojiSchema = { optional: false, nullable: false, }, + license: { + type: "string", + optional: false, + nullable: true, + }, }, } as const; diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index c40b3c6aeb..c387efe927 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -13,6 +13,7 @@ import processDb from "./processors/db/index.js"; import processObjectStorage from "./processors/object-storage/index.js"; import processSystemQueue from "./processors/system/index.js"; import processWebhookDeliver from "./processors/webhook-deliver.js"; +import processBackground from "./processors/background/index.js"; import { endedPollNotification } from "./processors/ended-poll-notification.js"; import { queueLogger } from "./logger.js"; import { getJobInfo } from "./get-job-info.js"; @@ -24,6 +25,7 @@ import { objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue, + backgroundQueue, } from "./queues.js"; import type { ThinUser } from "./types.js"; @@ -418,6 +420,17 @@ export function createCleanRemoteFilesJob() { ); } +export function createIndexAllNotesJob(data = {}) { + return backgroundQueue.add( + "indexAllNotes", + data, + { + removeOnComplete: true, + removeOnFail: true, + }, + ); +} + export function webhookDeliver( webhook: Webhook, type: typeof webhookEventTypes[number], @@ -454,6 +467,7 @@ export default function () { webhookDeliverQueue.process(64, processWebhookDeliver); processDb(dbQueue); processObjectStorage(objectStorageQueue); + processBackground(backgroundQueue); systemQueue.add( "tickCharts", diff --git a/packages/backend/src/queue/processors/background/index-all-notes.ts b/packages/backend/src/queue/processors/background/index-all-notes.ts new file mode 100644 index 0000000000..f032758864 --- /dev/null +++ b/packages/backend/src/queue/processors/background/index-all-notes.ts @@ -0,0 +1,76 @@ +import type Bull from "bull"; + +import { queueLogger } from "../../logger.js"; +import { Notes } from "@/models/index.js"; +import { MoreThan } from "typeorm"; +import { index } from "@/services/note/create.js" +import { Note } from "@/models/entities/note.js"; + +const logger = queueLogger.createSubLogger("index-all-notes"); + +export default async function indexAllNotes( + job: Bull.Job>, + done: ()=>void, +): Promise { + logger.info("Indexing all notes..."); + + let cursor: string|null = job.data.cursor as string ?? null; + let indexedCount: number = job.data.indexedCount as number ?? 0; + let total: number = job.data.total as number ?? 0; + + let running = true; + const take = 50000; + const batch = 100; + while (running) { + logger.info(`Querying for ${take} notes ${indexedCount}/${total ? total : '?'} at ${cursor}`); + + let notes: Note[] = []; + try { + notes = await Notes.find({ + where: { + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: take, + order: { + id: 1, + }, + }); + } catch (e) { + logger.error(`Failed to query notes ${e}`); + continue; + } + + if (notes.length === 0) { + job.progress(100); + running = false; + break; + } + + try { + const count = await Notes.count(); + total = count; + job.update({ indexedCount, cursor, total }) + } catch (e) { + } + + for (let i = 0; i < notes.length; i += batch) { + const chunk = notes.slice(i, i + batch); + await Promise.all(chunk.map(note => index(note))); + + indexedCount += chunk.length; + const pct = (indexedCount / total)*100; + job.update({ indexedCount, cursor, total }) + job.progress(+(pct.toFixed(1))); + logger.info(`Indexed notes ${indexedCount}/${total ? total : '?'}`); + } + cursor = notes[notes.length - 1].id; + job.update({ indexedCount, cursor, total }) + + if (notes.length < take) { + running = false; + } + } + + done(); + logger.info("All notes have been indexed."); +} diff --git a/packages/backend/src/queue/processors/background/index.ts b/packages/backend/src/queue/processors/background/index.ts new file mode 100644 index 0000000000..cf96b67ef6 --- /dev/null +++ b/packages/backend/src/queue/processors/background/index.ts @@ -0,0 +1,15 @@ +import type Bull from "bull"; +import indexAllNotes from "./index-all-notes.js"; + +const jobs = { + indexAllNotes, +} as Record< + string, + Bull.ProcessCallbackFunction> +>; + +export default function (q: Bull.Queue) { + for (const [k, v] of Object.entries(jobs)) { + q.process(k, 16, v); + } +} diff --git a/packages/backend/src/queue/processors/db/import-custom-emojis.ts b/packages/backend/src/queue/processors/db/import-custom-emojis.ts index 45ab5ea9a1..373a3a2daf 100644 --- a/packages/backend/src/queue/processors/db/import-custom-emojis.ts +++ b/packages/backend/src/queue/processors/db/import-custom-emojis.ts @@ -75,6 +75,7 @@ export async function importCustomEmojis( originalUrl: driveFile.url, publicUrl: driveFile.webpublicUrl ?? driveFile.url, type: driveFile.webpublicType ?? driveFile.type, + license: emojiInfo.license, }).then((x) => Emojis.findOneByOrFail(x.identifiers[0])); } diff --git a/packages/backend/src/queue/queues.ts b/packages/backend/src/queue/queues.ts index 12d9d66207..6d7fffcb30 100644 --- a/packages/backend/src/queue/queues.ts +++ b/packages/backend/src/queue/queues.ts @@ -27,6 +27,7 @@ export const webhookDeliverQueue = initializeQueue( "webhookDeliver", 64, ); +export const backgroundQueue = initializeQueue>("bg"); export const queues = [ systemQueue, @@ -36,4 +37,5 @@ export const queues = [ dbQueue, objectStorageQueue, webhookDeliverQueue, + backgroundQueue, ]; diff --git a/packages/backend/src/remote/activitypub/models/person.ts b/packages/backend/src/remote/activitypub/models/person.ts index 16b265b0b3..f4f792df15 100644 --- a/packages/backend/src/remote/activitypub/models/person.ts +++ b/packages/backend/src/remote/activitypub/models/person.ts @@ -198,9 +198,36 @@ export async function createPerson( const url = getOneApHrefNullable(person.url); if (url && !url.startsWith("https://")) { - throw new Error(`unexpected shcema of person url: ${url}`); + throw new Error(`unexpected schema of person url: ${url}`); } + let followersCount: number | undefined; + + if (typeof person.followers === "string") { + try { + let data = await fetch(person.followers, { headers: { "Accept": "application/json" } }); + let json_data = JSON.parse(await data.text()); + + followersCount = json_data.totalItems; + } catch { + followersCount = undefined; + } + } + + let followingCount: number | undefined; + + if (typeof person.following === "string") { + try { + let data = await fetch(person.following, { headers: { "Accept": "application/json" } }); + let json_data = JSON.parse(await data.text()); + + followingCount = json_data.totalItems; + } catch (e) { + followingCount = undefined; + } + } + + // Create user let user: IRemoteUser; try { @@ -228,6 +255,16 @@ export async function createPerson( followersUri: person.followers ? getApId(person.followers) : undefined, + followersCount: followersCount !== undefined + ? followersCount + : person.followers && typeof person.followers !== "string" && isCollectionOrOrderedCollection(person.followers) + ? person.followers.totalItems + : undefined, + followingCount: followingCount !== undefined + ? followingCount + : person.following && typeof person.following !== "string" && isCollectionOrOrderedCollection(person.following) + ? person.following.totalItems + : undefined, featured: person.featured ? getApId(person.featured) : undefined, uri: person.id, tags, @@ -396,7 +433,34 @@ export async function updatePerson( const url = getOneApHrefNullable(person.url); if (url && !url.startsWith("https://")) { - throw new Error(`unexpected shcema of person url: ${url}`); + throw new Error(`unexpected schema of person url: ${url}`); + } + + let followersCount: number | undefined; + + if (typeof person.followers === "string") { + try { + let data = await fetch(person.followers, { headers: { "Accept": "application/json" } } ); + let json_data = JSON.parse(await data.text()); + + followersCount = json_data.totalItems; + } catch { + followersCount = undefined; + } + } + + + let followingCount: number | undefined; + + if (typeof person.following === "string") { + try { + let data = await fetch(person.following, { headers: { "Accept": "application/json" } } ); + let json_data = JSON.parse(await data.text()); + + followingCount = json_data.totalItems; + } catch { + followingCount = undefined; + } } const updates = { @@ -406,6 +470,16 @@ export async function updatePerson( person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), followersUri: person.followers ? getApId(person.followers) : undefined, + followersCount: followersCount !== undefined + ? followersCount + : person.followers && typeof person.followers !== "string" && isCollectionOrOrderedCollection(person.followers) + ? person.followers.totalItems + : undefined, + followingCount: followingCount !== undefined + ? followingCount + : person.following && typeof person.following !== "string" && isCollectionOrOrderedCollection(person.following) + ? person.following.totalItems + : undefined, featured: person.featured, emojis: emojiNames, name: truncate(person.name, nameLength), diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 7fb5fd3209..ba0e721b9e 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -29,6 +29,7 @@ import * as ep___admin_emoji_list from "./endpoints/admin/emoji/list.js"; import * as ep___admin_emoji_removeAliasesBulk from "./endpoints/admin/emoji/remove-aliases-bulk.js"; import * as ep___admin_emoji_setAliasesBulk from "./endpoints/admin/emoji/set-aliases-bulk.js"; import * as ep___admin_emoji_setCategoryBulk from "./endpoints/admin/emoji/set-category-bulk.js"; +import * as ep___admin_emoji_setLicenseBulk from "./endpoints/admin/emoji/set-license-bulk.js"; import * as ep___admin_emoji_update from "./endpoints/admin/emoji/update.js"; import * as ep___admin_federation_deleteAllFiles from "./endpoints/admin/federation/delete-all-files.js"; import * as ep___admin_federation_refreshRemoteInstanceMetadata from "./endpoints/admin/federation/refresh-remote-instance-metadata.js"; @@ -50,6 +51,7 @@ import * as ep___admin_relays_list from "./endpoints/admin/relays/list.js"; import * as ep___admin_relays_remove from "./endpoints/admin/relays/remove.js"; import * as ep___admin_resetPassword from "./endpoints/admin/reset-password.js"; import * as ep___admin_resolveAbuseUserReport from "./endpoints/admin/resolve-abuse-user-report.js"; +import * as ep___admin_search_indexAll from "./endpoints/admin/search/index-all.js"; import * as ep___admin_sendEmail from "./endpoints/admin/send-email.js"; import * as ep___admin_serverInfo from "./endpoints/admin/server-info.js"; import * as ep___admin_showModerationLogs from "./endpoints/admin/show-moderation-logs.js"; @@ -131,6 +133,7 @@ import * as ep___drive_folders_show from "./endpoints/drive/folders/show.js"; import * as ep___drive_folders_update from "./endpoints/drive/folders/update.js"; import * as ep___drive_stream from "./endpoints/drive/stream.js"; import * as ep___emailAddress_available from "./endpoints/email-address/available.js"; +import * as ep___emoji from "./endpoints/emoji.js"; import * as ep___endpoint from "./endpoints/endpoint.js"; import * as ep___endpoints from "./endpoints/endpoints.js"; import * as ep___exportCustomEmojis from "./endpoints/export-custom-emojis.js"; @@ -363,6 +366,7 @@ const eps = [ ["admin/emoji/remove-aliases-bulk", ep___admin_emoji_removeAliasesBulk], ["admin/emoji/set-aliases-bulk", ep___admin_emoji_setAliasesBulk], ["admin/emoji/set-category-bulk", ep___admin_emoji_setCategoryBulk], + ["admin/emoji/set-license-bulk", ep___admin_emoji_setLicenseBulk], ["admin/emoji/update", ep___admin_emoji_update], ["admin/federation/delete-all-files", ep___admin_federation_deleteAllFiles], [ @@ -390,6 +394,7 @@ const eps = [ ["admin/relays/remove", ep___admin_relays_remove], ["admin/reset-password", ep___admin_resetPassword], ["admin/resolve-abuse-user-report", ep___admin_resolveAbuseUserReport], + ["admin/search/index-all", ep___admin_search_indexAll], ["admin/send-email", ep___admin_sendEmail], ["admin/server-info", ep___admin_serverInfo], ["admin/show-moderation-logs", ep___admin_showModerationLogs], @@ -471,6 +476,7 @@ const eps = [ ["drive/folders/update", ep___drive_folders_update], ["drive/stream", ep___drive_stream], ["email-address/available", ep___emailAddress_available], + ["emoji", ep___emoji], ["endpoint", ep___endpoint], ["endpoints", ep___endpoints], ["export-custom-emojis", ep___exportCustomEmojis], diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 601d754a0d..bfc025834f 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -49,6 +49,7 @@ export default define(meta, paramDef, async (ps, me) => { originalUrl: file.url, publicUrl: file.webpublicUrl ?? file.url, type: file.webpublicType ?? file.type, + license: null, }).then((x) => Emojis.findOneByOrFail(x.identifiers[0])); await db.queryResultCache!.remove(["meta_emojis"]); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 4a7f2bc611..951158f7d4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -73,6 +73,7 @@ export default define(meta, paramDef, async (ps, me) => { originalUrl: driveFile.url, publicUrl: driveFile.webpublicUrl ?? driveFile.url, type: driveFile.webpublicType ?? driveFile.type, + license: emoji.license, }).then((x) => Emojis.findOneByOrFail(x.identifiers[0])); await db.queryResultCache!.remove(["meta_emojis"]); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index 63f60bc991..fae986dd96 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -55,6 +55,11 @@ export const meta = { optional: false, nullable: false, }, + license: { + type: "string", + optional: false, + nullable: true, + }, }, }, }, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index bc4f1d29fa..aa49f14803 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -55,6 +55,11 @@ export const meta = { optional: false, nullable: false, }, + license: { + type: "string", + optional: false, + nullable: true, + }, }, }, }, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts new file mode 100644 index 0000000000..c98ca03fae --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts @@ -0,0 +1,45 @@ +import define from "../../../define.js"; +import { Emojis } from "@/models/index.js"; +import { In } from "typeorm"; +import { ApiError } from "../../../error.js"; +import { db } from "@/db/postgre.js"; + +export const meta = { + tags: ["admin"], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: "object", + properties: { + ids: { + type: "array", + items: { + type: "string", + format: "misskey:id", + }, + }, + license: { + type: "string", + nullable: true, + description: "Use `null` to reset the license.", + }, + }, + required: ["ids"], +} as const; + +export default define(meta, paramDef, async (ps) => { + await Emojis.update( + { + id: In(ps.ids), + }, + { + updatedAt: new Date(), + license: ps.license, + }, + ); + + await db.queryResultCache!.remove(["meta_emojis"]); +}); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 3f7f6639f5..9e2e854760 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -34,6 +34,10 @@ export const paramDef = { type: "string", }, }, + license: { + type: "string", + nullable: true, + }, }, required: ["id", "name", "aliases"], } as const; @@ -48,6 +52,7 @@ export default define(meta, paramDef, async (ps) => { name: ps.name, category: ps.category, aliases: ps.aliases, + license: ps.license, }); await db.queryResultCache!.remove(["meta_emojis"]); diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index ecd67d8936..4a437c3d12 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -3,6 +3,7 @@ import { inboxQueue, dbQueue, objectStorageQueue, + backgroundQueue, } from "@/queue/queues.js"; import define from "../../../define.js"; @@ -37,6 +38,11 @@ export const meta = { nullable: false, ref: "QueueCount", }, + backgroundQueue: { + optional: false, + nullable: false, + ref: "QueueCount", + }, }, }, } as const; @@ -52,11 +58,13 @@ export default define(meta, paramDef, async (ps) => { const inboxJobCounts = await inboxQueue.getJobCounts(); const dbJobCounts = await dbQueue.getJobCounts(); const objectStorageJobCounts = await objectStorageQueue.getJobCounts(); + const backgroundJobCounts = await backgroundQueue.getJobCounts(); return { deliver: deliverJobCounts, inbox: inboxJobCounts, db: dbJobCounts, objectStorage: objectStorageJobCounts, + backgroundQueue: backgroundJobCounts, }; }); diff --git a/packages/backend/src/server/api/endpoints/admin/search/index-all.ts b/packages/backend/src/server/api/endpoints/admin/search/index-all.ts new file mode 100644 index 0000000000..135b48eccd --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/search/index-all.ts @@ -0,0 +1,28 @@ +import define from "../../../define.js"; +import { createIndexAllNotesJob } from "@/queue/index.js"; + +export const meta = { + tags: ["admin"], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: "object", + properties: { + cursor: { + type: "string", + format: "misskey:id", + nullable: true, + default: null, + }, + }, + required: [], +} as const; + +export default define(meta, paramDef, async (ps, _me) => { + createIndexAllNotesJob({ + cursor: ps.cursor ?? undefined, + }); +}); diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index d113b978f6..e681f1294f 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -54,7 +54,7 @@ export const paramDef = { folderId: { type: "string", format: "misskey:id", nullable: true }, name: { type: "string" }, isSensitive: { type: "boolean" }, - comment: { type: "string", nullable: true, maxLength: 512 }, + comment: { type: "string", nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH }, }, required: ["fileId"], } as const; diff --git a/packages/backend/src/server/api/endpoints/emoji.ts b/packages/backend/src/server/api/endpoints/emoji.ts new file mode 100644 index 0000000000..5d3c77e5e1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/emoji.ts @@ -0,0 +1,38 @@ +import { IsNull } from "typeorm"; +import { Emojis } from "@/models/index.js"; +import define from "../define.js"; + +export const meta = { + tags: ["meta"], + + requireCredential: false, + allowGet: true, + cacheSec: 3600, + + res: { + type: "object", + optional: false, nullable: false, + ref: "Emoji", + }, +} as const; + +export const paramDef = { + type: "object", + properties: { + name: { + type: "string", + }, + }, + required: ["name"], +} as const; + +export default define(meta, paramDef, async (ps, me) => { + const emoji = await Emojis.findOneOrFail({ + where: { + name: ps.name, + host: IsNull(), + }, + }); + + return Emojis.pack(emoji); +}); diff --git a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts index 99d392ce45..03d762e977 100644 --- a/packages/backend/src/server/api/endpoints/messaging/messages/create.ts +++ b/packages/backend/src/server/api/endpoints/messaging/messages/create.ts @@ -151,7 +151,7 @@ export default define(meta, paramDef, async (ps, user) => { } // テキストが無いかつ添付ファイルも無かったらエラー - if (ps.text == null && file == null) { + if ((ps.text == null || ps.text.trim() === '') && file == null) { throw new ApiError(meta.errors.contentRequired); } diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index ce60436db8..5e431d4f7d 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,7 +1,9 @@ import { In } from "typeorm"; import { Notes } from "@/models/index.js"; +import { Note } from "@/models/entities/note.js"; import config from "@/config/index.js"; import es from "../../../../db/elasticsearch.js"; +import sonic from "../../../../db/sonic.js"; import define from "../../define.js"; import { makePaginationQuery } from "../../common/make-pagination-query.js"; import { generateVisibilityQuery } from "../../common/generate-visibility-query.js"; @@ -59,7 +61,7 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { - if (es == null) { + if (es == null && sonic == null) { const query = makePaginationQuery( Notes.createQueryBuilder("note"), ps.sinceId, @@ -92,9 +94,82 @@ export default define(meta, paramDef, async (ps, me) => { if (me) generateMutedUserQuery(query, me); if (me) generateBlockedUserQuery(query, me); - const notes = await query.take(ps.limit).getMany(); + const notes: Note[] = await query.take(ps.limit).getMany(); return await Notes.packMany(notes, me); + } else if (sonic) { + let start = 0; + const chunkSize = 100; + + // Use sonic to fetch and step through all search results that could match the requirements + const ids = []; + while (true) { + const results = await sonic.search.query( + sonic.collection, + sonic.bucket, + ps.query, + { + limit: chunkSize, + offset: start, + }, + ); + + start += chunkSize; + + if (results.length === 0) { + break; + } + + const res = results + .map((k) => JSON.parse(k)) + .filter((key) => { + if (ps.userId && key.userId !== ps.userId) { + return false; + } + if (ps.channelId && key.channelId !== ps.channelId) { + return false; + } + if (ps.sinceId && key.id <= ps.sinceId) { + return false; + } + if (ps.untilId && key.id >= ps.untilId) { + return false; + } + return true; + }) + .map((key) => key.id); + + ids.push(...res); + } + + // Sort all the results by note id DESC (newest first) + ids.sort((a, b) => b - a); + + // Fetch the notes from the database until we have enough to satisfy the limit + start = 0; + const found = []; + while (found.length < ps.limit && start < ids.length) { + const chunk = ids.slice(start, start + chunkSize); + const notes: Note[] = await Notes.find({ + where: { + id: In(chunk), + }, + order: { + id: "DESC", + }, + }); + + // The notes are checked for visibility and muted/blocked users when packed + found.push(...await Notes.packMany(notes, me)); + start += chunkSize; + } + + // If we have more results than the limit, trim them + if (found.length > ps.limit) { + found.length = ps.limit; + } + + return found; } else { const userQuery = ps.userId != null diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts index 1c5e31fe8d..ae8f9a786a 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -33,10 +33,10 @@ export function apiAccountMastodon(router: Router): void { let acct = data.data; acct.id = convertId(acct.id, IdType.MastodonId); acct.url = `${BASE_URL}/@${acct.url}`; - acct.note = ""; + acct.note = acct.note || ""; acct.avatar_static = acct.avatar; - acct.header = acct.header || ""; - acct.header_static = acct.header || ""; + acct.header = acct.header || "https://http.cat/404"; + acct.header_static = acct.header || "https://http.cat/404"; acct.source = { note: acct.note, fields: acct.fields, @@ -338,8 +338,13 @@ export function apiAccountMastodon(router: Router): void { ctx.body = [relationshipModel]; return; } + + let reqIds = []; + for (let i = 0; i < ids.length; i++) { + reqIds.push(convertId(ids[i], IdType.CalckeyId)); + } - const data = await client.getRelationships(ids); + const data = await client.getRelationships(reqIds); let resp = data.data; for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); @@ -359,7 +364,7 @@ export function apiAccountMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = (await client.getBookmarks(ctx.query as any)) as any; + const data = (await client.getBookmarks(limitToInt(ctx.query as any))) as any; let resp = data.data; for (let statIdx = 0; statIdx < resp.length; statIdx++) { resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); @@ -383,7 +388,7 @@ export function apiAccountMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getFavourites(ctx.query as any); + const data = await client.getFavourites(limitToInt(ctx.query as any)); let resp = data.data; for (let statIdx = 0; statIdx < resp.length; statIdx++) { resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); @@ -407,7 +412,7 @@ export function apiAccountMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getMutes(ctx.query as any); + const data = await client.getMutes(limitToInt(ctx.query as any)); let resp = data.data; for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); @@ -425,7 +430,7 @@ export function apiAccountMastodon(router: Router): void { const accessTokens = ctx.headers.authorization; const client = getClient(BASE_URL, accessTokens); try { - const data = await client.getBlocks(ctx.query as any); + const data = await client.getBlocks(limitToInt(ctx.query as any)); let resp = data.data; for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts index d04b7a8b95..38199740a3 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -4,6 +4,8 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js"; import axios from "axios"; import querystring from 'node:querystring' import qs from 'qs' +import { limitToInt } from "./timeline.js"; + function normalizeQuery(data: any) { const str = querystring.stringify(data); return qs.parse(str); @@ -101,9 +103,14 @@ export function apiStatusMastodon(router: Router): void { const client = getClient(BASE_URL, accessTokens); try { const id = ctx.params.id; - const data = await client.getStatusContext(id, ctx.query as any); + const data = await client.getStatusContext(id, limitToInt(ctx.query as any)); const status = await client.getStatus(id); - const reactionsAxios = await axios.get( + let reqInstance = axios.create({ + headers: { + Authorization : ctx.headers.authorization + } + }); + const reactionsAxios = await reqInstance.get( `${BASE_URL}/api/notes/reactions?noteId=${id}`, ); const reactions: IReaction[] = reactionsAxios.data; diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index 1b5afd6d06..23729e4804 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -15,13 +15,16 @@ export function limitToInt(q: ParsedUrlQuery) { } export function argsToBools(q: ParsedUrlQuery) { + // Values taken from https://docs.joinmastodon.org/client/intro/#boolean + const toBoolean = (value: string) => !['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value); + let object: any = q; if (q.only_media) if (typeof q.only_media === "string") - object.only_media = q.only_media.toLowerCase() === "true"; + object.only_media = toBoolean(q.only_media); if (q.exclude_replies) if (typeof q.exclude_replies === "string") - object.exclude_replies = q.exclude_replies.toLowerCase() === "true"; + object.exclude_replies = toBoolean(q.exclude_replies); return q; } @@ -92,8 +95,8 @@ export function apiTimelineMastodon(router: Router): void { try { const query: any = ctx.query; const data = query.local - ? await client.getLocalTimeline(limitToInt(query)) - : await client.getPublicTimeline(limitToInt(query)); + ? await client.getLocalTimeline(argsToBools(limitToInt(query))) + : await client.getPublicTimeline(argsToBools(limitToInt(query))); ctx.body = toTextWithReaction(data.data, ctx.hostname); } catch (e: any) { console.error(e); @@ -111,7 +114,7 @@ export function apiTimelineMastodon(router: Router): void { try { const data = await client.getTagTimeline( ctx.params.hashtag, - limitToInt(ctx.query), + argsToBools(limitToInt(ctx.query)), ); ctx.body = toTextWithReaction(data.data, ctx.hostname); } catch (e: any) { diff --git a/packages/backend/src/server/web/cli.css b/packages/backend/src/server/web/cli.css index 6903db9839..460a7b7f31 100644 --- a/packages/backend/src/server/web/cli.css +++ b/packages/backend/src/server/web/cli.css @@ -6,27 +6,38 @@ main { border-radius: 10px; } #tl > div { - padding: 16px; - border-bottom: 1px solid #908caa; + border: 1px solid #908caa; + border-radius: 10px; + margin: 10px; + padding: 10px; + width: fit-content; } #tl > div > header { font-weight: 700; + display: inline-flex; } -* { - font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; +img { + border-radius: 10px; + margin-right: 10px; } + +#form { + text-align: center; +} + #calckey_app { display: none !important; } + body, html { + font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif; background-color: #191724; color: #e0def4; justify-content: center; margin: auto; padding: 10px; - text-align: center; } button { border-radius:999px; diff --git a/packages/backend/src/server/web/cli.js b/packages/backend/src/server/web/cli.js index 290453f7e0..ca1c233759 100644 --- a/packages/backend/src/server/web/cli.js +++ b/packages/backend/src/server/web/cli.js @@ -45,12 +45,27 @@ window.onload = async () => { const tl = document.getElementById("tl"); for (const note of notes) { const el = document.createElement("div"); - const name = document.createElement("header"); + const header = document.createElement("header"); + const name = document.createElement("p"); + const avatar = document.createElement("img") name.textContent = `${note.user.name} @${note.user.username}`; + avatar.src = note.user.avatarUrl; + avatar.style = 'height: 40px' const text = document.createElement("div"); text.textContent = `${note.text}`; - el.appendChild(name); - el.appendChild(text); + el.appendChild(header); + header.appendChild(avatar); + header.appendChild(name); + if (note.text) { + el.appendChild(text); + } + if (note.files) { + for (const file of note.files) { + const img = document.createElement("img"); + img.src = file.properties.thumbnailUrl; + el.appendChild(img) + } + } tl.appendChild(el); } }); diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 968aed880f..6c7fd9ad57 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -1,5 +1,6 @@ import * as mfm from "mfm-js"; import es from "../../db/elasticsearch.js"; +import sonic from "../../db/sonic.js"; import { publishMainStream, publishNotesStream, @@ -588,7 +589,7 @@ export default async ( } // Register to search database - index(note); + await index(note); }); async function renderNoteOrRenoteActivity(data: Option, note: Note) { @@ -728,18 +729,34 @@ async function insertNote( } } -function index(note: Note) { - if (note.text == null || config.elasticsearch == null) return; +export async function index(note: Note): Promise { + if (!note.text) return; - es!.index({ - index: config.elasticsearch.index || "misskey_note", - id: note.id.toString(), - body: { - text: normalizeForSearch(note.text), - userId: note.userId, - userHost: note.userHost, - }, - }); + if (config.elasticsearch && es) { + es.index({ + index: config.elasticsearch.index || "misskey_note", + id: note.id.toString(), + body: { + text: normalizeForSearch(note.text), + userId: note.userId, + userHost: note.userHost, + }, + }); + } + + if (sonic) { + await sonic.ingest.push( + sonic.collection, + sonic.bucket, + JSON.stringify({ + id: note.id, + userId: note.userId, + userHost: note.userHost, + channelId: note.channelId, + }), + note.text, + ); + } } async function notifyToWatchersOfRenotee( diff --git a/packages/client/package.json b/packages/client/package.json index 1f3270b1da..be7538542f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -78,6 +78,7 @@ "uuid": "9.0.0", "vanilla-tilt": "1.8.0", "vite": "^4.1.1", + "vite-plugin-compression": "^0.5.1", "vue": "3.2.45", "vue-isyourpasswordsafe": "^2.0.0", "vue-plyr": "^7.0.0", diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue index 4e38b16b0f..909f0cca91 100644 --- a/packages/client/src/components/MkCwButton.vue +++ b/packages/client/src/components/MkCwButton.vue @@ -1,5 +1,5 @@ @@ -20,15 +20,26 @@ const props = defineProps<{ function menu(ev) { os.popupMenu([{ - type: 'label', - text: ':' + props.emoji.name + ':', + type: "label", + text: ":" + props.emoji.name + ":", }, { text: i18n.ts.copy, - icon: 'ph-clipboard-text ph-bold ph-lg', + icon: "ph-clipboard-text ph-bold ph-lg", action: () => { copyToClipboard(`:${props.emoji.name}:`); os.success(); - } + }, + }, { + text: i18n.ts.license, + icon: "ph-info ph-bold ph-lg", + action: () => { + os.apiGet("emoji", { name: props.emoji.name }).then(res => { + os.alert({ + type: "info", + text: `${res.license || i18n.ts.notSet}`, + }); + }); + }, }], ev.currentTarget ?? ev.target); } diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue index 4fe3722776..4e7fa30a68 100644 --- a/packages/client/src/pages/messaging/messaging-room.form.vue +++ b/packages/client/src/pages/messaging/messaging-room.form.vue @@ -18,7 +18,7 @@ @@ -57,7 +57,7 @@ const typing = throttle(3000, () => { }); let draftKey = $computed(() => props.user ? 'user:' + props.user.id : 'group:' + props.group?.id); -let canSend = $computed(() => (text != null && text !== '') || file != null); +let canSend = $computed(() => (text != null && text.trim() !== '') || file != null); watch([$$(text), $$(file)], saveDraft); diff --git a/packages/client/src/pages/note.vue b/packages/client/src/pages/note.vue index 9f400907d9..5298194a6d 100644 --- a/packages/client/src/pages/note.vue +++ b/packages/client/src/pages/note.vue @@ -1,7 +1,7 @@