Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: /award command & depending services #13

Merged
merged 6 commits into from
Feb 15, 2024
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
22 changes: 11 additions & 11 deletions app/(dashboard)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { getCurrentUser } from "@/lib/session";
import { TRepository } from "@/types/repository";

export const getAllRepositoriesAction = async (): Promise<{ error: string } | TRepository[]> => {
try {
const user = await getCurrentUser();
if (!user || !user.id) {
return ({ error: "User must be authenticated to perform this action." });
}
const repositories = await getAllRepositories();
return repositories as TRepository[];
} catch (error) {
console.error("Error fetching repositories:", error.message);
return { error: "An unexpected error occurred." };
try {
const user = await getCurrentUser();
if (!user || !user.id) {
return { error: "User must be authenticated to perform this action." };
}
};
const repositories = await getAllRepositories();
return repositories as TRepository[];
} catch (error) {
console.error("Error fetching repositories:", error.message);
return { error: "An unexpected error occurred." };
}
};
7 changes: 4 additions & 3 deletions lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import "server-only";

import { env } from "../env.mjs";

export const DEFAULT_CACHE_REVALIDATION_INTERVAL = 60 * 30; // 30 minutes
Expand All @@ -17,8 +15,11 @@ export enum EVENT_TRIGGERS {
INSTALLATION_CREATED = "installation.created",
ISSUE_COMMENTED = "issue_comment.created",
}
export const AWARD_POINTS_IDENTIFIER = "/award" as const;

export const ON_NEW_ISSUE = "Thanks for opening an issue! It's live on oss.gg!";
export const ON_REPO_NOT_REGISTERED = `This repository is not registered with oss.gg. Please register it at https://oss.gg.`;
export const GITHUB_APP_APP_ID = env.GITHUB_APP_APP_ID as string;
export const GITHUB_APP_PRIVATE_KEY = env.GITHUB_APP_PRIVATE_KEY as string;
export const GITHUB_WEBHOOK_SECRET = env.GITHUB_WEBHOOK_SECRET as string;

export const GITHUB_WEBHOOK_SECRET = env.GITHUB_WEBHOOK_SECRET as string;
91 changes: 90 additions & 1 deletion lib/github/hooks/issue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { ASSIGN_IDENTIFIER, EVENT_TRIGGERS, LEVEL_LABEL, UNASSIGN_IDENTIFIER } from "@/lib/constants";
import {
ASSIGN_IDENTIFIER,
AWARD_POINTS_IDENTIFIER,
EVENT_TRIGGERS,
LEVEL_LABEL,
UNASSIGN_IDENTIFIER,
} from "@/lib/constants";
import { assignUserPoints } from "@/lib/points/service";
import { getRepositoryByGithubId } from "@/lib/repository/service";
import { createUser, getUser, getUserByGithubId } from "@/lib/user/service";
import { Webhooks } from "@octokit/webhooks";

import { getOctokitInstance } from "../utils";
Expand Down Expand Up @@ -83,3 +92,83 @@ export const onUnassignCommented = async (webhooks: Webhooks) => {
}
});
};

export const onAwardPoints = async (webhooks: Webhooks) => {
webhooks.on(EVENT_TRIGGERS.ISSUE_COMMENTED, async (context) => {
try {
const octokit = getOctokitInstance(context.payload.installation?.id!);
const repo = context.payload.repository.name;
const issueCommentBody = context.payload.comment.body;
const awardPointsRegex = new RegExp(`${AWARD_POINTS_IDENTIFIER}\\s+(\\d+)`);
const match = issueCommentBody.match(awardPointsRegex);
const isPR = !!context.payload.issue.pull_request;
const issueNumber = isPR ? context.payload.issue.number : undefined;

let comment: string = "";

if (match) {
const points = parseInt(match[1], 10);

if (!issueNumber) {
console.error("Comment is not on a PR.");
return;
}

const ossGgRepo = await getRepositoryByGithubId(context.payload.repository.id);

let usersThatCanAwardPoints = ossGgRepo?.installation.memberships.map((m) => m.userId);
if (!usersThatCanAwardPoints) {
throw new Error("No admins for the given repo in oss.gg!");
}
const ossGgUsers = await Promise.all(
usersThatCanAwardPoints.map(async (userId) => {
const user = await getUser(userId);
return user?.githubId;
})
);
const isUserAllowedToAwardPoints = ossGgUsers?.includes(context.payload.comment.user.id);
if (!isUserAllowedToAwardPoints) {
comment = "You are not allowed to award points! Please contact an admin.";
} else {
if (!ossGgRepo) {
comment = "If you are the repo owner, please register at oss.gg to be able to award points";
} else {
const prAuthorGithubId = context.payload.issue.user.id;
const prAuthorUsername = context.payload.issue.user.login;
const { data: prAuthorProfile } = await octokit.users.getByUsername({
username: prAuthorUsername,
});
let user = await getUserByGithubId(prAuthorGithubId);
if (!user) {
user = await createUser({
githubId: prAuthorGithubId,
login: prAuthorUsername,
email: prAuthorProfile.email,
name: prAuthorProfile.name,
avatarUrl: context.payload.issue.user.avatar_url,
});
comment = "Please register at oss.gg to be able to claim your rewards.";
}
await assignUserPoints(
user?.id,
points,
"Awarded points",
context.payload.comment.html_url,
ossGgRepo?.id
);
comment = `Awarding ${user.login}: ${points} points!` + " " + comment;
}
}

await octokit.issues.createComment({
body: comment,
issue_number: issueNumber,
repo,
owner: "formbricks",
});
}
} catch (err) {
console.error(err);
}
});
};
5 changes: 3 additions & 2 deletions lib/github/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Webhooks, createNodeMiddleware } from "@octokit/webhooks";

import { onInstallationCreated } from "./hooks/installation";
import { onAssignCommented, onIssueOpened, onUnassignCommented } from "./hooks/issue";
import { GITHUB_WEBHOOK_SECRET } from "../constants";
import { onInstallationCreated } from "./hooks/installation";
import { onAssignCommented, onAwardPoints, onIssueOpened, onUnassignCommented } from "./hooks/issue";

const webhooks = new Webhooks({
secret: GITHUB_WEBHOOK_SECRET,
Expand All @@ -17,4 +17,5 @@ export const registerHooks = async () => {
onInstallationCreated(webhooks);
onAssignCommented(webhooks);
onUnassignCommented(webhooks);
onAwardPoints(webhooks);
};
40 changes: 40 additions & 0 deletions lib/points/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { db } from "@/lib/db";
import { Prisma } from "@prisma/client";

export const assignUserPoints = async (
userId: string,
points: number,
description: string,
url: string,
repositoryId: string
) => {
try {
const alreadyAssignedPoints = await db.pointTransaction.findFirst({
where: {
userId,
repositoryId,
url,
},
});
if (alreadyAssignedPoints) {
throw new Error("Points already assigned for this user for the given url");
}

const pointsUpdated = await db.pointTransaction.create({
data: {
points,
userId,
description,
url,
repositoryId,
},
});
return pointsUpdated;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error("An error occurred while updating user points:", error.message);
throw new Error("Database error occurred");
}
throw error;
}
};
24 changes: 24 additions & 0 deletions lib/repository/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,27 @@ export const getAllRepositories = async (): Promise<TRepository[]> => {
throw error;
}
};

export const getRepositoryByGithubId = async (githubId: number) => {
try {
const repository = await db.repository.findFirst({
where: {
githubId,
},
include: {
installation: {
include: {
memberships: true,
},
},
},
});
return repository;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error("An error occurred while fetching repository:", error.message);
throw new Error("Database error occurred");
}
throw error;
}
};
10 changes: 9 additions & 1 deletion lib/user/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { revalidateTag } from "next/cache";
interface RevalidateProps {
id?: string;
login?: string;
githubId?: number;
}

export const userCache = {
Expand All @@ -13,14 +14,21 @@ export const userCache = {
byLogin(login: string) {
return `users-${login}`;
},
byGithubId(githubId: number) {
return `users-${githubId}`;
},
},
revalidate({ id, login }: RevalidateProps): void {
revalidate({ id, login, githubId }: RevalidateProps): void {
if (id) {
revalidateTag(this.tag.byId(id));
}

if (login) {
revalidateTag(this.tag.byLogin(login));
}

if (githubId) {
revalidateTag(this.tag.byGithubId(githubId));
}
},
};
Loading
Loading