Skip to content

Commit 144d5ed

Browse files
committed
mostly done
1 parent 49575d3 commit 144d5ed

File tree

16 files changed

+293
-123
lines changed

16 files changed

+293
-123
lines changed

common/zod/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export const MessageSchema = z.object({
110110
creator: UserIDSchema,
111111
createdAt: z.date(),
112112
content: ContentSchema,
113+
isPicture: z.boolean(),
113114
edited: z.boolean(),
114115
});
115116

server/prisma/schema.prisma

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,17 @@ model User {
4141
receivingUsers Relationship[] @relation("receiving") // 自分にマッチリクエストを送ったユーザー
4242
}
4343

44-
// プロフィールの画像。
4544
model Avatar {
4645
guid String @id
4746
data Bytes
4847
}
4948

49+
model Picture {
50+
hash String @id
51+
data Bytes
52+
key String // password
53+
}
54+
5055
// enum Gender {
5156
// MALE
5257
// FEMALE
@@ -102,8 +107,6 @@ enum MatchingStatus {
102107
REJECTED
103108
}
104109

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

115118
model Message {
116119
id Int @id @default(autoincrement())
117-
creator Int // refers to UserId
120+
creator Int // refers to UserId
118121
createdAt DateTime @default(now()) // @readonly
119122
edited Boolean @default(false)
120123
content String
124+
isPicture Boolean // iff the message is a picture. if true, then content is a url of picture.
121125
relation Relationship? @relation(fields: [relationId], references: [id], onDelete: Cascade)
122126
relationId Int?
123127
sharedRoom SharedRoom? @relation(fields: [sharedRoomId], references: [id], onDelete: Cascade)

server/src/database/chat.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,13 @@ export async function getOverview(
7373
**/
7474
export async function sendDM(
7575
relation: RelationshipID,
76-
content: Omit<Message, "id">,
76+
content: Omit<Omit<Message, "id">, "isPicture">,
7777
): Promise<Result<Message>> {
7878
try {
7979
const message = await prisma.message.create({
8080
data: {
8181
relationId: relation,
82+
isPicture: false,
8283
...content,
8384
},
8485
});
@@ -87,6 +88,26 @@ export async function sendDM(
8788
return Err(e);
8889
}
8990
}
91+
/**
92+
this doesn't create the image. use uploadPic in database/picture.ts to create the image.
93+
**/
94+
export async function createImageMessage(
95+
sender: UserID,
96+
relation: RelationshipID,
97+
url: string,
98+
) {
99+
return prisma.message
100+
.create({
101+
data: {
102+
creator: sender,
103+
relationId: relation,
104+
content: url,
105+
isPicture: true,
106+
},
107+
})
108+
.then((val) => Ok(val))
109+
.catch((err) => Err(err));
110+
}
90111

91112
export async function createSharedRoom(
92113
room: InitRoom,

server/src/database/picture.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,47 @@ import { Err, Ok, type Result } from "../common/lib/result";
22
import type { GUID } from "../common/types";
33
import { prisma } from "./client";
44

5+
/**
6+
* @returns URL of the uploaded file.
7+
* @throws on database conn fail.
8+
**/
9+
export async function uploadPic(
10+
hash: string,
11+
buf: Buffer,
12+
passkey: string,
13+
): Promise<string> {
14+
await prisma.picture.upsert({
15+
where: { hash },
16+
create: { hash, data: buf, key: passkey },
17+
update: { data: buf, key: passkey },
18+
});
19+
const url = `/picture/${hash}?key=${passkey}`;
20+
return url;
21+
}
22+
23+
export async function getPic(hash: string, passkey: string) {
24+
return prisma.picture
25+
.findUnique({
26+
where: {
27+
hash,
28+
key: passkey,
29+
},
30+
})
31+
.then((val) => val?.data);
32+
}
33+
534
/**
635
* is safe to await.
736
* @returns URL of the file.
837
**/
9-
export async function set(guid: GUID, buf: Buffer): Promise<Result<string>> {
38+
export async function setProf(
39+
guid: GUID,
40+
buf: Buffer,
41+
): Promise<Result<string>> {
1042
return prisma.avatar
1143
.upsert({
1244
where: {
13-
guid: guid,
45+
guid,
1446
},
1547
create: { guid, data: buf },
1648
update: { data: buf },
@@ -27,7 +59,7 @@ export async function set(guid: GUID, buf: Buffer): Promise<Result<string>> {
2759
}
2860

2961
// is await-safe.
30-
export async function get(guid: GUID): Promise<Result<Buffer>> {
62+
export async function getProf(guid: GUID): Promise<Result<Buffer>> {
3163
return prisma.avatar
3264
.findUnique({
3365
where: { guid },

server/src/functions/chat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export async function sendDM(
4646
return http.forbidden("cannot send to non-friend");
4747

4848
// they are now MATCHED
49-
const msg: Omit<Message, "id"> = {
49+
const msg: Omit<Omit<Message, "id">, "isPicture"> = {
5050
creator: from,
5151
createdAt: new Date(),
5252
edited: false,

server/src/lib/hash.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import crypto from "node:crypto";
2+
3+
export function sha256(src: string): string {
4+
const hasher = crypto.createHash("sha256");
5+
hasher.update(src);
6+
return hasher.digest("hex");
7+
}

server/src/router/picture.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import bodyParser from "body-parser";
22
import express from "express";
3+
import { safeParseInt } from "../common/lib/result/safeParseInt";
4+
import * as chat from "../database/chat";
5+
import * as relation from "../database/matches";
36
import * as storage from "../database/picture";
7+
import { safeGetUserId } from "../firebase/auth/db";
48
import { safeGetGUID } from "../firebase/auth/lib";
59
import { compressImage } from "../functions/img/compress";
10+
import * as hashing from "../lib/hash";
611

712
const parseLargeBuffer = bodyParser.raw({
813
type: "image/png",
@@ -12,11 +17,59 @@ const router = express.Router();
1217

1318
/* General Pictures in chat */
1419

20+
router.post("/to/:userId", parseLargeBuffer, async (req, res) => {
21+
if (!Buffer.isBuffer(req.body)) return res.status(400).send("not buffer");
22+
const buf = req.body;
23+
24+
const sender = await safeGetUserId(req);
25+
if (!sender.ok) return res.status(401).end();
26+
const recv = safeParseInt(req.params.userId);
27+
if (!recv.ok) return res.status(400).end();
28+
29+
const rel = await relation.getRelation(sender.value, recv.value);
30+
if (!rel.ok) return res.status(401).send();
31+
if (rel.value.status !== "MATCHED") return res.status(401).send();
32+
33+
const hash = hashing.sha256(buf.toString("base64"));
34+
const passkey = hashing.sha256(crypto.randomUUID());
35+
36+
return storage
37+
.uploadPic(hash, buf, passkey)
38+
.then(async (url) => {
39+
await chat.createImageMessage(sender.value, rel.value.id, url);
40+
res.status(201).send(url).end();
41+
})
42+
.catch((err) => {
43+
console.log(err);
44+
res.status(500).send("Failed to upload image to database").end();
45+
});
46+
});
47+
48+
router.get("/:id", async (req, res) => {
49+
const hash = req.params.id;
50+
const key = req.query.key;
51+
if (!key) return res.status(400).send("key is required");
52+
53+
return storage
54+
.getPic(hash, String(key))
55+
.then((buf) => {
56+
if (buf) {
57+
res.status(200).send(buf).end();
58+
} else {
59+
res.status(404).send("not found").end();
60+
}
61+
})
62+
.catch((err) => {
63+
console.error(err);
64+
res.status(500).send("Failed to get image from database").end();
65+
});
66+
});
67+
1568
/* Profile Pictures */
1669

1770
router.get("/profile/:guid", async (req, res) => {
1871
const guid = req.params.guid;
19-
const result = await storage.get(guid);
72+
const result = await storage.getProf(guid);
2073
switch (result.ok) {
2174
case true:
2275
return res.send(result.value);
@@ -29,12 +82,12 @@ router.post("/profile", parseLargeBuffer, async (req, res) => {
2982
const guid = await safeGetGUID(req);
3083
if (!guid.ok) return res.status(401).send();
3184

32-
if (!Buffer.isBuffer(req.body)) return res.status(404).send("not buffer");
85+
if (!Buffer.isBuffer(req.body)) return res.status(400).send("not buffer");
3386

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

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

4093
return res.status(201).type("text/plain").send(url.value);

web/bun.lockb

342 Bytes
Binary file not shown.

web/src/api/image.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,13 @@
1-
export { uploadImage } from "./internal/fetch-func";
1+
import type { UserID } from "../common/types";
2+
import * as endpoints from "./internal/endpoints";
3+
import { uploadImage as uploader } from "./internal/fetch-func";
4+
export { MAX_IMAGE_SIZE } from "./internal/fetch-func";
5+
6+
export async function uploadAvatar(f: File) {
7+
return await uploader(endpoints.profilePicture, f);
8+
}
9+
10+
/** @throws if failed to send image. **/
11+
export async function sendImageTo(u: UserID, f: File) {
12+
await uploader(endpoints.sendPictureTo(u), f);
13+
}

web/src/api/internal/endpoints.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,11 @@ export const roomInvite = (roomId: ShareRoomID) =>
356356
export const message = (messageId: MessageID) =>
357357
`${origin}/chat/messages/id/${messageId}`;
358358

359+
/**
360+
* POST: send picture.
361+
*/
362+
export const sendPictureTo = (friendId: UserID) =>
363+
`${origin}/picture/to/${friendId}`;
359364
/**
360365
* GET: get profile picture of URL (this is usually hard-encoded in pictureURL so this variable is barely used)
361366
*/

0 commit comments

Comments
 (0)