Merge pull request #100 from Permissionless-Software-Foundation/trout-smAccount

Created Social Media account and REST endpoint
This commit is contained in:
Chris Troutner
2025-07-02 17:09:59 -07:00
committed by GitHub
10 changed files with 325 additions and 7 deletions
+3 -1
View File
@@ -47,7 +47,8 @@ export default {
useFullStackCash: process.env.USE_FULLSTACKCASH ? true : false,
consumerUrl: process.env.CONSUMER_URL
? process.env.CONSUMER_URL
: 'https://free-bch.fullstack.cash',
// : 'https://free-bch.fullstack.cash',
: 'https://dev-consumer.psfoundation.info',
// : 'https://wa-usa-bch-consumer.fullstackcash.nl',
// P2WDB URL that will accept API calls from the p2wdb npm library.
@@ -184,6 +185,7 @@ export default {
// Nostr
nostrRelay: process.env.NOSTR_RELAY ? process.env.NOSTR_RELAY : 'wss://nostr-relay.psfoundation.info',
nostrTopic: process.env.NOSTR_TOPIC ? process.env.NOSTR_TOPIC : 'bch-dex-test-topic-02',
nostrGlobalFeed: process.env.NOSTR_GLOBAL_FEED ? process.env.NOSTR_GLOBAL_FEED : 'slpdex-socialmedia',
// Account Configuration
disableNewAccounts: process.env.DISABLE_NEW_ACCOUNTS ? true : false,
+2
View File
@@ -8,6 +8,7 @@ import Entries from './models/entry.js'
import Order from './models/order.js'
import Offer from './models/offer.js'
import Usage from './models/usage.js'
import SmAccount from './models/smAccount.js'
class LocalDB {
constructor () {
@@ -17,6 +18,7 @@ class LocalDB {
this.Order = Order
this.Offer = Offer
this.Usage = Usage
this.SmAccount = SmAccount
}
}
+25
View File
@@ -0,0 +1,25 @@
/*
Social Media Account Model.
This model is used to track content creators using the DEX
Nostr social media channel.
*/
import mongoose from 'mongoose'
const SmAccount = new mongoose.Schema({
npub: { type: String, required: true },
bchAddr: { type: String, required: true, default: '' },
pubkey: { type: String, required: true },
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 },
posts: { type: Number, default: 0 },
likes: { type: Number, default: 0 },
comments: { type: Number, default: 0 },
reposts: { type: Number, default: 0 }
})
export default mongoose.model('smAccount', SmAccount)
+59
View File
@@ -12,6 +12,7 @@ class NostrAdapter {
constructor (localConfig = { nostrRelay: '', nostrTopic: '' }) {
this.relayWs = localConfig.nostrRelay //
this.topic = localConfig.nostrTopic
this.globalFeed = localConfig.nostrGlobalFeed
if (!this.relayWs) {
throw new Error(
@@ -39,6 +40,8 @@ class NostrAdapter {
this.post = this.post.bind(this)
this.read = this.read.bind(this)
this.eventId2note = this.eventId2note.bind(this)
this.pubkey2npub = this.pubkey2npub.bind(this)
this.readGlobalFeed = this.readGlobalFeed.bind(this)
}
// Create nostr keys.
@@ -131,6 +134,62 @@ class NostrAdapter {
eventId2note (eventId) {
return nip19.noteEncode(eventId)
}
// Convert a pubkey into a `npubabc..` syntax that Astral expects.
pubkey2npub (pubkey) {
return nip19.npubEncode(pubkey)
}
// Read the global feed.
async readGlobalFeed (inObj = {}) {
try {
const { limit = 10 } = inObj
if (typeof limit !== 'number' || limit < 1) {
throw new Error('Limit must be greater than 0')
}
const relays = [this.relayWs]
const pool = this.RelayPool(relays)
const nostrData = new Promise((resolve, reject) => {
const messages = []
pool.on('open', (relay) => {
// relay.subscribe('REQ', { ids: [eventId] })
relay.subscribe('REQ', { limit, kinds: [1], '#t': [this.globalFeed] })
})
pool.on('eose', (relay) => {
relay.close()
resolve(messages)
})
pool.on('event', (relay, subId, ev) => {
// console.log('ev: ', ev)
const { content, id, tags, pubkey } = ev
const msg = {
content,
id,
tags,
npub: this.pubkey2npub(pubkey),
pubkey
}
messages.push(msg)
})
})
const messages = await nostrData
return messages
} catch (error) {
console.log(`Error in nostr.js/readGlobalFeed() ${error.message} `)
throw error
}
}
}
export default NostrAdapter
+4
View File
@@ -18,6 +18,7 @@ import OfferRouter from './offer/index.js'
import OrderRouter from './order/index.js'
import P2WDBRouter from './p2wdb/index.js'
import UsageRESTController from './usage/index.js'
import SmAccountRouter from './smAccount/index.js'
class RESTControllers {
constructor (localConfig = {}) {
@@ -86,6 +87,9 @@ class RESTControllers {
// Attach the REST API Controllers associated with the /usage route
const usageRESTController = new UsageRESTController(dependencies)
usageRESTController.attach(app)
const smAccountRouter = new SmAccountRouter(dependencies)
smAccountRouter.attach(app)
}
}
@@ -0,0 +1,65 @@
/*
REST API Controller library for the /order route
*/
// const { wlogger } = require('../../../adapters/wlogger')
let _this
class SmAccountRESTControllerLib {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /order REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /order REST Controller.'
)
}
// Encapsulate dependencies
this.SmAccountModel = this.adapters.localdb.SmAccount
// this.userUseCases = this.useCases.user
_this = this
}
// curl -X GET http://localhost:5700/sm/list/all/0
async listAccounts (ctx) {
try {
let page = ctx.params.page
if (!page) page = 0
const offers = await _this.useCases.smAccount.listAccounts(page)
ctx.body = offers
} catch (err) {
console.log('Error in listAccounts REST API handler: ', err)
_this.handleError(ctx, err)
}
}
// DRY error handler
handleError (ctx, err) {
console.log('err', err)
// If an HTTP status is specified by the buisiness logic, use that.
if (err.status) {
if (err.message) {
ctx.throw(err.status, err.message)
} else {
ctx.throw(err.status)
}
} else {
// By default use a 422 error if the HTTP status is not specified.
ctx.throw(422, err.message)
}
}
}
export default SmAccountRESTControllerLib
@@ -0,0 +1,68 @@
/*
REST API library for /order route.
*/
// Public npm libraries.
import Router from 'koa-router'
// Local libraries.
import SmAccountRESTControllerLib from './controller.js'
import Validators from '../middleware/validators.js'
let _this
class SmAccountRouter {
constructor (localConfig = {}) {
// Dependency Injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of Adapters library required when instantiating /sm REST Controller.'
)
}
this.useCases = localConfig.useCases
if (!this.useCases) {
throw new Error(
'Instance of Use Cases library required when instantiating /sm REST Controller.'
)
}
const dependencies = {
adapters: this.adapters,
useCases: this.useCases
}
// Encapsulate dependencies.
this.validators = new Validators()
this.smAccountRESTController = new SmAccountRESTControllerLib(dependencies)
// Instantiate the router and set the base route.
const baseUrl = '/sm'
this.router = new Router({ prefix: baseUrl })
this.listAccounts = this.listAccounts.bind(this)
_this = this
}
attach (app) {
if (!app) {
throw new Error(
'Must pass app object when attached REST API controllers.'
)
}
// Define the routes and attach the controller.
this.router.get('/list/all/:page', _this.smAccountRESTController.listAccounts)
// Attach the Controller routes to the Koa app.
app.use(_this.router.routes())
app.use(_this.router.allowedMethods())
}
async listAccounts (ctx, next) {
// await _this.validators.ensureUser(ctx, next)
await _this.smAccountRESTController.listAccounts(ctx, next)
}
}
export default SmAccountRouter
+19 -6
View File
@@ -26,6 +26,7 @@ class TimerControllers {
// Constants
this.cleanUsageInterval = 60000 * 60 // 1 hour
this.backupUsageInterval = 60000 * 10 // 10 minutes
this.newSmAccountsInterval = 60000 * 60 // 60 minutes
// Encapsulate dependencies
this.config = config
@@ -36,17 +37,16 @@ class TimerControllers {
this.gcOffers = this.gcOffers.bind(this)
this.checkDupOffers = this.checkDupOffers.bind(this)
this.loadOffers = this.loadOffers.bind(this)
// Bind 'this' object to all subfunctions.
// this.exampleTimerFunc = this.exampleTimerFunc.bind(this)
this.cleanUsage = this.cleanUsage.bind(this)
this.backupUsage = this.backupUsage.bind(this)
this.newSmAccounts = this.newSmAccounts.bind(this)
// State
this.gcOrdersInt = null
this.gcOffersInt = null
this.checkDupOffersInt = null
this.loadOffersInt = null
this.cleanUsage = this.cleanUsage.bind(this)
this.backupUsage = this.backupUsage.bind(this)
this.newSmAccountsInt = null
}
// Start all the time-based controllers.
@@ -55,10 +55,9 @@ class TimerControllers {
this.gcOffersInt = setInterval(this.gcOffers, 60000 * 5)
// this.checkDupOffersInt = setInterval(this.checkDupOffers, 60000 * 4.5)
this.loadOffersInt = setInterval(this.loadOffers, 60000 * 2)
// Any new timer control functions can be added here. They will be started
// when the server starts.
this.cleanUsageHandle = setInterval(this.cleanUsage, this.cleanUsageInterval)
this.backupUsageHandle = setInterval(this.backupUsage, this.backupUsageInterval)
this.newSmAccountsInt = setInterval(this.newSmAccounts, this.newSmAccountsInterval)
return true
}
@@ -70,6 +69,7 @@ class TimerControllers {
clearInterval(this.optimizeWalletHandle)
clearInterval(this.cleanUsageHandle)
clearInterval(this.backupUsageHandle)
clearInterval(this.newSmAccountsInt)
}
// Garbage Collect the Orders.
@@ -165,6 +165,19 @@ class TimerControllers {
return false
}
}
// Check for new Social Media Accounts.
async newSmAccounts () {
try {
await this.useCases.smAccount.checkForNewSmAccounts()
return true
} catch (err) {
console.error('Error in time-controller.js/newSmAccounts(): ', err)
// Note: Do not throw an error. This is a top-level function.
return false
}
}
}
export default TimerControllers
+2
View File
@@ -10,6 +10,7 @@ import EntryUseCases from './entry.js'
import OfferUseCases from './offer/index.js'
import OrderUseCases from './order.js'
import { UsageUseCases } from './usage-use-cases.js'
import SmAccountUseCases from './smAccount-use-cases.js'
class UseCases {
constructor (localConfig = {}) {
@@ -27,6 +28,7 @@ class UseCases {
localConfig.order = this.order
this.offer = new OfferUseCases(localConfig)
this.usage = new UsageUseCases(localConfig)
this.smAccount = new SmAccountUseCases(localConfig)
}
// Run any startup Use Cases at the start of the app.
+78
View File
@@ -0,0 +1,78 @@
/*
Social Media Account Use Cases.
*/
class smAccountUseCases {
constructor (localConfig = {}) {
// console.log('User localConfig: ', localConfig)
// Dependency injection.
this.adapters = localConfig.adapters
if (!this.adapters) {
throw new Error(
'Instance of adapters must be passed in when instantiating Order Use Cases library.'
)
}
// Bind 'this' object to all methods
this.checkForNewSmAccounts = this.checkForNewSmAccounts.bind(this)
this.listAccounts = this.listAccounts.bind(this)
}
// Check for new Social Media Accounts.
async checkForNewSmAccounts () {
try {
console.log('Checking for new Social Media Accounts...')
const messages = await this.adapters.nostr.readGlobalFeed()
for (let i = 0; i < messages.length; i++) {
const msg = messages[i]
// const { content, id, tags, npub, pubkey } = msg
const { tags, npub, pubkey } = msg
let bchAddr = null
// console.log('msg: ', JSON.stringify(msg, null, 2))
// Find the 'u' tag if it exists
const uTag = tags.find(tag => tag[0] === 'u')
// If 'u' tag exists and contains a BCH address
if (uTag && uTag[1].includes('bitcoincash:')) {
bchAddr = uTag[1]
// console.log('BCH address found:', bchAddr)
}
const SmAccount = await this.adapters.localdb.SmAccount
if (bchAddr) {
const smAccount = await SmAccount.findOne({ npub })
if (!smAccount) {
console.log(`Creating new Social Media Account with npub: ${npub} and bchAddr: ${bchAddr}`)
const newSmAccount = new SmAccount({ npub, bchAddr, pubkey })
await newSmAccount.save()
} else {
console.log(`Social Media Account with npub: ${npub} already exists`)
}
}
}
return true
} catch (err) {
console.error('Error in smAccount-use-cases.js/checkForNewSmAccounts(): ', err)
throw err
}
}
// List all Social Media Accounts.
async listAccounts (page = 0) {
try {
const SmAccount = await this.adapters.localdb.SmAccount
const accounts = await SmAccount.find({}).skip(page * 10).limit(10)
return accounts
} catch (err) {
console.error('Error in smAccount-use-cases.js/listAccounts(): ', err)
throw err
}
}
}
export default smAccountUseCases