Skip to content

Commit

Permalink
Merge branch 'development' of https://github.com/ubiquibot/conversati…
Browse files Browse the repository at this point in the history
…on-rewards into development
  • Loading branch information
EresDev committed Sep 21, 2024
2 parents 2da6b86 + 8227710 commit 47e7df8
Show file tree
Hide file tree
Showing 37 changed files with 11,613 additions and 15,661 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Here is a possible valid configuration to enable this plugin. See [these files](
```yaml
plugin: ubiquibot/conversation-rewards
with:
logLevel: "info"
evmNetworkId: 100
evmPrivateEncrypted: "encrypted-key"
erc20RewardToken: "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"
Expand Down Expand Up @@ -153,3 +154,32 @@ with:
post: true
debug: false
```
## How to encrypt the `evmPrivateEncrypted` parameter

Partner private key (`evmPrivateEncrypted` config param in `conversation-rewards` plugin) supports 2 formats:
1. `PRIVATE_KEY:GITHUB_OWNER_ID`
2. `PRIVATE_KEY:GITHUB_OWNER_ID:GITHUB_REPOSITORY_ID`

Here `GITHUB_OWNER_ID` can be:
1. Github organization id (if ubiquibot is used within an organization)
2. Github user id (if ubiquibot is simply installed in a user's repository)

Format `PRIVATE_KEY:GITHUB_OWNER_ID` restricts in which particular organization (or user related repositories)
this private key can be used. It can be set either in the organization wide config either in the repository wide one.

Format `PRIVATE_KEY:GITHUB_OWNER_ID:GITHUB_REPOSITORY_ID` restricts organization (or user related repositories) and a particular repository where private key is allowed to be used.

How to encrypt for you local organization for testing purposes:
1. Get your organization (or user) id
```
curl -H "Accept: application/json" -H "Authorization: token GITHUB_PAT_TOKEN" https://api.github.com/orgs/ubiquity
```
2. Open https://keygen.ubq.fi/
3. Click "Generate" to create a new `x25519_PRIVATE_KEY` (which will be used in the `conversation-rewards` plugin to decrypt encrypted wallet private key)
4. Input a string in the format `PRIVATE_KEY:GITHUB_OWNER_ID` in the `PLAIN_TEXT` UI text input where:
- `PRIVATE_KEY`: your ethereum wallet private key without the `0x` prefix
- `GITHUB_OWNER_ID`: your github organization id or user id (which you got from step 1)
5. Click "Encrypt" to get an encrypted value in the `CIPHER_TEXT` field
6. Set the encrypted text (from step 5) in the `evmPrivateEncrypted` config parameter
7. Set `X25519_PRIVATE_KEY` environment variable in github secrets of your forked instance of the `conversation-rewards` plugin
2 changes: 1 addition & 1 deletion src/configuration/content-evaluator-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const openAiType = Type.Object(
/**
* AI model to use for comment evaluation.
*/
model: Type.String({ default: "gpt-4o" }),
model: Type.String({ default: "gpt-4o-2024-08-06" }),
/**
* Specific endpoint to send the comments to.
*/
Expand Down
1 change: 1 addition & 0 deletions src/configuration/formatting-evaluator-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export const formattingEvaluatorConfigurationType = Type.Object(
],
}
),
wordCountExponent: Type.Number({ default: 0.85 }),
},
{ default: {} }
);
Expand Down
2 changes: 2 additions & 0 deletions src/configuration/incentives.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { StaticDecode, Type as T } from "@sinclair/typebox";
import { LOG_LEVEL } from "@ubiquity-dao/ubiquibot-logger";
import { StandardValidator } from "typebox-validators";
import { contentEvaluatorConfigurationType } from "./content-evaluator-config";
import { dataCollectionConfigurationType } from "./data-collection-config";
Expand All @@ -10,6 +11,7 @@ import { userExtractorConfigurationType } from "./user-extractor-config";

export const incentivesConfigurationSchema = T.Object(
{
logLevel: T.Enum(LOG_LEVEL, { default: LOG_LEVEL.INFO }),
/**
* Network ID to run in, default to 100
*/
Expand Down
4 changes: 3 additions & 1 deletion src/github-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { RestEndpointMethodTypes } from "@octokit/rest";

export type GitHubIssue = RestEndpointMethodTypes["issues"]["get"]["response"]["data"];
export type GitHubPullRequest = RestEndpointMethodTypes["pulls"]["get"]["response"]["data"];
export type GitHubIssueComment = RestEndpointMethodTypes["issues"]["listComments"]["response"]["data"][0];
export type GitHubIssueComment = RestEndpointMethodTypes["issues"]["listComments"]["response"]["data"][0] & {
isMinimized?: boolean;
};
export type GitHubIssueEvent = RestEndpointMethodTypes["issues"]["listEvents"]["response"]["data"][0];
export type GitHubTimelineEvent = RestEndpointMethodTypes["issues"]["listEventsForTimeline"]["response"]["data"][0];
export type GitHubRepository = RestEndpointMethodTypes["repos"]["get"]["response"]["data"];
Expand Down
21 changes: 21 additions & 0 deletions src/helpers/get-comment-details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { GitHubIssueComment } from "../github-types";
import { getOctokitInstance } from "../octokit";
import { IssueComment } from "@octokit/graphql-schema";
import { QUERY_COMMENT_DETAILS } from "../types/requests";

export async function getMinimizedCommentStatus(comments: GitHubIssueComment[]) {
const octokit = getOctokitInstance();
const commentsData = await octokit.graphql<{ nodes?: IssueComment[] }>(QUERY_COMMENT_DETAILS, {
node_ids: comments.map((o) => o.node_id),
});

if (commentsData.nodes?.length) {
for (const commentNode of commentsData.nodes) {
const comment = comments.find((o) => o.node_id === commentNode.id);
// For each comment we add the 'isMinimized' info, which corresponds to a collapsed comment
if (comment) {
comment.isMinimized = commentNode.isMinimized;
}
}
}
}
3 changes: 2 additions & 1 deletion src/helpers/logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Logs } from "@ubiquity-dao/ubiquibot-logger";
import configuration from "../configuration/config-reader";

const logger = new Logs("debug");
const logger = new Logs(configuration.logLevel);

export default logger;
19 changes: 19 additions & 0 deletions src/helpers/result-replacer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,22 @@ export function typeReplacer(key: string, value: string | number) {
}
return value;
}

export function removeKeyFromObject<T extends Record<string, unknown>>(obj: T, keyToRemove: string): T {
if (Array.isArray(obj)) {
return obj.map((item) => removeKeyFromObject(item, keyToRemove)) as unknown as T;
}
if (obj !== null && typeof obj === "object") {
const newObj = {} as Record<string, unknown>;

Object.keys(obj).forEach((key) => {
if (key !== keyToRemove) {
newObj[key] = removeKeyFromObject(obj[key] as T, keyToRemove);
}
});

return newObj as T;
}

return obj;
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default run()
return result;
})
.catch(async (e) => {
const errorMessage = logger.error(`Failed to run comment evaluation. ${e.logMessage?.raw || e}`, e);
const errorMessage = logger.error(`Failed to run comment evaluation. ${e?.logMessage?.raw || e}`, e);
try {
await githubCommentModuleInstance.postComment(
`${errorMessage?.logMessage.diff}\n<!--\n${getGithubWorkflowRunUrl()}\n${JSON.stringify(errorMessage?.metadata, null, 2)}\n-->`
Expand Down
8 changes: 8 additions & 0 deletions src/parser/data-purge-module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Value } from "@sinclair/typebox/value";
import configuration from "../configuration/config-reader";
import { DataPurgeConfiguration, dataPurgeConfigurationType } from "../configuration/data-purge-config";
import logger from "../helpers/logger";
import { IssueActivity } from "../issue-activity";
import { Module, Result } from "./processor";
import { GitHubPullRequestReviewComment } from "../github-types";
Expand All @@ -21,12 +22,19 @@ export class DataPurgeModule implements Module {

async transform(data: Readonly<IssueActivity>, result: Result) {
for (const comment of data.allComments) {
// Skips comments if they are minimized
if ("isMinimized" in comment && comment.isMinimized) {
logger.debug("Skipping hidden comment", { comment });
continue;
}
if (comment.body && comment.user?.login && result[comment.user.login]) {
const newContent = comment.body
// Remove quoted text
.replace(/^>.*$/gm, "")
// Remove commands such as /start
.replace(/^\/.+/g, "")
// Remove HTML comments
.replace(/<!--[\s\S]*?-->/g, "")
// Keep only one new line needed by markdown-it package to convert to html
.replace(/\n\s*\n/g, "\n")
.trim();
Expand Down
80 changes: 58 additions & 22 deletions src/parser/formatting-evaluator-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class FormattingEvaluatorModule implements Module {
configuration.incentives.formattingEvaluator;
private readonly _md = new MarkdownIt();
private readonly _multipliers: { [k: number]: Multiplier } = {};
private readonly _wordCountExponent: number;

_getEnumValue(key: CommentType) {
let res = 0;
Expand All @@ -46,6 +47,7 @@ export class FormattingEvaluatorModule implements Module {
};
}, {});
}
this._wordCountExponent = this._configuration?.wordCountExponent ?? 0.85;
}

async transform(data: Readonly<IssueActivity>, result: Result) {
Expand All @@ -54,23 +56,9 @@ export class FormattingEvaluatorModule implements Module {
const comments = currentElement.comments || [];
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
// Count with html elements if any, otherwise just treat it as plain text
const { formatting } = this._getFormattingScore(comment);
const multiplierFactor = this._multipliers?.[comment.type] ?? { multiplier: 0 };
const formattingTotal = formatting
? Object.values(formatting).reduce((acc, curr) => {
let sum = new Decimal(0);
for (const symbol of Object.keys(curr.symbols)) {
sum = sum.add(
new Decimal(curr.symbols[symbol].count)
.mul(curr.symbols[symbol].multiplier)
.mul(multiplierFactor.multiplier)
.mul(curr.score)
);
}
return acc.add(sum);
}, new Decimal(0))
: new Decimal(0);
const formattingTotal = this._calculateFormattingTotal(formatting, multiplierFactor).toDecimalPlaces(2);
comment.score = {
...comment.score,
formatting: {
Expand All @@ -84,6 +72,33 @@ export class FormattingEvaluatorModule implements Module {
return result;
}

private _calculateFormattingTotal(
formatting: ReturnType<typeof this._getFormattingScore>["formatting"],
multiplierFactor: Multiplier
): Decimal {
if (!formatting) return new Decimal(0);

return Object.values(formatting).reduce((acc, curr) => {
let sum = new Decimal(0);

for (const symbol of Object.keys(curr.symbols)) {
const count = new Decimal(curr.symbols[symbol].count);
const symbolMultiplier = new Decimal(curr.symbols[symbol].multiplier);
const formattingElementScore = new Decimal(curr.score);
const exponent = this._wordCountExponent;

sum = sum.add(
count
.pow(exponent) // (count^exponent)
.mul(symbolMultiplier) // symbol multiplier
.mul(formattingElementScore) // comment type multiplier
.mul(multiplierFactor.multiplier) // formatting element score
);
}
return acc.add(sum);
}, new Decimal(0));
}

get enabled(): boolean {
if (!Value.Check(formattingEvaluatorConfigurationType, this._configuration)) {
console.warn("Invalid / missing configuration detected for FormattingEvaluatorModule, disabling.");
Expand All @@ -93,7 +108,9 @@ export class FormattingEvaluatorModule implements Module {
}

_getFormattingScore(comment: GithubCommentScore) {
const html = this._md.render(comment.content);
// Change the \r to \n to fix markup interpretation
const html = this._md.render(comment.content.replaceAll("\r", "\n"));
logger.debug("Will analyze formatting for the current content", { comment: comment.content, html });
const temp = new JSDOM(html);
if (temp.window.document.body) {
const res = this.classifyTagsWithWordCount(temp.window.document.body, comment.type);
Expand All @@ -103,7 +120,7 @@ export class FormattingEvaluatorModule implements Module {
}
}

_countWords(regexes: FormattingEvaluatorConfiguration["multipliers"][0]["rewards"]["regex"], text: string) {
_countSymbols(regexes: FormattingEvaluatorConfiguration["multipliers"][0]["rewards"]["regex"], text: string) {
const counts: { [p: string]: { count: number; multiplier: number } } = {};
for (const [regex, multiplier] of Object.entries(regexes)) {
const match = text.trim().match(new RegExp(regex, "g"));
Expand All @@ -124,17 +141,36 @@ export class FormattingEvaluatorModule implements Module {

for (const element of elements) {
const tagName = element.tagName.toLowerCase();
const wordCount = this._countWords(this._multipliers[commentType].regex, element.textContent || "");
// We cannot use textContent otherwise we would duplicate counts, so instead we extract text nodes
const textNodes = Array.from(element?.childNodes || []).filter((node) => node.nodeType === 3);
const innerText = textNodes
.map((node) => node.nodeValue?.trim())
.join(" ")
.trim();
const symbols = this._countSymbols(this._multipliers[commentType].regex, innerText);
let score = 0;
if (this._multipliers[commentType]?.html[tagName] !== undefined) {
score = this._multipliers[commentType].html[tagName];
} else {
logger.error(`Could not find multiplier for comment [${commentType}], <${tagName}>`);
}
tagWordCount[tagName] = {
symbols: wordCount,
score,
};
logger.debug("Tag content results", { tagName, symbols, text: element.textContent });
// If we already had that tag included in the result, merge them and update total count
if (Object.keys(tagWordCount).includes(tagName)) {
for (const [k, v] of Object.entries(symbols)) {
if (Object.keys(tagWordCount[tagName].symbols).includes(k)) {
tagWordCount[tagName].symbols[k] = {
...tagWordCount[tagName].symbols[k],
count: tagWordCount[tagName].symbols[k].count + v.count,
};
}
}
} else {
tagWordCount[tagName] = {
symbols: symbols,
score,
};
}
}

return tagWordCount;
Expand Down
Loading

0 comments on commit 47e7df8

Please sign in to comment.