Merge branch 'develop' into iceshrimp_mastodon

This commit is contained in:
naskya 2024-07-05 01:38:52 +09:00
commit c630187b2e
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
37 changed files with 336 additions and 97 deletions

View file

@ -90,6 +90,8 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
However, we are currently so understaffed that it is virtually impossible to
respond to every single proposal. So, feel free to implement it if there is no response
for more than a week or there is a thumbs-up emoji reaction from the project maintainer(s).
Many thanks for your involvement!
-->

View file

@ -58,6 +58,8 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
However, we are currently so understaffed that it is virtually impossible to
respond to every single proposal. So, feel free to implement it if there is no response
for more than a week or there is a thumbs-up emoji reaction from the project maintainer(s).
Many thanks for your involvement!
-->

View file

@ -58,6 +58,8 @@ By submitting this issue, you agree to follow our [Contribution Guidelines](http
However, we are currently so understaffed that it is virtually impossible to
respond to every single proposal. So, feel free to implement it if there is no response
for more than a week or there is a thumbs-up emoji reaction from the project maintainer(s).
Many thanks for your involvement!
-->

View file

@ -6,9 +6,7 @@
## Contribution Guidelines
By submitting this merge request, you agree to follow our [Contribution Guidelines](https://firefish.dev/firefish/firefish/-/blob/develop/CONTRIBUTING.md)
- [ ] This closes issue #0000 (please substitute the number)
- [ ] This is a minor bug fix or refactoring
- [ ] This closes #0000 (please substitute the issue number or open a new one unless this is a minor fix/refactor)
- [ ] I agree to follow this project's Contribution Guidelines
- [ ] I have made sure to test this merge request
- [ ] I have made sure to run `pnpm run format` before submitting this merge request

View file

@ -4,6 +4,7 @@ Breaking changes are indicated by the :warning: icon.
## Unreleased
- Added `readCatLanguage` field to the response of `i` and request of `i/update` (optional).
- The old Mastodon API has been replaced with a new implementation based on Iceshrimps.
- :warning: The new API uses a new format to manage Mastodon sessions in the database, whereas old implementation uses Misskey sessions. All previous client app and token registrations will not work with the new API. All clients need to be re-registered and all users need to re-authenticate.
- :warning: All IDs (of statuses/notes, notifications, users, etc.) will be using the alphanumerical format, aligning with the Firefish/Misskey API. The old numerical IDs will not work when queried against the new API.

View file

@ -7,8 +7,9 @@ Critical security updates are indicated by the :warning: icon.
## Unreleased
- Fix bugs
- Mastodon API implementation was ported from Iceshrimp, with added Firefish extensions including push notifications, post languages, schedule post support, and more. (#10880)
- Add ability to disable the cat language conversion (nyaification)
- Fix bugs
### Acknowledgement

View file

@ -5,6 +5,7 @@ DELETE FROM "migrations" WHERE name IN (
'SwSubscriptionAccessToken1709395223611',
'UserProfileMentions1711075007936',
'ClientCredentials1713108561474',
'TurnOffCatLanguage1720107645050',
'RefactorScheduledPosts1716804636187',
'RemoveEnumTypenameSuffix1716462794927',
'CreateScheduledNote1714728200194',
@ -52,6 +53,9 @@ ALTER TABLE "user_profile" DROP COLUMN "mentions";
-- client-credential-support
ALTER TABLE "access_token" ALTER COLUMN "userId" SET NOT NULL;
-- turn-off-cat-language
ALTER TABLE "user" DROP COLUMN "readCatLanguage";
-- refactor-scheduled-post
CREATE TABLE "scheduled_note" (
"id" character varying(32) NOT NULL PRIMARY KEY,

View file

@ -10,7 +10,15 @@ Please take a look at #10947.
### For all users
This is not related to the recent changes, but we have added a new section called "[Maintain the server](https://firefish.dev/firefish/firefish/-/blob/develop/docs/install.md#maintain-the-server)" in the installation guide. We suggest that you take a look at it. (and we welcome your docs contributions!)
This is not related to the recent changes, but we have added a new section called "[Maintain the server](https://firefish.dev/firefish/firefish/-/blob/develop/docs/install.md#maintain-the-server)" in the installation guide. We suggest that you take a look at it (and we welcome your docs contributions)!
### For systemd/pm2 users
[Node.js will release a new security fix on July 8th](<https://nodejs.org/en/blog/vulnerability/july-2024-security-releases>). It is highly recommended that you upgrade your Node.js version once it's released.
### For Docker/Podman users
[Node.js will release a new security fix on July 8th](<https://nodejs.org/en/blog/vulnerability/july-2024-security-releases>). Once it's released and the [docker.io/node](<https://hub.docker.com/_/node>) image is updated, we'll rebuild the OCI image based on the new `docker.io/node` image and reupload it as [`registry.firefish.dev/firefish/firefish:latest`](<https://firefish.dev/firefish/firefish/container_registry/1>).
### For systemd/pm2 users

View file

@ -1241,6 +1241,7 @@ showNoAltTextWarning: "Show a warning if you attempt to post files without a des
showAddFileDescriptionAtFirstPost: "Automatically open a form to write a description
when you attempt to post files without a description"
addAlt4MeTag: "Automatically append #Alt4Me hashtag to your post if attached file has no description"
turnOffCatLanguage: "Turn off cat language conversion"
_emojiModPerm:
unauthorized: "None"

View file

@ -1306,6 +1306,8 @@ export interface Services {
outbound: Array<Outbound>
}
export declare function shouldNyaify(readerUserId: string): Promise<boolean>
/** Prints the server hardware information as the server info log. */
export declare function showServerInfo(): void
@ -1429,6 +1431,7 @@ export interface User {
emojiModPerm: UserEmojiModPerm
isIndexable: boolean
alsoKnownAs: Array<string> | null
readCatLanguage: boolean
}
export const USER_ACTIVE_THRESHOLD: number
@ -1547,10 +1550,10 @@ export interface UserProfile {
preventAiLearning: boolean
isIndexable: boolean
mutedPatterns: Array<string>
mentions: Json
mutedInstances: Array<string>
mutedWords: Array<string>
lang: string | null
mentions: Json
}
export enum UserProfileFfvisibility {

View file

@ -432,6 +432,7 @@ module.exports.removeOldAttestationChallenges = nativeBinding.removeOldAttestati
module.exports.safeForSql = nativeBinding.safeForSql
module.exports.SECOND = nativeBinding.SECOND
module.exports.sendPushNotification = nativeBinding.sendPushNotification
module.exports.shouldNyaify = nativeBinding.shouldNyaify
module.exports.showServerInfo = nativeBinding.showServerInfo
module.exports.sqlLikeEscape = nativeBinding.sqlLikeEscape
module.exports.sqlRegexEscape = nativeBinding.sqlRegexEscape

View file

@ -9,6 +9,7 @@ pub enum Category {
FetchUrl,
Block,
Follow,
CatLang,
#[cfg(test)]
Test,
}
@ -33,6 +34,7 @@ fn categorize(category: Category, key: &str) -> String {
Category::FetchUrl => "fetchUrl",
Category::Block => "blocking",
Category::Follow => "following",
Category::CatLang => "catlang",
#[cfg(test)]
Category::Test => "usedOnlyForTesting",
};

View file

@ -15,4 +15,5 @@ pub mod nyaify;
pub mod password;
pub mod reaction;
pub mod remove_old_attestation_challenges;
pub mod should_nyaify;
pub mod system_info;

View file

@ -0,0 +1,45 @@
//! Determine whether to enable the cat language conversion
use crate::{
database::{cache, db_conn},
model::entity::user,
};
use sea_orm::{DbErr, EntityTrait, QuerySelect, SelectColumns};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[doc = "database error"]
#[error(transparent)]
Db(#[from] DbErr),
#[doc = "cache error"]
#[error(transparent)]
Cache(#[from] cache::Error),
#[error("user {0} not found")]
NotFound(String),
}
#[macros::export]
pub async fn should_nyaify(reader_user_id: &str) -> Result<bool, Error> {
let cached_value = cache::get_one::<bool>(cache::Category::CatLang, reader_user_id).await?;
if let Some(value) = cached_value {
return Ok(value);
}
let fetched_value = user::Entity::find_by_id(reader_user_id)
.select_only()
.select_column(user::Column::ReadCatLanguage)
.into_tuple::<bool>()
.one(db_conn().await?)
.await?
.ok_or_else(|| Error::NotFound(reader_user_id.to_owned()))?;
cache::set_one(
cache::Category::CatLang,
reader_user_id,
&fetched_value,
10 * 60,
)
.await?;
Ok(fetched_value)
}

View file

@ -77,6 +77,8 @@ pub struct Model {
pub is_indexable: bool,
#[sea_orm(column_name = "alsoKnownAs")]
pub also_known_as: Option<Vec<String>>,
#[sea_orm(column_name = "readCatLanguage")]
pub read_cat_language: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -68,13 +68,13 @@ pub struct Model {
pub is_indexable: bool,
#[sea_orm(column_name = "mutedPatterns")]
pub muted_patterns: Vec<String>,
#[sea_orm(column_type = "JsonBinary")]
pub mentions: Json,
#[sea_orm(column_name = "mutedInstances")]
pub muted_instances: Vec<String>,
#[sea_orm(column_name = "mutedWords")]
pub muted_words: Vec<String>,
pub lang: Option<String>,
#[sea_orm(column_type = "JsonBinary")]
pub mentions: Json,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -28,18 +28,18 @@
"@koa/router": "12.0.1",
"@ladjs/koa-views": "9.0.0",
"@peertube/http-signature": "1.7.0",
"@redocly/openapi-core": "1.17.0",
"@redocly/openapi-core": "1.17.1",
"@sinonjs/fake-timers": "11.2.2",
"adm-zip": "0.5.14",
"ajv": "8.16.0",
"archiver": "7.0.1",
"async-lock": "1.4.0",
"async-mutex": "0.5.0",
"aws-sdk": "2.1653.0",
"aws-sdk": "2.1654.0",
"axios": "1.7.2",
"backend-rs": "workspace:*",
"blurhash": "2.0.5",
"bull": "4.15.0",
"bull": "4.15.1",
"cacheable-lookup": "git+https://github.com/TheEssem/cacheable-lookup.git#dd2fb616366a3c68dcf321a57a67295967b204bf",
"cbor-x": "1.5.9",
"chalk": "5.3.0",
@ -174,6 +174,6 @@
"type-fest": "4.21.0",
"typescript": "5.5.3",
"webpack": "5.92.1",
"ws": "8.17.1"
"ws": "8.18.0"
}
}

View file

@ -25,10 +25,11 @@ export async function masterMain() {
// initialize app
try {
greet();
showEnvironment();
showServerInfo();
showEnvironment();
showNodejsVersion();
await connectDb();
await updateMetaCache();
} catch (e) {
bootLogger.error(
`Fatal error occurred during initialization:\n${inspect(e)}`,

View file

@ -0,0 +1,16 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
export class TurnOffCatLanguage1720107645050 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user" ADD COLUMN "readCatLanguage" boolean NOT NULL DEFAULT true`,
);
await queryRunner.query(
`COMMENT ON COLUMN "user"."readCatLanguage" IS 'Whether to enable the cat language conversion.'`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "readCatLanguage"`);
}
}

View file

@ -1,3 +1,5 @@
import { noteVisibilities } from "@/types.js";
export type Post = {
text: string | undefined;
cw: string | null;
@ -12,7 +14,9 @@ export function parse(acct: any): Post {
cw: acct.cw,
localOnly: acct.localOnly,
createdAt: new Date(acct.createdAt),
visibility: `hidden${acct.visibility || ""}`,
visibility: noteVisibilities.includes(acct.visibility)
? acct.visibility
: "specified",
};
}

View file

@ -159,6 +159,12 @@ export class User {
})
public speakAsCat: boolean;
@Column("boolean", {
default: true,
comment: "Whether to enable the cat language conversion.",
})
public readCatLanguage: boolean;
@Column("boolean", {
default: false,
comment: "Whether the User is the admin.",

View file

@ -14,7 +14,12 @@ import {
Notes,
} from "../index.js";
import type { Packed } from "@/misc/schema.js";
import { countReactions, decodeReaction, nyaify } from "backend-rs";
import {
countReactions,
decodeReaction,
nyaify,
shouldNyaify,
} from "backend-rs";
import { awaitAll } from "@/prelude/await-all.js";
import type { NoteReaction } from "@/models/entities/note-reaction.js";
import {
@ -311,7 +316,13 @@ export const NoteRepository = db.getRepository(Note).extend({
mentionedRemoteUsers: this.mentionedRemoteUsers(note),
});
if (packed.user.isCat && packed.user.speakAsCat && packed.text) {
if (
packed.user.isCat &&
packed.user.speakAsCat &&
packed.text != null &&
meId != null &&
(await shouldNyaify(meId))
) {
const tokens = packed.text ? mfm.parse(packed.text) : [];
function nyaifyNode(node: mfm.MfmNode) {
if (node.type === "quote") return;

View file

@ -464,7 +464,8 @@ export const UserRepository = db.getRepository(User).extend({
isLocked: user.isLocked,
isIndexable: user.isIndexable,
isCat: user.isCat || falsy,
speakAsCat: user.speakAsCat || falsy,
speakAsCat: user.speakAsCat,
readCatLanguage: user.readCatLanguage,
instance: user.host
? userInstanceCache
.fetch(

View file

@ -92,22 +92,29 @@ export async function importCkPost(
logger.info("Post updated");
}
if (note == null) {
note = await create(user, {
createdAt: createdAt,
files: files.length === 0 ? undefined : files,
poll: undefined,
text: text || undefined,
reply: post.replyId ? job.data.parent : null,
renote: post.renoteId ? job.data.parent : null,
cw: cw,
localOnly,
visibility: visibility,
visibleUsers: [],
channel: null,
apMentions: new Array(0),
apHashtags: undefined,
apEmojis: undefined,
});
note = await create(
user,
{
createdAt: createdAt,
scheduledAt: undefined,
files: files.length === 0 ? undefined : files,
poll: undefined,
text: text || undefined,
reply: post.replyId ? job.data.parent : null,
renote: post.renoteId ? job.data.parent : null,
cw: cw,
localOnly,
visibility: visibility,
visibleUsers: [],
channel: null,
apMentions: new Array(0),
apHashtags: undefined,
apEmojis: undefined,
},
false,
undefined,
true,
);
logger.debug("New post has been created");
} else {
logger.info("This post already exists");

View file

@ -10,6 +10,11 @@ import type { DriveFile } from "@/models/entities/drive-file.js";
import { Notes, NoteEdits } from "@/models/index.js";
import type { Note } from "@/models/entities/note.js";
import { genId } from "backend-rs";
import promiseLimit from "promise-limit";
import { unique, concat } from "@/prelude/array.js";
import type { CacheableUser } from "@/models/entities/user.js";
import { resolvePerson } from "@/remote/activitypub/models/person.js";
import { isPublic } from "@/remote/activitypub/audience.js";
const logger = queueLogger.createSubLogger("import-masto-post");
@ -118,24 +123,57 @@ export async function importMastoPost(
logger.info("Post updated");
}
if (note == null) {
note = await create(user, {
createdAt: isRenote
? new Date(post.published)
: new Date(post.object.published),
files: files.length === 0 ? undefined : files,
poll: undefined,
text: text || undefined,
reply,
renote,
cw: !isRenote && post.object.sensitive ? post.object.summary : undefined,
localOnly: false,
visibility: "hiddenpublic",
visibleUsers: [],
channel: null,
apMentions: new Array(0),
apHashtags: undefined,
apEmojis: undefined,
});
let visibility = "specified";
let visibleUsers: CacheableUser[] = [];
if ((post.to as string[]).some(isPublic)) {
visibility = "public";
} else if ((post.cc as string[]).some(isPublic)) {
visibility = "home";
} else if ((post.cc as string[]).some((cc) => cc.endsWith("/followers"))) {
visibility = "followers";
} else {
try {
const visibleUsersList = unique(concat([post.to, post.cc]));
const limit = promiseLimit<CacheableUser | null>(2);
visibleUsers = (
await Promise.all(
visibleUsersList.map((id) =>
limit(() => resolvePerson(id).catch(() => null)),
),
)
).filter((x): x is CacheableUser => x != null);
} catch {
// nothing need to do.
}
}
note = await create(
user,
{
createdAt: isRenote
? new Date(post.published)
: new Date(post.object.published),
scheduledAt: undefined,
files: files.length === 0 ? undefined : files,
poll: undefined,
text: text || undefined,
reply,
renote,
cw:
!isRenote && post.object.sensitive ? post.object.summary : undefined,
localOnly: false,
visibility,
visibleUsers,
channel: null,
apMentions: new Array(0),
apHashtags: undefined,
apEmojis: undefined,
},
false,
undefined,
true,
);
logger.debug("New post has been created");
} else {
logger.info("This post already exists");

View file

@ -90,7 +90,7 @@ function groupingAudience(ids: string[], actor: CacheableRemoteUser) {
return groups;
}
function isPublic(id: string) {
export function isPublic(id: string) {
return [
"https://www.w3.org/ns/activitystreams#Public",
"as:Public",
@ -98,6 +98,6 @@ function isPublic(id: string) {
].includes(id);
}
function isFollowers(id: string, actor: CacheableRemoteUser) {
export function isFollowers(id: string, actor: CacheableRemoteUser) {
return id === (actor.followersUri || `${actor.uri}/followers`);
}

View file

@ -114,6 +114,7 @@ export const paramDef = {
isBot: { type: "boolean" },
isCat: { type: "boolean" },
speakAsCat: { type: "boolean", nullable: true },
readCatLanguage: { type: "boolean", nullable: true },
isIndexable: { type: "boolean" },
injectFeaturedNote: { type: "boolean" },
receiveAnnouncementEmail: { type: "boolean" },
@ -220,6 +221,8 @@ export default define(meta, paramDef, async (ps, _user, token) => {
profileUpdates.isIndexable = ps.isIndexable;
}
if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat;
if (typeof ps.readCatLanguage === "boolean")
updates.readCatLanguage = ps.readCatLanguage;
if (typeof ps.injectFeaturedNote === "boolean")
profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === "boolean")

View file

@ -162,11 +162,12 @@ export default async (
data: NoteLike,
silent = false,
waitToPublish?: (note: Note) => Promise<void>,
dontFederateInitially = false,
) =>
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
new Promise<Note>(async (res, rej) => {
const dontFederateInitially =
data.visibility?.startsWith("hidden") === true;
dontFederateInitially =
dontFederateInitially || data.visibility?.startsWith("hidden");
// Whether this is a scheduled "draft" post (yet to be published)
const isDraft = data.scheduledAt != null;
@ -204,8 +205,6 @@ export default async (
if (data.channel != null) data.visibility = "public";
if (data.channel != null) data.visibleUsers = [];
if (data.channel != null) data.localOnly = true;
if (data.visibility.startsWith("hidden") && data.visibility !== "hidden")
data.visibility = data.visibility.slice(6);
// enforce silent clients on server
if (

View file

@ -78,7 +78,7 @@
<MkButton inline primary @click="search"
>{{ i18n.ts.search }}
</MkButton>
<MkButton inline @click="lookup">{{ i18n.ts.lookup }}</MkButton>
<MkButton inline primary @click="lookup">{{ i18n.ts.lookup }}</MkButton>
<MkButton inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
</div>
</div>

View file

@ -61,9 +61,11 @@ function checkForSplash() {
if (splash) {
splash.style.opacity = "0";
splash.style.pointerEvents = "none";
splash.addEventListener("transitionend", () => {
// remove splash screen
window.setTimeout(() => {
splash.remove();
});
}, 1000);
}
}

View file

@ -5,18 +5,76 @@ import { set, get } from "idb-keyval";
// TODO: 他のタブと永続化されたstateを同期
// TODO: get("instance") requires top-level await
let instance: entities.DetailedInstanceMetadata;
// TODO: fallback to defaults more nicely (with #10947)
// default values
let instanceMeta: entities.DetailedInstanceMetadata = {
maintainerName: "",
maintainerEmail: "",
version: "",
name: null,
uri: "",
tosUrl: null,
description: null,
disableRegistration: true,
disableLocalTimeline: false,
disableGlobalTimeline: false,
disableRecommendedTimeline: true,
enableGuestTimeline: false,
driveCapacityPerLocalUserMb: 1000,
driveCapacityPerRemoteUserMb: 0,
antennaLimit: 5,
enableHcaptcha: false,
hcaptchaSiteKey: null,
enableRecaptcha: false,
recaptchaSiteKey: null,
swPublickey: null,
maxNoteTextLength: 3000,
maxCaptionTextLength: 1500,
enableEmail: false,
enableServiceWorker: false,
markLocalFilesNsfwByDefault: false,
emojis: [],
ads: [],
langs: [],
moreUrls: [],
repositoryUrl: "https://firefish.dev/firefish/firefish",
feedbackUrl: "https://firefish.dev/firefish/firefish/-/issues",
defaultDarkTheme: null,
defaultLightTheme: null,
defaultReaction: "⭐",
cacheRemoteFiles: false,
proxyAccountName: null,
emailRequiredForSignup: false,
mascotImageUrl: "",
bannerUrl: "",
backgroundImageUrl: "",
errorImageUrl: "",
iconUrl: null,
requireSetup: false,
translatorAvailable: false,
features: {
registration: false,
localTimeLine: true,
recommendedTimeLine: false,
globalTimeLine: true,
searchFilters: true,
hcaptcha: false,
recaptcha: false,
objectStorage: false,
serviceWorker: false,
},
};
// get("instanceMeta") requires top-level await
export function getInstanceInfo(): entities.DetailedInstanceMetadata {
return instance;
return instanceMeta;
}
export async function initializeInstanceCache(): Promise<void> {
// Is the data stored in IndexDB?
const fromIdb = await get<string>("instance");
const fromIdb = await get<string>("instanceMeta");
if (fromIdb != null) {
instance = JSON.parse(fromIdb);
instanceMeta = JSON.parse(fromIdb);
}
// Call API
updateInstanceCache();
@ -27,29 +85,25 @@ export async function updateInstanceCache(): Promise<void> {
detail: true,
});
// TODO: set default values
instance = {} as entities.DetailedInstanceMetadata;
for (const [k, v] of Object.entries(meta)) {
instance[k] = v;
instanceMeta[k] = v;
}
set("instance", JSON.stringify(instance));
set("instanceMeta", JSON.stringify(instanceMeta));
}
export const emojiCategories = computed(() => {
if (instance.emojis == null) return [];
if (instanceMeta.emojis == null) return [];
const categories = new Set();
for (const emoji of instance.emojis) {
for (const emoji of instanceMeta.emojis) {
categories.add(emoji.category);
}
return Array.from(categories);
});
export const emojiTags = computed(() => {
if (instance.emojis == null) return [];
if (instanceMeta.emojis == null) return [];
const tags = new Set();
for (const emoji of instance.emojis) {
for (const emoji of instanceMeta.emojis) {
for (const tag of emoji.aliases) {
tags.add(tag);
}

View file

@ -200,6 +200,11 @@
i18n.ts.expandOnNoteClickDesc
}}</template>
</FormSwitch>
<FormSwitch v-model="turnOffCatLanguage" @update:modelValue="save()" class="_formBlock"
>{{ i18n.ts.turnOffCatLanguage }}<template #caption>{{
i18n.ts.reflectMayTakeTime
}}</template>
</FormSwitch>
<FormSwitch v-model="advancedMfm" class="_formBlock">
{{ i18n.ts._mfm.advanced
}}<template #caption>{{
@ -423,6 +428,13 @@ const serverLang = ref(me?.lang);
const translateLang = ref(localStorage.getItem("translateLang"));
const fontSize = ref(localStorage.getItem("fontSize"));
const useSystemFont = ref(localStorage.getItem("useSystemFont") !== "f");
const turnOffCatLanguage = ref(!me?.readCatLanguage);
function save() {
os.api("i/update", {
readCatLanguage: !turnOffCatLanguage.value,
});
}
async function reloadAsk() {
const { canceled } = await os.confirm({

View file

@ -110,6 +110,10 @@ provideMetadataReceiver((info) => {
});
const root = computed(() => mainRouter.currentRoute.value?.name === "index");
const showMenu = ref(false);
const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
const narrow = ref(window.innerWidth < 1280);
const meta = ref();
os.api("meta", { detail: true }).then((res) => {
meta.value = res;

View file

@ -598,6 +598,8 @@ export type Endpoints = {
preventAiLearning?: boolean;
isBot?: boolean;
isCat?: boolean;
speakAsCat?: boolean;
readCatLanguage?: boolean;
injectFeaturedNote?: boolean;
receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean;

View file

@ -30,6 +30,7 @@ export type UserLite = {
isIndexable: boolean;
isCat?: boolean;
speakAsCat?: boolean;
readCatLanguage?: boolean;
driveCapacityOverrideMb: number | null;
};
@ -395,7 +396,7 @@ export type DetailedInstanceMetadata = LiteInstanceMetadata & {
miauth?: boolean;
};
langs: string[];
moreUrls: object;
moreUrls: { name: string; url: string }[];
repositoryUrl: string;
feedbackUrl: string;
defaultDarkTheme: string | null;

View file

@ -76,6 +76,11 @@ export const packedUserLiteSchema = {
nullable: false,
optional: true,
},
readCatLanguage: {
type: "boolean",
nullable: false,
optional: true,
},
emojis: {
type: "array",
nullable: false,

View file

@ -67,8 +67,8 @@ importers:
specifier: 1.7.0
version: 1.7.0
'@redocly/openapi-core':
specifier: 1.17.0
version: 1.17.0
specifier: 1.17.1
version: 1.17.1
'@sinonjs/fake-timers':
specifier: 11.2.2
version: 11.2.2
@ -88,8 +88,8 @@ importers:
specifier: 0.5.0
version: 0.5.0
aws-sdk:
specifier: 2.1653.0
version: 2.1653.0
specifier: 2.1654.0
version: 2.1654.0
axios:
specifier: 1.7.2
version: 1.7.2
@ -100,8 +100,8 @@ importers:
specifier: 2.0.5
version: 2.0.5
bull:
specifier: 4.15.0
version: 4.15.0
specifier: 4.15.1
version: 4.15.1
cacheable-lookup:
specifier: git+https://github.com/TheEssem/cacheable-lookup.git#dd2fb616366a3c68dcf321a57a67295967b204bf
version: https://codeload.github.com/TheEssem/cacheable-lookup/tar.gz/dd2fb616366a3c68dcf321a57a67295967b204bf
@ -500,8 +500,8 @@ importers:
specifier: 5.92.1
version: 5.92.1
ws:
specifier: 8.17.1
version: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
specifier: 8.18.0
version: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
packages/backend-rs:
devDependencies:
@ -2099,8 +2099,8 @@ packages:
'@redocly/config@0.6.2':
resolution: {integrity: sha512-c3K5u64eMnr2ootPcpEI0ioIRLE8QP8ptvLxG9MwAmb2sU8HMRfVwXDU3AZiMVY2w4Ts0mDc+Xv4HTIk8DRqFw==}
'@redocly/openapi-core@1.17.0':
resolution: {integrity: sha512-XoNIuksnOGAzAcfpyJkHrMxwurXaQfglnovNE7/pTx4OEjik3OT91+tKAyRCkklVCdMtAA3YokGMZzdhjViUWA==}
'@redocly/openapi-core@1.17.1':
resolution: {integrity: sha512-PQxDLLNk5cBatJBBxvfk49HFw/nVozw1XZ6Dw/GX0Tviq+WxeEjEuLAKfnLVvb5L0wgs4TNmVG4Y+JyofSPu1A==}
engines: {node: '>=14.19.0', npm: '>=7.0.0'}
'@rollup/plugin-alias@5.1.0':
@ -2839,8 +2839,8 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
aws-sdk@2.1653.0:
resolution: {integrity: sha512-9f42kuLpMcL1EPZOsLM8u6wlnOMtFwED1b24SN0fBbi/N7N1xTLZ7vbEMt/haz06Lc3Vr3VMDyv0atfMmkboBw==}
aws-sdk@2.1654.0:
resolution: {integrity: sha512-b5ryvXipBJod9Uh1GUfQNgi5tIIiluxJbyqr/hZ/mr5U8WxrrfjVq3nGnx5JjevFKYRqXIywhumsVyanfACzFA==}
engines: {node: '>= 10.0.0'}
axios@0.24.0:
@ -2981,8 +2981,8 @@ packages:
resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
engines: {node: '>=6.14.2'}
bull@4.15.0:
resolution: {integrity: sha512-nOEAfUXwUXtFbRPQP3bWCwpQ/NAerAu2Nym/ucv5C1E+Qh2x6RGdKKsYIfZam4mYncayTynTUN/HLhRgGi2N8w==}
bull@4.15.1:
resolution: {integrity: sha512-knVKiZdrXbRkB+fWqNryDz85b3JfsT3dBrZexkztwvTH/AFmpHvsC933VB3JX18aJCz47E+xdO57xbDvxljoAg==}
engines: {node: '>=12'}
busboy@1.6.0:
@ -6856,8 +6856,8 @@ packages:
resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
ws@8.17.1:
resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==}
ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@ -8229,7 +8229,7 @@ snapshots:
'@redocly/config@0.6.2': {}
'@redocly/openapi-core@1.17.0':
'@redocly/openapi-core@1.17.1':
dependencies:
'@redocly/ajv': 8.11.0
'@redocly/config': 0.6.2
@ -9028,7 +9028,7 @@ snapshots:
dependencies:
possible-typed-array-names: 1.0.0
aws-sdk@2.1653.0:
aws-sdk@2.1654.0:
dependencies:
buffer: 4.9.2
events: 1.1.1
@ -9223,7 +9223,7 @@ snapshots:
dependencies:
node-gyp-build: 4.8.1
bull@4.15.0:
bull@4.15.1:
dependencies:
cron-parser: 4.9.0
get-port: 5.1.1
@ -11130,7 +11130,7 @@ snapshots:
whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0
whatwg-url: 14.0.0
ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
@ -13323,7 +13323,7 @@ snapshots:
imurmurhash: 0.1.4
signal-exit: 3.0.7
ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10):
ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10):
optionalDependencies:
bufferutil: 4.0.8
utf-8-validate: 5.0.10