add new chat logic and message service
This commit is contained in:
@@ -349,6 +349,43 @@ async function main() {
|
||||
],
|
||||
});
|
||||
|
||||
// Chat collections (global family messages + transient typing presence)
|
||||
ids.messages = await createCollection(token, {
|
||||
name: "messages",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
select("authorType", ["admin", "member"], true),
|
||||
text("authorId", true),
|
||||
text("authorName", true),
|
||||
text("authorColor"),
|
||||
text("content", true),
|
||||
],
|
||||
});
|
||||
|
||||
ids.chat_typing = await createCollection(token, {
|
||||
name: "chat_typing",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
rel("famId", ids.fams!, true),
|
||||
text("actorId", true),
|
||||
select("actorType", ["admin", "member"], true),
|
||||
text("authorName", true),
|
||||
text("authorColor"),
|
||||
bool("typing"),
|
||||
],
|
||||
});
|
||||
|
||||
console.log("\n✅ All collections created successfully");
|
||||
console.log("Collection IDs:", ids);
|
||||
}
|
||||
|
||||
@@ -2313,6 +2313,144 @@ app.post("/api/admin/:famId/debug/generate-data", requireAdmin, async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Chat ────────────────────────────────────────────────
|
||||
|
||||
// Resolve the acting user for a chat write. Admins authenticate via the
|
||||
// httpOnly `session` cookie (parsed server-side) or explicit session headers
|
||||
// (server-to-server); members via device token.
|
||||
async function resolveChatActor(c: any) {
|
||||
// Server-side admin (layout load forwards session headers).
|
||||
const hsFamId = c.req.header("x-session-famid");
|
||||
const hsUserId = c.req.header("x-session-userid");
|
||||
if (hsFamId && hsUserId) {
|
||||
const admins = await pb.getList(
|
||||
"fam_admins",
|
||||
`famId = '${hsFamId}' && userId = '${hsUserId}'`,
|
||||
);
|
||||
const admin = admins.items?.[0];
|
||||
if (admin) {
|
||||
return {
|
||||
famId: hsFamId,
|
||||
actor: {
|
||||
id: admin.id,
|
||||
type: "admin",
|
||||
name: admin.name,
|
||||
color: admin.color,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
const cookieHeader = c.req.header("cookie") || "";
|
||||
const cookieMatch = cookieHeader.match(/session=([^;]+)/);
|
||||
if (cookieMatch) {
|
||||
try {
|
||||
const s = JSON.parse(decodeURIComponent(cookieMatch[1]));
|
||||
if (s?.famId && s?.userId) {
|
||||
const admins = await pb.getList(
|
||||
"fam_admins",
|
||||
`famId = '${s.famId}' && userId = '${s.userId}'`,
|
||||
);
|
||||
const admin = admins.items?.[0];
|
||||
if (admin) {
|
||||
return {
|
||||
famId: s.famId,
|
||||
actor: {
|
||||
id: admin.id,
|
||||
type: "admin",
|
||||
name: admin.name,
|
||||
color: admin.color,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
const famId = c.req.header("x-device-famid");
|
||||
const deviceToken = c.req.header("x-device-token");
|
||||
if (famId && deviceToken) {
|
||||
const hash = crypto.createHash("sha256").update(deviceToken).digest("hex");
|
||||
const members = await pb.getList(
|
||||
"members",
|
||||
`famId = '${famId}' && deviceToken = '${hash}'`,
|
||||
);
|
||||
const member = members.items?.[0];
|
||||
if (member) {
|
||||
return {
|
||||
famId,
|
||||
actor: {
|
||||
id: member.id,
|
||||
type: "member",
|
||||
name: member.name,
|
||||
color: member.color,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
app.post("/api/chat/:famId/messages", async (c) => {
|
||||
try {
|
||||
const auth = await resolveChatActor(c);
|
||||
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
||||
const { content } = await c.req.json();
|
||||
if (!content || !content.trim()) {
|
||||
return c.json({ error: "content required" }, 400);
|
||||
}
|
||||
const record = await pb.create("messages", {
|
||||
famId: auth.famId,
|
||||
authorType: auth.actor.type,
|
||||
authorId: auth.actor.id,
|
||||
authorName: auth.actor.name,
|
||||
authorColor: auth.actor.color,
|
||||
content: content.trim(),
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
return c.json(record);
|
||||
} catch (err) {
|
||||
return handleError(c, err);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/chat/:famId/typing", async (c) => {
|
||||
try {
|
||||
const auth = await resolveChatActor(c);
|
||||
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
||||
const { typing } = await c.req.json();
|
||||
const existing = await pb.getList(
|
||||
"chat_typing",
|
||||
`famId = '${auth.famId}' && actorId = '${auth.actor.id}' && actorType = '${auth.actor.type}'`,
|
||||
);
|
||||
const row = {
|
||||
famId: auth.famId,
|
||||
actorId: auth.actor.id,
|
||||
actorType: auth.actor.type,
|
||||
authorName: auth.actor.name,
|
||||
authorColor: auth.actor.color,
|
||||
typing: !!typing,
|
||||
};
|
||||
if (existing.items?.length) {
|
||||
await pb.update("chat_typing", existing.items[0].id, row);
|
||||
} else {
|
||||
await pb.create("chat_typing", row);
|
||||
}
|
||||
return c.json({ ok: true });
|
||||
} catch (err) {
|
||||
return handleError(c, err);
|
||||
}
|
||||
});
|
||||
|
||||
// Current user's chat identity (name, color, actorId) + famId.
|
||||
app.get("/api/chat/me", async (c) => {
|
||||
try {
|
||||
const auth = await resolveChatActor(c);
|
||||
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
||||
return c.json({ famId: auth.famId, actor: auth.actor });
|
||||
} catch (err) {
|
||||
return handleError(c, err);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Start server ─────────────────────────────────────────
|
||||
|
||||
const port = parseInt(process.env.PROXY_PORT || "3456", 10);
|
||||
|
||||
@@ -1383,5 +1383,78 @@ export async function migrate(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
// ── 9. Create chat collections (messages + chat_typing) ──
|
||||
const famsColChat = await getCollection("fams");
|
||||
if (famsColChat) {
|
||||
// 9a. messages
|
||||
const msgsCol = await getCollection("messages");
|
||||
if (!msgsCol) {
|
||||
console.log("[migrate] Creating messages collection...");
|
||||
await createCollection({
|
||||
name: "messages",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
{
|
||||
name: "famId",
|
||||
type: "relation",
|
||||
required: true,
|
||||
collectionId: famsColChat.id,
|
||||
maxSelect: 1,
|
||||
cascadeDelete: true,
|
||||
},
|
||||
{ name: "authorType", type: "select", required: true, values: ["admin", "member"], maxSelect: 1 },
|
||||
{ name: "authorId", type: "text", required: true },
|
||||
{ name: "authorName", type: "text", required: true },
|
||||
{ name: "authorColor", type: "text", required: false },
|
||||
{ name: "content", type: "text", required: true },
|
||||
{ name: "createdAt", type: "date", required: false },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ messages already exists`);
|
||||
await updateCollection("messages", { listRule: "", viewRule: "" });
|
||||
}
|
||||
|
||||
// 9b. chat_typing (transient presence rows, one per actor)
|
||||
const typingCol = await getCollection("chat_typing");
|
||||
if (!typingCol) {
|
||||
console.log("[migrate] Creating chat_typing collection...");
|
||||
await createCollection({
|
||||
name: "chat_typing",
|
||||
type: "base",
|
||||
listRule: "",
|
||||
viewRule: "",
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
{
|
||||
name: "famId",
|
||||
type: "relation",
|
||||
required: true,
|
||||
collectionId: famsColChat.id,
|
||||
maxSelect: 1,
|
||||
cascadeDelete: true,
|
||||
},
|
||||
{ name: "actorId", type: "text", required: true },
|
||||
{ name: "actorType", type: "select", required: true, values: ["admin", "member"], maxSelect: 1 },
|
||||
{ name: "authorName", type: "text", required: true },
|
||||
{ name: "authorColor", type: "text", required: false },
|
||||
{ name: "typing", type: "bool", required: false },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
console.log(` ↳ chat_typing already exists`);
|
||||
await updateCollection("chat_typing", { listRule: "", viewRule: "" });
|
||||
}
|
||||
} else {
|
||||
console.log(` ↳ fams collection not found — chat collections deferred`);
|
||||
}
|
||||
|
||||
console.log("[migrate] Done");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user