Files
sapiens-web3/server/storage.ts
kimjaehyeon0101 fb1d150554 Add AI-powered chatbot for media outlets
Integrate an AI chatbot feature allowing users to interact with media outlets, fetch chat history, and generate AI responses using OpenAI.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9a264234-c5d7-4dcc-adf3-a954b149b30d
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/9a264234-c5d7-4dcc-adf3-a954b149b30d/d35d7YU
2025-10-15 02:06:25 +00:00

570 lines
18 KiB
TypeScript

import {
users,
mediaOutlets,
articles,
predictionMarkets,
auctions,
bids,
mediaOutletRequests,
comments,
predictionBets,
communityPosts,
communityReplies,
chatMessages,
type User,
type UpsertUser,
type MediaOutlet,
type InsertMediaOutlet,
type Article,
type InsertArticle,
type PredictionMarket,
type InsertPredictionMarket,
type Auction,
type InsertAuction,
type Bid,
type InsertBid,
type MediaOutletRequest,
type InsertMediaOutletRequest,
type Comment,
type InsertComment,
type PredictionBet,
type InsertPredictionBet,
type CommunityPost,
type InsertCommunityPost,
type CommunityReply,
type InsertCommunityReply,
type ChatMessage,
type InsertChatMessage,
} from "@shared/schema";
import { db } from "./db";
import { eq, desc, asc, and, ilike, sql } from "drizzle-orm";
// Interface for storage operations
export interface IStorage {
// User operations (mandatory for Replit Auth)
getUser(id: string): Promise<User | undefined>;
upsertUser(user: UpsertUser): Promise<User>;
// Media outlet operations
getMediaOutlets(category?: string): Promise<MediaOutlet[]>;
getMediaOutletBySlug(slug: string): Promise<MediaOutlet | undefined>;
createMediaOutlet(outlet: InsertMediaOutlet): Promise<MediaOutlet>;
updateMediaOutlet(id: string, outlet: Partial<InsertMediaOutlet>): Promise<MediaOutlet>;
// Article operations
getArticles(): Promise<Article[]>;
getArticlesByOutlet(mediaOutletId: string): Promise<Article[]>;
getArticleBySlug(slug: string): Promise<Article | undefined>;
getArticleById(id: string): Promise<Article | undefined>;
createArticle(article: InsertArticle): Promise<Article>;
updateArticle(id: string, article: Partial<InsertArticle>): Promise<Article>;
deleteArticle(id: string): Promise<void>;
getFeaturedArticles(limit?: number): Promise<Article[]>;
// Prediction market operations
getPredictionMarkets(articleId?: string): Promise<PredictionMarket[]>;
getPredictionMarketById(id: string): Promise<PredictionMarket | undefined>;
createPredictionMarket(market: InsertPredictionMarket): Promise<PredictionMarket>;
// Auction operations
getActiveAuctions(): Promise<Auction[]>;
getAuctionById(id: string): Promise<Auction | undefined>;
getAuctionByMediaOutlet(mediaOutletId: string): Promise<Auction | undefined>;
createAuction(auction: InsertAuction): Promise<Auction>;
placeBid(bid: InsertBid): Promise<Bid>;
getBidsByAuctionId(auctionId: string): Promise<Bid[]>;
// Media outlet request operations
getMediaOutletRequests(status?: string): Promise<MediaOutletRequest[]>;
createMediaOutletRequest(request: InsertMediaOutletRequest): Promise<MediaOutletRequest>;
updateMediaOutletRequestStatus(id: string, status: string, reviewerId: string): Promise<MediaOutletRequest>;
// Prediction bet operations
createPredictionBet(bet: InsertPredictionBet): Promise<PredictionBet>;
getPredictionBetsByMarket(marketId: string): Promise<PredictionBet[]>;
getPredictionBetsByUser(userId: string): Promise<PredictionBet[]>;
// Comment operations
getCommentsByArticle(articleId: string): Promise<Comment[]>;
createComment(comment: InsertComment): Promise<Comment>;
// Community operations
getCommunityPostsByOutlet(mediaOutletId: string, sort?: string): Promise<CommunityPost[]>;
getCommunityPostById(id: string): Promise<CommunityPost | undefined>;
createCommunityPost(post: InsertCommunityPost): Promise<CommunityPost>;
updateCommunityPost(id: string, post: Partial<InsertCommunityPost>): Promise<CommunityPost>;
deleteCommunityPost(id: string): Promise<void>;
incrementPostViews(id: string): Promise<void>;
incrementPostLikes(id: string): Promise<void>;
getRepliesByPost(postId: string): Promise<CommunityReply[]>;
createCommunityReply(reply: InsertCommunityReply): Promise<CommunityReply>;
// Chatbot operations
getChatMessages(mediaOutletId: string, userId: string): Promise<ChatMessage[]>;
createChatMessage(message: InsertChatMessage): Promise<ChatMessage>;
// Analytics operations
getAnalytics(): Promise<{
totalArticles: number;
activePredictions: number;
liveAuctions: number;
totalRevenue: number;
}>;
// Search operations
search(query: string): Promise<{
outlets: MediaOutlet[];
articles: Article[];
}>;
}
export class DatabaseStorage implements IStorage {
// User operations (mandatory for Replit Auth)
async getUser(id: string): Promise<User | undefined> {
const [user] = await db.select().from(users).where(eq(users.id, id));
return user;
}
async upsertUser(userData: UpsertUser): Promise<User> {
const [user] = await db
.insert(users)
.values(userData)
.onConflictDoUpdate({
target: users.email,
set: {
firstName: userData.firstName,
lastName: userData.lastName,
profileImageUrl: userData.profileImageUrl,
updatedAt: new Date(),
},
})
.returning();
return user;
}
// Media outlet operations
async getMediaOutlets(category?: string): Promise<MediaOutlet[]> {
if (category) {
return await db.select().from(mediaOutlets).where(and(eq(mediaOutlets.isActive, true), eq(mediaOutlets.category, category)));
}
return await db.select().from(mediaOutlets).where(eq(mediaOutlets.isActive, true));
}
async getMediaOutletBySlug(slug: string): Promise<MediaOutlet | undefined> {
const [outlet] = await db.select().from(mediaOutlets).where(eq(mediaOutlets.slug, slug));
return outlet;
}
async createMediaOutlet(outlet: InsertMediaOutlet): Promise<MediaOutlet> {
const [newOutlet] = await db.insert(mediaOutlets).values(outlet).returning();
return newOutlet;
}
async updateMediaOutlet(id: string, outlet: Partial<InsertMediaOutlet>): Promise<MediaOutlet> {
const [updated] = await db
.update(mediaOutlets)
.set({ ...outlet, updatedAt: new Date() })
.where(eq(mediaOutlets.id, id))
.returning();
return updated;
}
// Article operations
async getArticles(): Promise<Article[]> {
return await db
.select()
.from(articles)
.orderBy(desc(articles.publishedAt));
}
async getArticlesByOutlet(mediaOutletId: string): Promise<Article[]> {
return await db
.select()
.from(articles)
.where(eq(articles.mediaOutletId, mediaOutletId))
.orderBy(asc(articles.publishedMinutesAgo), desc(articles.publishedAt));
}
async getArticleBySlug(slug: string): Promise<Article | undefined> {
const [article] = await db.select().from(articles).where(eq(articles.slug, slug));
return article;
}
async createArticle(article: InsertArticle): Promise<Article> {
const [newArticle] = await db.insert(articles).values(article).returning();
return newArticle;
}
async updateArticle(id: string, article: Partial<InsertArticle>): Promise<Article> {
const [updated] = await db
.update(articles)
.set({ ...article, updatedAt: new Date() })
.where(eq(articles.id, id))
.returning();
return updated;
}
async getFeaturedArticles(limit = 10): Promise<Article[]> {
return await db
.select()
.from(articles)
.where(eq(articles.isFeatured, true))
.orderBy(desc(articles.publishedAt))
.limit(limit);
}
async getArticleById(id: string): Promise<Article | undefined> {
const [article] = await db.select().from(articles).where(eq(articles.id, id));
return article;
}
async deleteArticle(id: string): Promise<void> {
await db.delete(articles).where(eq(articles.id, id));
}
// Prediction market operations
async getPredictionMarkets(articleId?: string): Promise<PredictionMarket[]> {
if (articleId) {
return await db.select().from(predictionMarkets).where(and(eq(predictionMarkets.isActive, true), eq(predictionMarkets.articleId, articleId)));
}
return await db.select().from(predictionMarkets).where(eq(predictionMarkets.isActive, true)).orderBy(desc(predictionMarkets.createdAt));
}
async getPredictionMarketById(id: string): Promise<PredictionMarket | undefined> {
const [market] = await db.select().from(predictionMarkets).where(eq(predictionMarkets.id, id));
return market;
}
async createPredictionMarket(market: InsertPredictionMarket): Promise<PredictionMarket> {
const [newMarket] = await db.insert(predictionMarkets).values(market).returning();
return newMarket;
}
// Auction operations
async getActiveAuctions(): Promise<Auction[]> {
return await db
.select()
.from(auctions)
.where(and(eq(auctions.isActive, true), sql`end_date > NOW()`))
.orderBy(auctions.endDate);
}
async getAuctionById(id: string): Promise<Auction | undefined> {
const [auction] = await db.select().from(auctions).where(eq(auctions.id, id));
return auction;
}
async getAuctionByMediaOutlet(mediaOutletId: string): Promise<Auction | undefined> {
const [auction] = await db.select().from(auctions).where(
and(eq(auctions.mediaOutletId, mediaOutletId), eq(auctions.isActive, true))
);
return auction;
}
async createAuction(auction: InsertAuction): Promise<Auction> {
const [newAuction] = await db.insert(auctions).values(auction).returning();
return newAuction;
}
async placeBid(bid: InsertBid): Promise<Bid> {
// First, get the auction to validate
const auction = await this.getAuctionById(bid.auctionId);
if (!auction) {
throw new Error("Auction not found");
}
// Validate auction status
if (!auction.isActive) {
throw new Error("Auction is not active");
}
// Validate auction end date
if (new Date() > auction.endDate) {
throw new Error("Auction has ended");
}
// Validate bid amount
const currentBidAmount = parseFloat(auction.currentBid || "0");
const bidAmount = parseFloat(bid.amount.toString());
if (bidAmount <= currentBidAmount) {
throw new Error(`Bid amount must be higher than current bid of ${currentBidAmount}`);
}
const [newBid] = await db.insert(bids).values(bid).returning();
// Update auction with highest bid
await db
.update(auctions)
.set({
currentBid: bid.amount,
highestBidderId: bid.bidderId,
updatedAt: new Date()
})
.where(eq(auctions.id, bid.auctionId));
return newBid;
}
async getBidsByAuctionId(auctionId: string): Promise<Bid[]> {
return await db
.select()
.from(bids)
.where(eq(bids.auctionId, auctionId))
.orderBy(desc(bids.createdAt));
}
// Media outlet request operations
async getMediaOutletRequests(status?: string): Promise<MediaOutletRequest[]> {
const query = db.select().from(mediaOutletRequests);
if (status) {
return await query.where(eq(mediaOutletRequests.status, status));
}
return await query.orderBy(desc(mediaOutletRequests.createdAt));
}
async createMediaOutletRequest(request: InsertMediaOutletRequest): Promise<MediaOutletRequest> {
const [newRequest] = await db.insert(mediaOutletRequests).values(request).returning();
return newRequest;
}
async updateMediaOutletRequestStatus(id: string, status: string, reviewerId: string): Promise<MediaOutletRequest> {
const [updated] = await db
.update(mediaOutletRequests)
.set({
status,
reviewedBy: reviewerId,
reviewedAt: new Date()
})
.where(eq(mediaOutletRequests.id, id))
.returning();
return updated;
}
// Comment operations
async getCommentsByArticle(articleId: string): Promise<Comment[]> {
return await db
.select()
.from(comments)
.where(eq(comments.articleId, articleId))
.orderBy(desc(comments.isPinned), desc(comments.createdAt));
}
async createComment(comment: InsertComment): Promise<Comment> {
const [newComment] = await db.insert(comments).values(comment).returning();
return newComment;
}
// Prediction bet operations
async createPredictionBet(bet: InsertPredictionBet): Promise<PredictionBet> {
// Get the current market to determine the price
const market = await this.getPredictionMarketById(bet.marketId);
if (!market) {
throw new Error("Prediction market not found");
}
// Use current market price for the bet
const price = bet.side === "yes" ? market.yesPrice : market.noPrice;
const [newBet] = await db.insert(predictionBets).values({
...bet,
price: price?.toString() || "0.5"
}).returning();
// Update market volume and bet count
await db
.update(predictionMarkets)
.set({
totalVolume: sql`total_volume + ${bet.amount}`,
totalBets: sql`total_bets + 1`,
updatedAt: new Date()
})
.where(eq(predictionMarkets.id, bet.marketId));
return newBet;
}
async getPredictionBetsByMarket(marketId: string): Promise<PredictionBet[]> {
return await db
.select()
.from(predictionBets)
.where(eq(predictionBets.marketId, marketId))
.orderBy(desc(predictionBets.createdAt));
}
async getPredictionBetsByUser(userId: string): Promise<PredictionBet[]> {
return await db
.select()
.from(predictionBets)
.where(eq(predictionBets.userId, userId))
.orderBy(desc(predictionBets.createdAt));
}
// Community operations
async getCommunityPostsByOutlet(mediaOutletId: string, sort: string = 'latest'): Promise<CommunityPost[]> {
const query = db
.select()
.from(communityPosts)
.where(eq(communityPosts.mediaOutletId, mediaOutletId));
if (sort === 'views') {
return await query.orderBy(desc(communityPosts.isPinned), desc(communityPosts.viewCount), desc(communityPosts.createdAt));
} else if (sort === 'likes') {
return await query.orderBy(desc(communityPosts.isPinned), desc(communityPosts.likeCount), desc(communityPosts.createdAt));
} else if (sort === 'replies') {
return await query.orderBy(desc(communityPosts.isPinned), desc(communityPosts.replyCount), desc(communityPosts.createdAt));
} else {
return await query.orderBy(desc(communityPosts.isPinned), desc(communityPosts.createdAt));
}
}
async getCommunityPostById(id: string): Promise<CommunityPost | undefined> {
const [post] = await db.select().from(communityPosts).where(eq(communityPosts.id, id));
return post;
}
async createCommunityPost(post: InsertCommunityPost): Promise<CommunityPost> {
const [newPost] = await db.insert(communityPosts).values(post).returning();
return newPost;
}
async updateCommunityPost(id: string, post: Partial<InsertCommunityPost>): Promise<CommunityPost> {
const [updated] = await db
.update(communityPosts)
.set({ ...post, updatedAt: new Date() })
.where(eq(communityPosts.id, id))
.returning();
return updated;
}
async deleteCommunityPost(id: string): Promise<void> {
await db.delete(communityPosts).where(eq(communityPosts.id, id));
}
async incrementPostViews(id: string): Promise<void> {
await db
.update(communityPosts)
.set({ viewCount: sql`view_count + 1` })
.where(eq(communityPosts.id, id));
}
async incrementPostLikes(id: string): Promise<void> {
await db
.update(communityPosts)
.set({ likeCount: sql`like_count + 1` })
.where(eq(communityPosts.id, id));
}
async getRepliesByPost(postId: string): Promise<CommunityReply[]> {
return await db
.select()
.from(communityReplies)
.where(eq(communityReplies.postId, postId))
.orderBy(asc(communityReplies.createdAt));
}
async createCommunityReply(reply: InsertCommunityReply): Promise<CommunityReply> {
const [newReply] = await db.insert(communityReplies).values(reply).returning();
// Increment reply count on the post
await db
.update(communityPosts)
.set({ replyCount: sql`reply_count + 1` })
.where(eq(communityPosts.id, reply.postId));
return newReply;
}
// Chatbot operations
async getChatMessages(mediaOutletId: string, userId: string): Promise<ChatMessage[]> {
return await db
.select()
.from(chatMessages)
.where(and(eq(chatMessages.mediaOutletId, mediaOutletId), eq(chatMessages.userId, userId)))
.orderBy(asc(chatMessages.createdAt));
}
async createChatMessage(message: InsertChatMessage): Promise<ChatMessage> {
const [newMessage] = await db.insert(chatMessages).values(message).returning();
return newMessage;
}
// Analytics operations
async getAnalytics(): Promise<{
totalArticles: number;
activePredictions: number;
liveAuctions: number;
totalRevenue: number;
}> {
const [articleCount] = await db
.select({ count: sql<number>`count(*)` })
.from(articles);
const [predictionCount] = await db
.select({ count: sql<number>`count(*)` })
.from(predictionMarkets)
.where(eq(predictionMarkets.isActive, true));
const [auctionCount] = await db
.select({ count: sql<number>`count(*)` })
.from(auctions)
.where(and(eq(auctions.isActive, true), sql`end_date > NOW()`));
const [revenueSum] = await db
.select({ sum: sql<number>`COALESCE(SUM(current_bid), 0)` })
.from(auctions);
return {
totalArticles: articleCount.count,
activePredictions: predictionCount.count,
liveAuctions: auctionCount.count,
totalRevenue: revenueSum.sum
};
}
// Search operations
async search(query: string): Promise<{
outlets: MediaOutlet[];
articles: Article[];
}> {
const searchTerm = query.toLowerCase();
// Search media outlets
const foundOutlets = await db
.select()
.from(mediaOutlets)
.where(eq(mediaOutlets.isActive, true))
.limit(50);
const filteredOutlets = foundOutlets.filter(outlet =>
outlet.name.toLowerCase().includes(searchTerm) ||
(outlet.description && outlet.description.toLowerCase().includes(searchTerm))
).slice(0, 10);
// Search articles
const foundArticles = await db
.select()
.from(articles)
.orderBy(desc(articles.publishedAt))
.limit(100);
const filteredArticles = foundArticles.filter(article =>
article.title.toLowerCase().includes(searchTerm) ||
(article.content && article.content.toLowerCase().includes(searchTerm)) ||
(article.excerpt && article.excerpt.toLowerCase().includes(searchTerm))
).slice(0, 20);
return {
outlets: filteredOutlets,
articles: filteredArticles
};
}
}
export const storage = new DatabaseStorage();