diff --git a/src/config/index.ts b/src/config/index.ts index 7699172..11db304 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -56,6 +56,7 @@ const envVarsSchema = Joi.object() REDIS_PORT: Joi.string().required().description('Redis port'), REDIS_PASSWORD: Joi.string().required().description('Reids password').allow(''), DISCOURSE_EXTRACTION_URL: Joi.string().required().description('Discourse extraction url'), + OCI_BACKEND_URL: Joi.string().required().description('Oci Backend url'), }) .unknown(); @@ -158,4 +159,5 @@ export default { discourse: { extractionURL: envVars.DISCOURSE_EXTRACTION_URL, }, + ociBackendURL: envVars.OCI_BACKEND_URL, }; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index eedcda1..8d09a2a 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -9,6 +9,8 @@ import announcementController from './announcement.controller'; import categoryController from './category.controller'; import moduleController from './module.controller'; import discourseController from './discourse.controller'; +import nftController from './nft.controller'; + export { authController, userController, @@ -21,4 +23,5 @@ export { categoryController, moduleController, discourseController, + nftController, }; diff --git a/src/controllers/nft.controller.ts b/src/controllers/nft.controller.ts new file mode 100644 index 0000000..d284d68 --- /dev/null +++ b/src/controllers/nft.controller.ts @@ -0,0 +1,102 @@ +import { Request, Response } from 'express'; +import { catchAsync } from '../utils'; +import parentLogger from '../config/logger'; +import { moduleService, platformService, ociService } from '../services'; +import { ApiError } from '../utils'; +import { IModule, IPlatform } from '@togethercrew.dev/db'; +import { HydratedDocument } from 'mongoose'; +import * as Neo4j from '../neo4j'; +import { NEO4J_PLATFORM_INFO } from '../constants/neo4j.constant'; +import { SupportedNeo4jPlatforms } from '../types/neo4j.type'; + +const logger = parentLogger.child({ module: 'NftController' }); + +const getReputationScore = catchAsync(async function (req: Request, res: Response) { + const { tokenId, address } = req.params; + const supportedPlatforms = ['discord', 'discourse']; + + let repuationScore; + logger.debug(tokenId, address); + const profiles: Array = await getProfilesOnAllSupportedChains(address); + logger.debug(profiles); + const dynamicNftModule = await moduleService.getModuleByFilter({ 'options.platforms.0.metadata.tokenId': tokenId }); + logger.debug(dynamicNftModule); + + for (let i = 0; i < supportedPlatforms.length; i++) { + const platform = await platformService.getPlatformByFilter({ + name: supportedPlatforms[i], + community: dynamicNftModule?.community, + }); + logger.debug({ i, platform, supportedPlatforms: supportedPlatforms[i] }); + for (let j = 0; j < profiles.length; j++) { + const profile = profiles[j]; + logger.debug({ i, j, profile, supportedPlatforms: supportedPlatforms[i] }); + const temp = platform?.name as SupportedNeo4jPlatforms; + if (profile.profile.provider === supportedPlatforms[i]) { + const reputationScoreQuery = ` + MATCH (:${NEO4J_PLATFORM_INFO[temp].member} {id: "${profile.profile.id}"})-[r:HAVE_METRICS {platformId: "${platform?.id}"}]->(a) + WITH r.date as metrics_date, r.closenessCentrality as memberScore + ORDER BY metrics_date DESC + LIMIT 1 + MATCH (user:${NEO4J_PLATFORM_INFO[temp].member})-[user_r:HAVE_METRICS {platformId: "${platform?.id}", date: metrics_date}]->(user) + WITH memberScore, MAX(user_r.closenessCentrality) as maxScore + RETURN memberScore / maxScore AS reputation_score + `; + + const neo4jData = await Neo4j.read(reputationScoreQuery); + const { records } = neo4jData; + logger.debug(records); + + const reputationScoreResponse = records[0]; + + logger.debug(reputationScoreResponse); + + const { _fieldLookup, _fields } = reputationScoreResponse as unknown as { + _fieldLookup: Record; + _fields: number[]; + }; + + repuationScore = _fields[_fieldLookup['reputation_score']]; + logger.debug(repuationScore); + } + } + } + return repuationScore; +}); + +async function getProfilesOnAllSupportedChains(address: string) { + let profiles: Array = []; + const supportedChainIds = [11155111]; + for (let i = 0; i < supportedChainIds.length; i++) { + const chainProfiles = await ociService.getProfiles(address, supportedChainIds[i]); + profiles = profiles.concat(chainProfiles); + } + return profiles; +} + +function shouldProfilesExist(profiles: Array) { + if (profiles.length < 0) { + throw new ApiError(400, 'User has no any onchain profiles'); + } +} + +function shouldDynamicNftModuleExist(dynamicNftModule: HydratedDocument | null) { + if (!dynamicNftModule) { + throw new ApiError(400, "There's not any assoicated dynamic nft module to the token Id"); + } +} + +function shouldPlatformExist(platform: HydratedDocument | null) { + if (!platform) { + throw new ApiError(400, "There's not any platform connected for requested platform"); + } +} + +function shouldProfileExist(profile: any) { + if (!profile) { + throw new ApiError(400, "There's not any user oncahin profile for requested platform"); + } +} +export default { + getReputationScore, +}; diff --git a/src/routes/v1/index.ts b/src/routes/v1/index.ts index 2d010e1..bfd2136 100644 --- a/src/routes/v1/index.ts +++ b/src/routes/v1/index.ts @@ -11,6 +11,7 @@ import announcementRoute from './announcement.route'; import categoryRoute from './category.route'; import moduleRoute from './module.route'; import discourseRoute from './discourse.route'; +import nftRoute from './nft.route'; const router = express.Router(); const defaultRoutes = [ @@ -63,6 +64,10 @@ const defaultRoutes = [ path: '/discourse', route: discourseRoute, }, + { + path: '/nft', + route: nftRoute, + }, ]; defaultRoutes.forEach((route) => { diff --git a/src/routes/v1/nft.route.ts b/src/routes/v1/nft.route.ts new file mode 100644 index 0000000..c1a4b0f --- /dev/null +++ b/src/routes/v1/nft.route.ts @@ -0,0 +1,14 @@ +import express from 'express'; +import { nftController } from '../../controllers'; +import { nftValidation } from '../../validations'; +import { validate } from '../../middlewares'; +const router = express.Router(); + +// Routes +router.post( + '/:tokenId/:address/reputation-score', + validate(nftValidation.getReputationScore), + nftController.getReputationScore, +); + +export default router; diff --git a/src/services/index.ts b/src/services/index.ts index cfb20ce..d2d1cd5 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -14,6 +14,7 @@ import googleService from './google'; import githubService from './github'; import notionService from './notion'; import discourseService from './discourse'; +import ociService from './oci.service'; export { userService, authService, @@ -31,4 +32,5 @@ export { githubService, notionService, discourseService, + ociService, }; diff --git a/src/services/nft.service.ts b/src/services/nft.service.ts new file mode 100644 index 0000000..01afec2 --- /dev/null +++ b/src/services/nft.service.ts @@ -0,0 +1,35 @@ +import { HydratedDocument, Types } from 'mongoose'; +import httpStatus from 'http-status'; +import { Platform, IPlatform } from '@togethercrew.dev/db'; +import ApiError from '../utils/ApiError'; +import sagaService from './saga.service'; +import discourseService from './discourse'; +import { Snowflake } from 'discord.js'; +import { analyzerAction, analyzerWindow } from '../config/analyzer.statics'; +import { PlatformNames } from '@togethercrew.dev/db'; + +/** + * get reputation score + * @param {IPlatform} PlatformBody + * @returns {Promise>} + */ +const getReputationScore = async (PlatformBody: IPlatform): Promise> => { + if (PlatformBody.name === PlatformNames.Discord || PlatformBody.name === PlatformNames.Discourse) { + if (PlatformBody.metadata) { + PlatformBody.metadata = { + action: analyzerAction, + window: analyzerWindow, + ...PlatformBody.metadata, + }; + } + } + const platform = await Platform.create(PlatformBody); + if (PlatformBody.name === PlatformNames.Discord) { + await sagaService.createAndStartFetchMemberSaga(platform._id); + } + return platform; +}; + +export default { + getReputationScore, +}; diff --git a/src/services/oci.service.ts b/src/services/oci.service.ts new file mode 100644 index 0000000..f31f5ce --- /dev/null +++ b/src/services/oci.service.ts @@ -0,0 +1,29 @@ +import fetch from 'node-fetch'; +import config from '../config'; +import { ApiError } from '../utils'; +import parentLogger from '../config/logger'; + +const logger = parentLogger.child({ module: 'OciService' }); + +async function getProfiles(address: string, chainId: number) { + try { + logger.debug(`${config.ociBackendURL}/oci/profiles/${chainId}/${address}`); + const response = await fetch(`${config.ociBackendURL}/api/v1/oci/profiles/${chainId}/${address}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); + if (response.ok) { + return await response.json(); + } else { + const errorResponse = await response.text(); + throw new Error(errorResponse); + } + } catch (error: any) { + logger.error(error, 'Failed to get profiles from oci backend'); + throw new ApiError(590, 'Failed to get profiles from oci backend '); + } +} + +export default { + getProfiles, +}; diff --git a/src/services/platform.service.ts b/src/services/platform.service.ts index cd2260b..b70dfef 100644 --- a/src/services/platform.service.ts +++ b/src/services/platform.service.ts @@ -1,4 +1,4 @@ -import { HydratedDocument, Types } from 'mongoose'; +import { HydratedDocument, Types, FilterQuery } from 'mongoose'; import httpStatus from 'http-status'; import { Platform, IPlatform } from '@togethercrew.dev/db'; import ApiError from '../utils/ApiError'; @@ -38,7 +38,7 @@ const createPlatform = async (PlatformBody: IPlatform): Promise { +const queryPlatforms = async (filter: FilterQuery, options: object) => { return Platform.paginate(filter, options); }; @@ -47,7 +47,7 @@ const queryPlatforms = async (filter: object, options: object) => { * @param {Object} filter - Mongo filter * @returns {Promise | null>} */ -const getPlatformByFilter = async (filter: object): Promise | null> => { +const getPlatformByFilter = async (filter: FilterQuery): Promise | null> => { return Platform.findOne(filter); }; diff --git a/src/validations/index.ts b/src/validations/index.ts index 60f35d8..6636c8c 100644 --- a/src/validations/index.ts +++ b/src/validations/index.ts @@ -8,6 +8,8 @@ import platformValidation from './platform.validation'; import announcementValidation from './announcement.validation'; import moduleValidation from './module.validation'; import discourseValidation from './discourse.validation'; +import nftValidation from './nft.validation'; + export { authValidation, guildValidation, @@ -19,4 +21,5 @@ export { announcementValidation, moduleValidation, discourseValidation, + nftValidation, }; diff --git a/src/validations/nft.validation.ts b/src/validations/nft.validation.ts new file mode 100644 index 0000000..be86127 --- /dev/null +++ b/src/validations/nft.validation.ts @@ -0,0 +1,15 @@ +import Joi from 'joi'; +import { PlatformNames } from '@togethercrew.dev/db'; + +const getReputationScore = { + params: Joi.object().keys({ + tokenId: Joi.string().required(), + address: Joi.string() + .regex(/^0x[a-fA-F0-9]{40}$/) + .required(), + }), +}; + +export default { + getReputationScore, +};