Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ setup-ci:
make generate-sql

sync: sync-server sync-web sync-root sync-common
bunx lefthook install || true
@echo '----------------------------------------------------------------------------------------------------------'
@echo '| Most work is done. now running prisma-generate-sql (which might fail if .env.dev is not set configured)|'
@echo '----------------------------------------------------------------------------------------------------------'
make generate-sql || true

generate-sql:
@cd server; bun run prisma-generate-sql
@cd server; \
if command -v dotenv && command -v prisma; \
then dotenv -e .env.dev -- prisma generate --sql; \
else bunx dotenv -e .env.dev -- bunx prisma generate --sql; \
fi

start: start-all # build -> serve
build: build-server build-web
Expand Down Expand Up @@ -59,7 +62,7 @@ docker-watch:
docker compose up --build --watch

seed:
cd server; bunx prisma db seed
cd server; if command -v prisma; then prisma db seed; else bunx prisma db seed; fi

## server/.envをDATABASE_URL=postgres://user:password@localhost:5432/databaseにしてから行う
dev-db: export DATABASE_URL=$(LOCAL_DB)
Expand All @@ -79,12 +82,9 @@ dev-db:
sleep 1; \
done
@echo "PostgreSQL is ready. Running seed..."
@cd server; \
if command -v prisma; then\
prisma generate; prisma db push;\
else \
bunx prisma generate; bunx prisma db push;\
fi
@cd server; if command -v prisma; then \
prisma generate; prisma db push; else \
bunx prisma generate; bunx prisma db push; fi
@make seed
@echo "Seeding completed."

Expand Down
Binary file modified bun.lockb
Binary file not shown.
3 changes: 3 additions & 0 deletions common/zod/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const MessageSchema = z.object({
creator: UserIDSchema,
createdAt: z.date(),
content: ContentSchema,
isPicture: z.boolean(),
edited: z.boolean(),
});

Expand Down Expand Up @@ -172,6 +173,8 @@ export const PersonalizedDMRoomSchema = z.object({
name: NameSchema,
thumbnail: z.string(),
matchingStatus: MatchingStatusSchema,
unreadMessages: z.number(),
friendId: z.number(),
});

export const SharedRoomSchema = z.object({
Expand Down
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
openssl
lefthook
pkgs.prisma
dotenv-cli
] ++ [
rust-pkgs
];
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"prepare": "lefthook install"
},
"keywords": [],
"author": "",
Expand Down
3 changes: 1 addition & 2 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "bun --watch src/main.ts",
"build": "tsc",
"serve": "bun target/main.js",
"prisma-generate-sql": "bunx dotenv -e .env.dev -- prisma generate --sql"
"serve": "bun target/main.js"
},
"prisma": {
"seed": "bun src/seeds/seed-test.ts"
Expand Down
12 changes: 8 additions & 4 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,17 @@ model User {
interests Interest[]
}

// プロフィールの画像。
model Avatar {
guid String @id
data Bytes
}

model Picture {
hash String @id
data Bytes
key String // password
}

model InterestSubject {
id Int @id @default(autoincrement())
name String
Expand Down Expand Up @@ -122,8 +127,6 @@ enum MatchingStatus {
REJECTED
}

// TODO: lazy load MessageLog s.t. it doesn't need to be loaded on Overview query.
// https://www.prisma.io/docs/orm/prisma-client/queries/select-fields
model SharedRoom {
id Int @id @default(autoincrement())
thumbnail String // URL to thumbnail picture
Expand All @@ -134,10 +137,11 @@ model SharedRoom {

model Message {
id Int @id @default(autoincrement())
creator Int // refers to UserId
creator Int // refers to UserId
createdAt DateTime @default(now()) // @readonly
edited Boolean @default(false)
content String
isPicture Boolean // iff the message is a picture. if true, then content is a url of picture.
read Boolean @default(false)
relation Relationship? @relation(fields: [relationId], references: [id], onDelete: Cascade)
relationId Int?
Expand Down
23 changes: 22 additions & 1 deletion server/src/database/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,14 @@ export async function markAsRead(
**/
export async function sendDM(
relation: RelationshipID,
content: Omit<Message, "id">,
content: Omit<Omit<Message, "id">, "isPicture">,
): Promise<Result<Message>> {
try {
const message = await prisma.message.create({
data: {
// isPicture: false, // todo: bring it back
relationId: relation,
isPicture: false,
read: false,
...content,
},
Expand All @@ -167,6 +168,26 @@ export async function sendDM(
return Err(e);
}
}
/**
this doesn't create the image. use uploadPic in database/picture.ts to create the image.
**/
export async function createImageMessage(
sender: UserID,
relation: RelationshipID,
url: string,
) {
return prisma.message
.create({
data: {
creator: sender,
relationId: relation,
content: url,
isPicture: true,
},
})
.then((val) => Ok(val))
.catch((err) => Err(err));
}

export async function createSharedRoom(
room: InitRoom,
Expand Down
40 changes: 36 additions & 4 deletions server/src/database/picture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,54 @@ import { Err, Ok, type Result } from "common/lib/result";
import type { GUID } from "common/types";
import { prisma } from "./client";

/**
* @returns URL of the uploaded file.
* @throws on database conn fail.
**/
export async function uploadPic(
hash: string,
buf: Buffer,
passkey: string,
): Promise<string> {
await prisma.picture.upsert({
where: { hash },
create: { hash, data: buf, key: passkey },
update: { data: buf, key: passkey },
});
const url = `/picture/${hash}?key=${passkey}`;
return url;
}

export async function getPic(hash: string, passkey: string) {
return prisma.picture
.findUnique({
where: {
hash,
key: passkey,
},
})
.then((val) => val?.data);
}

/**
* is safe to await.
* @returns URL of the file.
**/
export async function set(guid: GUID, buf: Buffer): Promise<Result<string>> {
export async function setProf(
guid: GUID,
buf: Buffer,
): Promise<Result<string>> {
return prisma.avatar
.upsert({
where: {
guid: guid,
guid,
},
create: { guid, data: buf },
update: { data: buf },
})
.then(() => {
// ?update=${date} is necessary to let the browsers properly cache the image.
const pictureUrl = `/picture/${guid}?update=${new Date().getTime()}`;
const pictureUrl = `/picture/profile/${guid}?update=${new Date().getTime()}`;
return Ok(pictureUrl);
})
.catch((err) => {
Expand All @@ -27,7 +59,7 @@ export async function set(guid: GUID, buf: Buffer): Promise<Result<string>> {
}

// is await-safe.
export async function get(guid: GUID): Promise<Result<Buffer>> {
export async function getProf(guid: GUID): Promise<Result<Buffer>> {
return prisma.avatar
.findUnique({
where: { guid },
Expand Down
10 changes: 8 additions & 2 deletions server/src/functions/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
ShareRoomID,
} from "common/types";
import * as db from "../database/chat";
import { areAllMatched, areMatched, getRelation } from "../database/matches";
import { areAllMatched, getRelation } from "../database/matches";
import { getUserByID } from "../database/users";
import * as http from "./share/http";

Expand Down Expand Up @@ -48,7 +48,7 @@ export async function sendDM(
);

// they are now MATCHED
const msg: Omit<Message, "id"> = {
const msg: Omit<Omit<Message, "id">, "isPicture"> = {
creator: from,
createdAt: new Date(),
edited: false,
Expand All @@ -73,8 +73,14 @@ export async function getDM(

const friendData = await getUserByID(friend);
if (!friendData.ok) return http.notFound("friend not found");
const unreadCount = db.unreadMessages(user, rel.value.id).then((val) => {
if (val.ok) return val.value;
throw val.error;
});

const personalized: PersonalizedDMRoom & DMRoom = {
unreadMessages: await unreadCount,
friendId: friendData.value.id,
name: friendData.value.name,
thumbnail: friendData.value.pictureUrl,
matchingStatus:
Expand Down
7 changes: 7 additions & 0 deletions server/src/lib/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import crypto from "node:crypto";

export function sha256(src: string): string {
const hasher = crypto.createHash("sha256");
hasher.update(src);
return hasher.digest("hex");
}
69 changes: 62 additions & 7 deletions server/src/router/picture.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,75 @@
import bodyParser from "body-parser";
import { safeParseInt } from "common/lib/result/safeParseInt";
import express from "express";
import * as chat from "../database/chat";
import * as relation from "../database/matches";
import * as storage from "../database/picture";
import { safeGetUserId } from "../firebase/auth/db";
import { safeGetGUID } from "../firebase/auth/lib";
import { compressImage } from "../functions/img/compress";
import * as hashing from "../lib/hash";

// TODO: truncate file at frontend s.t. even the largest file won't trigger the limit
const parseLargeBuffer = bodyParser.raw({
type: "image/png",
// TODO: block large files (larger than 1mb? idk)
limit: "5mb",
});
const router = express.Router();

router.get("/:guid", async (req, res) => {
/* General Pictures in chat */

router.post("/to/:userId", parseLargeBuffer, async (req, res) => {
if (!Buffer.isBuffer(req.body)) return res.status(400).send("not buffer");
const buf = req.body;

const sender = await safeGetUserId(req);
if (!sender.ok) return res.status(401).end();
const recv = safeParseInt(req.params.userId);
if (!recv.ok) return res.status(400).end();

const rel = await relation.getRelation(sender.value, recv.value);
if (!rel.ok) return res.status(401).send();
if (rel.value.status !== "MATCHED") return res.status(401).send();

const hash = hashing.sha256(buf.toString("base64"));
const passkey = hashing.sha256(crypto.randomUUID());

return storage
.uploadPic(hash, buf, passkey)
.then(async (url) => {
await chat.createImageMessage(sender.value, rel.value.id, url);
res.status(201).send(url).end();
})
.catch((err) => {
console.log(err);
res.status(500).send("Failed to upload image to database").end();
});
});

router.get("/:id", async (req, res) => {
const hash = req.params.id;
const key = req.query.key;
if (!key) return res.status(400).send("key is required");

return storage
.getPic(hash, String(key))
.then((buf) => {
if (buf) {
res.status(200).send(buf).end();
} else {
res.status(404).send("not found").end();
}
})
.catch((err) => {
console.error(err);
res.status(500).send("Failed to get image from database").end();
});
});

/* Profile Pictures */

router.get("/profile/:guid", async (req, res) => {
const guid = req.params.guid;
const result = await storage.get(guid);
const result = await storage.getProf(guid);
switch (result.ok) {
case true:
return res.send(new Buffer(result.value));
Expand All @@ -23,16 +78,16 @@ router.get("/:guid", async (req, res) => {
}
});

router.post("/", parseLargeBuffer, async (req, res) => {
router.post("/profile", parseLargeBuffer, async (req, res) => {
const guid = await safeGetGUID(req);
if (!guid.ok) return res.status(401).send();

if (!Buffer.isBuffer(req.body)) return res.status(404).send("not buffer");
if (!Buffer.isBuffer(req.body)) return res.status(400).send("not buffer");

const buf = await compressImage(req.body);
if (!buf.ok) return res.status(500).send("failed to compress image");

const url = await storage.set(guid.value, buf.value);
const url = await storage.setProf(guid.value, buf.value);
if (!url.ok) return res.status(500).send("failed to upload image");

return res.status(201).type("text/plain").send(url.value);
Expand Down
15 changes: 14 additions & 1 deletion web/api/image.ts
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
export { uploadImage } from "./internal/fetch-func";
import type { UserID } from "common/types";
import * as endpoints from "./internal/endpoints";
import { uploadImage as uploader } from "./internal/fetch-func";
export { MAX_IMAGE_SIZE } from "./internal/fetch-func";

export async function uploadAvatar(f: File) {
return await uploader(endpoints.profilePicture, f);
}

/** @throws if failed to send image. **/
export async function sendImageTo(u: UserID, f: File) {
console.log("sendImageTo");
await uploader(endpoints.sendPictureTo(u), f);
}
Loading
Loading