Merge branch 'develop' of https://codeberg.org/calckey/calckey into develop

This commit is contained in:
Kio-td 2023-02-12 12:11:21 -05:00
commit ec79e1c143
70 changed files with 541 additions and 187 deletions

2
.gitignore vendored
View file

@ -42,6 +42,8 @@ api-docs.json
files files
ormconfig.json ormconfig.json
packages/backend/assets/instance.css packages/backend/assets/instance.css
packages/backend/assets/sounds/None.mp3
# blender backups # blender backups
*.blend1 *.blend1

View file

@ -124,6 +124,8 @@ psql postgres -c "create database calckey with encoding = 'UTF8';"
- To add custom CSS for all users, edit `./custom/assets/instance.css`. - To add custom CSS for all users, edit `./custom/assets/instance.css`.
- To add static assets (such as images for the splash screen), place them in the `./custom/assets/` directory. They'll then be available on `https://yourinstance.tld/static-assets/filename.ext`. - To add static assets (such as images for the splash screen), place them in the `./custom/assets/` directory. They'll then be available on `https://yourinstance.tld/static-assets/filename.ext`.
- To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`) - To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`)
- To add custom error images, place them in the `./custom/assets/badges` directory, replacing the files already there.
- To add custom sounds, place only mp3 files in the `./custom/assets/sounds` directory.
- To update custom assets without rebuilding, just run `pnpm run gulp`. - To update custom assets without rebuilding, just run `pnpm run gulp`.
## 🧑‍🔬 Configuring a new instance ## 🧑‍🔬 Configuring a new instance

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View file

@ -15,8 +15,9 @@ gulp.task('copy:backend:views', () =>
gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views')) gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views'))
); );
gulp.task('copy:backend:custom', () => gulp.task('copy:backend:custom', () =>
gulp.src('./custom/assets/*').pipe(gulp.dest('./packages/backend/assets/')) gulp.src('./custom/assets/**/*').pipe(gulp.dest('./packages/backend/assets/'))
); );
gulp.task('copy:client:fonts', () => gulp.task('copy:client:fonts', () =>

View file

@ -32,6 +32,7 @@ uploading: "Uploading..."
save: "Save" save: "Save"
users: "Users" users: "Users"
addUser: "Add a user" addUser: "Add a user"
addInstance: "Add an instance"
favorite: "Add to bookmarks" favorite: "Add to bookmarks"
favorites: "Bookmarks" favorites: "Bookmarks"
unfavorite: "Remove from bookmarks" unfavorite: "Remove from bookmarks"
@ -160,6 +161,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." 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" host: "Host"
selectUser: "Select a user" selectUser: "Select a user"
selectInstance: "Select an instance"
recipient: "Recipient(s)" recipient: "Recipient(s)"
annotation: "Comments" annotation: "Comments"
federation: "Federation" federation: "Federation"
@ -197,6 +199,7 @@ muteAndBlock: "Mutes and Blocks"
mutedUsers: "Muted users" mutedUsers: "Muted users"
blockedUsers: "Blocked users" blockedUsers: "Blocked users"
noUsers: "There are no users" noUsers: "There are no users"
noInstances: "There are no instances"
editProfile: "Edit profile" editProfile: "Edit profile"
noteDeleteConfirm: "Are you sure you want to delete this post?" noteDeleteConfirm: "Are you sure you want to delete this post?"
pinLimitExceeded: "You cannot pin any more posts" pinLimitExceeded: "You cannot pin any more posts"
@ -363,6 +366,7 @@ notifyAntenna: "Notify about new posts"
withFileAntenna: "Only posts with files" withFileAntenna: "Only posts with files"
enableServiceworker: "Enable Push-Notifications for your Browser" enableServiceworker: "Enable Push-Notifications for your Browser"
antennaUsersDescription: "List one username per line" antennaUsersDescription: "List one username per line"
antennaInstancesDescription: "List one instance host per line"
caseSensitive: "Case sensitive" caseSensitive: "Case sensitive"
withReplies: "Include replies" withReplies: "Include replies"
connectedTo: "Following account(s) are connected" connectedTo: "Following account(s) are connected"
@ -1301,6 +1305,7 @@ _antennaSources:
users: "Posts from specific users" users: "Posts from specific users"
userList: "Posts from a specified list of users" userList: "Posts from a specified list of users"
userGroup: "Posts from users in a specified group" userGroup: "Posts from users in a specified group"
instances: "Posts from all users on an instance"
_weekday: _weekday:
sunday: "Sunday" sunday: "Sunday"
monday: "Monday" monday: "Monday"

View file

@ -1,6 +1,6 @@
{ {
"name": "calckey", "name": "calckey",
"version": "13.2.0-dev", "version": "13.2.0-dev2",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",
@ -39,7 +39,7 @@
"@bull-board/api": "^4.10.2", "@bull-board/api": "^4.10.2",
"@bull-board/ui": "^4.10.2", "@bull-board/ui": "^4.10.2",
"@tensorflow/tfjs": "^3.21.0", "@tensorflow/tfjs": "^3.21.0",
"calckey-js": "^0.0.20", "calckey-js": "^0.0.22",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"phosphor-icons": "^1.4.2", "phosphor-icons": "^1.4.2",
"seedrandom": "^3.0.5" "seedrandom": "^3.0.5"

View file

@ -0,0 +1,17 @@
export class AntennaInstances1676093997212 {
name = 'AntennaInstances1676093997212'
async up(queryRunner) {
await queryRunner.query(`ALTER TYPE "antenna_src_enum" ADD VALUE 'instances'`);
await queryRunner.query(`ALTER TABLE "antenna" ADD "instances" jsonb NOT NULL DEFAULT '[]'`);
}
async down(queryRunner) {
await queryRunner.query(`DELETE FROM "antenna" WHERE "src" = 'instances'`);
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "instances"`);
await queryRunner.query(`CREATE TYPE "public"."antenna_src_enum_old" AS ENUM('home', 'all', 'users', 'list', 'group')`);
await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "src" TYPE "public"."antenna_src_enum_old" USING "src"::"text"::"public"."antenna_src_enum_old"`);
await queryRunner.query(`DROP TYPE "public"."antenna_src_enum"`);
await queryRunner.query(`ALTER TYPE "public"."antenna_src_enum_old" RENAME TO "antenna_src_enum"`);
}
}

View file

@ -47,7 +47,7 @@
"blurhash": "1.1.5", "blurhash": "1.1.5",
"bull": "4.10.2", "bull": "4.10.2",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"calckey-js": "^0.0.20", "calckey-js": "^0.0.22",
"cbor": "8.1.0", "cbor": "8.1.0",
"chalk": "5.2.0", "chalk": "5.2.0",
"chalk-template": "0.4.0", "chalk-template": "0.4.0",
@ -79,7 +79,7 @@
"koa-send": "5.0.1", "koa-send": "5.0.1",
"koa-slow": "2.1.0", "koa-slow": "2.1.0",
"koa-views": "7.0.2", "koa-views": "7.0.2",
"@cutls/megalodon": "5.1.15", "@calckey/megalodon": "5.1.2",
"mfm-js": "0.23.2", "mfm-js": "0.23.2",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"multer": "1.4.4-lts.1", "multer": "1.4.4-lts.1",

View file

@ -80,6 +80,13 @@ export async function checkHitAntenna(
) )
) )
return false; return false;
} else if (antenna.src === "instances") {
const instances = antenna.instances
.filter((x) => x !== "")
.map((host) => {
return host.toLowerCase();
});
if (!instances.includes(noteUser.host?.toLowerCase() ?? "")) return false;
} }
const keywords = antenna.keywords const keywords = antenna.keywords

View file

@ -40,8 +40,8 @@ export class Antenna {
}) })
public name: string; public name: string;
@Column('enum', { enum: ['home', 'all', 'users', 'list', 'group'] }) @Column('enum', { enum: ['home', 'all', 'users', 'list', 'group', 'instances'] })
public src: "home" | "all" | "users" | "list" | "group"; public src: "home" | "all" | "users" | "list" | "group" | "instances";
@Column({ @Column({
...id(), ...id(),
@ -73,6 +73,11 @@ export class Antenna {
}) })
public users: string[]; public users: string[];
@Column('jsonb', {
default: [],
})
public instances: string[];
@Column('jsonb', { @Column('jsonb', {
default: [], default: [],
}) })

View file

@ -25,6 +25,7 @@ export const AntennaRepository = db.getRepository(Antenna).extend({
userListId: antenna.userListId, userListId: antenna.userListId,
userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null, userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null,
users: antenna.users, users: antenna.users,
instances: antenna.instances,
caseSensitive: antenna.caseSensitive, caseSensitive: antenna.caseSensitive,
notify: antenna.notify, notify: antenna.notify,
withReplies: antenna.withReplies, withReplies: antenna.withReplies,

View file

@ -52,7 +52,7 @@ export const packedAntennaSchema = {
type: "string", type: "string",
optional: false, optional: false,
nullable: false, nullable: false,
enum: ["home", "all", "users", "list", "group"], enum: ["home", "all", "users", "list", "group", "instances"],
}, },
userListId: { userListId: {
type: "string", type: "string",
@ -76,6 +76,16 @@ export const packedAntennaSchema = {
nullable: false, nullable: false,
}, },
}, },
instances: {
type: "array",
optional: false,
nullable: false,
items: {
type: "string",
optional: false,
nullable: false,
},
},
caseSensitive: { caseSensitive: {
type: "boolean", type: "boolean",
optional: false, optional: false,

View file

@ -122,26 +122,26 @@ export async function createNote(
} }
logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
logger.info(`Creating the Note: ${note.id}`); logger.info(`Creating the Note: ${note.id}`);
// Skip if note is made before 2007 (1yr before Fedi was created) // Skip if note is made before 2007 (1yr before Fedi was created)
// OR skip if note is made 3 days in advance // OR skip if note is made 3 days in advance
if (note.published) { if (note.published) {
const DateChecker = new Date(note.published) const DateChecker = new Date(note.published);
const FutureCheck = new Date() const FutureCheck = new Date();
FutureCheck.setDate(FutureCheck.getDate() + 3) // Allow some wiggle room for misconfigured hosts FutureCheck.setDate(FutureCheck.getDate() + 3); // Allow some wiggle room for misconfigured hosts
if (DateChecker.getFullYear() < 2007) { if (DateChecker.getFullYear() < 2007) {
logger.warn('Note somehow made before Activitypub was created; discarding'); logger.warn(
"Note somehow made before Activitypub was created; discarding",
);
return null; return null;
} }
if (DateChecker > FutureCheck) { if (DateChecker > FutureCheck) {
logger.warn('Note somehow made after today; discarding') logger.warn("Note somehow made after today; discarding");
return null; return null;
} }
} }
// Fetch author // Fetch author
const actor = (await resolvePerson( const actor = (await resolvePerson(
getOneApId(note.attributedTo), getOneApId(note.attributedTo),

View file

@ -222,6 +222,7 @@ import * as ep___messaging_messages_create from "./endpoints/messaging/messages/
import * as ep___messaging_messages_delete from "./endpoints/messaging/messages/delete.js"; import * as ep___messaging_messages_delete from "./endpoints/messaging/messages/delete.js";
import * as ep___messaging_messages_read from "./endpoints/messaging/messages/read.js"; import * as ep___messaging_messages_read from "./endpoints/messaging/messages/read.js";
import * as ep___meta from "./endpoints/meta.js"; import * as ep___meta from "./endpoints/meta.js";
import * as ep___sounds from "./endpoints/get-sounds.js";
import * as ep___miauth_genToken from "./endpoints/miauth/gen-token.js"; import * as ep___miauth_genToken from "./endpoints/miauth/gen-token.js";
import * as ep___mute_create from "./endpoints/mute/create.js"; import * as ep___mute_create from "./endpoints/mute/create.js";
import * as ep___mute_delete from "./endpoints/mute/delete.js"; import * as ep___mute_delete from "./endpoints/mute/delete.js";
@ -668,6 +669,7 @@ const eps = [
["users/stats", ep___users_stats], ["users/stats", ep___users_stats],
["admin/drive-capacity-override", ep___admin_driveCapOverride], ["admin/drive-capacity-override", ep___admin_driveCapOverride],
["fetch-rss", ep___fetchRss], ["fetch-rss", ep___fetchRss],
["get-sounds", ep___sounds],
]; ];
export interface IEndpointMeta { export interface IEndpointMeta {

View file

@ -37,7 +37,10 @@ export const paramDef = {
type: "object", type: "object",
properties: { properties: {
name: { type: "string", minLength: 1, maxLength: 100 }, name: { type: "string", minLength: 1, maxLength: 100 },
src: { type: "string", enum: ["home", "all", "users", "list", "group"] }, src: {
type: "string",
enum: ["home", "all", "users", "list", "group", "instances"],
},
userListId: { type: "string", format: "misskey:id", nullable: true }, userListId: { type: "string", format: "misskey:id", nullable: true },
userGroupId: { type: "string", format: "misskey:id", nullable: true }, userGroupId: { type: "string", format: "misskey:id", nullable: true },
keywords: { keywords: {
@ -64,6 +67,12 @@ export const paramDef = {
type: "string", type: "string",
}, },
}, },
instances: {
type: "array",
items: {
type: "string",
},
},
caseSensitive: { type: "boolean" }, caseSensitive: { type: "boolean" },
withReplies: { type: "boolean" }, withReplies: { type: "boolean" },
withFile: { type: "boolean" }, withFile: { type: "boolean" },
@ -75,6 +84,7 @@ export const paramDef = {
"keywords", "keywords",
"excludeKeywords", "excludeKeywords",
"users", "users",
"instances",
"caseSensitive", "caseSensitive",
"withReplies", "withReplies",
"withFile", "withFile",
@ -118,6 +128,7 @@ export default define(meta, paramDef, async (ps, user) => {
keywords: ps.keywords, keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords, excludeKeywords: ps.excludeKeywords,
users: ps.users, users: ps.users,
instances: ps.instances,
caseSensitive: ps.caseSensitive, caseSensitive: ps.caseSensitive,
withReplies: ps.withReplies, withReplies: ps.withReplies,
withFile: ps.withFile, withFile: ps.withFile,

View file

@ -43,7 +43,10 @@ export const paramDef = {
properties: { properties: {
antennaId: { type: "string", format: "misskey:id" }, antennaId: { type: "string", format: "misskey:id" },
name: { type: "string", minLength: 1, maxLength: 100 }, name: { type: "string", minLength: 1, maxLength: 100 },
src: { type: "string", enum: ["home", "all", "users", "list", "group"] }, src: {
type: "string",
enum: ["home", "all", "users", "list", "group", "instances"],
},
userListId: { type: "string", format: "misskey:id", nullable: true }, userListId: { type: "string", format: "misskey:id", nullable: true },
userGroupId: { type: "string", format: "misskey:id", nullable: true }, userGroupId: { type: "string", format: "misskey:id", nullable: true },
keywords: { keywords: {
@ -70,6 +73,12 @@ export const paramDef = {
type: "string", type: "string",
}, },
}, },
instances: {
type: "array",
items: {
type: "string",
},
},
caseSensitive: { type: "boolean" }, caseSensitive: { type: "boolean" },
withReplies: { type: "boolean" }, withReplies: { type: "boolean" },
withFile: { type: "boolean" }, withFile: { type: "boolean" },
@ -82,6 +91,7 @@ export const paramDef = {
"keywords", "keywords",
"excludeKeywords", "excludeKeywords",
"users", "users",
"instances",
"caseSensitive", "caseSensitive",
"withReplies", "withReplies",
"withFile", "withFile",
@ -131,6 +141,7 @@ export default define(meta, paramDef, async (ps, user) => {
keywords: ps.keywords, keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords, excludeKeywords: ps.excludeKeywords,
users: ps.users, users: ps.users,
instances: ps.instances,
caseSensitive: ps.caseSensitive, caseSensitive: ps.caseSensitive,
withReplies: ps.withReplies, withReplies: ps.withReplies,
withFile: ps.withFile, withFile: ps.withFile,

View file

@ -102,10 +102,13 @@ export default define(meta, paramDef, async (ps, me) => {
if (typeof ps.blocked === "boolean") { if (typeof ps.blocked === "boolean") {
const meta = await fetchMeta(true); const meta = await fetchMeta(true);
if (ps.blocked) { if (ps.blocked) {
if (meta.blockedHosts.length === 0) {
return [];
}
query.andWhere("instance.host IN (:...blocks)", { query.andWhere("instance.host IN (:...blocks)", {
blocks: meta.blockedHosts, blocks: meta.blockedHosts,
}); });
} else { } else if (meta.blockedHosts.length > 0) {
query.andWhere("instance.host NOT IN (:...blocks)", { query.andWhere("instance.host NOT IN (:...blocks)", {
blocks: meta.blockedHosts, blocks: meta.blockedHosts,
}); });

View file

@ -0,0 +1,30 @@
import { readdir } from "fs/promises";
import define from "../define.js";
export const meta = {
tags: ["meta"],
requireCredential: false,
requireCredentialPrivateMode: false,
} as const;
export const paramDef = {
type: "object",
properties: {},
required: [],
} as const;
export default define(meta, paramDef, async () => {
const music_files: (string | null)[] = [null];
const directory = (
await readdir("./assets/sounds", { withFileTypes: true })
).filter((potentialFolder) => potentialFolder.isDirectory());
for await (const folder of directory) {
const files = (await readdir(`./assets/sounds/${folder.name}`)).filter(
(potentialSong) => potentialSong.endsWith(".mp3"),
);
for await (const file of files) {
music_files.push(`${folder.name}/${file.replace(".mp3", "")}`);
}
}
return music_files;
});

View file

@ -61,10 +61,12 @@ router.use(
}), }),
); );
mastoRouter.use(koaBody({ mastoRouter.use(
koaBody({
multipart: true, multipart: true,
urlencoded: true urlencoded: true,
})); }),
);
apiMastodonCompatible(mastoRouter); apiMastodonCompatible(mastoRouter);

View file

@ -1,5 +1,5 @@
import Router from "@koa/router"; import Router from "@koa/router";
import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import { apiAuthMastodon } from "./endpoints/auth.js"; import { apiAuthMastodon } from "./endpoints/auth.js";
import { apiAccountMastodon } from "./endpoints/account.js"; import { apiAccountMastodon } from "./endpoints/account.js";
import { apiStatusMastodon } from "./endpoints/status.js"; import { apiStatusMastodon } from "./endpoints/status.js";

View file

@ -1,11 +1,26 @@
import megalodon, { MegalodonInterface } from "@cutls/megalodon";
import Router from "@koa/router"; import Router from "@koa/router";
import { koaBody } from "koa-body";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import { toLimitToInt } from "./timeline.js"; import { toLimitToInt } from "./timeline.js";
const relationshopModel = {
id: "",
following: false,
followed_by: false,
delivery_following: false,
blocking: false,
blocked_by: false,
muting: false,
muting_notifications: false,
requested: false,
domain_blocking: false,
showing_reblogs: false,
endorsed: false,
notifying: false,
note: "",
};
export function apiAccountMastodon(router: Router): void { export function apiAccountMastodon(router: Router): void {
router.get("/v1/accounts/verify_credentials", async (ctx, next) => { router.get("/v1/accounts/verify_credentials", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -48,9 +63,23 @@ export function apiAccountMastodon(router: Router): void {
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });
router.get<{ Params: { id: string } }>( router.get("/v1/accounts/lookup", async (ctx) => {
"/v1/accounts/:id", const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
async (ctx, next) => { const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccount(
`@${(ctx.query.acct || "").toString()}`,
);
ctx.body = data.data;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
ctx.status = 401;
ctx.body = e.response.data;
}
});
router.get<{ Params: { id: string } }>("/v1/accounts/:id", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -63,11 +92,10 @@ export function apiAccountMastodon(router: Router): void {
ctx.status = 401; ctx.status = 401;
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}, });
);
router.get<{ Params: { id: string } }>( router.get<{ Params: { id: string } }>(
"/v1/accounts/:id/statuses", "/v1/accounts/:id/statuses",
async (ctx, next) => { async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -87,7 +115,7 @@ export function apiAccountMastodon(router: Router): void {
); );
router.get<{ Params: { id: string } }>( router.get<{ Params: { id: string } }>(
"/v1/accounts/:id/followers", "/v1/accounts/:id/followers",
async (ctx, next) => { async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -107,7 +135,7 @@ export function apiAccountMastodon(router: Router): void {
); );
router.get<{ Params: { id: string } }>( router.get<{ Params: { id: string } }>(
"/v1/accounts/:id/following", "/v1/accounts/:id/following",
async (ctx, next) => { async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -127,7 +155,7 @@ export function apiAccountMastodon(router: Router): void {
); );
router.get<{ Params: { id: string } }>( router.get<{ Params: { id: string } }>(
"/v1/accounts/:id/lists", "/v1/accounts/:id/lists",
async (ctx, next) => { async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -144,7 +172,7 @@ export function apiAccountMastodon(router: Router): void {
); );
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/follow", "/v1/accounts/:id/follow",
async (ctx, next) => { async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -163,7 +191,7 @@ export function apiAccountMastodon(router: Router): void {
); );
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/unfollow", "/v1/accounts/:id/unfollow",
async (ctx, next) => { async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -182,7 +210,7 @@ export function apiAccountMastodon(router: Router): void {
); );
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/block", "/v1/accounts/:id/block",
async (ctx, next) => { async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -199,7 +227,7 @@ export function apiAccountMastodon(router: Router): void {
); );
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/unblock", "/v1/accounts/:id/unblock",
async (ctx, next) => { async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -236,7 +264,7 @@ export function apiAccountMastodon(router: Router): void {
); );
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(
"/v1/accounts/:id/unmute", "/v1/accounts/:id/unmute",
async (ctx, next) => { async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -251,13 +279,15 @@ export function apiAccountMastodon(router: Router): void {
} }
}, },
); );
router.get("/v1/accounts/relationships", async (ctx, next) => { router.get("/v1/accounts/relationships", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const idsRaw = (ctx.query as any)["id[]"]; const idsRaw = (ctx.query as any)["id[]"];
const ids = typeof idsRaw === "string" ? [idsRaw] : idsRaw; const ids = typeof idsRaw === "string" ? [idsRaw] : idsRaw;
relationshopModel.id = idsRaw || "1";
if (!idsRaw) return [relationshopModel];
const data = (await client.getRelationships(ids)) as any; const data = (await client.getRelationships(ids)) as any;
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
@ -267,7 +297,7 @@ export function apiAccountMastodon(router: Router): void {
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });
router.get("/v1/bookmarks", async (ctx, next) => { router.get("/v1/bookmarks", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -281,7 +311,7 @@ export function apiAccountMastodon(router: Router): void {
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });
router.get("/v1/favourites", async (ctx, next) => { router.get("/v1/favourites", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -295,7 +325,7 @@ export function apiAccountMastodon(router: Router): void {
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });
router.get("/v1/mutes", async (ctx, next) => { router.get("/v1/mutes", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -309,7 +339,7 @@ export function apiAccountMastodon(router: Router): void {
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });
router.get("/v1/blocks", async (ctx, next) => { router.get("/v1/blocks", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -323,7 +353,7 @@ export function apiAccountMastodon(router: Router): void {
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });
router.get("/v1/follow_ctxs", async (ctx, next) => { router.get("/v1/follow_ctxs", async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -341,7 +371,7 @@ export function apiAccountMastodon(router: Router): void {
}); });
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(
"/v1/follow_ctxs/:id/authorize", "/v1/follow_ctxs/:id/authorize",
async (ctx, next) => { async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
@ -358,7 +388,7 @@ export function apiAccountMastodon(router: Router): void {
); );
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(
"/v1/follow_ctxs/:id/reject", "/v1/follow_ctxs/:id/reject",
async (ctx, next) => { async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);

View file

@ -1,4 +1,4 @@
import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import Router from "@koa/router"; import Router from "@koa/router";
import { koaBody } from "koa-body"; import { koaBody } from "koa-body";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";

View file

@ -1,4 +1,4 @@
import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import Router from "@koa/router"; import Router from "@koa/router";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";

View file

@ -1,4 +1,4 @@
import { Entity } from "@cutls/megalodon"; import { Entity } from "@calckey/megalodon";
// TODO: add calckey features // TODO: add calckey features
export function getInstance(response: Entity.Instance) { export function getInstance(response: Entity.Instance) {
return { return {

View file

@ -1,4 +1,4 @@
import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import Router from "@koa/router"; import Router from "@koa/router";
import { koaBody } from "koa-body"; import { koaBody } from "koa-body";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";

View file

@ -1,4 +1,4 @@
import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import Router from "@koa/router"; import Router from "@koa/router";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";

View file

@ -1,5 +1,5 @@
import Router from "@koa/router"; import Router from "@koa/router";
import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import fs from "fs"; import fs from "fs";
import { pipeline } from "node:stream"; import { pipeline } from "node:stream";
@ -435,7 +435,7 @@ export function statusModel(
id: "9arzuvv0sw", id: "9arzuvv0sw",
username: "ReactionBot", username: "ReactionBot",
acct: "ReactionBot", acct: "ReactionBot",
display_name: "ReactionOfThisPost", display_name: "ReactionsToThisPost",
locked: false, locked: false,
created_at: now, created_at: now,
followers_count: 0, followers_count: 0,

View file

@ -1,13 +1,14 @@
import Router from "@koa/router"; import Router from "@koa/router";
import megalodon, { Entity, MegalodonInterface } from "@cutls/megalodon"; import megalodon, { Entity, MegalodonInterface } from "@calckey/megalodon";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import { statusModel } from "./status.js"; import { statusModel } from "./status.js";
import Autolinker from "autolinker"; import Autolinker from "autolinker";
import { ParsedUrlQuery } from "querystring"; import { ParsedUrlQuery } from "querystring";
export function toLimitToInt(q: ParsedUrlQuery) { export function toLimitToInt(q: ParsedUrlQuery) {
let object: any = q;
if (q.limit) if (q.limit)
if (typeof q.limit === "string") q.limit = parseInt(q.limit, 10).toString(); if (typeof q.limit === "string") object.limit = parseInt(q.limit, 10);
return q; return q;
} }

View file

@ -24,7 +24,7 @@ import { readNotification } from "../common/read-notification.js";
import channels from "./channels/index.js"; import channels from "./channels/index.js";
import type Channel from "./channel.js"; import type Channel from "./channel.js";
import type { StreamEventEmitter, StreamMessages } from "./types.js"; import type { StreamEventEmitter, StreamMessages } from "./types.js";
import { Converter } from "@cutls/megalodon"; import { Converter } from "@calckey/megalodon";
import { getClient } from "../mastodon/ApiMastodonCompatibleService.js"; import { getClient } from "../mastodon/ApiMastodonCompatibleService.js";
import { toTextWithReaction } from "../mastodon/endpoints/timeline.js"; import { toTextWithReaction } from "../mastodon/endpoints/timeline.js";

View file

@ -135,6 +135,9 @@ export interface NoteStreamTypes {
reaction: string; reaction: string;
userId: User["id"]; userId: User["id"];
}; };
replied: {
id: Note["id"];
};
} }
type NoteStreamEventTypes = { type NoteStreamEventTypes = {
[key in keyof NoteStreamTypes]: { [key in keyof NoteStreamTypes]: {

View file

@ -20,7 +20,7 @@ import { createTemp } from "@/misc/create-temp.js";
import { publishMainStream } from "@/services/stream.js"; import { publishMainStream } from "@/services/stream.js";
import * as Acct from "@/misc/acct.js"; import * as Acct from "@/misc/acct.js";
import { envOption } from "@/env.js"; import { envOption } from "@/env.js";
import megalodon, { MegalodonInterface } from "@cutls/megalodon"; import megalodon, { MegalodonInterface } from "@calckey/megalodon";
import activityPub from "./activitypub.js"; import activityPub from "./activitypub.js";
import nodeinfo from "./nodeinfo.js"; import nodeinfo from "./nodeinfo.js";
import wellKnown from "./well-known.js"; import wellKnown from "./well-known.js";
@ -72,9 +72,11 @@ app.use(mount("/proxy", proxyServer));
const router = new Router(); const router = new Router();
const mastoRouter = new Router(); const mastoRouter = new Router();
mastoRouter.use(koaBody({ mastoRouter.use(
urlencoded: true koaBody({
})); urlencoded: true,
}),
);
// Routing // Routing
router.use(activityPub.routes()); router.use(activityPub.routes());
@ -161,7 +163,7 @@ mastoRouter.post("/oauth/token", async (ctx) => {
} }
} }
if (client_id instanceof Array) { if (client_id instanceof Array) {
client_id = client_id.toString();; client_id = client_id.toString();
} else if (!client_id) { } else if (!client_id) {
client_id = null; client_id = null;
} }
@ -169,7 +171,7 @@ mastoRouter.post("/oauth/token", async (ctx) => {
const atData = await client.fetchAccessToken( const atData = await client.fetchAccessToken(
client_id, client_id,
body.client_secret, body.client_secret,
m ? m[0] : '', m ? m[0] : "",
); );
ctx.body = { ctx.body = {
access_token: atData.accessToken, access_token: atData.accessToken,

View file

@ -1,4 +1,4 @@
html { html, body {
background-color: var(--bg); background-color: var(--bg);
color: var(--fg); color: var(--fg);
} }

View file

@ -1,6 +1,10 @@
import * as mfm from "mfm-js"; import * as mfm from "mfm-js";
import es from "../../db/elasticsearch.js"; import es from "../../db/elasticsearch.js";
import { publishMainStream, publishNotesStream } from "@/services/stream.js"; import {
publishMainStream,
publishNotesStream,
publishNoteStream,
} from "@/services/stream.js";
import DeliverManager from "@/remote/activitypub/deliver-manager.js"; import DeliverManager from "@/remote/activitypub/deliver-manager.js";
import renderNote from "@/remote/activitypub/renderer/note.js"; import renderNote from "@/remote/activitypub/renderer/note.js";
import renderCreate from "@/remote/activitypub/renderer/create.js"; import renderCreate from "@/remote/activitypub/renderer/create.js";
@ -430,6 +434,12 @@ export default async (
} }
publishNotesStream(note); publishNotesStream(note);
if (note.replyId != null) {
// Only provide the reply note id here as the recipient may not be authorized to see the note.
publishNoteStream(note.replyId, "replied", {
id: note.id,
});
}
const webhooks = await getActiveWebhooks().then((webhooks) => const webhooks = await getActiveWebhooks().then((webhooks) =>
webhooks.filter((x) => x.userId === user.id && x.on.includes("note")), webhooks.filter((x) => x.userId === user.id && x.on.includes("note")),

View file

@ -33,7 +33,7 @@
"blurhash": "1.1.5", "blurhash": "1.1.5",
"broadcast-channel": "4.19.1", "broadcast-channel": "4.19.1",
"browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git", "browser-image-resizer": "https://github.com/misskey-dev/browser-image-resizer.git",
"calckey-js": "^0.0.20", "calckey-js": "^0.0.22",
"chart.js": "4.1.1", "chart.js": "4.1.1",
"chartjs-adapter-date-fns": "2.0.1", "chartjs-adapter-date-fns": "2.0.1",
"chartjs-plugin-gradient": "0.5.1", "chartjs-plugin-gradient": "0.5.1",

View file

@ -0,0 +1,157 @@
<template>
<XModalWindow
ref="dialogEl"
:with-ok-button="true"
:ok-button-disabled="selected == null"
@click="cancel()"
@close="cancel()"
@ok="ok()"
@closed="$emit('closed')"
>
<template #header>{{ i18n.ts.selectInstance }}</template>
<div class="mehkoush">
<div class="form">
<MkInput v-model="hostname" :autofocus="true" @update:modelValue="search">
<template #label>{{ i18n.ts.instance }}</template>
</MkInput>
</div>
<div v-if="hostname != ''" class="result" :class="{ hit: instances.length > 0 }">
<div v-if="instances.length > 0" class="instances">
<div v-for="item in instances" :key="item.id" class="instance" :class="{ selected: selected && selected.id === item.id }" @click="selected = item" @dblclick="ok()">
<div class="body">
<img class="icon" :src="item.iconUrl ?? '/client-assets/dummy.png'" aria-hidden="true"/>
<span class="name">{{ item.host }}</span>
</div>
</div>
</div>
<div v-else class="empty">
<span>{{ i18n.ts.noInstances }}</span>
</div>
</div>
</div>
</XModalWindow>
</template>
<script lang="ts" setup>
import MkInput from '@/components/form/input.vue';
import XModalWindow from '@/components/MkModalWindow.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { Instance } from 'calckey-js/built/entities';
const emit = defineEmits<{
(ev: 'ok', selected: Instance): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
let hostname = $ref('');
let instances: Instance[] = $ref([]);
let selected: Instance | null = $ref(null);
let dialogEl = $ref<InstanceType<typeof XModalWindow>>();
let searchOrderLatch = 0;
const search = () => {
if (hostname === '') {
instances = [];
return;
}
const searchId = ++searchOrderLatch;
os.api('federation/instances', {
host: hostname,
limit: 10,
blocked: false,
suspended: false,
sort: '+pubSub',
}).then(_instances => {
if (searchId !== searchOrderLatch) return;
instances = _instances.map(x => ({
id: x.id,
host: x.host,
iconUrl: x.iconUrl,
} as Instance));
});
};
const ok = () => {
if (selected == null) return;
emit('ok', selected);
dialogEl?.close();
};
const cancel = () => {
emit('cancel');
dialogEl?.close();
};
</script>
<style lang="scss" scoped>
.mehkoush {
margin: var(--marginFull) 0;
> .form {
padding: 0 var(--root-margin);
}
> .result {
display: flex;
flex-direction: column;
overflow: auto;
height: 100%;
&.result.hit {
padding: 0;
}
> .instances {
flex: 1;
overflow: auto;
padding: 8px 0;
> .instance {
display: flex;
align-items: center;
padding: 8px var(--root-margin);
font-size: 14px;
&:hover {
background: var(--X7);
}
&.selected {
background: var(--accent);
color: #fff;
}
> * {
pointer-events: none;
user-select: none;
}
> .body {
padding: 0 8px;
width: 100%;
> .name {
display: block;
font-weight: bold;
}
> .icon {
width: 16px;
height: 16px;
margin-right: 8px;
float: left;
}
}
}
}
> .empty {
opacity: 0.7;
text-align: center;
}
}
}
</style>

View file

@ -142,6 +142,8 @@ import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu'; import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture'; import { useNoteCapture } from '@/scripts/use-note-capture';
import { deepClone } from '@/scripts/clone'; import { deepClone } from '@/scripts/clone';
import { stream } from '@/stream';
import { NoteUpdatedEvent } from 'calckey-js/built/streaming.types';
const router = useRouter(); const router = useRouter();
@ -302,6 +304,31 @@ if (appearNote.replyId) {
conversation.value = res.reverse(); conversation.value = res.reverse();
}); });
} }
function onNoteReplied(noteData: NoteUpdatedEvent): void {
const { type, id, body } = noteData;
if (type === 'replied' && id === appearNote.id) {
const { id: createdId } = body;
os.api('notes/show', {
noteId: createdId,
}).then(note => {
if (note.replyId === appearNote.id) {
replies.value.unshift(note);
directReplies.value.unshift(note);
}
});
}
}
onMounted(() => {
stream.on("noteUpdated", onNoteReplied);
});
onUnmounted(() => {
stream.off("noteUpdated", onNoteReplied);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -37,7 +37,6 @@ import { computed, onMounted, onUnmounted, ref, inject, watch, shallowReactive,
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { popupMenu } from '@/os'; import { popupMenu } from '@/os';
import { scrollToTop } from '@/scripts/scroll'; import { scrollToTop } from '@/scripts/scroll';
import { i18n } from '@/i18n';
import { globalEvents } from '@/events'; import { globalEvents } from '@/events';
import { deviceKind } from '@/scripts/device-kind'; import { deviceKind } from '@/scripts/device-kind';
import { isTouchUsing } from '@/scripts/touch'; import { isTouchUsing } from '@/scripts/touch';
@ -153,12 +152,14 @@ onMounted(() => {
if (tabEl && tabHighlightEl) { if (tabEl && tabHighlightEl) {
// offsetWidth offsetLeft getBoundingClientRect 使 // offsetWidth offsetLeft getBoundingClientRect 使
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4 // https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
tabEl.addEventListener("transitionend", () => {
const parentRect = tabsEl.getBoundingClientRect(); const parentRect = tabsEl.getBoundingClientRect();
const rect = tabEl.getBoundingClientRect(); const rect = tabEl.getBoundingClientRect();
const left = (rect.left - parentRect.left + tabsEl?.scrollLeft); const left = (rect.left - parentRect.left + tabsEl?.scrollLeft);
tabHighlightEl.style.width = rect.width + 'px'; tabHighlightEl.style.width = rect.width + 'px';
tabHighlightEl.style.left = left + 'px'; tabHighlightEl.style.left = left + 'px';
tabsEl.scrollTo({left: left - 80, behavior: "smooth"}); tabsEl?.scrollTo({left: left - 80, behavior: "smooth"});
})
} }
}); });
}, { }, {
@ -229,6 +230,14 @@ onUnmounted(() => {
display: inline-block; display: inline-block;
min-width: 20%; min-width: 20%;
} }
> .tab {
&:not(.active) > .title {
font-size: 0;
opacity: 0;
margin-inline: 0;
transition: font-size .2s, opacity .1s;
}
}
} }
} }
@ -373,6 +382,9 @@ onUnmounted(() => {
> .icon + .title { > .icon + .title {
margin-left: 8px; margin-left: 8px;
} }
> .title {
transition: font-size .2s, opacity .2s .15s;
}
} }
> .highlight { > .highlight {
@ -381,7 +393,8 @@ onUnmounted(() => {
height: 3px; height: 3px;
background: var(--accent); background: var(--accent);
border-radius: 999px; border-radius: 999px;
transition: all 0.2s ease; transition: width .2s, left .2s;
transition-timing-function: cubic-bezier(0,0,0,1.2);
pointer-events: none; pointer-events: none;
} }
} }

View file

@ -545,6 +545,23 @@ export async function selectUser() {
}); });
} }
export async function selectInstance(): Promise<Misskey.entities.Instance> {
return new Promise((resolve, reject) => {
popup(
defineAsyncComponent(
() => import("@/components/MkInstanceSelectDialog.vue"),
),
{},
{
ok: (instance) => {
resolve(instance);
},
},
"closed",
);
});
}
export async function selectDriveFile(multiple: boolean) { export async function selectDriveFile(multiple: boolean) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
popup( popup(

View file

@ -5,7 +5,6 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { inject } from 'vue';
import XAntenna from './editor.vue'; import XAntenna from './editor.vue';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
@ -19,6 +18,7 @@ let draft = $ref({
userListId: null, userListId: null,
userGroupId: null, userGroupId: null,
users: [], users: [],
instances: [],
keywords: [], keywords: [],
excludeKeywords: [], excludeKeywords: [],
withReplies: false, withReplies: false,
@ -31,10 +31,6 @@ function onAntennaCreated() {
router.push('/my/antennas'); router.push('/my/antennas');
} }
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({ definePageMetadata({
title: i18n.ts.manageAntennas, title: i18n.ts.manageAntennas,
icon: 'ph-flying-saucer-bold ph-lg', icon: 'ph-flying-saucer-bold ph-lg',

View file

@ -11,6 +11,7 @@
<option value="users">{{ i18n.ts._antennaSources.users }}</option> <option value="users">{{ i18n.ts._antennaSources.users }}</option>
<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>--> <!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
<!--<option value="group">{{ i18n.ts._antennaSources.userGroup }}</option>--> <!--<option value="group">{{ i18n.ts._antennaSources.userGroup }}</option>-->
<option value="instances">{{ i18n.ts._antennaSources.instances }}</option>
</MkSelect> </MkSelect>
<MkSelect v-if="src === 'list'" v-model="userListId" class="_formBlock"> <MkSelect v-if="src === 'list'" v-model="userListId" class="_formBlock">
<template #label>{{ i18n.ts.userList }}</template> <template #label>{{ i18n.ts.userList }}</template>
@ -24,6 +25,10 @@
<template #label>{{ i18n.ts.users }}</template> <template #label>{{ i18n.ts.users }}</template>
<template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template> <template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template>
</MkTextarea> </MkTextarea>
<MkTextarea v-else-if="src === 'instances'" v-model="instances" class="_formBlock">
<template #label>{{ i18n.ts.instances }}</template>
<template #caption>{{ i18n.ts.antennaInstancesDescription }} <button class="_textButton" @click="addInstance">{{ i18n.ts.addInstance }}</button></template>
</MkTextarea>
<MkSwitch v-model="withReplies" class="_formBlock">{{ i18n.ts.withReplies }}</MkSwitch> <MkSwitch v-model="withReplies" class="_formBlock">{{ i18n.ts.withReplies }}</MkSwitch>
<MkTextarea v-model="keywords" class="_formBlock"> <MkTextarea v-model="keywords" class="_formBlock">
<template #label>{{ i18n.ts.antennaKeywords }}</template> <template #label>{{ i18n.ts.antennaKeywords }}</template>
@ -70,6 +75,7 @@ let src: string = $ref(props.antenna.src);
let userListId: any = $ref(props.antenna.userListId); let userListId: any = $ref(props.antenna.userListId);
let userGroupId: any = $ref(props.antenna.userGroupId); let userGroupId: any = $ref(props.antenna.userGroupId);
let users: string = $ref(props.antenna.users.join('\n')); let users: string = $ref(props.antenna.users.join('\n'));
let instances: string = $ref(props.antenna.instances.join('\n'));
let keywords: string = $ref(props.antenna.keywords.map(x => x.join(' ')).join('\n')); let keywords: string = $ref(props.antenna.keywords.map(x => x.join(' ')).join('\n'));
let excludeKeywords: string = $ref(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n')); let excludeKeywords: string = $ref(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
let caseSensitive: boolean = $ref(props.antenna.caseSensitive); let caseSensitive: boolean = $ref(props.antenna.caseSensitive);
@ -103,6 +109,7 @@ async function saveAntenna() {
notify, notify,
caseSensitive, caseSensitive,
users: users.trim().split('\n').map(x => x.trim()), users: users.trim().split('\n').map(x => x.trim()),
instances: instances.trim().split('\n').map(x => x.trim()),
keywords: keywords.trim().split('\n').map(x => x.trim().split(' ')), keywords: keywords.trim().split('\n').map(x => x.trim().split(' ')),
excludeKeywords: excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')), excludeKeywords: excludeKeywords.trim().split('\n').map(x => x.trim().split(' ')),
}; };
@ -135,10 +142,18 @@ async function deleteAntenna() {
function addUser() { function addUser() {
os.selectUser().then(user => { os.selectUser().then(user => {
users = users.trim(); users = users.trim();
users += '\n@' + Acct.toString(user as any); users += `\n@${Acct.toString(user as any)}`;
users = users.trim(); users = users.trim();
}); });
} }
function addInstance() {
os.selectInstance().then(instance => {
instances = instances.trim();
instances += '\n' + instance.host;
instances = instances.trim();
});
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -50,32 +50,8 @@ const sounds = ref({
channel: ColdDeviceStorage.get('sound_channel'), channel: ColdDeviceStorage.get('sound_channel'),
}); });
const soundsTypes = [ const soundsTypes = await os.api('get-sounds')
null,
'syuilo/up',
'syuilo/down',
'syuilo/pope1',
'syuilo/pope2',
'syuilo/waon',
'syuilo/popo',
'syuilo/triple',
'syuilo/poi1',
'syuilo/poi2',
'syuilo/pirori',
'syuilo/pirori-wet',
'syuilo/pirori-square-wet',
'syuilo/square-pico',
'syuilo/reverved',
'syuilo/ryukyu',
'syuilo/kick',
'syuilo/snare',
'syuilo/queue-jammed',
'aisha/1',
'aisha/2',
'aisha/3',
'noizenecio/kick_gaba',
'noizenecio/kick_gaba2',
];
async function edit(type) { async function edit(type) {
const { canceled, result } = await os.form(i18n.t('_sfx.' + type), { const { canceled, result } = await os.form(i18n.t('_sfx.' + type), {

View file

@ -7,7 +7,7 @@ export function getAudio(file: string, useCache = true): HTMLAudioElement {
if (useCache && cache.has(file)) { if (useCache && cache.has(file)) {
audio = cache.get(file); audio = cache.get(file);
} else { } else {
audio = new Audio(`/client-assets/sounds/${file}.mp3`); audio = new Audio(`/static-assets/sounds/${file}.mp3`);
if (useCache) cache.set(file, audio); if (useCache) cache.set(file, audio);
} }
return audio; return audio;

View file

@ -13,7 +13,7 @@
"@swc/cli": "^0.1.59", "@swc/cli": "^0.1.59",
"@swc/core": "^1.3.26", "@swc/core": "^1.3.26",
"@swc/core-android-arm64": "1.3.11", "@swc/core-android-arm64": "1.3.11",
"calckey-js": "^0.0.20", "calckey-js": "^0.0.22",
"idb-keyval": "^6.2.0", "idb-keyval": "^6.2.0",
"swc-loader": "^0.2.3", "swc-loader": "^0.2.3",
"webpack": "^5.75.0" "webpack": "^5.75.0"

View file

@ -12,7 +12,7 @@ importers:
'@tensorflow/tfjs': ^3.21.0 '@tensorflow/tfjs': ^3.21.0
'@types/gulp': 4.0.10 '@types/gulp': 4.0.10
'@types/gulp-rename': 2.0.1 '@types/gulp-rename': 2.0.1
calckey-js: ^0.0.20 calckey-js: ^0.0.22
cross-env: 7.0.3 cross-env: 7.0.3
cypress: 10.11.0 cypress: 10.11.0
execa: 5.1.1 execa: 5.1.1
@ -32,7 +32,7 @@ importers:
'@bull-board/api': 4.10.2 '@bull-board/api': 4.10.2
'@bull-board/ui': 4.10.2 '@bull-board/ui': 4.10.2
'@tensorflow/tfjs': 3.21.0_seedrandom@3.0.5 '@tensorflow/tfjs': 3.21.0_seedrandom@3.0.5
calckey-js: 0.0.20 calckey-js: 0.0.22
js-yaml: 4.1.0 js-yaml: 4.1.0
phosphor-icons: 1.4.2 phosphor-icons: 1.4.2
seedrandom: 3.0.5 seedrandom: 3.0.5
@ -57,7 +57,7 @@ importers:
'@bull-board/api': ^4.6.4 '@bull-board/api': ^4.6.4
'@bull-board/koa': ^4.6.4 '@bull-board/koa': ^4.6.4
'@bull-board/ui': ^4.6.4 '@bull-board/ui': ^4.6.4
'@cutls/megalodon': 5.1.15 '@calckey/megalodon': 5.1.2
'@discordapp/twemoji': 14.0.2 '@discordapp/twemoji': 14.0.2
'@elastic/elasticsearch': 7.17.0 '@elastic/elasticsearch': 7.17.0
'@koa/cors': 3.4.3 '@koa/cors': 3.4.3
@ -126,7 +126,7 @@ importers:
blurhash: 1.1.5 blurhash: 1.1.5
bull: 4.10.2 bull: 4.10.2
cacheable-lookup: 7.0.0 cacheable-lookup: 7.0.0
calckey-js: ^0.0.20 calckey-js: ^0.0.22
cbor: 8.1.0 cbor: 8.1.0
chalk: 5.2.0 chalk: 5.2.0
chalk-template: 0.4.0 chalk-template: 0.4.0
@ -223,7 +223,7 @@ importers:
'@bull-board/api': 4.10.2 '@bull-board/api': 4.10.2
'@bull-board/koa': 4.10.2_6tybghmia4wsnt33xeid7y4rby '@bull-board/koa': 4.10.2_6tybghmia4wsnt33xeid7y4rby
'@bull-board/ui': 4.10.2 '@bull-board/ui': 4.10.2
'@cutls/megalodon': 5.1.15 '@calckey/megalodon': 5.1.2
'@discordapp/twemoji': 14.0.2 '@discordapp/twemoji': 14.0.2
'@elastic/elasticsearch': 7.17.0 '@elastic/elasticsearch': 7.17.0
'@koa/cors': 3.4.3 '@koa/cors': 3.4.3
@ -245,7 +245,7 @@ importers:
blurhash: 1.1.5 blurhash: 1.1.5
bull: 4.10.2 bull: 4.10.2
cacheable-lookup: 7.0.0 cacheable-lookup: 7.0.0
calckey-js: 0.0.20 calckey-js: 0.0.22
cbor: 8.1.0 cbor: 8.1.0
chalk: 5.2.0 chalk: 5.2.0
chalk-template: 0.4.0 chalk-template: 0.4.0
@ -414,7 +414,7 @@ importers:
blurhash: 1.1.5 blurhash: 1.1.5
broadcast-channel: 4.19.1 broadcast-channel: 4.19.1
browser-image-resizer: https://github.com/misskey-dev/browser-image-resizer.git browser-image-resizer: https://github.com/misskey-dev/browser-image-resizer.git
calckey-js: ^0.0.20 calckey-js: ^0.0.22
chart.js: 4.1.1 chart.js: 4.1.1
chartjs-adapter-date-fns: 2.0.1 chartjs-adapter-date-fns: 2.0.1
chartjs-plugin-gradient: 0.5.1 chartjs-plugin-gradient: 0.5.1
@ -488,7 +488,7 @@ importers:
blurhash: 1.1.5 blurhash: 1.1.5
broadcast-channel: 4.19.1 broadcast-channel: 4.19.1
browser-image-resizer: github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c browser-image-resizer: github.com/misskey-dev/browser-image-resizer/0380d12c8e736788ea7f4e6e985175521ea7b23c
calckey-js: 0.0.20 calckey-js: 0.0.22
chart.js: 4.1.1 chart.js: 4.1.1
chartjs-adapter-date-fns: 2.0.1_chart.js@4.1.1 chartjs-adapter-date-fns: 2.0.1_chart.js@4.1.1
chartjs-plugin-gradient: 0.5.1_chart.js@4.1.1 chartjs-plugin-gradient: 0.5.1_chart.js@4.1.1
@ -542,7 +542,7 @@ importers:
'@swc/cli': ^0.1.59 '@swc/cli': ^0.1.59
'@swc/core': ^1.3.26 '@swc/core': ^1.3.26
'@swc/core-android-arm64': 1.3.11 '@swc/core-android-arm64': 1.3.11
calckey-js: ^0.0.20 calckey-js: ^0.0.22
idb-keyval: ^6.2.0 idb-keyval: ^6.2.0
swc-loader: ^0.2.3 swc-loader: ^0.2.3
webpack: ^5.75.0 webpack: ^5.75.0
@ -550,7 +550,7 @@ importers:
'@swc/cli': 0.1.59_@swc+core@1.3.26 '@swc/cli': 0.1.59_@swc+core@1.3.26
'@swc/core': 1.3.26 '@swc/core': 1.3.26
'@swc/core-android-arm64': 1.3.11 '@swc/core-android-arm64': 1.3.11
calckey-js: 0.0.20 calckey-js: 0.0.22
idb-keyval: 6.2.0 idb-keyval: 6.2.0
swc-loader: 0.2.3_v4imsvpumnwpgduroyqmpcfjiy swc-loader: 0.2.3_v4imsvpumnwpgduroyqmpcfjiy
webpack: 5.75.0_@swc+core@1.3.26 webpack: 5.75.0_@swc+core@1.3.26
@ -746,6 +746,30 @@ packages:
'@bull-board/api': 4.10.2 '@bull-board/api': 4.10.2
dev: false dev: false
/@calckey/megalodon/5.1.2:
resolution: {integrity: sha512-bUjPOfASy8X2NxdBvYDOWN9Rw/KdkfbTxy5vMQBcrGXepFbT4M+00blEYNc00Uu/epwH9YoNqpQC8PKQr/WU4w==}
engines: {node: '>=15.0.0'}
dependencies:
'@types/oauth': 0.9.1
'@types/ws': 8.5.4
axios: 1.2.2
dayjs: 1.11.7
form-data: 4.0.0
https-proxy-agent: 5.0.1
oauth: 0.10.0
object-assign-deep: 0.4.0
parse-link-header: 2.0.0
socks-proxy-agent: 7.0.0
typescript: 4.9.4
uuid: 9.0.0
ws: 8.12.0
transitivePeerDependencies:
- bufferutil
- debug
- supports-color
- utf-8-validate
dev: false
/@colors/colors/1.5.0: /@colors/colors/1.5.0:
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'} engines: {node: '>=0.1.90'}
@ -847,30 +871,6 @@ packages:
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.9 '@jridgewell/trace-mapping': 0.3.9
/@cutls/megalodon/5.1.15:
resolution: {integrity: sha512-4+mIKUYYr2CLY3idSxXk56WSTG9ww3opeenmsPRxftTwcjQTYxGntNkWmJWEbzeJ4rPslnvpwD7cFR62bPf41g==}
engines: {node: '>=15.0.0'}
dependencies:
'@types/oauth': 0.9.1
'@types/ws': 8.5.4
axios: 1.2.2
dayjs: 1.11.7
form-data: 4.0.0
https-proxy-agent: 5.0.1
oauth: 0.10.0
object-assign-deep: 0.4.0
parse-link-header: 2.0.0
socks-proxy-agent: 7.0.0
typescript: 4.9.4
uuid: 9.0.0
ws: 8.12.0
transitivePeerDependencies:
- bufferutil
- debug
- supports-color
- utf-8-validate
dev: false
/@cypress/request/2.88.11: /@cypress/request/2.88.11:
resolution: {integrity: sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==} resolution: {integrity: sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -3335,7 +3335,7 @@ packages:
/axios/0.24.0: /axios/0.24.0:
resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==} resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==}
dependencies: dependencies:
follow-redirects: 1.15.2_debug@4.3.4 follow-redirects: 1.15.2
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
dev: false dev: false
@ -3343,7 +3343,7 @@ packages:
/axios/0.25.0_debug@4.3.4: /axios/0.25.0_debug@4.3.4:
resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==} resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==}
dependencies: dependencies:
follow-redirects: 1.15.2_debug@4.3.4 follow-redirects: 1.15.2
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
dev: true dev: true
@ -3351,7 +3351,7 @@ packages:
/axios/1.2.2: /axios/1.2.2:
resolution: {integrity: sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==} resolution: {integrity: sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==}
dependencies: dependencies:
follow-redirects: 1.15.2_debug@4.3.4 follow-redirects: 1.15.2
form-data: 4.0.0 form-data: 4.0.0
proxy-from-env: 1.1.0 proxy-from-env: 1.1.0
transitivePeerDependencies: transitivePeerDependencies:
@ -3361,7 +3361,7 @@ packages:
/axios/1.3.2: /axios/1.3.2:
resolution: {integrity: sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==} resolution: {integrity: sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==}
dependencies: dependencies:
follow-redirects: 1.15.2_debug@4.3.4 follow-redirects: 1.15.2
form-data: 4.0.0 form-data: 4.0.0
proxy-from-env: 1.1.0 proxy-from-env: 1.1.0
transitivePeerDependencies: transitivePeerDependencies:
@ -3820,8 +3820,8 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/calckey-js/0.0.20: /calckey-js/0.0.22:
resolution: {integrity: sha512-KqydxFuMKAEmi+NPOpNZfnq5Ik7w2eApY7hc2MxrqIkcuy/lgUv8io4rnNx512fHppEHtV7AVxovU2//buvgZA==} resolution: {integrity: sha512-So4Jc3w5QiwNO+8yLomjgNvHR9luKZ5bGLqjIEzAMsD/bIvUqWvriQAyWHN4EweyQUJ8UZCXBG9iyVX8VjtiXw==}
dependencies: dependencies:
autobind-decorator: 2.4.0 autobind-decorator: 2.4.0
eventemitter3: 4.0.7 eventemitter3: 4.0.7
@ -6301,7 +6301,7 @@ packages:
readable-stream: 2.3.7 readable-stream: 2.3.7
dev: true dev: true
/follow-redirects/1.15.2_debug@4.3.4: /follow-redirects/1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
peerDependencies: peerDependencies:
@ -6309,8 +6309,6 @@ packages:
peerDependenciesMeta: peerDependenciesMeta:
debug: debug:
optional: true optional: true
dependencies:
debug: 4.3.4
/for-each/0.3.3: /for-each/0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}