Feat: メッセージの送受信 / New: ユーザーのiconプロパティ / New: logエンティティ・リポジトリ / Chg: コミュニティリポジトリのスキーマのiconをoptionalに / Del: 不要なimport / Fix: Vue起動前のindex.htmlの背景色をVueと同期 / Enhance: Service Workerを改善 / Fix: 最初に開いたページが動作しない問題 / Feat: 上部の通知モーダル / Feat: 閉じることができないエラーのモーダルに再読み込みボタンを追加 / Fix: はみ出す挙動などのCSSを修正

This commit is contained in:
2026-05-31 13:54:55 +09:00
parent beb0e25ad9
commit cbf18aec8f
37 changed files with 1174 additions and 83 deletions
+1 -1
View File
@@ -22,12 +22,12 @@
"@mikro-orm/postgresql": "^6.6.7",
"@mikro-orm/reflection": "^6.6.7",
"@types/web-push": "^3.6.4",
"lynqchat-js": "workspace:*",
"argon2": "^0.44.0",
"cross-env": "^10.1.0",
"fastify": "^5.7.4",
"fastify-plugin": "^5.1.0",
"fs": "0.0.1-security",
"lynqchat-js": "workspace:*",
"os": "^0.1.2",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3",
+4
View File
@@ -0,0 +1,4 @@
export const Pick = <T extends Object>(object: T, keys: (keyof T)[]) =>
Object.entries(object)
.filter(([key]) => keys.includes(key as keyof T))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
@@ -0,0 +1,37 @@
import { Entity, EntityRepositoryType, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
import { LogRepository } from "@/modules/repositories/Log";
import generateUniqueId from "@/lib/id";
import { UserEntity } from "@/modules/entities/User";
@Entity({
tableName: "log",
repository: () => LogRepository,
})
export class LogEntity {
[OptionalProps]?: "id" | "createdBy" | "createdAt";
[EntityRepositoryType]?: LogRepository;
@PrimaryKey({
type: "string",
length: 10,
onCreate: () => generateUniqueId(),
})
id!: string;
@Property({
type: "string",
length: 4096
})
text!: string;
@ManyToOne(() => UserEntity, {
nullable: true,
})
createdBy?: UserEntity;
@Property({
type: "datetime",
onCreate: () => new Date(),
})
createdAt!: Date;
}
@@ -1,12 +1,15 @@
import generateUniqueId from "@/lib/id";
import { Entity, Index, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
import { Entity, EntityRepositoryType, ManyToOne, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core";
import { UserEntity } from "@/modules/entities/User";
import { ChannelEntity } from "@/modules/entities/Channel";
import { MessageRepository } from "@/modules/repositories/Message";
@Entity({
tableName: "message",
repository: () => MessageRepository,
})
export class MessageEntity {
[EntityRepositoryType]?: MessageRepository;
[OptionalProps]?: "id" | "createdAt";
@PrimaryKey({
@@ -30,6 +30,12 @@ export class UserEntity {
})
username!: string;
@Property({
type: "string",
nullable: true,
})
icon?: string;
@Property({
type: "string",
length: 4096,
@@ -9,7 +9,7 @@ export class CommunityRepository extends EntityRepository<CommunityEntity> {
name: z.string().trim().min(1).max(20),
description: z.string().trim().min(1).max(4096),
userid: UserRepository.schema.shape.userid,
icon: z.string().url(),
icon: z.string().url().optional(),
});
async listCommunity(limit: number = 20, sinceData?: string) {
@@ -0,0 +1,17 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import type { LogEntity } from "@/modules/entities/Log";
import z from "zod/v3";
export class LogRepository extends EntityRepository<LogEntity> {
public static schema = z.string().min(1).max(4096);
async insertLog(text: string, userid?: string) {
const log = this.create({
text,
createdBy: userid,
});
await this.em.persist(log).flush();
return log.id;
}
}
@@ -0,0 +1,63 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import type { MessageEntity } from "@/modules/entities/Message";
import z from "zod/v3";
import { ErrorBase } from "@/errors";
export class MessageRepository extends EntityRepository<MessageEntity> {
public static schema = z.object({
message: z.string().trim().min(0).max(4096),
channel: z.string().length(10),
userid: z.string().length(10),
});
async sendMessage(messageText: string, channel: string, user: string) {
const message = this.create({
message: messageText,
channel,
createdBy: user,
});
await this.em.persist(message).flush();
return message.id;
}
async deleteMessage(id: string) {
const message = this.getReference(id);
await this.em.remove(message).flush();
}
async listMessage(channel: string, limit: number = 20, untilData?: string) {
let until = untilData ?? new Date();
if (
untilData &&
isNaN(new Date(untilData).getTime())
) {
const itMessage = await this.findOne({ id: untilData });
if (!itMessage) {
return ErrorBase({
bad: "client",
code: "message_not_found",
message: "対象のメッセージが見つかりませんでした。",
});
}
until = itMessage.createdAt;
}
const findResult = await this.find({
channel,
createdAt: {
$lt: until,
},
}, {
orderBy: {
createdAt: "DESC",
},
limit: limit,
});
return findResult ?? [];
}
}
@@ -8,6 +8,7 @@ export class UserRepository extends EntityRepository<UserEntity> {
public static schema = z.object({
userid: z.string().trim().min(3).max(20),
username: z.string().trim().min(3).max(30),
icon: z.string().url().optional(),
profile: z.string().max(4096).optional(),
email: z.string().trim().min(6).max(254).regex(EmailRegex),
password: z.string().trim().min(8),
@@ -1,7 +1,6 @@
import { DatabaseError, InputError } from "@/errors";
import Logger from "@/lib/logger";
import { CommunityEntity } from "@/modules/entities/Community";
import { UserEntity } from "@/modules/entities/User";
import type { FastifyInstance } from "fastify";
import z from "zod/v3";
+5
View File
@@ -5,6 +5,7 @@ import Me from "./me";
import ServerInfo from "./server-info";
import Community from "./community";
import Channel from "./channel";
import Message from "./message";
export default async function Routes(fastify: FastifyInstance) {
await fastify.register(Setup, {
@@ -30,4 +31,8 @@ export default async function Routes(fastify: FastifyInstance) {
await fastify.register(Channel, {
prefix: "/channel",
});
await fastify.register(Message, {
prefix: "/message",
});
}
@@ -0,0 +1,37 @@
import { DatabaseError, InputError } from "@/errors";
import Logger from "@/lib/logger";
import { MessageEntity } from "@/modules/entities/Message";
import type { FastifyInstance } from "fastify";
import z from "zod/v3";
export default async function MessageDelete(fastify: FastifyInstance) {
const logger = new Logger("Endpoint | message/delete");
fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json");
if ("error" in req.token)
return res.code(400).send(req.token);
const result = z.object({
id: z.string().length(10),
}).safeParse(req.body);
if (!result.success) {
return res.code(400).send(InputError(result.error.issues));
}
try {
const messageRepo = fastify.orm.em.getRepository(MessageEntity);
await messageRepo.deleteMessage(result.data.id);
return res.send({
success: true,
});
} catch (err) {
logger.error("Database Error:", err);
return res.code(500).send(DatabaseError());
}
});
}
@@ -0,0 +1,18 @@
import type { FastifyInstance } from "fastify";
import MessageList from "./list";
import MessageSend from "./send";
import MessageDelete from "./delete";
export default async function Message(fastify: FastifyInstance) {
await fastify.register(MessageList, {
prefix: "/list",
});
await fastify.register(MessageSend, {
prefix: "/send",
});
await fastify.register(MessageDelete, {
prefix: "/delete",
});
}
@@ -0,0 +1,55 @@
import { DatabaseError, InputError } from "@/errors";
import Logger from "@/lib/logger";
import { Pick } from "@/lib/object";
import { MessageEntity } from "@/modules/entities/Message";
import type { FastifyInstance } from "fastify";
import z from "zod/v3";
export default async function MessageList(fastify: FastifyInstance) {
const logger = new Logger("Endpoint | message/list");
fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json");
if ("error" in req.token)
return res.code(400).send(req.token);
const bodySchema = z.object({
limit: z.number().optional(),
until: z.string().optional(),
channel: z.string().length(10),
});
const result = bodySchema.safeParse(req.body);
if (!result.success) {
return res.code(400).send(InputError(result.error.issues));
}
try {
const messageRepo = fastify.orm.em.getRepository(MessageEntity);
const findResult = await messageRepo.listMessage(
result.data.channel,
result.data.limit,
result.data.until,
);
if ("error" in findResult) {
return res.code(400).send(findResult);
}
return res.send({
success: true,
messages: findResult.map(message => ({
...message,
createdBy: Pick(message.createdBy, ["id", "userid", "username", "icon"]),
})),
});
} catch (err) {
logger.error("Database Error:", err);
return res.code(500).send(DatabaseError());
}
});
}
@@ -0,0 +1,39 @@
import { DatabaseError, InputError } from "@/errors";
import Logger from "@/lib/logger";
import { MessageEntity } from "@/modules/entities/Message";
import { MessageRepository } from "@/modules/repositories/Message";
import type { FastifyInstance } from "fastify";
export default async function MessageSend(fastify: FastifyInstance) {
const logger = new Logger("Endpoint | message/send");
fastify.post("/", async (req, res) => {
res.header("Content-Type", "application/json");
res.header("Connection", "close");
if ("error" in req.token)
return res.code(400).send(req.token);
const result = MessageRepository.schema
.omit({ userid: true })
.safeParse(req.body);
if (!result.success) {
return res.code(400).send(InputError(result.error.issues));
}
try {
const messageRepo = fastify.orm.em.getRepository(MessageEntity);
const id = await messageRepo.sendMessage(result.data.message, result.data.channel, req.token.user.id);
return res.send({
success: true,
id,
});
} catch (err) {
logger.error("Database Error:", err);
return res.code(500).send(DatabaseError());
}
});
}