Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -158,4 +159,5 @@ export default {
discourse: {
extractionURL: envVars.DISCOURSE_EXTRACTION_URL,
},
ociBackendURL: envVars.OCI_BACKEND_URL,
};
3 changes: 3 additions & 0 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,4 +23,5 @@ export {
categoryController,
moduleController,
discourseController,
nftController,
};
102 changes: 102 additions & 0 deletions src/controllers/nft.controller.ts
Original file line number Diff line number Diff line change
@@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential undefined 'repuationScore' before returning

If none of the profiles match the supported platforms, repuationScore may remain undefined, leading to unexpected behavior when returned. Initialize repuationScore to a default value and ensure it is always defined before returning.

Apply the following diff to initialize repuationScore and handle the response correctly:

- let repuationScore;
+ let repuationScore = 0;

...

- return repuationScore;
+ res.json({ reputationScore: repuationScore });

Also applies to: 64-64

logger.debug(tokenId, address);
const profiles: Array<any> = 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
`;
Comment on lines +36 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent injection attacks by parameterizing Neo4j queries

Directly interpolating variables into Neo4j query strings can lead to injection vulnerabilities if any variables contain malicious input. Use parameterized queries to securely pass variables to the query.

Refactor the code to use parameterized queries:

- 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 reputationScoreQuery = `
+   MATCH (:${NEO4J_PLATFORM_INFO[temp].member} {id: $profileId})-[r:HAVE_METRICS {platformId: $platformId}]->(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: $platformId, 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 neo4jData = await Neo4j.read(reputationScoreQuery, {
+   profileId: profile.profile.id,
+   platformId: platform?.id,
+ });

This refactoring ensures that variables are passed safely to the query, mitigating the risk of injection attacks.

Committable suggestion skipped: line range outside the PR's diff.


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<string, number>;
_fields: number[];
};
Comment on lines +54 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid using internal properties of Neo4j response objects

Accessing internal properties like _fieldLookup and _fields is not recommended, as they are subject to change and not part of the public API. Use the official Neo4j driver methods to extract data from the result records.

Refactor the code to use public methods:

- const { _fieldLookup, _fields } = reputationScoreResponse as unknown as {
-   _fieldLookup: Record<string, number>;
-   _fields: number[];
- };
- repuationScore = _fields[_fieldLookup['reputation_score']];
+ const reputationScoreRecord = reputationScoreResponse.get('reputation_score');
+ repuationScore = reputationScoreRecord;

This approach uses the get method to safely access the desired field from the record.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { _fieldLookup, _fields } = reputationScoreResponse as unknown as {
_fieldLookup: Record<string, number>;
_fields: number[];
};
const reputationScoreRecord = reputationScoreResponse.get('reputation_score');
repuationScore = reputationScoreRecord;


repuationScore = _fields[_fieldLookup['reputation_score']];
logger.debug(repuationScore);
}
}
}
return repuationScore;
});

async function getProfilesOnAllSupportedChains(address: string) {
let profiles: Array<any> = [];
const supportedChainIds = [11155111];
for (let i = 0; i < supportedChainIds.length; i++) {
const chainProfiles = await ociService.getProfiles(address, supportedChainIds[i]);
profiles = profiles.concat(chainProfiles);
Comment on lines +71 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential errors from OCI service calls

The call to ociService.getProfiles might fail or throw an exception. It's important to handle such errors to prevent the application from crashing.

Add error handling for the OCI service call:

for (let i = 0; i < supportedChainIds.length; i++) {
- const chainProfiles = await ociService.getProfiles(address, supportedChainIds[i]);
+ let chainProfiles;
+ try {
+   chainProfiles = await ociService.getProfiles(address, supportedChainIds[i]);
+ } catch (error) {
+   logger.error(`Failed to get profiles for chain ID ${supportedChainIds[i]}:`, error);
+   continue;
+ }
  profiles = profiles.concat(chainProfiles);
}

This ensures that a failure in one chain does not halt the entire process.

Committable suggestion skipped: line range outside the PR's diff.

}
return profiles;
}

function shouldProfilesExist(profiles: Array<any>) {
if (profiles.length < 0) {
throw new ApiError(400, 'User has no any onchain profiles');
}
}

function shouldDynamicNftModuleExist(dynamicNftModule: HydratedDocument<IModule> | null) {
if (!dynamicNftModule) {
throw new ApiError(400, "There's not any assoicated dynamic nft module to the token Id");
}
}

function shouldPlatformExist(platform: HydratedDocument<IPlatform> | 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,
};
5 changes: 5 additions & 0 deletions src/routes/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -63,6 +64,10 @@ const defaultRoutes = [
path: '/discourse',
route: discourseRoute,
},
{
path: '/nft',
route: nftRoute,
},
];

defaultRoutes.forEach((route) => {
Expand Down
14 changes: 14 additions & 0 deletions src/routes/v1/nft.route.ts
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -31,4 +32,5 @@ export {
githubService,
notionService,
discourseService,
ociService,
};
35 changes: 35 additions & 0 deletions src/services/nft.service.ts
Original file line number Diff line number Diff line change
@@ -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<HydratedDocument<IPlatform>>}
*/
const getReputationScore = async (PlatformBody: IPlatform): Promise<HydratedDocument<IPlatform>> => {
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,
};
29 changes: 29 additions & 0 deletions src/services/oci.service.ts
Original file line number Diff line number Diff line change
@@ -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,
};
6 changes: 3 additions & 3 deletions src/services/platform.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -38,7 +38,7 @@ const createPlatform = async (PlatformBody: IPlatform): Promise<HydratedDocument
* @param {number} [options.limit] - Maximum number of results per page (default = 10)
* @param {number} [options.page] - Current page (default = 1)
*/
const queryPlatforms = async (filter: object, options: object) => {
const queryPlatforms = async (filter: FilterQuery<IPlatform>, options: object) => {
return Platform.paginate(filter, options);
};

Expand All @@ -47,7 +47,7 @@ const queryPlatforms = async (filter: object, options: object) => {
* @param {Object} filter - Mongo filter
* @returns {Promise<HydratedDocument<IPlatform> | null>}
*/
const getPlatformByFilter = async (filter: object): Promise<HydratedDocument<IPlatform> | null> => {
const getPlatformByFilter = async (filter: FilterQuery<IPlatform>): Promise<HydratedDocument<IPlatform> | null> => {
return Platform.findOne(filter);
};

Expand Down
3 changes: 3 additions & 0 deletions src/validations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -19,4 +21,5 @@ export {
announcementValidation,
moduleValidation,
discourseValidation,
nftValidation,
};
15 changes: 15 additions & 0 deletions src/validations/nft.validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Joi from 'joi';
import { PlatformNames } from '@togethercrew.dev/db';

Comment on lines +1 to +3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove unused import PlatformNames

The PlatformNames import from '@togethercrew.dev/db' is not used in this file.

import Joi from 'joi';
-import { PlatformNames } from '@togethercrew.dev/db';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import Joi from 'joi';
import { PlatformNames } from '@togethercrew.dev/db';
import Joi from 'joi';

const getReputationScore = {
params: Joi.object().keys({
tokenId: Joi.string().required(),
address: Joi.string()
.regex(/^0x[a-fA-F0-9]{40}$/)
.required(),
}),
};

export default {
getReputationScore,
};