Compare commits

..
4 Commits
Author SHA1 Message Date
Chris Troutner 578aade39b Merge pull request #102 from Permissionless-Software-Foundation/ct-unstable
fix(metrics): Adding timer-controller that updates metrics of SM Acco…
2025-07-14 19:30:21 -07:00
Chris Troutner b09890a527 fix(metrics): Adding timer-controller that updates metrics of SM Accounts 2025-07-14 19:16:00 -07:00
Chris Troutner a32bd840b9 Merge pull request #101 from Permissionless-Software-Foundation/ct-unstable
fix(smAccount): Adding GET endpoints
2025-07-08 09:24:11 -07:00
Chris Troutner 0bdc232938 fix(smAccount): Adding GET endpoints 2025-07-08 08:58:31 -07:00
6 changed files with 203 additions and 3 deletions
+2 -2
View File
@@ -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 },
+57
View File
@@ -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
@@ -29,6 +29,16 @@ class SmAccountRESTControllerLib {
_this = this
}
/**
* @api {get} /sm/list/all/{page} List all Social Media Accounts
* @apiPermission public
* @apiName ListAccounts
* @apiGroup REST Social Media Accounts
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/list/all/0
*
*/
// curl -X GET http://localhost:5700/sm/list/all/0
async listAccounts (ctx) {
try {
@@ -44,6 +54,68 @@ class SmAccountRESTControllerLib {
}
}
/**
* @api {get} /sm/npub/{npub} Get Social Media Account by Npub
* @apiPermission public
* @apiName GetAccountByNpub
* @apiGroup REST Social Media Accounts
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/npub/npub1y3xq402pu9aqms3khetnt5gzm54t63pzcla56wwfmwshde0ws77qkff5gc
*
*/
// curl -X GET http://localhost:5700/sm/npub/npub1y3xq402pu9aqms3khetnt5gzm54t63pzcla56wwfmwshde0ws77qkff5gc
async getAccountByNpub (ctx) {
try {
const npub = ctx.params.npub
if (!npub) {
ctx.throw(400, 'Npub is required')
}
const account = await _this.useCases.smAccount.getAccountByNpub(npub)
if (!account) {
ctx.throw(404, 'Account not found')
}
ctx.body = account
} catch (err) {
// console.log('Error in getAccountByNpub REST API handler: ', err)
_this.handleError(ctx, err)
}
}
/**
* @api {get} /sm/bchAddr/{bchAddr} Get Social Media Account by BCH address
* @apiPermission public
* @apiName GetAccountByBchAddr
* @apiGroup REST Social Media Accounts
*
* @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5700/sm/bchAddr/bitcoincash:qz6lf6gpmn3secx73g7ucfcgy8mrh3sz2y2ylk643x
*
*/
// curl -X GET http://localhost:5700/sm/bchAddr/bitcoincash:qz6lf6gpmn3secx73g7ucfcgy8mrh3sz2y2ylk643x
async getAccountByBchAddr (ctx) {
try {
const bchAddr = ctx.params.bchAddr
if (!bchAddr) {
ctx.throw(400, 'BCH address is required')
}
const account = await _this.useCases.smAccount.getAccountByBchAddr(bchAddr)
if (!account) {
ctx.throw(404, 'Account not found')
}
ctx.body = account
} catch (err) {
// console.log('Error in getAccountByBchAddr REST API handler: ', err)
_this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
console.log('err', err)
@@ -53,6 +53,8 @@ class SmAccountRouter {
// Define the routes and attach the controller.
this.router.get('/list/all/:page', _this.smAccountRESTController.listAccounts)
this.router.get('/npub/:npub', _this.smAccountRESTController.getAccountByNpub)
this.router.get('/bchAddr/:bchAddr', _this.smAccountRESTController.getAccountByBchAddr)
// Attach the Controller routes to the Koa app.
app.use(_this.router.routes())
+16 -1
View File
@@ -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
+54
View File
@@ -17,6 +17,9 @@ class smAccountUseCases {
// Bind 'this' object to all methods
this.checkForNewSmAccounts = this.checkForNewSmAccounts.bind(this)
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.
@@ -73,6 +76,57 @@ class smAccountUseCases {
throw err
}
}
// Get a Social Media Account by Npub.
async getAccountByNpub (npub) {
try {
const SmAccount = await this.adapters.localdb.SmAccount
const account = await SmAccount.findOne({ npub })
return account
} catch (err) {
console.error('Error in smAccount-use-cases.js/getAccountByNpub(): ', err)
throw err
}
}
// Get a Social Media Account by BCH address.
async getAccountByBchAddr (bchAddr) {
try {
const SmAccount = await this.adapters.localdb.SmAccount
const account = await SmAccount.findOne({ bchAddr })
console.log('Account found: ', account)
return account
} catch (err) {
console.error('Error in smAccount-use-cases.js/getAccountByBchAddr(): ', err)
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