diff --git a/config/env/common.js b/config/env/common.js index a3add07..9f55345 100644 --- a/config/env/common.js +++ b/config/env/common.js @@ -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, diff --git a/src/adapters/localdb/index.js b/src/adapters/localdb/index.js index f725b44..df8df8e 100644 --- a/src/adapters/localdb/index.js +++ b/src/adapters/localdb/index.js @@ -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 } } diff --git a/src/adapters/localdb/models/smAccount.js b/src/adapters/localdb/models/smAccount.js new file mode 100644 index 0000000..e9ef94e --- /dev/null +++ b/src/adapters/localdb/models/smAccount.js @@ -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) diff --git a/src/adapters/nostr.js b/src/adapters/nostr.js index 07bdb63..d795279 100644 --- a/src/adapters/nostr.js +++ b/src/adapters/nostr.js @@ -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 diff --git a/src/controllers/rest-api/index.js b/src/controllers/rest-api/index.js index e23c7a2..7d90ab2 100644 --- a/src/controllers/rest-api/index.js +++ b/src/controllers/rest-api/index.js @@ -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) } } diff --git a/src/controllers/rest-api/smAccount/controller.js b/src/controllers/rest-api/smAccount/controller.js new file mode 100644 index 0000000..7ba1e43 --- /dev/null +++ b/src/controllers/rest-api/smAccount/controller.js @@ -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 diff --git a/src/controllers/rest-api/smAccount/index.js b/src/controllers/rest-api/smAccount/index.js new file mode 100644 index 0000000..349877e --- /dev/null +++ b/src/controllers/rest-api/smAccount/index.js @@ -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 diff --git a/src/controllers/timer-controllers.js b/src/controllers/timer-controllers.js index ceb813a..3dcc17b 100644 --- a/src/controllers/timer-controllers.js +++ b/src/controllers/timer-controllers.js @@ -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 diff --git a/src/use-cases/index.js b/src/use-cases/index.js index 1383eca..ae123a3 100644 --- a/src/use-cases/index.js +++ b/src/use-cases/index.js @@ -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. diff --git a/src/use-cases/smAccount-use-cases.js b/src/use-cases/smAccount-use-cases.js new file mode 100644 index 0000000..d340d36 --- /dev/null +++ b/src/use-cases/smAccount-use-cases.js @@ -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