diff --git a/src/adapters/localdb/models/smAccount.js b/src/adapters/localdb/models/smAccount.js index e9ef94e..c6cd412 100644 --- a/src/adapters/localdb/models/smAccount.js +++ b/src/adapters/localdb/models/smAccount.js @@ -14,8 +14,8 @@ const SmAccount = new mongoose.Schema({ pfp: { type: String, default: '' }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, - followers: { type: Number, default: 0 }, - following: { type: Number, default: 0 }, + followerCnt: { type: Number, default: 0 }, + followingCnt: { type: Number, default: 0 }, posts: { type: Number, default: 0 }, likes: { type: Number, default: 0 }, comments: { type: Number, default: 0 }, diff --git a/src/adapters/nostr.js b/src/adapters/nostr.js index d795279..b37b162 100644 --- a/src/adapters/nostr.js +++ b/src/adapters/nostr.js @@ -41,7 +41,9 @@ class NostrAdapter { this.read = this.read.bind(this) this.eventId2note = this.eventId2note.bind(this) this.pubkey2npub = this.pubkey2npub.bind(this) + this.npub2pubkey = this.npub2pubkey.bind(this) this.readGlobalFeed = this.readGlobalFeed.bind(this) + this.getFollowers = this.getFollowers.bind(this) } // Create nostr keys. @@ -140,6 +142,10 @@ class NostrAdapter { return nip19.npubEncode(pubkey) } + npub2pubkey (npub) { + return nip19.decode(npub) + } + // Read the global feed. async readGlobalFeed (inObj = {}) { try { @@ -190,6 +196,57 @@ class NostrAdapter { throw error } } + + async getFollowers (inObj = {}) { + try { + const { pubkey } = inObj + + if (!pubkey || typeof pubkey !== 'string') { + throw new Error('pubkey must be a string!') + } + + const relays = [this.relayWs] + const pool = this.RelayPool(relays) + + const nostrData = new Promise((resolve, reject) => { + const followers = [] + + pool.on('open', (relay) => { + // Query for kind 3 events where the target pubkey appears in tags + // This finds all contact lists that include the target pubkey + relay.subscribe('subid', { + limit: 100, + kinds: [3], + '#p': [pubkey] // Filter by tag 'p' containing the target pubkey + }) + }) + + pool.on('eose', relay => { + relay.close() + resolve(followers) + }) + + pool.on('event', (relay, subId, ev) => { + // Each event represents a contact list from someone who follows the target + // The author of this event is a follower + const follower = { + pubkey: ev.pubkey, + npub: this.pubkey2npub(ev.pubkey), + contactList: ev.tags, + createdAt: ev.created_at + } + + followers.push(follower) + }) + }) + + const followers = await nostrData + return followers + } catch (error) { + console.log(`Error in nostr.js/getFollowers() ${error.message} `) + throw error + } + } } export default NostrAdapter diff --git a/src/controllers/timer-controllers.js b/src/controllers/timer-controllers.js index 3dcc17b..6ece3b9 100644 --- a/src/controllers/timer-controllers.js +++ b/src/controllers/timer-controllers.js @@ -27,6 +27,7 @@ class TimerControllers { this.cleanUsageInterval = 60000 * 60 // 1 hour this.backupUsageInterval = 60000 * 10 // 10 minutes this.newSmAccountsInterval = 60000 * 60 // 60 minutes + this.updateSmAccountsInterval = 60000 * 10 // 10 minutes // Encapsulate dependencies this.config = config @@ -40,6 +41,7 @@ class TimerControllers { this.cleanUsage = this.cleanUsage.bind(this) this.backupUsage = this.backupUsage.bind(this) this.newSmAccounts = this.newSmAccounts.bind(this) + this.updateSmAccounts = this.updateSmAccounts.bind(this) // State this.gcOrdersInt = null @@ -47,6 +49,7 @@ class TimerControllers { this.checkDupOffersInt = null this.loadOffersInt = null this.newSmAccountsInt = null + this.updateSmAccountsInt = null } // Start all the time-based controllers. @@ -58,7 +61,7 @@ class TimerControllers { this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval) this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval) this.newSmAccountsInt = setInterval(this.newSmAccounts, this.newSmAccountsInterval) - + this.updateSmAccountsInt = setInterval(this.updateSmAccounts, this.updateSmAccountsInterval) return true } @@ -70,6 +73,7 @@ class TimerControllers { clearInterval(this.cleanUsageHandle) clearInterval(this.backupUsageHandle) clearInterval(this.newSmAccountsInt) + clearInterval(this.updateSmAccountsInt) } // Garbage Collect the Orders. @@ -178,6 +182,17 @@ class TimerControllers { return false } } + + // Update the Social Media Accounts. + async updateSmAccounts () { + try { + await this.useCases.smAccount.updateSmAccounts() + return true + } catch (err) { + console.error('Error in time-controller.js/updateSmAccounts(): ', err) + return false + } + } } export default TimerControllers diff --git a/src/use-cases/smAccount-use-cases.js b/src/use-cases/smAccount-use-cases.js index e70b484..36081e2 100644 --- a/src/use-cases/smAccount-use-cases.js +++ b/src/use-cases/smAccount-use-cases.js @@ -19,6 +19,7 @@ class smAccountUseCases { this.listAccounts = this.listAccounts.bind(this) this.getAccountByNpub = this.getAccountByNpub.bind(this) this.getAccountByBchAddr = this.getAccountByBchAddr.bind(this) + this.updateSmAccounts = this.updateSmAccounts.bind(this) } // Check for new Social Media Accounts. @@ -101,6 +102,31 @@ class smAccountUseCases { throw err } } + + // Update metadata about social media accounts being tracked. + async updateSmAccounts () { + try { + console.log('Updating Social Media Accounts...') + const SmAccount = await this.adapters.localdb.SmAccount + const accounts = await SmAccount.find({}) + for (let i = 0; i < accounts.length; i++) { + const account = accounts[i] + // console.log(`Account ${i}: `, account) + + // Get the number of accounts following this account + const pubkey = account.pubkey + const followList = await this.adapters.nostr.getFollowers({ pubkey }) + // console.log(`Follow list ${i}: `, JSON.stringify(followList, null, 2)) + + // Update the follower count for this account. + const followerCnt = followList.length + await SmAccount.updateOne({ _id: account._id }, { $set: { followerCnt } }) + } + } catch (err) { + console.error('Error in smAccount-use-cases.js/updateSmAccounts(): ', err) + throw err + } + } } export default smAccountUseCases