Compare commits

...

2 commits

14 changed files with 303 additions and 110 deletions

View file

@ -1,11 +1,23 @@
import { CreateMessageOptions, EditMessageOptions, MessageComponent, MessageFlags } from "oceanic.js";
import {
CreateMessageOptions,
EditMessageOptions,
InteractionOptions,
MessageComponent,
MessageFlags
} from "oceanic.js";
import { childrenToArray } from "./utils";
type MessageOptions = CreateMessageOptions | EditMessageOptions;
export type ComponentMessageProps = MessageOptions & { children: MessageComponent[]; };
export type ComponentMessageProps = MessageOptions & {
children: MessageComponent[];
};
export function ComponentMessage({ children, flags, ...props }: ComponentMessageProps): MessageOptions {
export function ComponentMessage({
children,
flags,
...props
}: ComponentMessageProps): MessageOptions {
return {
flags: MessageFlags.IS_COMPONENTS_V2 | (flags ?? 0),
components: childrenToArray(children),

View file

@ -1,6 +1,10 @@
export const Constants = {
PENDING_CHANNEL_ID: "1370539719842070683",
REJECTION_CHANNEL_ID: "1371073658403164261",
REVIEWER_ROLE_ID: "1375617676139040909",
MOD_ROLE_ID: "1370539692143153152",
MANAGER_ROLE_ID: "1370539689659863091",
OWNER_ID: "886685857560539176",
COLORS: {
NEW_REQ: 0xaaaaff,
BAD: 0xffaaaa,

View file

@ -1,5 +1,5 @@
import { TextDisplay } from "components-jsx/TextDisplay";
import { Response } from "~/types";
import { Response } from "~/utils/types";
export function ApplicationContent(props: {
response: Response;

View file

@ -5,16 +5,19 @@ import { Container } from "components-jsx/Container";
import { Divider } from "components-jsx/Divider";
import { TextDisplay } from "components-jsx/TextDisplay";
import { ButtonStyles, User as OUser } from "oceanic.js";
import { Response } from "~/types";
import { Response } from "~/utils/types";
import { User } from "./User";
import { Constants } from "~/Constants";
import { ApplicationContent } from "./ApplicationContent";
import { StringSelect } from "components-jsx/StringSelect";
import { rejectReasons } from "~/utils/rejectReasons";
export function PendingApplicationMessage(props: {
id: string;
user: OUser;
response: Response;
locked?: boolean;
threadID: string;
}) {
const { id, user, response } = props;
const locked = props.locked || false;
@ -30,14 +33,8 @@ export function PendingApplicationMessage(props: {
<Divider />
<User user={user} />
<Divider />
<ApplicationContent response={response} />
<Divider />
<TextDisplay>
### Details
<br />
{response.details}
</TextDisplay>
<Divider />
<ApplicationContent response={response} includeDetails={true} />
</Container>
<ActionRow>
<Button
style={ButtonStyles.SUCCESS}
@ -47,20 +44,23 @@ export function PendingApplicationMessage(props: {
id: "1375806000312881233"
}}
disabled={locked}
>
Accept
</Button>
/>
<Button
style={ButtonStyles.SUCCESS}
customID={`accept-friend-${id}`}
emoji={{
name: "blobcatgreen",
id: "1375806000312881233"
name: "hugcatcozy",
id: "1375955808826687508"
}}
disabled={locked}
>
Accept + Friend
</Button>
/>
<Button
style={ButtonStyles.SECONDARY}
customID={`interview-${id}`}
emoji={{
name: "💬"
}}
/>
</ActionRow>
<ActionRow>
<Button
@ -71,9 +71,7 @@ export function PendingApplicationMessage(props: {
id: "1375806202470203513"
}}
disabled={locked}
>
Deny
</Button>
/>
<Button
style={ButtonStyles.DANGER}
customID={`reject-ban-${id}`}
@ -82,31 +80,37 @@ export function PendingApplicationMessage(props: {
id: "1375806319621046313"
}}
disabled={locked}
>
Deny + Ban
</Button>
</ActionRow>
<ActionRow>
<Button
style={ButtonStyles.SECONDARY}
customID={`interview-${id}`}
emoji={{
name: "💬"
}}
>
Interview
</Button>
/>
<Button
style={ButtonStyles.SECONDARY}
customID={`${locked ? "un" : ""}lock-${id}`}
emoji={{
name: "🔑"
}}
>
{!locked ? "Lock" : "Unlock"}
</Button>
/>
</ActionRow>
</Container>
<ActionRow>
<StringSelect
customID="deny-reasons"
placeholder="Deny reasons"
disabled={locked}
// @ts-expect-error
options={Object.keys(rejectReasons).map((key) => {
return {
label: rejectReasons[key].shortDesc,
description:
rejectReasons[key].reason.length > 100
? `${rejectReasons[key].reason.slice(
0,
97
)}...`
: rejectReasons[key].reason,
value: key
};
})}
/>
</ActionRow>
<TextDisplay>-# Discuss in {`<#${props.threadID}>`}</TextDisplay>
</ComponentMessage>
);
}

View file

@ -1,8 +1,8 @@
import { AllIntents, Client, ComponentTypes, MessageFlags } from "oceanic.js";
import { selfappReq } from "./selfappReq";
import { selfappReq } from "./utils/selfappReq";
import { setupJoinRequestHandler } from "./joinRequestHandler";
import { Constants } from "./Constants";
import { openDb } from "./database";
import { openDb } from "./utils/database";
export const client = new Client({
auth: `Bot ${process.env.BOT_TOKEN}`,
@ -22,6 +22,12 @@ client.on("ready", async () => {
setupJoinRequestHandler(client.shards.first()!);
});
client.on("messageCreate", (msg) => {
if (msg.messageReference && msg.author.id === client.user.id) {
if (!msg.messageReference.messageID) msg.delete();
}
});
process.on("uncaughtException", (e) => {
console.error(e);
});

View file

@ -0,0 +1,37 @@
import { ComponentTypes, InteractionTypes, MessageFlags } from "oceanic.js";
import { client } from "..";
import { canUser } from "../utils/utils";
import { selfappReq } from "../utils/selfappReq";
client.on("interactionCreate", async (interaction) => {
if (interaction.type === InteractionTypes.MESSAGE_COMPONENT)
if (interaction.data.componentType === ComponentTypes.BUTTON) {
if (interaction.data.customID.split("-")[0] === "interview") {
await interaction.defer(MessageFlags.EPHEMERAL);
if (!canUser(interaction.member!, "review"))
return await interaction.createFollowup({
flags: MessageFlags.EPHEMERAL,
content: "💢 nop"
});
const gdmID = (
(await selfappReq(
`/join-requests/${
interaction.data.customID.split("-")[1]
}/interview`,
"POST"
)) as any
).id;
const gdmInvite = (
(await selfappReq(
`/channels/${gdmID}/invites`,
"POST"
)) as any
).code;
return await interaction.createFollowup({
flags: MessageFlags.EPHEMERAL,
content: `https://discord.gg/${gdmInvite}`
});
}
}
});

View file

@ -0,0 +1,55 @@
import { ComponentTypes, InteractionTypes, MessageFlags } from "oceanic.js";
import { client } from "..";
import { db } from "../utils/database";
import { PendingApplicationMessage } from "../components/PendingApplicationMessage";
import { canUser } from "../utils/utils";
client.on("interactionCreate", async (interaction) => {
if (interaction.type === InteractionTypes.MESSAGE_COMPONENT)
if (interaction.data.componentType === ComponentTypes.BUTTON) {
if (!interaction.data.customID.includes("lock")) return;
if (!canUser(interaction.member!, "owner"))
return await interaction.createMessage({
flags: MessageFlags.EPHEMERAL,
content: "💢 nop"
});
const application = await db.get(
"SELECT * FROM applications WHERE message_id=?",
interaction.message.id
);
console.log(application);
switch (interaction.data.customID.split("-")[0]) {
case "lock": {
await interaction.editParent(
<PendingApplicationMessage
id={application.id}
response={JSON.parse(application.responses)}
user={await client.rest.users.get(
application.user_id
)}
locked={true}
threadID={application.thread_id}
/>
);
break;
}
case "unlock": {
await interaction.editParent(
<PendingApplicationMessage
id={application.id}
response={JSON.parse(application.responses)}
user={await client.rest.users.get(
application.user_id
)}
locked={false}
threadID={application.thread_id}
/>
);
break;
}
}
}
});

View file

@ -1,7 +1,13 @@
import { ButtonStyles, ComponentTypes, MessageFlags, Shard } from "oceanic.js";
import {
ButtonStyles,
ChannelTypes,
ComponentTypes,
MessageFlags,
Shard
} from "oceanic.js";
import { client } from ".";
import { Constants } from "./Constants";
import { db } from "./database";
import { db } from "./utils/database";
import {
ActionRow,
Button,
@ -12,13 +18,16 @@ import {
Separator,
TextDisplay
} from "~/components";
import { selfappReq } from "./selfappReq";
import { selfappReq } from "./utils/selfappReq";
import { User } from "./components/User";
import { Response } from "./types";
import { match } from "./utils";
import { Response } from "./utils/types";
import { match } from "./utils/utils";
import { PendingApplicationMessage } from "./components/PendingApplicationMessage";
import { ApplicationContent } from "./components/ApplicationContent";
import("./joinRequestActions/lockUnlock");
import("./joinRequestActions/interview");
export async function setupJoinRequestHandler(shard: Shard) {
shard.ws?.on("message", async (d) => {
const data = JSON.parse(d.toString("utf8"));
@ -51,6 +60,16 @@ export async function setupJoinRequestHandler(shard: Shard) {
const user = await client.rest.users.get(
applicationData.request.user.id
);
const thread =
await client.rest.channels.startThreadWithoutMessage(
Constants.PENDING_CHANNEL_ID,
{
name: `${user.tag}'s application`,
type: ChannelTypes.PUBLIC_THREAD,
autoArchiveDuration: 10080
}
);
const pendingMsg =
await client.rest.channels.createMessage(
Constants.PENDING_CHANNEL_ID,
@ -61,14 +80,10 @@ export async function setupJoinRequestHandler(shard: Shard) {
locked={
response.foundThrough === "sold"
}
threadID={thread.id}
/>
);
const thread = await pendingMsg.startThread({
name: `${user.tag}'s application`,
autoArchiveDuration: 10080
});
const fullApplicantProfile: any = await selfappReq(
`/users/${user.id}/profile`,
"GET",
@ -127,7 +142,7 @@ export async function setupJoinRequestHandler(shard: Shard) {
}
const response: Response = JSON.parse(
application.responses
application ? application.responses : {} // this will never be used if no applications
);
const msg = await client.rest.channels.createMessage(

View file

@ -1,9 +0,0 @@
export function match(
value: any,
matches: {
[key: number | string]: any;
}
) {
if (matches[value]) return matches[value];
else return null;
}

View file

@ -0,0 +1,23 @@
export const rejectReasons: {
[key: string]: {
shortDesc: string;
reason: string;
};
} = {
low_quality: {
shortDesc: "Too low quality",
reason: "Your application is blatantly low effort (eg. not a language in languages or a single word application). Feel free to reapply, but actually read the questions"
},
no_english: {
shortDesc: "No English",
reason: "You must be fluent in English to join"
},
elaborate: {
shortDesc: "Didn't say enough about themselves",
reason: "Write a bit more about yourself and it's very likely that you'll be accepted!"
},
ai: {
shortDesc: "AI",
reason: "You have obviously used AI in your application. Therefore, you will not be allowed to reapply."
}
};

View file

@ -12,8 +12,6 @@ export async function selfappReq(
return `?${new URLSearchParams(data).toString()}`;
})();
console.log(url);
return await fetch(url, {
method,
headers: {

48
src/utils/utils.ts Normal file
View file

@ -0,0 +1,48 @@
import { Member } from "oceanic.js";
import { Constants } from "../Constants";
export function match(
value: any,
matches: {
[key: number | string]: any;
}
) {
if (matches[value]) return matches[value];
else return null;
}
export function canUser(
member: Member,
can: "review" | "moderate" | "manage" | "owner"
) {
switch (can) {
case "review":
return (
member.roles.some((role) =>
[
Constants.REVIEWER_ROLE_ID,
Constants.MOD_ROLE_ID,
Constants.MANAGER_ROLE_ID
].includes(role)
) || member.id === Constants.OWNER_ID
);
case "moderate":
return (
member.roles.some((role) =>
[Constants.MOD_ROLE_ID, Constants.MANAGER_ROLE_ID].includes(
role
)
) || member.id === Constants.OWNER_ID
);
case "manage":
return (
member.roles.some((role) =>
[Constants.MANAGER_ROLE_ID].includes(role)
) || member.id === Constants.OWNER_ID
);
case "owner":
return member.id === Constants.OWNER_ID;
default:
return false;
}
}