mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-09 11:41:30 -07:00
apply bite.patch
This commit is contained in:
parent
dd6bd0267c
commit
727c691a6b
29 changed files with 691 additions and 1 deletions
|
@ -1,3 +1,7 @@
|
|||
This repository includes [bite.patch](https://iceshrimp.dev/mia/withdrawal/src/branch/main/patches/bite.patch), which changes the database schema. Please contact @mia@void.rehab before migrating from iceshrimp-bite to another software, including back to vanilla Iceshrimp and to Iceshrimp.NET.
|
||||
|
||||
---
|
||||
|
||||
<p><img src="assets/logo.png" alt="Iceshrimp" width="400px"></p>
|
||||
<p><strong>Iceshrimp</strong> is a decentralized and federated social networking service, implementing the <strong>ActivityPub</strong> standard.<br>
|
||||
It was forked from <del>Calckey</del> Firefish (itself a fork of Misskey) in mid-2023, to focus on stability, performance and usability instead of new features.</p>
|
||||
|
|
|
@ -1132,6 +1132,11 @@ openInMainColumn: "Open in main column"
|
|||
searchNotLoggedIn_1: "You have to be authenticated in order to use full text search."
|
||||
searchNotLoggedIn_2: "However, you can search using hashtags, and search users."
|
||||
searchEmptyQuery: "Please enter a search term."
|
||||
bite: "Bite"
|
||||
biteBack: "Bite back"
|
||||
bittenBack: "Bitten back"
|
||||
bitYou: "bit you"
|
||||
bitYouBack: "bit you back"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing
|
||||
|
|
|
@ -76,6 +76,7 @@ import { OAuthApp } from "@/models/entities/oauth-app.js";
|
|||
import { OAuthToken } from "@/models/entities/oauth-token.js";
|
||||
import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js";
|
||||
import { HtmlUserCacheEntry } from "@/models/entities/html-user-cache-entry.js";
|
||||
import { Bite } from "@/models/entities/bite.js";
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger("sql", "gray", false);
|
||||
class MyCustomLogger implements Logger {
|
||||
|
@ -179,6 +180,7 @@ export const entities = [
|
|||
OAuthToken,
|
||||
HtmlNoteCacheEntry,
|
||||
HtmlUserCacheEntry,
|
||||
Bite,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class FederatedBite1705528046452 implements MigrationInterface {
|
||||
name = 'FederatedBite1705528046452'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TYPE "public"."bite_targettype_enum" AS ENUM('user', 'bite')`);
|
||||
await queryRunner.query(`CREATE TABLE "bite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "uri" character varying(512), "userId" character varying(32) NOT NULL, "targetType" "public"."bite_targettype_enum" NOT NULL, "targetUserId" character varying(32), "targetBiteId" character varying(32), "replied" boolean NOT NULL DEFAULT true, CONSTRAINT "CHK_c3a20c5756ccff3133f8927500" CHECK ("targetUserId" IS NOT NULL OR "targetBiteId" IS NOT NULL), CONSTRAINT "PK_1887f3f621a4a7655a1b78bfd66" PRIMARY KEY ("id")); COMMENT ON COLUMN "bite"."uri" IS 'null if local'`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ADD "biteId" character varying(32)`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'bite')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
|
||||
await queryRunner.query(`ALTER TABLE "bite" ADD CONSTRAINT "FK_8d00aa79e157364ac1f60c15098" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "bite" ADD CONSTRAINT "FK_a646fbbeb6efa2531c75fec46b9" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "bite" ADD CONSTRAINT "FK_5d5f68610583f2e0b6785d3c0e9" FOREIGN KEY ("targetBiteId") REFERENCES "bite"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_c54844158c1eead7042e7ca4c83" FOREIGN KEY ("biteId") REFERENCES "bite"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'bite')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_c54844158c1eead7042e7ca4c83"`);
|
||||
await queryRunner.query(`ALTER TABLE "bite" DROP CONSTRAINT "FK_5d5f68610583f2e0b6785d3c0e9"`);
|
||||
await queryRunner.query(`ALTER TABLE "bite" DROP CONSTRAINT "FK_a646fbbeb6efa2531c75fec46b9"`);
|
||||
await queryRunner.query(`ALTER TABLE "bite" DROP CONSTRAINT "FK_8d00aa79e157364ac1f60c15098"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "biteId"`);
|
||||
await queryRunner.query(`DROP TABLE "bite"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."bite_targettype_enum"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ import { packedQueueCountSchema } from "@/models/schema/queue.js";
|
|||
import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js";
|
||||
import { packedEmojiSchema } from "@/models/schema/emoji.js";
|
||||
import { packedNoteEdit } from "@/models/schema/note-edit.js";
|
||||
import { packedBiteSchema } from "@/models/schema/bite.js";
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
|
@ -65,6 +66,7 @@ export const refs = {
|
|||
FederationInstance: packedFederationInstanceSchema,
|
||||
GalleryPost: packedGalleryPostSchema,
|
||||
Emoji: packedEmojiSchema,
|
||||
Bite: packedBiteSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
|
|
56
packages/backend/src/models/entities/bite.ts
Normal file
56
packages/backend/src/models/entities/bite.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { Check, Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
|
||||
import { id } from "../id.js";
|
||||
import { User } from "./user.js";
|
||||
|
||||
@Entity()
|
||||
@Check(`"targetUserId" IS NOT NULL OR "targetBiteId" IS NOT NULL`)
|
||||
export class Bite {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column("timestamp with time zone")
|
||||
public createdAt: Date;
|
||||
|
||||
@Column("varchar", {
|
||||
length: 512,
|
||||
nullable: true,
|
||||
comment: "null if local",
|
||||
})
|
||||
public uri: string | null;
|
||||
|
||||
@Column(id())
|
||||
public userId: string;
|
||||
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
public user: User;
|
||||
|
||||
@Column("enum", {
|
||||
enum: ["user", "bite"],
|
||||
})
|
||||
public targetType: "user" | "bite";
|
||||
|
||||
@Column({ ...id(), nullable: true })
|
||||
public targetUserId: string | null;
|
||||
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
nullable: true,
|
||||
})
|
||||
public targetUser: User | null;
|
||||
|
||||
@Column({ ...id(), nullable: true })
|
||||
public targetBiteId: string | null;
|
||||
|
||||
@ManyToOne(() => Bite, {
|
||||
onDelete: "CASCADE",
|
||||
nullable: true,
|
||||
})
|
||||
public targetBite: Bite | null;
|
||||
|
||||
@Column("boolean", {
|
||||
default: true,
|
||||
})
|
||||
public replied: boolean;
|
||||
}
|
|
@ -13,6 +13,7 @@ import { FollowRequest } from "./follow-request.js";
|
|||
import { UserGroupInvitation } from "./user-group-invitation.js";
|
||||
import { AccessToken } from "./access-token.js";
|
||||
import { notificationTypes } from "@/types.js";
|
||||
import { Bite } from "./bite.js";
|
||||
|
||||
@Entity()
|
||||
export class Notification {
|
||||
|
@ -181,4 +182,12 @@ export class Notification {
|
|||
})
|
||||
@JoinColumn()
|
||||
public appAccessToken: AccessToken | null;
|
||||
|
||||
@Column({ ...id(), nullable: true })
|
||||
public biteId: Bite["id"] | null;
|
||||
|
||||
@ManyToOne((type) => Bite, {
|
||||
onDelete: "CASCADE", nullable: true
|
||||
})
|
||||
public bite: Bite | null;
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ import { OAuthToken } from "@/models/entities/oauth-token.js";
|
|||
import { UserProfileRepository } from "@/models/repositories/user-profile.js";
|
||||
import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js";
|
||||
import { HtmlUserCacheEntry } from "@/models/entities/html-user-cache-entry.js";
|
||||
import { BiteRespository } from "./repositories/bite.js";
|
||||
|
||||
export const Announcements = db.getRepository(Announcement);
|
||||
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
||||
|
@ -138,3 +139,4 @@ export const OAuthApps = db.getRepository(OAuthApp);
|
|||
export const OAuthTokens = db.getRepository(OAuthToken);
|
||||
export const HtmlUserCacheEntries = db.getRepository(HtmlUserCacheEntry);
|
||||
export const HtmlNoteCacheEntries = db.getRepository(HtmlNoteCacheEntry);
|
||||
export const Bites = BiteRespository;
|
||||
|
|
71
packages/backend/src/models/repositories/bite.ts
Normal file
71
packages/backend/src/models/repositories/bite.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { db } from "@/db/postgre.js";
|
||||
import { Bite } from "../entities/bite.js";
|
||||
import { Packed } from "@/misc/schema.js";
|
||||
import { Bites, Users } from "../index.js";
|
||||
import { User } from "../entities/user.js";
|
||||
import { awaitAll } from "@/prelude/await-all.js";
|
||||
import config from "@/config/index.js";
|
||||
|
||||
export const BiteRespository = db.getRepository(Bite).extend({
|
||||
async pack(
|
||||
src: Bite | Bite["id"],
|
||||
me?: { id: User["id"] } | null | undefined,
|
||||
): Promise<Packed<"Bite">> {
|
||||
const bite =
|
||||
typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
|
||||
return await awaitAll({
|
||||
id: bite.id,
|
||||
user: Users.pack(bite.user ?? bite.userId, me, { detail: false }),
|
||||
targetType: bite.targetType,
|
||||
target: this.packTarget(bite, me),
|
||||
replied: bite.replied,
|
||||
});
|
||||
},
|
||||
|
||||
async packTarget(
|
||||
bite: Bite,
|
||||
me?: { id: User["id"] } | null | undefined,
|
||||
): Promise<Packed<"UserLite"> | Packed<"Bite">> {
|
||||
switch (bite.targetType) {
|
||||
case "user":
|
||||
return await Users.pack(bite.targetUser ?? bite.targetUserId!, me, {
|
||||
detail: false,
|
||||
});
|
||||
case "bite":
|
||||
return await this.pack(bite.targetBite ?? bite.targetBiteId!, me);
|
||||
}
|
||||
},
|
||||
|
||||
async targetUri(bite: Bite): Promise<string> {
|
||||
switch (bite.targetType) {
|
||||
case "user": {
|
||||
bite.targetUser =
|
||||
bite.targetUser ??
|
||||
(await Users.findOneOrFail({ where: { id: bite.targetUserId! } }));
|
||||
return (
|
||||
bite.targetUser.uri || `${config.url}/users/${bite.targetUserId}`
|
||||
);
|
||||
}
|
||||
case "bite": {
|
||||
bite.targetBite =
|
||||
bite.targetBite ??
|
||||
(await Bites.findOneOrFail({ where: { id: bite.targetBiteId! } }));
|
||||
return (
|
||||
bite.targetBite.uri || `${config.url}/bites/${bite.targetBiteId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async targetUserId(bite: Bite): Promise<User["id"]> {
|
||||
switch (bite.targetType) {
|
||||
case "user":
|
||||
return bite.targetUserId!;
|
||||
case "bite":
|
||||
bite.targetBite =
|
||||
bite.targetBite ??
|
||||
(await Bites.findOneByOrFail({ id: bite.targetBiteId! }));
|
||||
return bite.targetBite.userId;
|
||||
}
|
||||
},
|
||||
});
|
|
@ -14,6 +14,7 @@ import {
|
|||
UserGroupInvitations,
|
||||
AccessTokens,
|
||||
NoteReactions,
|
||||
Bites,
|
||||
} from "../index.js";
|
||||
|
||||
export const NotificationRepository = db.getRepository(Notification).extend({
|
||||
|
@ -143,6 +144,11 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
|||
icon: notification.customIcon || token?.iconUrl,
|
||||
}
|
||||
: {}),
|
||||
...(notification.type === "bite"
|
||||
? {
|
||||
bite: notification.bite ?? await Bites.findOneBy({ id: notification.biteId! }),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
|
||||
|
|
31
packages/backend/src/models/schema/bite.ts
Normal file
31
packages/backend/src/models/schema/bite.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
export const packedBiteSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
format: "id",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
},
|
||||
user: {
|
||||
type: "object",
|
||||
ref: "UserLite",
|
||||
},
|
||||
targetType: {
|
||||
type: "string",
|
||||
enum: ["user", "bite"],
|
||||
},
|
||||
target: {
|
||||
oneOf: [
|
||||
{
|
||||
type: "object",
|
||||
ref: "UserLite",
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
ref: "Bite",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as const;
|
|
@ -75,5 +75,11 @@ export const packedNotificationSchema = {
|
|||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
bite: {
|
||||
type: "object",
|
||||
ref: "Bite",
|
||||
optional: true,
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
79
packages/backend/src/remote/activitypub/kernel/bite.ts
Normal file
79
packages/backend/src/remote/activitypub/kernel/bite.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { CacheableRemoteUser } from "@/models/entities/user.js";
|
||||
import { IActivity, IBite } from "../type.js";
|
||||
import Resolver from "../resolver.js";
|
||||
import { fetchPerson } from "../models/person.js";
|
||||
import config from "@/config/index.js";
|
||||
import { genId } from "@/misc/gen-id.js";
|
||||
import { createBite } from "@/services/create-bite.js";
|
||||
import { Bite } from "@/models/entities/bite.js";
|
||||
|
||||
export default async (
|
||||
actor: CacheableRemoteUser,
|
||||
bite: IBite,
|
||||
): Promise<string> => {
|
||||
if (actor.uri !== bite.actor) {
|
||||
return "skip: actor uri mismatch";
|
||||
}
|
||||
|
||||
if (bite.id === null) {
|
||||
return "skip: bite id not specified";
|
||||
}
|
||||
|
||||
const resolver = new Resolver();
|
||||
const biteActor = await fetchPerson(bite.actor, resolver);
|
||||
if (biteActor === null) {
|
||||
return "skip: biteActor is null";
|
||||
}
|
||||
if (!bite.target.startsWith(`${config.url}/`)) {
|
||||
return "skip: target is not local";
|
||||
}
|
||||
|
||||
const localId = genId();
|
||||
const fields = {
|
||||
id: localId,
|
||||
userId: biteActor.id,
|
||||
replied: false,
|
||||
} as any;
|
||||
|
||||
const parts = bite.target.split("/");
|
||||
const targetDbId = parts.pop();
|
||||
const targetPathType = parts.pop();
|
||||
|
||||
let targetType: Bite["targetType"];
|
||||
let targetId;
|
||||
|
||||
if (targetPathType === "users") {
|
||||
targetType = "user";
|
||||
targetId = targetDbId;
|
||||
} else if (targetPathType === "bites") {
|
||||
targetType = "bite";
|
||||
targetId = targetDbId;
|
||||
} else {
|
||||
// fallback for unknown object types
|
||||
targetType = "user";
|
||||
if (bite.to !== undefined) {
|
||||
const to = Array.isArray(bite.to) ? bite.to[0] : bite.to;
|
||||
targetId = (to as string).split("/").pop();
|
||||
} else {
|
||||
const biteTarget = await resolver.resolve(bite.target);
|
||||
const targetActor =
|
||||
(biteTarget as IActivity).actor || biteTarget.attributedTo;
|
||||
const targetActorId =
|
||||
typeof targetActor === "string" ? targetActor : (targetActor as any).id;
|
||||
if (!targetActorId.startsWith(`${config.url}/`)) {
|
||||
return "skip: indirect target is not local";
|
||||
}
|
||||
targetId = targetActorId.split("/").pop();
|
||||
}
|
||||
}
|
||||
|
||||
await createBite(
|
||||
biteActor,
|
||||
targetType,
|
||||
targetId,
|
||||
bite.id!,
|
||||
bite.published ? new Date(bite.published) : null,
|
||||
);
|
||||
|
||||
return "ok";
|
||||
};
|
|
@ -19,6 +19,7 @@ import {
|
|||
isFlag,
|
||||
isMove,
|
||||
getApId,
|
||||
isBite,
|
||||
} from "../type.js";
|
||||
import { apLogger } from "../logger.js";
|
||||
import Resolver from "../resolver.js";
|
||||
|
@ -37,6 +38,7 @@ import remove from "./remove/index.js";
|
|||
import block from "./block/index.js";
|
||||
import flag from "./flag/index.js";
|
||||
import move from "./move/index.js";
|
||||
import bite from "./bite.js";
|
||||
import type { IObject } from "../type.js";
|
||||
import { extractDbHost } from "@/misc/convert-host.js";
|
||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||
|
@ -105,6 +107,8 @@ async function performOneActivity(
|
|||
await flag(actor, activity);
|
||||
} else if (isMove(activity)) {
|
||||
await move(actor, activity);
|
||||
} else if (isBite(activity)) {
|
||||
await bite(actor, activity);
|
||||
} else {
|
||||
apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
|
||||
}
|
||||
|
|
12
packages/backend/src/remote/activitypub/renderer/bite.ts
Normal file
12
packages/backend/src/remote/activitypub/renderer/bite.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import config from "@/config/index.js";
|
||||
import { Bites } from "@/models/index.js";
|
||||
import { Bite } from "@/models/entities/bite.js";
|
||||
|
||||
export default async (bite: Bite) => ({
|
||||
id: `${config.url}/bites/${bite.id}`,
|
||||
type: "Bite",
|
||||
actor: `${config.url}/users/${bite.userId}`,
|
||||
target: await Bites.targetUri(bite),
|
||||
published: bite.createdAt.toISOString(),
|
||||
to: await Bites.targetUserId(bite),
|
||||
});
|
|
@ -47,6 +47,8 @@ export const renderActivity = (x: any): IActivity | null => {
|
|||
fedibird: "http://fedibird.com/ns#",
|
||||
// vcard
|
||||
vcard: "http://www.w3.org/2006/vcard/ns#",
|
||||
// delusion
|
||||
Bite: "https://ns.mia.jetzt/as#Bite",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
NoteReactions,
|
||||
Polls,
|
||||
Users,
|
||||
Bites,
|
||||
} from "@/models/index.js";
|
||||
import { parseUri } from "./db-resolver.js";
|
||||
import renderNote from "@/remote/activitypub/renderer/note.js";
|
||||
|
@ -24,6 +25,7 @@ import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
|||
import renderFollow from "@/remote/activitypub/renderer/follow.js";
|
||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||
import { apLogger } from "@/remote/activitypub/logger.js";
|
||||
import renderBite from "@/remote/activitypub/renderer/bite.js";
|
||||
|
||||
export default class Resolver {
|
||||
private history: Set<string>;
|
||||
|
@ -181,6 +183,10 @@ export default class Resolver {
|
|||
).then(([follower, followee]) =>
|
||||
renderActivity(renderFollow(follower, followee, url)),
|
||||
);
|
||||
case "bites":
|
||||
return Bites.findOneByOrFail({ id: parsed.id }).then((bite) =>
|
||||
renderActivity(renderBite(bite)),
|
||||
);
|
||||
default:
|
||||
throw new Error(`resolveLocal: type ${type} unhandled`);
|
||||
}
|
||||
|
|
|
@ -322,6 +322,12 @@ export interface IMove extends IActivity {
|
|||
target: IObject | string;
|
||||
}
|
||||
|
||||
export interface IBite extends IActivity {
|
||||
type: "Bite";
|
||||
actor: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export const isCreate = (object: IObject): object is ICreate =>
|
||||
getApType(object) === "Create";
|
||||
export const isDelete = (object: IObject): object is IDelete =>
|
||||
|
@ -354,3 +360,5 @@ export const isFlag = (object: IObject): object is IFlag =>
|
|||
getApType(object) === "Flag";
|
||||
export const isMove = (object: IObject): object is IMove =>
|
||||
getApType(object) === "Move";
|
||||
export const isBite = (object: IObject): object is IBite =>
|
||||
getApType(object) === "Bite";
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
Emojis,
|
||||
NoteReactions,
|
||||
FollowRequests,
|
||||
Bites,
|
||||
} from "@/models/index.js";
|
||||
import type { ILocalUser, User } from "@/models/entities/user.js";
|
||||
import { renderLike } from "@/remote/activitypub/renderer/like.js";
|
||||
|
@ -35,6 +36,7 @@ import Outbox, { packActivity } from "./activitypub/outbox.js";
|
|||
import { serverLogger } from "./index.js";
|
||||
import config from "@/config/index.js";
|
||||
import Koa from "koa";
|
||||
import renderBite from "@/remote/activitypub/renderer/bite.js";
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
|
@ -480,4 +482,34 @@ router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => {
|
|||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
// bite
|
||||
router.get("/bites/:biteId", async (ctx: Router.RouterContext) => {
|
||||
tickFetch();
|
||||
const verify = await checkFetch(ctx.req);
|
||||
if (verify !== 200) {
|
||||
ctx.status = verify;
|
||||
return;
|
||||
}
|
||||
|
||||
const bite = await Bites.findOne({
|
||||
where: { id: ctx.params.biteId },
|
||||
relations: ["targetUser", "targetBite"],
|
||||
});
|
||||
|
||||
if (bite === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const meta = await fetchMeta();
|
||||
if (meta.secureMode || meta.privateMode) {
|
||||
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
|
||||
} else {
|
||||
ctx.set("Cache-Control", "public, max-age=180");
|
||||
}
|
||||
|
||||
ctx.body = renderActivity(await renderBite(bite));
|
||||
setResponseType(ctx);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -334,6 +334,8 @@ import * as ep___users_show from "./endpoints/users/show.js";
|
|||
import * as ep___users_stats from "./endpoints/users/stats.js";
|
||||
import * as ep___fetchRss from "./endpoints/fetch-rss.js";
|
||||
import * as ep___admin_driveCapOverride from "./endpoints/admin/drive-capacity-override.js";
|
||||
import * as ep___bites_create from "./endpoints/bites/create.js";
|
||||
import * as ep___bites_show from "./endpoints/bites/show.js";
|
||||
|
||||
//Iceshrimp Move
|
||||
import * as ep___i_move from "./endpoints/i/move.js";
|
||||
|
@ -682,6 +684,8 @@ const eps = [
|
|||
["admin/drive-capacity-override", ep___admin_driveCapOverride],
|
||||
["fetch-rss", ep___fetchRss],
|
||||
["get-sounds", ep___sounds],
|
||||
["bites/create", ep___bites_create],
|
||||
["bites/show", ep___bites_show],
|
||||
];
|
||||
|
||||
export interface IEndpointMeta {
|
||||
|
|
37
packages/backend/src/server/api/endpoints/bites/create.ts
Normal file
37
packages/backend/src/server/api/endpoints/bites/create.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Bites } from "@/models/index.js";
|
||||
import define from "../../define.js";
|
||||
import { createBite } from "@/services/create-bite.js";
|
||||
import { MINUTE } from "@/const.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["bites"],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
limit: {
|
||||
duration: MINUTE,
|
||||
max: 30,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "Bite",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
targetType: { type: "string", enum: ["user", "bite"] },
|
||||
targetId: { type: "string", format: "misskey:id" },
|
||||
},
|
||||
required: ["targetType", "targetId"],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const biteId = await createBite(user, ps.targetType, ps.targetId);
|
||||
|
||||
return await Bites.pack(biteId, user);
|
||||
});
|
25
packages/backend/src/server/api/endpoints/bites/show.ts
Normal file
25
packages/backend/src/server/api/endpoints/bites/show.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Bites } from "@/models/index.js";
|
||||
import define from "../../define.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["bites"],
|
||||
|
||||
res: {
|
||||
type: "object",
|
||||
optional: false,
|
||||
nullable: false,
|
||||
ref: "Bite",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {
|
||||
biteId: { type: "string", format: "misskey:id" },
|
||||
},
|
||||
required: ["biteId"],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
return await Bites.pack(ps.biteId, user);
|
||||
});
|
74
packages/backend/src/services/create-bite.ts
Normal file
74
packages/backend/src/services/create-bite.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { genId } from "@/misc/gen-id.js";
|
||||
import { Bites, Users } from "@/models/index.js";
|
||||
import { Bite } from "@/models/entities/bite.js";
|
||||
import { User } from "@/models/entities/user.js";
|
||||
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
||||
import renderBite from "@/remote/activitypub/renderer/bite.js";
|
||||
import { deliverToUser } from "@/remote/activitypub/deliver-manager.js";
|
||||
import { createNotification } from "./create-notification.js";
|
||||
|
||||
export async function createBite(
|
||||
sender: User,
|
||||
targetType: Bite["targetType"],
|
||||
targetId: string,
|
||||
remoteUri: Bite["uri"] = null,
|
||||
createdAt: Date | null = null,
|
||||
): Promise<Bite["id"]> {
|
||||
const id = genId();
|
||||
|
||||
const insert = {
|
||||
id,
|
||||
createdAt: createdAt ?? new Date(),
|
||||
userId: sender.id,
|
||||
targetType,
|
||||
replied: false,
|
||||
uri: remoteUri,
|
||||
} as any;
|
||||
|
||||
switch (targetType) {
|
||||
case "user":
|
||||
insert.targetUserId = targetId;
|
||||
break;
|
||||
case "bite":
|
||||
insert.targetBiteId = targetId;
|
||||
break;
|
||||
}
|
||||
|
||||
await Bites.insert(insert);
|
||||
|
||||
const bite = await Bites.findOneOrFail({
|
||||
where: { id },
|
||||
relations: ["targetUser", "targetBite"],
|
||||
});
|
||||
|
||||
let deliverTarget: User;
|
||||
|
||||
switch (targetType) {
|
||||
case "user":
|
||||
deliverTarget = bite.targetUser!;
|
||||
break;
|
||||
case "bite":
|
||||
await Bites.update({ id: bite.targetBiteId! }, { replied: true });
|
||||
deliverTarget =
|
||||
bite.targetBite!.user ??
|
||||
(await Users.findOneByOrFail({ id: bite.targetBite!.userId }));
|
||||
break;
|
||||
}
|
||||
|
||||
if (Users.isLocalUser(sender) && Users.isRemoteUser(deliverTarget)) {
|
||||
await deliverToUser(
|
||||
sender,
|
||||
renderActivity(await renderBite(bite)),
|
||||
deliverTarget,
|
||||
);
|
||||
}
|
||||
|
||||
if (Users.isLocalUser(deliverTarget)) {
|
||||
await createNotification(deliverTarget.id, "bite", {
|
||||
notifierId: sender.id,
|
||||
biteId: bite.id,
|
||||
});
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
|
@ -25,7 +25,7 @@ export async function createNotification(
|
|||
|
||||
if (
|
||||
data.notifierId &&
|
||||
["mention", "reply", "renote", "quote", "reaction"].includes(type)
|
||||
["mention", "reply", "renote", "quote", "reaction", "bite"].includes(type)
|
||||
) {
|
||||
const notifier = await Users.findOneBy({ id: data.notifierId });
|
||||
// suppress if the notifier does not exist or is silenced.
|
||||
|
|
|
@ -11,6 +11,7 @@ export const notificationTypes = [
|
|||
"followRequestAccepted",
|
||||
"groupInvited",
|
||||
"app",
|
||||
"bite",
|
||||
] as const;
|
||||
|
||||
export const noteVisibilities = [
|
||||
|
|
124
packages/client/src/components/MkBiteButton.vue
Normal file
124
packages/client/src/components/MkBiteButton.vue
Normal file
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<button class="kpoogebi _button bite-button" :class="{
|
||||
full,
|
||||
large,
|
||||
wait,
|
||||
active: hasBittenBack,
|
||||
}" :disabled="wait" @click.stop="onClick" :aria-label="`bite ${user.name || user.username} back`">
|
||||
<span>{{ i18n.ts.biteBack }}</span><i class="ph-tooth ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type * as Misskey from "iceshrimp-js";
|
||||
import * as os from "@/os";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
user: Misskey.entities.UserLite,
|
||||
bite: Misskey.entities.Bite,
|
||||
full: boolean,
|
||||
large: boolean,
|
||||
}>(),
|
||||
{
|
||||
full: false,
|
||||
large: false
|
||||
},
|
||||
);
|
||||
|
||||
let wait = $ref(false);
|
||||
let hasBittenBack = $ref<boolean>(props.bite.replied);
|
||||
|
||||
async function onClick() {
|
||||
wait = true;
|
||||
|
||||
try {
|
||||
await os.api("bites/create", {
|
||||
targetType: "bite",
|
||||
targetId: props.bite.id,
|
||||
});
|
||||
hasBittenBack = true;
|
||||
} finally {
|
||||
wait = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bite-button {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
border: solid 1px var(--accent);
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
border-radius: 100px;
|
||||
background: var(--bg);
|
||||
vertical-align: middle;
|
||||
margin-left: 0.5em;
|
||||
|
||||
&.full {
|
||||
padding: 0.2em 0.7em;
|
||||
width: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 16px;
|
||||
height: 38px;
|
||||
padding: 0 12px 0 16px;
|
||||
}
|
||||
|
||||
&:not(.full) {
|
||||
width: 31px;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
&:after {
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid var(--focus);
|
||||
border-radius: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--fgOnAccent);
|
||||
background: var(--accent);
|
||||
|
||||
&:hover {
|
||||
background: var(--accentLighten);
|
||||
border-color: var(--accentLighten);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--accentDarken);
|
||||
border-color: var(--accentDarken);
|
||||
}
|
||||
}
|
||||
|
||||
&.wait {
|
||||
cursor: wait !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
>span {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -222,6 +222,21 @@
|
|||
:hideMenu="true"
|
||||
/></div
|
||||
></span>
|
||||
<span
|
||||
v-if="notification.type === 'bite'"
|
||||
class="text"
|
||||
style="opacity: 0.7">{{
|
||||
notification.bite.targetType === 'user'
|
||||
? i18n.ts.bitYou
|
||||
: i18n.ts.bitYouBack
|
||||
}}
|
||||
<div v-if="full">
|
||||
<MkBiteButton
|
||||
:user="notification.user"
|
||||
:bite="notification.bite"
|
||||
:full="true"
|
||||
/></div
|
||||
></span>
|
||||
<span
|
||||
v-if="notification.type === 'followRequestAccepted'"
|
||||
class="text"
|
||||
|
@ -277,6 +292,7 @@ import { ref, onMounted, onUnmounted, watch } from "vue";
|
|||
import * as misskey from "iceshrimp-js";
|
||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||
import MkFollowButton from "@/components/MkFollowButton.vue";
|
||||
import MkBiteButton from "@/components/MkBiteButton.vue";
|
||||
import XReactionTooltip from "@/components/MkReactionTooltip.vue";
|
||||
import { getNoteSummary } from "@/scripts/get-note-summary";
|
||||
import { notePage } from "@/filters/note";
|
||||
|
|
|
@ -59,6 +59,13 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
|||
});
|
||||
}
|
||||
|
||||
async function bite() {
|
||||
await os.apiWithDialog("bites/create", {
|
||||
targetType: "user",
|
||||
targetId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleMute() {
|
||||
if (user.isMuted) {
|
||||
os.apiWithDialog("mute/delete", {
|
||||
|
@ -310,6 +317,13 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
|||
action: inviteGroup,
|
||||
}
|
||||
: undefined,
|
||||
meId !== user.id
|
||||
? {
|
||||
icon: "ph-tooth ph-bold ph-lg",
|
||||
text: i18n.ts.bite,
|
||||
action: bite,
|
||||
}
|
||||
: undefined,
|
||||
null,
|
||||
{
|
||||
icon: user.isRenoteMuted
|
||||
|
|
|
@ -250,6 +250,12 @@ export type Notification = {
|
|||
body: string;
|
||||
icon?: string | null;
|
||||
}
|
||||
| {
|
||||
type: "bite";
|
||||
user: User;
|
||||
userId: User["id"];
|
||||
bite: Bite;
|
||||
}
|
||||
);
|
||||
|
||||
export type MessagingMessage = {
|
||||
|
@ -492,3 +498,10 @@ export type UserSorting =
|
|||
| "+updatedAt"
|
||||
| "-updatedAt";
|
||||
export type OriginType = "combined" | "local" | "remote";
|
||||
|
||||
export type Bite = {
|
||||
id: ID;
|
||||
user: UserLite,
|
||||
targetType: "user" | "bite",
|
||||
target: UserLite | Bite,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue