Replace PostgreSQL and Docker deployment with embedded Turso, Drizzle relational queries v2, native systemd deployment on port 43821, and fresh SQLite migrations.
Hop-State: A_06FP5KEDBTB4A9ZT7QB498G Hop-Proposal: R_06FP5KDWMCKVVMQZT4MVZ28 Hop-Task: T_06FP5DZ7T0G45FG93PT90B8 Hop-Attempt: AT_06FP5DZ7T0PKQW99V27JCV8
This commit is contained in:
@@ -89,7 +89,7 @@ async function loadSessionsByTokens(tokens: string[]): Promise<SessionRecord[]>
|
||||
}
|
||||
|
||||
const sessionRecords = await db.query.sessions.findMany({
|
||||
where: inArray(sessions.token, uniqueTokens),
|
||||
where: { token: { in: uniqueTokens } },
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
@@ -296,7 +296,7 @@ export async function registerUser(
|
||||
|
||||
// Check if handle is taken
|
||||
const existingHandle = await db.query.users.findFirst({
|
||||
where: eq(users.handle, handle.toLowerCase()),
|
||||
where: { handle: handle.toLowerCase() },
|
||||
});
|
||||
|
||||
if (existingHandle) {
|
||||
@@ -305,7 +305,7 @@ export async function registerUser(
|
||||
|
||||
// Check if email is taken
|
||||
const existingEmail = await db.query.users.findFirst({
|
||||
where: eq(users.email, email.toLowerCase()),
|
||||
where: { email: email.toLowerCase() },
|
||||
});
|
||||
|
||||
if (existingEmail) {
|
||||
@@ -339,7 +339,7 @@ export async function registerUser(
|
||||
const did = generateDID(publicKey);
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
|
||||
// Generate avatar and upload to user's S3 storage
|
||||
const fullHandle = `${handle.toLowerCase()}@${nodeDomain}`;
|
||||
@@ -393,7 +393,7 @@ export async function authenticateUser(
|
||||
password: string
|
||||
): Promise<typeof users.$inferSelect> {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.email, email.toLowerCase()),
|
||||
where: { email: email.toLowerCase() },
|
||||
});
|
||||
|
||||
if (!user || !user.passwordHash) {
|
||||
|
||||
@@ -26,7 +26,7 @@ interface RemoteIdentity {
|
||||
export async function lookupRemoteKey(did: string): Promise<string> {
|
||||
// 1. Check Cache
|
||||
const cached = await db.query.remoteIdentityCache.findFirst({
|
||||
where: eq(remoteIdentityCache.did, did),
|
||||
where: { did: did },
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
|
||||
@@ -96,7 +96,7 @@ export async function verifyUserAction(signedAction: SignedAction): Promise<{
|
||||
|
||||
// 3. FETCH USER & KEY
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.did, payload.did),
|
||||
where: { did: payload.did },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -120,7 +120,7 @@ export function startBackgroundTasks(origin?: string) {
|
||||
isStarted = true;
|
||||
|
||||
// Default origin for remote sync (can be overridden)
|
||||
const syncOrigin = origin || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
|
||||
const syncOrigin = origin || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:43821';
|
||||
|
||||
log('STARTUP', 'Background task scheduler starting...');
|
||||
log('STARTUP', `Bot interval: ${BOT_INTERVAL_MS / 1000}s, Gossip interval: ${GOSSIP_INTERVAL_MS / 1000}s, Remote sync interval: ${REMOTE_SYNC_INTERVAL_MS / 1000}s`);
|
||||
|
||||
@@ -95,26 +95,19 @@ export async function getLogsForBot(
|
||||
botId: string,
|
||||
options: LogQueryOptions = {}
|
||||
): Promise<ActivityLog[]> {
|
||||
const conditions = [eq(botActivityLogs.botId, botId)];
|
||||
|
||||
// Filter by action types
|
||||
if (options.actionTypes && options.actionTypes.length > 0) {
|
||||
conditions.push(inArray(botActivityLogs.action, options.actionTypes));
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
if (options.startDate) {
|
||||
conditions.push(gte(botActivityLogs.createdAt, options.startDate));
|
||||
}
|
||||
|
||||
if (options.endDate) {
|
||||
conditions.push(lte(botActivityLogs.createdAt, options.endDate));
|
||||
}
|
||||
|
||||
// Build query
|
||||
let query = db.query.botActivityLogs.findMany({
|
||||
where: and(...conditions),
|
||||
orderBy: [desc(botActivityLogs.createdAt)], // Reverse chronological
|
||||
where: {
|
||||
botId,
|
||||
...(options.actionTypes?.length ? { action: { in: options.actionTypes } } : {}),
|
||||
...(options.startDate || options.endDate ? {
|
||||
createdAt: {
|
||||
...(options.startDate ? { gte: options.startDate } : {}),
|
||||
...(options.endDate ? { lte: options.endDate } : {}),
|
||||
},
|
||||
} : {}),
|
||||
},
|
||||
orderBy: () => [desc(botActivityLogs.createdAt)], // Reverse chronological
|
||||
limit: options.limit || 100,
|
||||
offset: options.offset || 0,
|
||||
});
|
||||
@@ -146,11 +139,8 @@ export async function getErrorLogs(
|
||||
limit: number = 50
|
||||
): Promise<ActivityLog[]> {
|
||||
const logs = await db.query.botActivityLogs.findMany({
|
||||
where: and(
|
||||
eq(botActivityLogs.botId, botId),
|
||||
eq(botActivityLogs.success, false)
|
||||
),
|
||||
orderBy: [desc(botActivityLogs.createdAt)],
|
||||
where: { AND: [{ botId: botId }, { success: false }] },
|
||||
orderBy: () => [desc(botActivityLogs.createdAt)],
|
||||
limit,
|
||||
});
|
||||
|
||||
|
||||
+11
-17
@@ -114,10 +114,10 @@ export const MIN_INTEREST_SCORE = 60;
|
||||
*/
|
||||
async function hasContentSources(botId: string): Promise<boolean> {
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
with: {
|
||||
contentSources: {
|
||||
where: (sources, { eq }) => eq(sources.isActive, true),
|
||||
where: { isActive: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -139,10 +139,10 @@ async function getUnprocessedContentItems(
|
||||
): Promise<ContentItem[]> {
|
||||
// Get bot's content sources
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
with: {
|
||||
contentSources: {
|
||||
where: (sources, { eq }) => eq(sources.isActive, true),
|
||||
where: { isActive: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -155,9 +155,7 @@ async function getUnprocessedContentItems(
|
||||
|
||||
// Get unprocessed content items from these sources
|
||||
const items = await db.query.botContentItems.findMany({
|
||||
where: and(
|
||||
eq(botContentItems.isProcessed, false)
|
||||
),
|
||||
where: { AND: [{ isProcessed: false }] },
|
||||
orderBy: (items, { desc }) => [desc(items.publishedAt)],
|
||||
limit,
|
||||
});
|
||||
@@ -284,7 +282,7 @@ export async function evaluateContentForPosting(
|
||||
): Promise<AutonomousPostEvaluation> {
|
||||
// Get bot with user relation
|
||||
const dbBot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
with: { user: true },
|
||||
});
|
||||
|
||||
@@ -373,7 +371,7 @@ export async function attemptAutonomousPost(
|
||||
|
||||
// Get bot with schedule config
|
||||
const dbBot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
with: { user: true },
|
||||
});
|
||||
|
||||
@@ -541,11 +539,7 @@ export async function processAllAutonomousBots(): Promise<Array<{
|
||||
}>> {
|
||||
// Get all active bots with autonomous mode enabled
|
||||
const autonomousBots = await db.query.bots.findMany({
|
||||
where: and(
|
||||
eq(bots.isActive, true),
|
||||
eq(bots.isSuspended, false),
|
||||
eq(bots.autonomousMode, true)
|
||||
),
|
||||
where: { AND: [{ isActive: true }, { isSuspended: false }, { autonomousMode: true }] },
|
||||
with: { user: true },
|
||||
});
|
||||
|
||||
@@ -590,7 +584,7 @@ export async function canPostAutonomously(botId: string): Promise<{
|
||||
}> {
|
||||
// Get bot
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
@@ -686,7 +680,7 @@ export async function getAutonomousPostingStats(botId: string): Promise<{
|
||||
}> {
|
||||
// Get bot's content sources
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
with: {
|
||||
contentSources: true,
|
||||
},
|
||||
@@ -706,7 +700,7 @@ export async function getAutonomousPostingStats(botId: string): Promise<{
|
||||
|
||||
// Get all content items for this bot's sources
|
||||
const allItems = await db.query.botContentItems.findMany({
|
||||
where: (items, { inArray }) => inArray(items.sourceId, sourceIds),
|
||||
where: { sourceId: { in: sourceIds } },
|
||||
});
|
||||
|
||||
const processedItems = allItems.filter(item => item.isProcessed);
|
||||
|
||||
+18
-21
@@ -396,7 +396,7 @@ export async function createBot(ownerId: string, config: BotCreateInput): Promis
|
||||
|
||||
// Check if handle is taken (in users table now)
|
||||
const existingUser = await db.query.users.findFirst({
|
||||
where: eq(users.handle, config.handle.toLowerCase()),
|
||||
where: { handle: config.handle.toLowerCase() },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
@@ -405,7 +405,7 @@ export async function createBot(ownerId: string, config: BotCreateInput): Promis
|
||||
|
||||
// Get owner to access their S3 storage for bot avatar
|
||||
const owner = await db.query.users.findFirst({
|
||||
where: eq(users.id, ownerId),
|
||||
where: { id: ownerId },
|
||||
});
|
||||
|
||||
if (!owner) {
|
||||
@@ -420,7 +420,7 @@ export async function createBot(ownerId: string, config: BotCreateInput): Promis
|
||||
const encryptedPrivateKey = encryptApiKey(privateKey);
|
||||
|
||||
// Generate a DID for the bot
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
const botDid = `did:web:${nodeDomain}:users:${config.handle.toLowerCase()}`;
|
||||
|
||||
// Generate bot avatar using owner's S3 storage if no avatar provided
|
||||
@@ -515,7 +515,7 @@ export async function updateBot(botId: string, config: BotUpdateInput): Promise<
|
||||
|
||||
// Check if bot exists and get its user account
|
||||
const existingBot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
});
|
||||
|
||||
if (!existingBot) {
|
||||
@@ -524,7 +524,7 @@ export async function updateBot(botId: string, config: BotUpdateInput): Promise<
|
||||
|
||||
// Get the bot's user account
|
||||
const botUser = await db.query.users.findFirst({
|
||||
where: eq(users.id, existingBot.userId),
|
||||
where: { id: existingBot.userId },
|
||||
});
|
||||
|
||||
if (!botUser) {
|
||||
@@ -629,7 +629,7 @@ export async function updateBot(botId: string, config: BotUpdateInput): Promise<
|
||||
|
||||
// Get updated user
|
||||
const updatedUser = await db.query.users.findFirst({
|
||||
where: eq(users.id, existingBot.userId),
|
||||
where: { id: existingBot.userId },
|
||||
});
|
||||
|
||||
return dbBotToBot(updatedBot, updatedUser!);
|
||||
@@ -647,7 +647,7 @@ export async function updateBot(botId: string, config: BotUpdateInput): Promise<
|
||||
export async function deleteBot(botId: string): Promise<void> {
|
||||
// Check if bot exists
|
||||
const existingBot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
});
|
||||
|
||||
if (!existingBot) {
|
||||
@@ -671,14 +671,14 @@ export async function deleteBot(botId: string): Promise<void> {
|
||||
*/
|
||||
export async function getBotsByUser(ownerId: string): Promise<Bot[]> {
|
||||
const userBots = await db.query.bots.findMany({
|
||||
where: eq(bots.ownerId, ownerId),
|
||||
where: { ownerId: ownerId },
|
||||
orderBy: (bots, { desc }) => [desc(bots.createdAt)],
|
||||
});
|
||||
|
||||
// Get all bot user accounts
|
||||
const botUserIds = userBots.map(b => b.userId);
|
||||
const botUsers = await db.query.users.findMany({
|
||||
where: (users, { inArray }) => inArray(users.id, botUserIds),
|
||||
where: { id: { in: botUserIds } },
|
||||
});
|
||||
|
||||
const userMap = new Map(botUsers.map(u => [u.id, u]));
|
||||
@@ -702,7 +702,7 @@ export async function getBotsByUser(ownerId: string): Promise<Bot[]> {
|
||||
*/
|
||||
export async function getBotById(botId: string): Promise<Bot | null> {
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
@@ -711,7 +711,7 @@ export async function getBotById(botId: string): Promise<Bot | null> {
|
||||
|
||||
// Get the bot's user account
|
||||
const botUser = await db.query.users.findFirst({
|
||||
where: eq(users.id, bot.userId),
|
||||
where: { id: bot.userId },
|
||||
});
|
||||
|
||||
if (!botUser) {
|
||||
@@ -730,10 +730,7 @@ export async function getBotById(botId: string): Promise<Bot | null> {
|
||||
export async function getBotByHandle(handle: string): Promise<Bot | null> {
|
||||
// Find the user with this handle that is a bot
|
||||
const botUser = await db.query.users.findFirst({
|
||||
where: and(
|
||||
eq(users.handle, handle.toLowerCase()),
|
||||
eq(users.isBot, true)
|
||||
),
|
||||
where: { AND: [{ handle: handle.toLowerCase() }, { isBot: true }] },
|
||||
});
|
||||
|
||||
if (!botUser) {
|
||||
@@ -742,7 +739,7 @@ export async function getBotByHandle(handle: string): Promise<Bot | null> {
|
||||
|
||||
// Find the bot config for this user
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.userId, botUser.id),
|
||||
where: { userId: botUser.id },
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
@@ -789,7 +786,7 @@ export async function canUserCreateBot(ownerId: string): Promise<boolean> {
|
||||
*/
|
||||
export async function userOwnsBot(ownerId: string, botId: string): Promise<boolean> {
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: and(eq(bots.id, botId), eq(bots.ownerId, ownerId)),
|
||||
where: { AND: [{ id: botId }, { ownerId: ownerId }] },
|
||||
});
|
||||
|
||||
return bot !== undefined;
|
||||
@@ -836,7 +833,7 @@ export async function setApiKey(
|
||||
): Promise<void> {
|
||||
// Check if bot exists
|
||||
const existingBot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
});
|
||||
|
||||
if (!existingBot) {
|
||||
@@ -885,7 +882,7 @@ export async function setApiKey(
|
||||
export async function removeApiKey(botId: string): Promise<void> {
|
||||
// Check if bot exists
|
||||
const existingBot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
});
|
||||
|
||||
if (!existingBot) {
|
||||
@@ -930,7 +927,7 @@ export interface ApiKeyStatus {
|
||||
export async function getApiKeyStatus(botId: string): Promise<ApiKeyStatus> {
|
||||
// Get the bot with encrypted API key
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
@@ -984,7 +981,7 @@ export async function botHasApiKey(botId: string): Promise<boolean> {
|
||||
export async function getDecryptedApiKey(botId: string): Promise<string | null> {
|
||||
// Get the bot with encrypted API key
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
|
||||
@@ -562,10 +562,7 @@ export async function contentItemExists(
|
||||
externalId: string
|
||||
): Promise<boolean> {
|
||||
const existing = await db.query.botContentItems.findFirst({
|
||||
where: and(
|
||||
eq(botContentItems.sourceId, sourceId),
|
||||
eq(botContentItems.externalId, externalId)
|
||||
),
|
||||
where: { AND: [{ sourceId: sourceId }, { externalId: externalId }] },
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
@@ -684,7 +681,7 @@ export async function recordFetchError(
|
||||
): Promise<void> {
|
||||
// Get current consecutive errors
|
||||
const source = await db.query.botContentSources.findFirst({
|
||||
where: eq(botContentSources.id, sourceId),
|
||||
where: { id: sourceId },
|
||||
columns: { consecutiveErrors: true },
|
||||
});
|
||||
|
||||
@@ -768,7 +765,7 @@ export async function fetchContent(
|
||||
case 'news_api':
|
||||
// Decrypt API key
|
||||
const dbSource = await db.query.botContentSources.findFirst({
|
||||
where: eq(botContentSources.id, sourceId),
|
||||
where: { id: sourceId },
|
||||
columns: { apiKeyEncrypted: true },
|
||||
});
|
||||
|
||||
@@ -785,7 +782,7 @@ export async function fetchContent(
|
||||
case 'brave_news':
|
||||
// Decrypt API key
|
||||
const braveDbSource = await db.query.botContentSources.findFirst({
|
||||
where: eq(botContentSources.id, sourceId),
|
||||
where: { id: sourceId },
|
||||
columns: { apiKeyEncrypted: true, sourceConfig: true },
|
||||
});
|
||||
|
||||
@@ -945,10 +942,7 @@ export async function fetchAllSourcesForBot(
|
||||
options: FetchOptions = {}
|
||||
): Promise<FetchResult[]> {
|
||||
const sources = await db.query.botContentSources.findMany({
|
||||
where: and(
|
||||
eq(botContentSources.botId, botId),
|
||||
eq(botContentSources.isActive, true)
|
||||
),
|
||||
where: { AND: [{ botId: botId }, { isActive: true }] },
|
||||
});
|
||||
|
||||
const results: FetchResult[] = [];
|
||||
@@ -994,10 +988,7 @@ export async function getUnprocessedItems(
|
||||
limit: number = 10
|
||||
): Promise<StoredContentItem[]> {
|
||||
const items = await db.query.botContentItems.findMany({
|
||||
where: and(
|
||||
eq(botContentItems.sourceId, sourceId),
|
||||
eq(botContentItems.isProcessed, false)
|
||||
),
|
||||
where: { AND: [{ sourceId: sourceId }, { isProcessed: false }] },
|
||||
orderBy: (items, { asc }) => [asc(items.publishedAt)],
|
||||
limit,
|
||||
});
|
||||
|
||||
@@ -592,7 +592,7 @@ export async function addSource(
|
||||
|
||||
// Check if bot exists
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
@@ -646,7 +646,7 @@ export async function addSource(
|
||||
export async function removeSource(sourceId: string): Promise<void> {
|
||||
// Check if source exists
|
||||
const existingSource = await db.query.botContentSources.findFirst({
|
||||
where: eq(botContentSources.id, sourceId),
|
||||
where: { id: sourceId },
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
@@ -666,7 +666,7 @@ export async function removeSource(sourceId: string): Promise<void> {
|
||||
*/
|
||||
export async function getSourceById(sourceId: string): Promise<ContentSource | null> {
|
||||
const source = await db.query.botContentSources.findFirst({
|
||||
where: eq(botContentSources.id, sourceId),
|
||||
where: { id: sourceId },
|
||||
});
|
||||
|
||||
if (!source) {
|
||||
@@ -686,7 +686,7 @@ export async function getSourceById(sourceId: string): Promise<ContentSource | n
|
||||
*/
|
||||
export async function getSourcesByBot(botId: string): Promise<ContentSource[]> {
|
||||
const sources = await db.query.botContentSources.findMany({
|
||||
where: eq(botContentSources.botId, botId),
|
||||
where: { botId: botId },
|
||||
orderBy: (sources, { desc }) => [desc(sources.createdAt)],
|
||||
});
|
||||
|
||||
@@ -701,10 +701,7 @@ export async function getSourcesByBot(botId: string): Promise<ContentSource[]> {
|
||||
*/
|
||||
export async function getActiveSourcesByBot(botId: string): Promise<ContentSource[]> {
|
||||
const sources = await db.query.botContentSources.findMany({
|
||||
where: and(
|
||||
eq(botContentSources.botId, botId),
|
||||
eq(botContentSources.isActive, true)
|
||||
),
|
||||
where: { AND: [{ botId: botId }, { isActive: true }] },
|
||||
orderBy: (sources, { desc }) => [desc(sources.createdAt)],
|
||||
});
|
||||
|
||||
@@ -728,7 +725,7 @@ export async function updateSource(
|
||||
): Promise<ContentSource> {
|
||||
// Check if source exists
|
||||
const existingSource = await db.query.botContentSources.findFirst({
|
||||
where: eq(botContentSources.id, sourceId),
|
||||
where: { id: sourceId },
|
||||
});
|
||||
|
||||
if (!existingSource) {
|
||||
@@ -811,10 +808,7 @@ export async function deactivateSource(sourceId: string): Promise<void> {
|
||||
*/
|
||||
export async function botOwnsSource(botId: string, sourceId: string): Promise<boolean> {
|
||||
const source = await db.query.botContentSources.findFirst({
|
||||
where: and(
|
||||
eq(botContentSources.id, sourceId),
|
||||
eq(botContentSources.botId, botId)
|
||||
),
|
||||
where: { AND: [{ id: sourceId }, { botId: botId }] },
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
@@ -829,7 +823,7 @@ export async function botOwnsSource(botId: string, sourceId: string): Promise<bo
|
||||
*/
|
||||
export async function getSourceCountForBot(botId: string): Promise<number> {
|
||||
const sources = await db.query.botContentSources.findMany({
|
||||
where: eq(botContentSources.botId, botId),
|
||||
where: { botId: botId },
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
@@ -850,10 +844,7 @@ export async function getSourcesByType(
|
||||
type: ContentSourceType
|
||||
): Promise<ContentSource[]> {
|
||||
const sources = await db.query.botContentSources.findMany({
|
||||
where: and(
|
||||
eq(botContentSources.botId, botId),
|
||||
eq(botContentSources.type, type)
|
||||
),
|
||||
where: { AND: [{ botId: botId }, { type: type }] },
|
||||
orderBy: (sources, { desc }) => [desc(sources.createdAt)],
|
||||
});
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ export async function detectMentions(botId: string): Promise<MentionDetectionRes
|
||||
try {
|
||||
// Get the bot's handle
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
with: {
|
||||
user: {
|
||||
columns: { handle: true },
|
||||
@@ -124,7 +124,7 @@ export async function detectMentions(botId: string): Promise<MentionDetectionRes
|
||||
|
||||
// Get existing mention post IDs to avoid duplicates
|
||||
const existingMentions = await db.query.botMentions.findMany({
|
||||
where: eq(botMentions.botId, botId),
|
||||
where: { botId: botId },
|
||||
columns: { postId: true },
|
||||
});
|
||||
|
||||
@@ -140,9 +140,7 @@ export async function detectMentions(botId: string): Promise<MentionDetectionRes
|
||||
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
|
||||
|
||||
const recentPosts = await db.query.posts.findMany({
|
||||
where: and(
|
||||
eq(posts.isRemoved, false)
|
||||
),
|
||||
where: { AND: [{ isRemoved: false }] },
|
||||
with: {
|
||||
author: {
|
||||
columns: {
|
||||
@@ -152,7 +150,7 @@ export async function detectMentions(botId: string): Promise<MentionDetectionRes
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [desc(posts.createdAt)],
|
||||
orderBy: () => [desc(posts.createdAt)],
|
||||
limit: 1000, // Reasonable limit for scanning
|
||||
});
|
||||
|
||||
@@ -218,11 +216,8 @@ export async function detectMentions(botId: string): Promise<MentionDetectionRes
|
||||
export async function getUnprocessedMentions(botId: string): Promise<Mention[]> {
|
||||
try {
|
||||
const mentions = await db.query.botMentions.findMany({
|
||||
where: and(
|
||||
eq(botMentions.botId, botId),
|
||||
eq(botMentions.isProcessed, false)
|
||||
),
|
||||
orderBy: [asc(botMentions.createdAt)], // Chronological order (oldest first)
|
||||
where: { AND: [{ botId: botId }, { isProcessed: false }] },
|
||||
orderBy: () => [asc(botMentions.createdAt)], // Chronological order (oldest first)
|
||||
});
|
||||
|
||||
return mentions.map(m => ({
|
||||
@@ -256,8 +251,8 @@ export async function getUnprocessedMentions(botId: string): Promise<Mention[]>
|
||||
export async function getAllMentions(botId: string): Promise<Mention[]> {
|
||||
try {
|
||||
const mentions = await db.query.botMentions.findMany({
|
||||
where: eq(botMentions.botId, botId),
|
||||
orderBy: [desc(botMentions.createdAt)],
|
||||
where: { botId: botId },
|
||||
orderBy: () => [desc(botMentions.createdAt)],
|
||||
});
|
||||
|
||||
return mentions.map(m => ({
|
||||
@@ -307,7 +302,7 @@ export async function getConversationContext(
|
||||
|
||||
while (currentPostId && depth < maxDepth) {
|
||||
const post: any = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, currentPostId),
|
||||
where: { id: currentPostId },
|
||||
with: {
|
||||
author: {
|
||||
columns: {
|
||||
@@ -365,7 +360,7 @@ export async function processMention(mentionId: string): Promise<MentionResponse
|
||||
try {
|
||||
// Get the mention
|
||||
const mention = await db.query.botMentions.findFirst({
|
||||
where: eq(botMentions.id, mentionId),
|
||||
where: { id: mentionId },
|
||||
});
|
||||
|
||||
if (!mention) {
|
||||
@@ -394,7 +389,7 @@ export async function processMention(mentionId: string): Promise<MentionResponse
|
||||
|
||||
// Get the bot
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, mention.botId),
|
||||
where: { id: mention.botId },
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
@@ -414,7 +409,7 @@ export async function processMention(mentionId: string): Promise<MentionResponse
|
||||
|
||||
// Get the mentioning post with author info
|
||||
const mentionPost = await db.query.posts.findFirst({
|
||||
where: eq(posts.id, mention.postId),
|
||||
where: { id: mention.postId },
|
||||
with: {
|
||||
author: {
|
||||
columns: {
|
||||
@@ -607,7 +602,7 @@ export async function storeMention(data: {
|
||||
export async function getMentionById(mentionId: string): Promise<Mention | null> {
|
||||
try {
|
||||
const mention = await db.query.botMentions.findFirst({
|
||||
where: eq(botMentions.id, mentionId),
|
||||
where: { id: mentionId },
|
||||
});
|
||||
|
||||
if (!mention) {
|
||||
|
||||
@@ -335,7 +335,7 @@ export function deserializePersonalityConfig(json: string): PersonalityConfig {
|
||||
*/
|
||||
export async function getPersonalityConfig(botId: string): Promise<PersonalityConfig | null> {
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
columns: {
|
||||
personalityConfig: true,
|
||||
},
|
||||
@@ -372,7 +372,7 @@ export async function updatePersonalityConfig(
|
||||
|
||||
// Check if bot exists
|
||||
const existingBot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
|
||||
+20
-26
@@ -169,7 +169,7 @@ function getDecryptedApiKeyForBot(bot: typeof bots.$inferSelect): string {
|
||||
*/
|
||||
async function getContentItemById(contentItemId: string): Promise<ContentItem | null> {
|
||||
const item = await db.query.botContentItems.findFirst({
|
||||
where: eq(botContentItems.id, contentItemId),
|
||||
where: { id: contentItemId },
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
@@ -197,10 +197,10 @@ async function getContentItemById(contentItemId: string): Promise<ContentItem |
|
||||
async function getNextUnprocessedContentItem(botId: string): Promise<ContentItem | null> {
|
||||
// Get bot's content sources
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
with: {
|
||||
contentSources: {
|
||||
where: (sources, { eq }) => eq(sources.isActive, true),
|
||||
where: { isActive: true },
|
||||
},
|
||||
user: true,
|
||||
},
|
||||
@@ -214,7 +214,7 @@ async function getNextUnprocessedContentItem(botId: string): Promise<ContentItem
|
||||
|
||||
// Get ALL posts by this bot to avoid duplicates (no time limit)
|
||||
const allBotPosts = await db.query.posts.findMany({
|
||||
where: eq(posts.userId, bot.userId),
|
||||
where: { userId: bot.userId },
|
||||
columns: {
|
||||
linkPreviewUrl: true,
|
||||
},
|
||||
@@ -230,10 +230,7 @@ async function getNextUnprocessedContentItem(botId: string): Promise<ContentItem
|
||||
|
||||
// Get unprocessed content items from this bot's sources
|
||||
const items = await db.query.botContentItems.findMany({
|
||||
where: and(
|
||||
eq(botContentItems.isProcessed, false),
|
||||
inArray(botContentItems.sourceId, sourceIds)
|
||||
),
|
||||
where: { AND: [{ isProcessed: false }, { sourceId: { in: sourceIds } }] },
|
||||
orderBy: (items, { asc }) => [asc(items.publishedAt)],
|
||||
limit: 100, // Get more items to have options after filtering
|
||||
});
|
||||
@@ -278,7 +275,7 @@ async function getNextUnprocessedContentItem(botId: string): Promise<ContentItem
|
||||
async function getBotPreviousPosts(botId: string, limit: number = 40): Promise<string[]> {
|
||||
// Get bot to find its user ID
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
@@ -287,7 +284,7 @@ async function getBotPreviousPosts(botId: string, limit: number = 40): Promise<s
|
||||
|
||||
// Get the bot's recent posts
|
||||
const recentPosts = await db.query.posts.findMany({
|
||||
where: eq(posts.userId, bot.userId),
|
||||
where: { userId: bot.userId },
|
||||
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
|
||||
limit,
|
||||
columns: {
|
||||
@@ -512,7 +509,7 @@ export async function selectContentForPosting(
|
||||
|
||||
// Verify the content belongs to this bot's sources
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
with: {
|
||||
contentSources: true,
|
||||
user: true,
|
||||
@@ -691,7 +688,7 @@ async function resolveContentItemSourceUrl(contentItem?: ContentItem | null): Pr
|
||||
}
|
||||
|
||||
const source = await db.query.botContentSources.findFirst({
|
||||
where: eq(botContentSources.id, contentItem.sourceId),
|
||||
where: { id: contentItem.sourceId },
|
||||
columns: {
|
||||
type: true,
|
||||
subreddit: true,
|
||||
@@ -726,7 +723,7 @@ async function createPostInDatabase(
|
||||
): Promise<typeof posts.$inferSelect> {
|
||||
// Get bot config
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
@@ -738,7 +735,7 @@ async function createPostInDatabase(
|
||||
|
||||
// Get the bot's own user account (not the owner)
|
||||
const botUser = await db.query.users.findFirst({
|
||||
where: eq(users.id, bot.userId),
|
||||
where: { id: bot.userId },
|
||||
});
|
||||
|
||||
if (!botUser) {
|
||||
@@ -755,7 +752,7 @@ async function createPostInDatabase(
|
||||
linkPreview = await fetchLinkPreview(sourceUrl);
|
||||
}
|
||||
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
const postUuid = crypto.randomUUID();
|
||||
|
||||
try {
|
||||
@@ -807,7 +804,7 @@ async function federatePost(
|
||||
try {
|
||||
// Get bot config
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
@@ -817,7 +814,7 @@ async function federatePost(
|
||||
|
||||
// Get the bot's own user account
|
||||
const botUser = await db.query.users.findFirst({
|
||||
where: eq(users.id, bot.userId),
|
||||
where: { id: bot.userId },
|
||||
});
|
||||
|
||||
if (!botUser) {
|
||||
@@ -825,7 +822,7 @@ async function federatePost(
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
|
||||
// Use swarm delivery for bot posts
|
||||
const { deliverPostToSwarmFollowers } = await import('@/lib/swarm/interactions');
|
||||
@@ -993,7 +990,7 @@ export async function triggerPost(
|
||||
|
||||
// Get bot from database for generation
|
||||
const dbBot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
with: { user: true }, // Required for generatePostContent
|
||||
});
|
||||
|
||||
@@ -1142,7 +1139,7 @@ export async function getPostingStats(botId: string): Promise<{
|
||||
}> {
|
||||
// Get bot
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
with: {
|
||||
contentSources: true,
|
||||
},
|
||||
@@ -1160,7 +1157,7 @@ export async function getPostingStats(botId: string): Promise<{
|
||||
|
||||
// Get user's post count
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.id, bot.userId),
|
||||
where: { id: bot.userId },
|
||||
});
|
||||
|
||||
const totalPosts = user?.postsCount || 0;
|
||||
@@ -1170,10 +1167,7 @@ export async function getPostingStats(botId: string): Promise<{
|
||||
today.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
const postsToday = await db.query.posts.findMany({
|
||||
where: and(
|
||||
eq(posts.userId, bot.userId),
|
||||
// Note: This is a simplified query. In production, you'd want to use a proper date comparison
|
||||
),
|
||||
where: { AND: [{ userId: bot.userId }] },
|
||||
});
|
||||
|
||||
// Get content item stats
|
||||
@@ -1184,7 +1178,7 @@ export async function getPostingStats(botId: string): Promise<{
|
||||
|
||||
if (sourceIds.length > 0) {
|
||||
const allItems = await db.query.botContentItems.findMany({
|
||||
where: (items, { inArray }) => inArray(items.sourceId, sourceIds),
|
||||
where: { sourceId: { in: sourceIds } },
|
||||
});
|
||||
|
||||
contentItemsProcessed = allItems.filter(item => item.isProcessed).length;
|
||||
|
||||
@@ -129,11 +129,7 @@ async function getOrCreateWindow(
|
||||
): Promise<typeof botRateLimits.$inferSelect> {
|
||||
// Try to find existing window
|
||||
const existing = await db.query.botRateLimits.findFirst({
|
||||
where: and(
|
||||
eq(botRateLimits.botId, botId),
|
||||
eq(botRateLimits.windowType, windowType),
|
||||
eq(botRateLimits.windowStart, windowStart)
|
||||
),
|
||||
where: { AND: [{ botId }, { windowType }, { windowStart: { eq: windowStart } }] },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
@@ -158,11 +154,7 @@ async function getOrCreateWindow(
|
||||
async function getDailyPostCount(botId: string): Promise<number> {
|
||||
const windowStart = getDailyWindowStart();
|
||||
const window = await db.query.botRateLimits.findFirst({
|
||||
where: and(
|
||||
eq(botRateLimits.botId, botId),
|
||||
eq(botRateLimits.windowType, 'daily'),
|
||||
eq(botRateLimits.windowStart, windowStart)
|
||||
),
|
||||
where: { AND: [{ botId }, { windowType: 'daily' }, { windowStart: { eq: windowStart } }] },
|
||||
});
|
||||
|
||||
return window?.postCount ?? 0;
|
||||
@@ -174,11 +166,7 @@ async function getDailyPostCount(botId: string): Promise<number> {
|
||||
async function getHourlyReplyCount(botId: string): Promise<number> {
|
||||
const windowStart = getHourlyWindowStart();
|
||||
const window = await db.query.botRateLimits.findFirst({
|
||||
where: and(
|
||||
eq(botRateLimits.botId, botId),
|
||||
eq(botRateLimits.windowType, 'hourly'),
|
||||
eq(botRateLimits.windowStart, windowStart)
|
||||
),
|
||||
where: { AND: [{ botId }, { windowType: 'hourly' }, { windowStart: { eq: windowStart } }] },
|
||||
});
|
||||
|
||||
return window?.replyCount ?? 0;
|
||||
@@ -189,7 +177,7 @@ async function getHourlyReplyCount(botId: string): Promise<number> {
|
||||
*/
|
||||
async function getLastPostAt(botId: string): Promise<Date | null> {
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
columns: { lastPostAt: true },
|
||||
});
|
||||
|
||||
@@ -368,11 +356,7 @@ export async function getPostCount(botId: string, windowHours: number): Promise<
|
||||
|
||||
// Get all daily windows that overlap with the requested time range
|
||||
const windows = await db.query.botRateLimits.findMany({
|
||||
where: and(
|
||||
eq(botRateLimits.botId, botId),
|
||||
eq(botRateLimits.windowType, 'daily'),
|
||||
gte(botRateLimits.windowStart, getDailyWindowStart(windowStart))
|
||||
),
|
||||
where: { AND: [{ botId: botId }, { windowType: 'daily' }, { windowStart: { gte: getDailyWindowStart(windowStart) } }] },
|
||||
});
|
||||
|
||||
return windows.reduce((sum, w) => sum + w.postCount, 0);
|
||||
|
||||
@@ -679,10 +679,7 @@ export function isDue(
|
||||
export async function hasUnprocessedContent(botId: string): Promise<boolean> {
|
||||
// Get all content sources for the bot
|
||||
const sources = await db.query.botContentSources.findMany({
|
||||
where: and(
|
||||
eq(botContentSources.botId, botId),
|
||||
eq(botContentSources.isActive, true)
|
||||
),
|
||||
where: { AND: [{ botId: botId }, { isActive: true }] },
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
@@ -693,10 +690,7 @@ export async function hasUnprocessedContent(botId: string): Promise<boolean> {
|
||||
// Check if any source has unprocessed content
|
||||
for (const source of sources) {
|
||||
const unprocessedItem = await db.query.botContentItems.findFirst({
|
||||
where: and(
|
||||
eq(botContentItems.sourceId, source.id),
|
||||
eq(botContentItems.isProcessed, false)
|
||||
),
|
||||
where: { AND: [{ sourceId: source.id }, { isProcessed: false }] },
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
@@ -724,10 +718,7 @@ export async function getNextUnprocessedContent(botId: string): Promise<{
|
||||
} | null> {
|
||||
// Get all content sources for the bot
|
||||
const sources = await db.query.botContentSources.findMany({
|
||||
where: and(
|
||||
eq(botContentSources.botId, botId),
|
||||
eq(botContentSources.isActive, true)
|
||||
),
|
||||
where: { AND: [{ botId: botId }, { isActive: true }] },
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
@@ -740,10 +731,7 @@ export async function getNextUnprocessedContent(botId: string): Promise<{
|
||||
|
||||
for (const source of sources) {
|
||||
const item = await db.query.botContentItems.findFirst({
|
||||
where: and(
|
||||
eq(botContentItems.sourceId, source.id),
|
||||
eq(botContentItems.isProcessed, false)
|
||||
),
|
||||
where: { AND: [{ sourceId: source.id }, { isProcessed: false }] },
|
||||
orderBy: (items, { asc }) => [asc(items.publishedAt)],
|
||||
});
|
||||
|
||||
@@ -789,10 +777,7 @@ export async function processScheduledPosts(): Promise<ProcessScheduledPostsResu
|
||||
|
||||
// Get all active bots with schedules
|
||||
const activeBots = await db.query.bots.findMany({
|
||||
where: and(
|
||||
eq(bots.isActive, true),
|
||||
eq(bots.isSuspended, false)
|
||||
),
|
||||
where: { AND: [{ isActive: true }, { isSuspended: false }] },
|
||||
});
|
||||
|
||||
for (const bot of activeBots) {
|
||||
@@ -933,10 +918,7 @@ export async function processScheduledPosts(): Promise<ProcessScheduledPostsResu
|
||||
*/
|
||||
async function botHasActiveSources(botId: string): Promise<boolean> {
|
||||
const sources = await db.query.botContentSources.findMany({
|
||||
where: and(
|
||||
eq(botContentSources.botId, botId),
|
||||
eq(botContentSources.isActive, true)
|
||||
),
|
||||
where: { AND: [{ botId: botId }, { isActive: true }] },
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
@@ -951,7 +933,7 @@ async function botHasActiveSources(botId: string): Promise<boolean> {
|
||||
*/
|
||||
export async function getBotSchedule(botId: string): Promise<ScheduleConfig | null> {
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
columns: { scheduleConfig: true },
|
||||
});
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function reinstateBot(botId: string) {
|
||||
*/
|
||||
export async function isBotSuspended(botId: string): Promise<boolean> {
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
columns: { isSuspended: true },
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function isBotSuspended(botId: string): Promise<boolean> {
|
||||
*/
|
||||
export async function ensureBotNotSuspended(botId: string): Promise<void> {
|
||||
const bot = await db.query.bots.findFirst({
|
||||
where: eq(bots.id, botId),
|
||||
where: { id: botId },
|
||||
columns: { isSuspended: true, suspensionReason: true },
|
||||
});
|
||||
|
||||
|
||||
+3
-3
@@ -9,13 +9,13 @@ export async function getRuntimeConfig() {
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
cachedConfig = {
|
||||
domain: data.domain || 'localhost:3000',
|
||||
domain: data.domain || 'localhost:43821',
|
||||
};
|
||||
return cachedConfig;
|
||||
})
|
||||
.catch(() => {
|
||||
cachedConfig = {
|
||||
domain: process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000',
|
||||
domain: process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821',
|
||||
};
|
||||
return cachedConfig;
|
||||
});
|
||||
@@ -24,7 +24,7 @@ export async function getRuntimeConfig() {
|
||||
}
|
||||
|
||||
export function getDomain(): string {
|
||||
return cachedConfig?.domain || process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
return cachedConfig?.domain || process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
}
|
||||
|
||||
export function clearCachedConfig() {
|
||||
|
||||
@@ -26,13 +26,13 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setConfig({
|
||||
domain: data.domain || 'localhost:3000',
|
||||
domain: data.domain || 'localhost:43821',
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// Fallback to build-time value if fetch fails
|
||||
setConfig({
|
||||
domain: process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000',
|
||||
domain: process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821',
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -59,7 +59,7 @@ export function useDomain(): string {
|
||||
const { config, isLoading } = useRuntimeConfig();
|
||||
// Return runtime domain if loaded, otherwise fall back to build-time value
|
||||
if (isLoading || !config) {
|
||||
return process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
return process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
}
|
||||
return config.domain;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function upsertHandleEntries(entries: HandleEntry[]) {
|
||||
}
|
||||
|
||||
const existing = await db.query.handleRegistry.findFirst({
|
||||
where: eq(handleRegistry.handle, cleanHandle),
|
||||
where: { handle: cleanHandle },
|
||||
});
|
||||
|
||||
// If no timestamp provided, treat it as "now" but be careful
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import http from 'http';
|
||||
|
||||
const DEFAULT_SOCKET_PATH = '/var/run/synapsis-updater/updater.sock';
|
||||
const REQUEST_TIMEOUT_MS = 4000;
|
||||
|
||||
export interface HostUpdaterStatus {
|
||||
available: boolean;
|
||||
status: 'unavailable' | 'idle' | 'updating' | 'success' | 'error';
|
||||
message?: string;
|
||||
lastStartedAt?: string | null;
|
||||
lastFinishedAt?: string | null;
|
||||
lastExitCode?: number | null;
|
||||
lastError?: string | null;
|
||||
pid?: number | null;
|
||||
trigger?: 'manual' | 'auto' | null;
|
||||
config?: {
|
||||
autoUpdateEnabled: boolean;
|
||||
intervalMinutes: number;
|
||||
};
|
||||
}
|
||||
|
||||
function getUpdaterConfig() {
|
||||
const socketPath = process.env.HOST_UPDATER_SOCKET || DEFAULT_SOCKET_PATH;
|
||||
const token = process.env.HOST_UPDATER_TOKEN || '';
|
||||
|
||||
return {
|
||||
socketPath,
|
||||
token,
|
||||
enabled: Boolean(socketPath && token),
|
||||
};
|
||||
}
|
||||
|
||||
function requestUpdater<T>(method: 'GET' | 'POST' | 'PATCH', path: string, body?: unknown): Promise<T> {
|
||||
const { socketPath, token, enabled } = getUpdaterConfig();
|
||||
|
||||
if (!enabled) {
|
||||
return Promise.reject(new Error('Host updater is not configured'));
|
||||
}
|
||||
|
||||
const payload = body ? JSON.stringify(body) : undefined;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = http.request(
|
||||
{
|
||||
method,
|
||||
socketPath,
|
||||
path,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json',
|
||||
...(payload
|
||||
? {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
response.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
||||
response.on('end', () => {
|
||||
const text = Buffer.concat(chunks).toString('utf8');
|
||||
const data = text ? JSON.parse(text) : {};
|
||||
|
||||
if ((response.statusCode || 500) >= 400) {
|
||||
const message = data.error || `Updater request failed with ${response.statusCode}`;
|
||||
reject(new Error(message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(data as T);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
request.setTimeout(REQUEST_TIMEOUT_MS, () => {
|
||||
request.destroy(new Error('Host updater request timed out'));
|
||||
});
|
||||
|
||||
request.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
if (payload) {
|
||||
request.write(payload);
|
||||
}
|
||||
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
export async function getHostUpdaterStatus(): Promise<HostUpdaterStatus> {
|
||||
const config = getUpdaterConfig();
|
||||
if (!config.enabled) {
|
||||
return {
|
||||
available: false,
|
||||
status: 'unavailable',
|
||||
message: 'Host updater is not configured for this install.',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await requestUpdater<HostUpdaterStatus>('GET', '/status');
|
||||
return {
|
||||
...status,
|
||||
available: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
status: 'unavailable',
|
||||
message: error instanceof Error ? error.message : 'Host updater is unavailable.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function triggerHostUpdate() {
|
||||
return requestUpdater<{ ok: boolean; status: string; message?: string }>('POST', '/update');
|
||||
}
|
||||
|
||||
export async function updateHostUpdaterConfig(autoUpdateEnabled: boolean) {
|
||||
return requestUpdater<{ ok: boolean; config: { autoUpdateEnabled: boolean; intervalMinutes: number } }>(
|
||||
'PATCH',
|
||||
'/config',
|
||||
{ autoUpdateEnabled }
|
||||
);
|
||||
}
|
||||
@@ -22,7 +22,7 @@ function normalizeOptionalUrl(value: string | null | undefined): string | undefi
|
||||
* Build this node's announcement payload
|
||||
*/
|
||||
export async function buildAnnouncement(): Promise<SwarmAnnouncement> {
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
|
||||
let name = 'Synapsis Node';
|
||||
let description: string | undefined;
|
||||
@@ -35,7 +35,7 @@ export async function buildAnnouncement(): Promise<SwarmAnnouncement> {
|
||||
if (db) {
|
||||
// Get node info
|
||||
const node = await db.query.nodes.findFirst({
|
||||
where: eq(nodes.domain, domain),
|
||||
where: { domain: domain },
|
||||
});
|
||||
|
||||
if (node) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { buildAnnouncement } from './discovery';
|
||||
* Build a gossip payload to send to another node
|
||||
*/
|
||||
export async function buildGossipPayload(since?: string): Promise<SwarmGossipPayload> {
|
||||
const ourDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const ourDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
|
||||
// Get nodes to share
|
||||
let nodes: SwarmNodeInfo[];
|
||||
@@ -56,8 +56,8 @@ export async function buildGossipPayload(since?: string): Promise<SwarmGossipPay
|
||||
if (db) {
|
||||
const sinceDate = since ? new Date(since) : undefined;
|
||||
const handleEntries = await db.query.handleRegistry.findMany({
|
||||
where: sinceDate ? gt(handleRegistry.updatedAt, sinceDate) : undefined,
|
||||
orderBy: [desc(handleRegistry.updatedAt)],
|
||||
where: sinceDate ? { updatedAt: { gt: sinceDate } } : undefined,
|
||||
orderBy: () => [desc(handleRegistry.updatedAt)],
|
||||
limit: SWARM_CONFIG.maxHandlesPerGossip,
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ interface IdentityCacheEntry {
|
||||
*/
|
||||
export async function getCachedIdentity(did: string): Promise<IdentityCacheEntry | null> {
|
||||
const cached = await db.query.remoteIdentityCache.findFirst({
|
||||
where: eq(remoteIdentityCache.did, did),
|
||||
where: { did: did },
|
||||
});
|
||||
|
||||
return cached || null;
|
||||
|
||||
@@ -465,7 +465,7 @@ export async function cacheSwarmUserPosts(
|
||||
|
||||
// Check if we already have this post
|
||||
const existing = await db.query.remotePosts.findFirst({
|
||||
where: eq(remotePosts.apId, apId),
|
||||
where: { apId: apId },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
@@ -673,7 +673,7 @@ export async function getSwarmFollowerDomains(userId: string): Promise<string[]>
|
||||
if (!db) return [];
|
||||
|
||||
const followers = await db.query.remoteFollowers.findMany({
|
||||
where: eq(remoteFollowers.userId, userId),
|
||||
where: { userId: userId },
|
||||
});
|
||||
|
||||
// Filter for swarm followers (actorUrl starts with swarm://)
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function isNodeBlocked(domain: string | null | undefined): Promise<
|
||||
if (!normalized) return false;
|
||||
|
||||
const node = await db.query.swarmNodes.findFirst({
|
||||
where: eq(swarmNodes.domain, normalized),
|
||||
where: { domain: normalized },
|
||||
columns: {
|
||||
isBlocked: true,
|
||||
},
|
||||
@@ -30,7 +30,7 @@ export async function getBlockedNodeDomains(): Promise<Set<string>> {
|
||||
if (!db) return new Set();
|
||||
|
||||
const rows = await db.query.swarmNodes.findMany({
|
||||
where: eq(swarmNodes.isBlocked, true),
|
||||
where: { isBlocked: true },
|
||||
columns: {
|
||||
domain: true,
|
||||
},
|
||||
@@ -46,10 +46,7 @@ export async function filterBlockedDomains(domains: string[]): Promise<string[]>
|
||||
if (normalized.length === 0) return [];
|
||||
|
||||
const blocked = await db.query.swarmNodes.findMany({
|
||||
where: and(
|
||||
inArray(swarmNodes.domain, normalized),
|
||||
eq(swarmNodes.isBlocked, true),
|
||||
),
|
||||
where: { AND: [{ domain: { in: normalized } }, { isBlocked: true }] },
|
||||
columns: {
|
||||
domain: true,
|
||||
},
|
||||
@@ -66,7 +63,7 @@ export async function upsertBlockedNode(domain: string, reason?: string | null)
|
||||
if (!normalized) return null;
|
||||
|
||||
const existing = await db.query.swarmNodes.findFirst({
|
||||
where: eq(swarmNodes.domain, normalized),
|
||||
where: { domain: normalized },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
@@ -105,7 +102,7 @@ export async function unblockNode(domain: string) {
|
||||
if (!normalized) return null;
|
||||
|
||||
const existing = await db.query.swarmNodes.findFirst({
|
||||
where: eq(swarmNodes.domain, normalized),
|
||||
where: { domain: normalized },
|
||||
});
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
@@ -68,11 +68,11 @@ export async function getNodeKeypair(): Promise<{ privateKey: string; publicKey:
|
||||
throw new Error('Database not available');
|
||||
}
|
||||
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
|
||||
// Try to get existing node
|
||||
let node = await db.query.nodes.findFirst({
|
||||
where: eq(nodes.domain, domain),
|
||||
where: { domain: domain },
|
||||
});
|
||||
|
||||
// If node doesn't exist, create it
|
||||
@@ -118,9 +118,9 @@ export async function getNodeKeypair(): Promise<{ privateKey: string; publicKey:
|
||||
export async function getNodePublicKey(): Promise<string | null> {
|
||||
if (!db) return null;
|
||||
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
const node = await db.query.nodes.findFirst({
|
||||
where: eq(nodes.domain, domain),
|
||||
where: { domain: domain },
|
||||
});
|
||||
|
||||
if (!node?.publicKey) {
|
||||
|
||||
+11
-21
@@ -22,7 +22,7 @@ export async function upsertSwarmNode(
|
||||
}
|
||||
|
||||
const existing = await db.query.swarmNodes.findFirst({
|
||||
where: eq(swarmNodes.domain, normalizeNodeDomain(node.domain)),
|
||||
where: { domain: normalizeNodeDomain(node.domain) },
|
||||
});
|
||||
|
||||
const normalizedDomain = normalizeNodeDomain(node.domain);
|
||||
@@ -110,11 +110,8 @@ export async function getActiveSwarmNodes(limit = 100): Promise<SwarmNodeInfo[]>
|
||||
}
|
||||
|
||||
const nodes = await db.query.swarmNodes.findMany({
|
||||
where: and(
|
||||
eq(swarmNodes.isActive, true),
|
||||
eq(swarmNodes.isBlocked, false),
|
||||
),
|
||||
orderBy: [desc(swarmNodes.lastSeenAt)],
|
||||
where: { AND: [{ isActive: true }, { isBlocked: false }] },
|
||||
orderBy: () => [desc(swarmNodes.lastSeenAt)],
|
||||
limit,
|
||||
});
|
||||
|
||||
@@ -131,12 +128,8 @@ export async function getNodesForGossip(count: number): Promise<SwarmNodeInfo[]>
|
||||
|
||||
// Get active nodes with decent trust scores, ordered randomly
|
||||
const nodes = await db.query.swarmNodes.findMany({
|
||||
where: and(
|
||||
eq(swarmNodes.isActive, true),
|
||||
eq(swarmNodes.isBlocked, false),
|
||||
gt(swarmNodes.trustScore, 20)
|
||||
),
|
||||
orderBy: sql`RANDOM()`,
|
||||
where: { AND: [{ isActive: true }, { isBlocked: false }, { trustScore: { gt: 20 } }] },
|
||||
orderBy: () => sql`RANDOM()`,
|
||||
limit: count,
|
||||
});
|
||||
|
||||
@@ -152,11 +145,8 @@ export async function getNodesSince(since: Date, limit = 100): Promise<SwarmNode
|
||||
}
|
||||
|
||||
const nodes = await db.query.swarmNodes.findMany({
|
||||
where: and(
|
||||
gt(swarmNodes.updatedAt, since),
|
||||
eq(swarmNodes.isBlocked, false),
|
||||
),
|
||||
orderBy: [desc(swarmNodes.updatedAt)],
|
||||
where: { AND: [{ updatedAt: { gt: since } }, { isBlocked: false }] },
|
||||
orderBy: () => [desc(swarmNodes.updatedAt)],
|
||||
limit,
|
||||
});
|
||||
|
||||
@@ -173,7 +163,7 @@ export async function markNodeFailure(domain: string): Promise<void> {
|
||||
|
||||
try {
|
||||
const node = await db.query.swarmNodes.findFirst({
|
||||
where: eq(swarmNodes.domain, domain),
|
||||
where: { domain: domain },
|
||||
});
|
||||
|
||||
if (!node) return;
|
||||
@@ -209,7 +199,7 @@ export async function markNodeSuccess(domain: string): Promise<void> {
|
||||
|
||||
try {
|
||||
const node = await db.query.swarmNodes.findFirst({
|
||||
where: eq(swarmNodes.domain, domain),
|
||||
where: { domain: domain },
|
||||
});
|
||||
|
||||
if (!node) return;
|
||||
@@ -274,8 +264,8 @@ export async function getSeedNodes(): Promise<string[]> {
|
||||
}
|
||||
|
||||
const seeds = await db.query.swarmSeeds.findMany({
|
||||
where: eq(swarmSeeds.isEnabled, true),
|
||||
orderBy: [swarmSeeds.priority],
|
||||
where: { isEnabled: true },
|
||||
orderBy: () => [swarmSeeds.priority],
|
||||
});
|
||||
|
||||
if (seeds.length === 0) {
|
||||
|
||||
@@ -21,11 +21,7 @@ export async function getViewerSwarmRepostedPostIds(
|
||||
const originalPostIds = Array.from(new Set(targets.map((target) => target.originalPostId)));
|
||||
|
||||
const rows = await db.query.userSwarmReposts.findMany({
|
||||
where: and(
|
||||
eq(userSwarmReposts.userId, viewerId),
|
||||
inArray(userSwarmReposts.nodeDomain, domains),
|
||||
inArray(userSwarmReposts.originalPostId, originalPostIds),
|
||||
),
|
||||
where: { AND: [{ userId: viewerId }, { nodeDomain: { in: domains } }, { originalPostId: { in: originalPostIds } }] },
|
||||
});
|
||||
|
||||
const rowKeys = new Set(rows.map((row) => `${row.nodeDomain}:${row.originalPostId}`));
|
||||
|
||||
@@ -141,7 +141,7 @@ export async function verifyUserInteraction(
|
||||
// Try to get cached user
|
||||
const fullHandle = `${userHandle}@${normalizedDomain}`;
|
||||
let user = await db?.query.users.findFirst({
|
||||
where: eq(users.handle, fullHandle),
|
||||
where: { handle: fullHandle },
|
||||
});
|
||||
|
||||
let publicKey: string | null = null;
|
||||
|
||||
@@ -21,10 +21,7 @@ export async function upsertRemoteUser(profile: RemoteProfile): Promise<void> {
|
||||
try {
|
||||
// Check if user already exists
|
||||
const existing = await db.query.users.findFirst({
|
||||
where: or(
|
||||
eq(users.did, profile.did),
|
||||
eq(users.handle, profile.handle),
|
||||
),
|
||||
where: { OR: [{ did: profile.did }, { handle: profile.handle }] },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
|
||||
@@ -4,9 +4,9 @@ import { eq } from 'drizzle-orm';
|
||||
export async function verifyTurnstileToken(token: string, ip?: string): Promise<boolean> {
|
||||
try {
|
||||
// Get node settings to check if Turnstile is enabled
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
const node = await db.query.nodes.findFirst({
|
||||
where: eq(nodes.domain, domain),
|
||||
where: { domain: domain },
|
||||
});
|
||||
|
||||
// If no secret key is configured, skip verification (Turnstile is disabled)
|
||||
@@ -38,9 +38,9 @@ export async function verifyTurnstileToken(token: string, ip?: string): Promise<
|
||||
|
||||
export async function getTurnstileSiteKey(): Promise<string | null> {
|
||||
try {
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
const node = await db.query.nodes.findFirst({
|
||||
where: eq(nodes.domain, domain),
|
||||
where: { domain: domain },
|
||||
});
|
||||
|
||||
return node?.turnstileSiteKey || null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useDomain } from '@/lib/contexts/ConfigContext';
|
||||
|
||||
// Build-time domain fallback (for SSR/non-React contexts)
|
||||
export const NODE_DOMAIN = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||
export const NODE_DOMAIN = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:43821';
|
||||
|
||||
/**
|
||||
* Formats a handle into its full federated form: @user@domain
|
||||
|
||||
+3
-229
@@ -1,241 +1,15 @@
|
||||
import packageJson from '../../package.json';
|
||||
|
||||
const DEFAULT_IMAGE_REPO = 'ghcr.io/gnosyslabs/synapsis';
|
||||
const VERSION_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
export interface BuildInfo {
|
||||
version: string;
|
||||
commit: string | null;
|
||||
buildDate: string | null;
|
||||
githubUrl: string | null;
|
||||
imageDigest: string | null;
|
||||
imageRepo: string;
|
||||
sourceRepo: string;
|
||||
}
|
||||
|
||||
type CachedLatest = {
|
||||
expiresAt: number;
|
||||
value: PublishedBuildInfo | null;
|
||||
};
|
||||
|
||||
export interface PublishedBuildInfo extends BuildInfo {
|
||||
tag: string;
|
||||
}
|
||||
|
||||
let latestVersionCache: CachedLatest | null = null;
|
||||
|
||||
function normalizeImageRepo(value: string): string {
|
||||
if (!value) {
|
||||
return DEFAULT_IMAGE_REPO;
|
||||
}
|
||||
|
||||
return value.startsWith('ghcr.io/') ? value : `ghcr.io/${value}`;
|
||||
}
|
||||
|
||||
function imageRepoPath(value: string): string {
|
||||
return normalizeImageRepo(value).replace(/^ghcr\.io\//, '');
|
||||
}
|
||||
|
||||
function buildGithubUrl(sourceRepo: string, commit: string | null): string | null {
|
||||
if (!commit || !sourceRepo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${sourceRepo.replace(/\/$/, '')}/commit/${commit}`;
|
||||
}
|
||||
|
||||
export function getCurrentBuildInfo(): BuildInfo {
|
||||
const sourceRepo = process.env.APP_SOURCE_REPO || 'https://github.com/GnosysLabs/Synapsis';
|
||||
const version = process.env.APP_VERSION || packageJson.version || 'dev';
|
||||
const commit = process.env.APP_COMMIT || null;
|
||||
const buildDate = process.env.APP_BUILD_DATE || null;
|
||||
const imageDigest = process.env.APP_IMAGE_DIGEST || null;
|
||||
const imageRepo = normalizeImageRepo(process.env.APP_IMAGE_REPO || DEFAULT_IMAGE_REPO);
|
||||
const githubUrl = process.env.APP_GITHUB_URL || buildGithubUrl(sourceRepo, commit);
|
||||
|
||||
return {
|
||||
version,
|
||||
commit,
|
||||
buildDate,
|
||||
githubUrl,
|
||||
imageDigest,
|
||||
imageRepo,
|
||||
sourceRepo,
|
||||
version: process.env.APP_VERSION || packageJson.version || 'dev',
|
||||
commit: process.env.APP_COMMIT || null,
|
||||
buildDate: process.env.APP_BUILD_DATE || null,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseVersionTuple(version: string): [number, number, number, number] | null {
|
||||
const match = /^(\d{4})\.(\d{2})\.(\d{2})\.(\d+)$/.exec(version);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
Number(match[1]),
|
||||
Number(match[2]),
|
||||
Number(match[3]),
|
||||
Number(match[4]),
|
||||
];
|
||||
}
|
||||
|
||||
export function compareBuildVersions(a: string, b: string): number {
|
||||
const aTuple = parseVersionTuple(a);
|
||||
const bTuple = parseVersionTuple(b);
|
||||
|
||||
if (!aTuple && !bTuple) {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
if (!aTuple) {
|
||||
return -1;
|
||||
}
|
||||
if (!bTuple) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (let i = 0; i < aTuple.length; i += 1) {
|
||||
if (aTuple[i] !== bTuple[i]) {
|
||||
return aTuple[i] - bTuple[i];
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function fetchGhcrToken(repoPath: string): Promise<string | null> {
|
||||
const tokenResponse = await fetch(`https://ghcr.io/token?scope=repository:${repoPath}:pull`, {
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
return tokenData.token || null;
|
||||
}
|
||||
|
||||
async function fetchRegistryJson(
|
||||
repoPath: string,
|
||||
reference: string,
|
||||
accept: string,
|
||||
token: string
|
||||
) {
|
||||
const response = await fetch(`https://ghcr.io/v2/${repoPath}/manifests/${reference}`, {
|
||||
headers: {
|
||||
Accept: accept,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Registry request failed with ${response.status}`);
|
||||
}
|
||||
|
||||
return {
|
||||
digest: response.headers.get('docker-content-digest'),
|
||||
body: await response.json(),
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchRegistryBlob(repoPath: string, digest: string, token: string) {
|
||||
const response = await fetch(`https://ghcr.io/v2/${repoPath}/blobs/${digest}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Registry blob request failed with ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function loadLatestPublishedBuild(): Promise<PublishedBuildInfo | null> {
|
||||
const current = getCurrentBuildInfo();
|
||||
const repoPath = imageRepoPath(current.imageRepo);
|
||||
const token = await fetchGhcrToken(repoPath);
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const manifestAccept = [
|
||||
'application/vnd.oci.image.index.v1+json',
|
||||
'application/vnd.docker.distribution.manifest.list.v2+json',
|
||||
'application/vnd.oci.image.manifest.v1+json',
|
||||
'application/vnd.docker.distribution.manifest.v2+json',
|
||||
].join(', ');
|
||||
|
||||
const latestManifest = await fetchRegistryJson(repoPath, 'latest', manifestAccept, token);
|
||||
const latestBody = latestManifest.body;
|
||||
|
||||
let configDigest: string | undefined;
|
||||
|
||||
if (Array.isArray(latestBody.manifests)) {
|
||||
const platformManifest =
|
||||
latestBody.manifests.find(
|
||||
(manifest: any) =>
|
||||
manifest.platform?.os === 'linux' && manifest.platform?.architecture === 'amd64'
|
||||
) ||
|
||||
latestBody.manifests.find((manifest: any) => manifest.platform?.os && manifest.platform?.architecture);
|
||||
|
||||
if (!platformManifest?.digest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const platformManifestResponse = await fetchRegistryJson(repoPath, platformManifest.digest, manifestAccept, token);
|
||||
configDigest = platformManifestResponse.body?.config?.digest;
|
||||
} else {
|
||||
configDigest = latestBody?.config?.digest;
|
||||
}
|
||||
|
||||
if (!configDigest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const configBlob = await fetchRegistryBlob(repoPath, configDigest, token);
|
||||
const labels = configBlob?.config?.Labels || {};
|
||||
const sourceRepo = labels['org.opencontainers.image.source'] || current.sourceRepo;
|
||||
const commit = labels['org.opencontainers.image.revision'] || null;
|
||||
const version = labels['org.opencontainers.image.version'] || null;
|
||||
|
||||
if (!version) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
tag: 'latest',
|
||||
version,
|
||||
commit,
|
||||
buildDate: labels['org.opencontainers.image.created'] || null,
|
||||
githubUrl: buildGithubUrl(sourceRepo, commit),
|
||||
imageDigest: latestManifest.digest || null,
|
||||
imageRepo: current.imageRepo,
|
||||
sourceRepo,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLatestPublishedBuild(): Promise<PublishedBuildInfo | null> {
|
||||
if (latestVersionCache && latestVersionCache.expiresAt > Date.now()) {
|
||||
return latestVersionCache.value;
|
||||
}
|
||||
|
||||
try {
|
||||
const latest = await loadLatestPublishedBuild();
|
||||
latestVersionCache = {
|
||||
expiresAt: Date.now() + VERSION_CACHE_TTL_MS,
|
||||
value: latest,
|
||||
};
|
||||
return latest;
|
||||
} catch (error) {
|
||||
console.error('[Version] Failed to fetch latest published build:', error);
|
||||
latestVersionCache = {
|
||||
expiresAt: Date.now() + VERSION_CACHE_TTL_MS,
|
||||
value: null,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user