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
570 lines
18 KiB
TypeScript
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();
|