Portal backend: Facebook OAuth, social features, portal comments
- Facebook Login OAuth (meta-web auth service + routes) - Account linking (merge Quest metaId + Facebook facebookId) - User profile updates (bio, isPublic, displayName) - Social endpoints: follow/unfollow, feed (trending/following/recent), likes - Portal comments via WebSocket (subscribe_portal, send_portal_comment) - Prisma migration: Follow, Like models, facebookId/bio/isPublic on User - Provider OAuth source=web redirect support for portal callbacks - Docker compose portal service, CORS multi-origin support
This commit is contained in:
@@ -73,8 +73,16 @@ async function getDecryptedToken(
|
||||
};
|
||||
}
|
||||
|
||||
interface PortalSubscriber {
|
||||
userId: string;
|
||||
displayName: string;
|
||||
avatarUrl: string | null;
|
||||
socket: WebSocket;
|
||||
}
|
||||
|
||||
export class ChatManager {
|
||||
private sessions = new Map<string, ChatSession>(); // key: `${userId}:${planId}`
|
||||
private portalSubscribers = new Map<string, Set<PortalSubscriber>>(); // key: planId
|
||||
private prisma: PrismaClient;
|
||||
private logger: FastifyBaseLogger;
|
||||
|
||||
@@ -184,6 +192,106 @@ export class ChatManager {
|
||||
});
|
||||
}
|
||||
|
||||
async subscribePortalChat(planId: string, userId: string, socket: WebSocket): Promise<void> {
|
||||
const user = await (this.prisma as any).user.findUnique({ where: { id: userId } });
|
||||
if (!user) return;
|
||||
|
||||
if (!this.portalSubscribers.has(planId)) {
|
||||
this.portalSubscribers.set(planId, new Set());
|
||||
}
|
||||
|
||||
// Remove existing subscription for this user+plan
|
||||
const subs = this.portalSubscribers.get(planId)!;
|
||||
for (const sub of subs) {
|
||||
if (sub.userId === userId) {
|
||||
subs.delete(sub);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
subs.add({
|
||||
userId,
|
||||
displayName: user.displayName,
|
||||
avatarUrl: user.avatarUrl,
|
||||
socket,
|
||||
});
|
||||
|
||||
this.logger.info({ planId, userId }, 'Portal chat subscribed');
|
||||
}
|
||||
|
||||
async handlePortalComment(planId: string, userId: string, text: string): Promise<void> {
|
||||
const user = await (this.prisma as any).user.findUnique({ where: { id: userId } });
|
||||
if (!user) return;
|
||||
|
||||
const message = {
|
||||
id: `portal-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`,
|
||||
authorName: user.displayName,
|
||||
authorImageUrl: user.avatarUrl,
|
||||
text,
|
||||
timestamp: Date.now(),
|
||||
isModerator: false,
|
||||
isBroadcaster: false,
|
||||
color: '#00BCD4',
|
||||
};
|
||||
|
||||
// Broadcast to all portal subscribers watching this plan
|
||||
const subs = this.portalSubscribers.get(planId);
|
||||
if (subs) {
|
||||
for (const sub of subs) {
|
||||
this.sendToSocket(sub.socket, {
|
||||
type: 'chat_message',
|
||||
planId,
|
||||
service: 'PORTAL',
|
||||
destinationId: 'portal',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Also send to the plan owner's chat session (so it shows up in their Android app)
|
||||
const plan = await (this.prisma as any).streamPlan.findUnique({ where: { id: planId } });
|
||||
if (plan) {
|
||||
const ownerSession = this.sessions.get(`${plan.userId}:${planId}`);
|
||||
if (ownerSession) {
|
||||
this.sendToSocket(ownerSession.socket, {
|
||||
type: 'chat_message',
|
||||
planId,
|
||||
service: 'PORTAL',
|
||||
destinationId: 'portal',
|
||||
message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleLike(planId: string, userId: string): Promise<void> {
|
||||
// Toggle like in DB
|
||||
const existing = await (this.prisma as any).like.findUnique({
|
||||
where: { userId_planId: { userId, planId } },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await (this.prisma as any).like.delete({ where: { id: existing.id } });
|
||||
} else {
|
||||
await (this.prisma as any).like.create({ data: { userId, planId } });
|
||||
}
|
||||
|
||||
const count = await (this.prisma as any).like.count({ where: { planId } });
|
||||
|
||||
// Broadcast like update to all portal subscribers
|
||||
const subs = this.portalSubscribers.get(planId);
|
||||
if (subs) {
|
||||
for (const sub of subs) {
|
||||
this.sendToSocket(sub.socket, {
|
||||
type: 'like_update',
|
||||
planId,
|
||||
count,
|
||||
isLiked: sub.userId === userId ? !existing : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async stopChat(planId: string, userId: string): Promise<void> {
|
||||
const sessionKey = `${userId}:${planId}`;
|
||||
const session = this.sessions.get(sessionKey);
|
||||
@@ -222,6 +330,18 @@ export class ChatManager {
|
||||
this.sessions.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up portal subscriptions for this socket
|
||||
for (const [planId, subs] of this.portalSubscribers) {
|
||||
for (const sub of subs) {
|
||||
if (sub.socket === socket) {
|
||||
subs.delete(sub);
|
||||
}
|
||||
}
|
||||
if (subs.size === 0) {
|
||||
this.portalSubscribers.delete(planId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async startYouTubeChat(session: ChatSession, dest: any): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user