@defikitdotnet/x-ai-combat
Version:
XCombatAI - Social Media Engagement Template for the Agent Framework
551 lines (544 loc) • 19.8 kB
JavaScript
import { log, error } from '../utils/logger';
/**
* SQL schema for x-ai-combat tables
*/
export const sqliteTables = `
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
-- Users table for storing Twitter users
CREATE TABLE IF NOT EXISTS "users" (
"id" TEXT PRIMARY KEY,
"twitterId" TEXT NOT NULL,
"username" TEXT NOT NULL,
"linkedAt" TEXT NOT NULL,
"updatedAt" TEXT,
"agentId" TEXT REFERENCES accounts(id) ON DELETE CASCADE
);
-- Create index for faster queries
CREATE INDEX IF NOT EXISTS "idx_users_twitterId" ON "users" ("twitterId");
CREATE INDEX IF NOT EXISTS "idx_users_agentId" ON "users" ("agentId");
-- Posts table for storing Twitter posts with stats
CREATE TABLE IF NOT EXISTS "posts" (
"id" TEXT PRIMARY KEY,
"tweetId" TEXT NOT NULL,
"twitterUserId" TEXT NOT NULL,
"userId" TEXT,
"content" TEXT NOT NULL,
"raw" TEXT NOT NULL,
"stats" TEXT DEFAULT '{"likes":0,"retweets":0,"quotes":0,"replies":0,"total":0,"famePoints":0}',
"lastInteractionAt" TEXT,
"createdAt" TEXT NOT NULL,
"updatedAt" TEXT,
"agentId" TEXT REFERENCES accounts(id) ON DELETE CASCADE
);
-- Create indexes for faster queries
CREATE INDEX IF NOT EXISTS "idx_posts_tweetId" ON "posts" ("tweetId");
CREATE INDEX IF NOT EXISTS "idx_posts_twitterUserId" ON "posts" ("twitterUserId");
CREATE INDEX IF NOT EXISTS "idx_posts_userId" ON "posts" ("userId");
CREATE INDEX IF NOT EXISTS "idx_posts_agentId" ON "posts" ("agentId");
-- Replied tweets table for tracking which tweets have been replied to
CREATE TABLE IF NOT EXISTS "replied_tweets" (
"tweetId" TEXT PRIMARY KEY,
"repliedAt" TEXT NOT NULL,
"agentId" TEXT REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_replied_tweets_agentId" ON "replied_tweets" ("agentId");
COMMIT;
`;
/**
* XAIAdapter - Consolidated adapter for x-ai-combat module
* Implements database operations for User, Post, and Interaction models
*/
export class XAIAdapter {
/**
* Initialize XAIAdapter with SQLite database
*/
constructor(databaseAdapter) {
this.databaseAdapter = databaseAdapter;
}
/**
* Initialize the database schema for x-ai-combat tables
*/
init() {
try {
this.databaseAdapter.db.exec(sqliteTables);
log("X-AI-Combat database tables initialized successfully");
}
catch (err) {
error("Failed to initialize x-ai-combat database tables:", err);
throw err;
}
}
/**
* Get the underlying database instance
*/
getDatabase() {
return this.databaseAdapter;
}
/**
* Get the database adapter - required for ModuleAdapterInterface
*/
getDatabaseAdapter() {
return this.databaseAdapter;
}
// ====== Tweet Reply Tracking Methods ======
/**
* Mark a tweet as having been replied to
*/
async markInteractionAsReplied(tweetId, agentId) {
try {
const sql = `
INSERT OR REPLACE INTO replied_tweets (tweetId, repliedAt, agentId)
VALUES (?, ?, ?)
`;
const stmt = this.databaseAdapter.db.prepare(sql);
stmt.run(tweetId, new Date().toISOString(), agentId || null);
return true;
}
catch (error) {
console.error("Error marking tweet as replied:", error);
return false;
}
}
/**
* Get all tweets that have been replied to
*/
async getRepliedInteractions(agentId) {
try {
let sql = "SELECT * FROM replied_tweets";
let params = [];
if (agentId) {
sql += " WHERE agentId = ?";
params.push(agentId);
}
const stmt = this.databaseAdapter.db.prepare(sql);
const rows = stmt.all(...params);
return rows.map(row => ({
tweetId: row.tweetId,
repliedAt: new Date(row.repliedAt),
agentId: row.agentId
}));
}
catch (error) {
console.error("Error getting replied tweets:", error);
return [];
}
}
/**
* Check if a tweet has been replied to
*/
async hasTweetBeenReplied(tweetId) {
try {
const sql = "SELECT COUNT(*) as count FROM replied_tweets WHERE tweetId = ?";
const stmt = this.databaseAdapter.db.prepare(sql);
const result = stmt.get(tweetId);
return result.count > 0;
}
catch (error) {
console.error("Error checking if tweet has been replied to:", error);
return false;
}
}
// ====== User Methods ======
/**
* Find user by ID
*/
async findUserById(id) {
try {
const sql = "SELECT * FROM users WHERE id = ?";
const stmt = this.databaseAdapter.db.prepare(sql);
const user = stmt.get(id);
if (!user)
return null;
// Convert date strings to Date objects
user.linkedAt = new Date(user.linkedAt);
if (user.updatedAt) {
user.updatedAt = new Date(user.updatedAt);
}
return user;
}
catch (error) {
console.error("Error finding user by ID:", error);
return null;
}
}
/**
* Find user by Twitter ID
*/
async findUserByTwitterId(twitterId, agentId) {
try {
let sql, params;
if (agentId) {
sql = "SELECT * FROM users WHERE twitterId = ? AND agentId = ?";
params = [twitterId, agentId];
}
else {
sql = "SELECT * FROM users WHERE twitterId = ?";
params = [twitterId];
}
const stmt = this.databaseAdapter.db.prepare(sql);
const user = stmt.get(...params);
if (!user)
return null;
// Convert date strings to Date objects
user.linkedAt = new Date(user.linkedAt);
if (user.updatedAt) {
user.updatedAt = new Date(user.updatedAt);
}
return user;
}
catch (error) {
console.error("Error finding user by Twitter ID:", error);
return null;
}
}
/**
* Create or update a user
*/
async createUser(user) {
try {
const sql = `
INSERT OR REPLACE INTO users (id, twitterId, username, linkedAt, updatedAt, agentId)
VALUES (?, ?, ?, ?, ?, ?)
`;
const stmt = this.databaseAdapter.db.prepare(sql);
stmt.run(user.id, user.twitterId, user.username, user.linkedAt.toISOString(), user.updatedAt ? user.updatedAt.toISOString() : null, user.agentId || null);
// After creating a user, update any posts or interactions associated with this twitter ID
this.linkPostsToUser(user.twitterId, user.id);
return true;
}
catch (error) {
console.error("Error creating user:", error);
return false;
}
}
/**
* Link existing posts to a user when they connect their account
*/
async linkPostsToUser(twitterId, userId) {
try {
const sql = `UPDATE posts SET userId = ? WHERE twitterUserId = ? AND (userId IS NULL OR userId = '')`;
const stmt = this.databaseAdapter.db.prepare(sql);
stmt.run(userId, twitterId);
log(`Linked posts with Twitter ID ${twitterId} to user ${userId}`);
}
catch (err) {
error(`Error linking posts to user:`, err);
}
}
/**
* Update an existing user
*/
async updateUser(user) {
try {
const sql = `
UPDATE users
SET twitterId = ?, username = ?, linkedAt = ?, updatedAt = ?
WHERE id = ?
`;
const stmt = this.databaseAdapter.db.prepare(sql);
stmt.run(user.twitterId, user.username, user.linkedAt.toISOString(), user.updatedAt ? user.updatedAt.toISOString() : null, user.id);
return true;
}
catch (error) {
console.error("Error updating user:", error);
return false;
}
}
/**
* Get all users
*/
async getAllUsers(agentId) {
try {
let sql = "SELECT * FROM users";
let params = [];
if (agentId) {
sql += " WHERE agentId = ?";
params.push(agentId);
}
const stmt = this.databaseAdapter.db.prepare(sql);
const rows = stmt.all(...params);
return rows.map(row => {
return {
id: row.id,
twitterId: row.twitterId,
username: row.username,
linkedAt: new Date(row.linkedAt),
updatedAt: row.updatedAt ? new Date(row.updatedAt) : undefined,
agentId: row.agentId
};
});
}
catch (error) {
console.error("Error getting all users:", error);
return [];
}
}
// ====== Post Methods ======
/**
* Find post by ID
*/
async findPostById(id) {
try {
const sql = "SELECT * FROM posts WHERE id = ?";
const stmt = this.databaseAdapter.db.prepare(sql);
const post = stmt.get(id);
if (!post)
return null;
// Convert date strings to Date objects
post.createdAt = new Date(post.createdAt);
if (post.updatedAt) {
post.updatedAt = new Date(post.updatedAt);
}
if (post.lastInteractionAt) {
post.lastInteractionAt = new Date(post.lastInteractionAt);
}
// Parse stats JSON
if (typeof post.stats === 'string') {
post.stats = JSON.parse(post.stats);
}
return post;
}
catch (error) {
console.error("Error finding post by ID:", error);
return null;
}
}
/**
* Find post by Tweet ID
*/
async findPostByTweetId(tweetId) {
try {
const sql = "SELECT * FROM posts WHERE tweetId = ?";
const stmt = this.databaseAdapter.db.prepare(sql);
const post = stmt.get(tweetId);
if (!post)
return null;
// Convert date strings to Date objects
post.createdAt = new Date(post.createdAt);
if (post.updatedAt) {
post.updatedAt = new Date(post.updatedAt);
}
if (post.lastInteractionAt) {
post.lastInteractionAt = new Date(post.lastInteractionAt);
}
// Parse stats JSON
if (typeof post.stats === 'string') {
post.stats = JSON.parse(post.stats);
}
return post;
}
catch (error) {
console.error("Error finding post by Tweet ID:", error);
return null;
}
}
/**
* Create or update a post
*/
async createPost(post) {
try {
// Ensure stats is converted to a string if it's an object
const statsStr = typeof post.stats === 'object' ? JSON.stringify(post.stats) : post.stats;
const sql = `
INSERT OR REPLACE INTO posts (
id, tweetId, twitterUserId, userId, content, raw,
stats, lastInteractionAt, createdAt, updatedAt, agentId
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const stmt = this.databaseAdapter.db.prepare(sql);
stmt.run(post.id, post.tweetId, post.twitterUserId, post.userId || null, post.content, post.raw, statsStr, post.lastInteractionAt ? post.lastInteractionAt.toISOString() : null, post.createdAt.toISOString(), post.updatedAt ? post.updatedAt.toISOString() : null, post.agentId || null);
return true;
}
catch (error) {
console.error("Error creating post:", error);
return false;
}
}
/**
* Get posts by user ID with pagination
*/
async getPostsByUserId(userId, page = 1, limit = 10, agentId) {
try {
const offset = (page - 1) * limit;
// Add optional agentId filter
const agentFilter = agentId ? 'AND agentId = ?' : '';
const params = agentId ? [userId, agentId, limit, offset] : [userId, limit, offset];
const sql = `
SELECT * FROM posts
WHERE userId = ? ${agentFilter}
ORDER BY createdAt DESC
LIMIT ? OFFSET ?
`;
const stmt = this.databaseAdapter.db.prepare(sql);
const rows = stmt.all(...params);
// Count total posts
const countSql = `SELECT COUNT(*) as count FROM posts WHERE userId = ? ${agentFilter}`;
const countStmt = this.databaseAdapter.db.prepare(countSql);
const countParams = agentId ? [userId, agentId] : [userId];
const countResult = countStmt.get(...countParams);
const posts = rows.map(row => this.parsePostRow(row));
return { posts, total: countResult.count };
}
catch (error) {
console.error("Error getting posts by user ID:", error);
return { posts: [], total: 0 };
}
}
/**
* Get all posts from the database
*/
async getAllPosts(agentId) {
try {
let sql = "SELECT * FROM posts";
let params = [];
if (agentId) {
sql += " WHERE agentId = ?";
params.push(agentId);
}
sql += " ORDER BY createdAt DESC";
const stmt = this.databaseAdapter.db.prepare(sql);
const rows = stmt.all(...params);
return rows.map(row => this.parsePostRow(row));
}
catch (error) {
console.error("Error getting all posts:", error);
return [];
}
}
/**
* Get posts by Twitter user ID
*/
async getPostsByTwitterUserId(twitterUserId, agentId) {
try {
let sql = "SELECT * FROM posts WHERE twitterUserId = ?";
let params = [twitterUserId];
if (agentId) {
sql += " AND agentId = ?";
params.push(agentId);
}
sql += " ORDER BY createdAt DESC";
const stmt = this.databaseAdapter.db.prepare(sql);
const rows = stmt.all(...params);
return rows.map(row => this.parsePostRow(row));
}
catch (error) {
console.error("Error getting posts by Twitter user ID:", error);
return [];
}
}
/**
* Get leaderboard data with user fame points and stats
*/
async getLeaderboard(period = 'all', limit = 10, agentId) {
try {
// Get the SQL condition for the date period
const periodLabel = this.getPeriodLabel(period);
// Add the agentId condition to the SQL query if provided
let agentCondition = '';
let params = [];
if (agentId) {
agentCondition = 'AND p.agentId = ? ';
params.push(agentId);
}
// Calculate stats for all users with Twitter interactions
const sql = `
WITH user_stats AS (
SELECT
p.userId,
u.username,
u.twitterId,
SUM(json_extract(p.stats, '$.famePoints')) as famePoints,
SUM(json_extract(p.stats, '$.likes')) as likes,
SUM(json_extract(p.stats, '$.retweets')) as retweets,
SUM(json_extract(p.stats, '$.quotes')) as quotes,
SUM(json_extract(p.stats, '$.replies')) as replies,
MAX(p.lastInteractionAt) as lastActive
FROM posts p
LEFT JOIN users u ON p.userId = u.id
WHERE
p.userId IS NOT NULL
${periodLabel ? `AND ${periodLabel}` : ''}
${agentCondition}
GROUP BY p.userId
)
SELECT
userId, username, twitterId,
famePoints, likes, retweets, quotes, replies,
lastActive
FROM user_stats
ORDER BY famePoints DESC
LIMIT ?
`;
const stmt = this.databaseAdapter.db.prepare(sql);
params.push(limit);
const rows = stmt.all(...params);
// Count total users in the leaderboard (without limit)
const countSql = `
SELECT COUNT(DISTINCT p.userId) as total
FROM posts p
WHERE
p.userId IS NOT NULL
${periodLabel ? `AND ${periodLabel}` : ''}
${agentCondition}
`;
const countStmt = this.databaseAdapter.db.prepare(countSql);
const countResult = countStmt.get(...params.slice(0, -1));
return {
users: rows.map(row => ({
userId: row.userId,
username: row.username,
twitterId: row.twitterId,
famePoints: row.famePoints || 0,
stats: {
likes: row.likes || 0,
retweets: row.retweets || 0,
quotes: row.quotes || 0,
replies: row.replies || 0,
total: (row.likes || 0) + (row.retweets || 0) + (row.quotes || 0) + (row.replies || 0)
},
lastActive: row.lastActive ? new Date(row.lastActive) : undefined
})),
totalUsers: countResult.total
};
}
catch (error) {
console.error("Error getting leaderboard:", error);
return { users: [], totalUsers: 0 };
}
}
/**
* Get period SQL condition for leaderboard
*/
getPeriodLabel(period) {
switch (period) {
case 'current_day':
return "DATE(p.lastInteractionAt) = DATE('now')";
case 'current_week':
return "strftime('%Y-%W', p.lastInteractionAt) = strftime('%Y-%W', 'now')";
case 'current_month':
return "strftime('%Y-%m', p.lastInteractionAt) = strftime('%Y-%m', 'now')";
case 'all':
default:
return ''; // No additional time constraint for all time
}
}
parsePostRow(row) {
const post = {
id: row.id,
tweetId: row.tweetId,
twitterUserId: row.twitterUserId,
userId: row.userId,
content: row.content,
raw: row.raw,
createdAt: new Date(row.createdAt),
updatedAt: row.updatedAt ? new Date(row.updatedAt) : undefined,
lastInteractionAt: row.lastInteractionAt ? new Date(row.lastInteractionAt) : undefined,
agentId: row.agentId,
stats: typeof row.stats === 'string' ? JSON.parse(row.stats) : row.stats
};
return post;
}
}
//# sourceMappingURL=XAIAdapter.js.map