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"; import { childrenToArray } from "./utils";
type MessageOptions = CreateMessageOptions | EditMessageOptions; 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 { return {
flags: MessageFlags.IS_COMPONENTS_V2 | (flags ?? 0), flags: MessageFlags.IS_COMPONENTS_V2 | (flags ?? 0),
components: childrenToArray(children), components: childrenToArray(children),

View file

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

View file

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

View file

@ -5,16 +5,19 @@ import { Container } from "components-jsx/Container";
import { Divider } from "components-jsx/Divider"; import { Divider } from "components-jsx/Divider";
import { TextDisplay } from "components-jsx/TextDisplay"; import { TextDisplay } from "components-jsx/TextDisplay";
import { ButtonStyles, User as OUser } from "oceanic.js"; import { ButtonStyles, User as OUser } from "oceanic.js";
import { Response } from "~/types"; import { Response } from "~/utils/types";
import { User } from "./User"; import { User } from "./User";
import { Constants } from "~/Constants"; import { Constants } from "~/Constants";
import { ApplicationContent } from "./ApplicationContent"; import { ApplicationContent } from "./ApplicationContent";
import { StringSelect } from "components-jsx/StringSelect";
import { rejectReasons } from "~/utils/rejectReasons";
export function PendingApplicationMessage(props: { export function PendingApplicationMessage(props: {
id: string; id: string;
user: OUser; user: OUser;
response: Response; response: Response;
locked?: boolean; locked?: boolean;
threadID: string;
}) { }) {
const { id, user, response } = props; const { id, user, response } = props;
const locked = props.locked || false; const locked = props.locked || false;
@ -30,14 +33,8 @@ export function PendingApplicationMessage(props: {
<Divider /> <Divider />
<User user={user} /> <User user={user} />
<Divider /> <Divider />
<ApplicationContent response={response} /> <ApplicationContent response={response} includeDetails={true} />
<Divider /> </Container>
<TextDisplay>
### Details
<br />
{response.details}
</TextDisplay>
<Divider />
<ActionRow> <ActionRow>
<Button <Button
style={ButtonStyles.SUCCESS} style={ButtonStyles.SUCCESS}
@ -47,20 +44,23 @@ export function PendingApplicationMessage(props: {
id: "1375806000312881233" id: "1375806000312881233"
}} }}
disabled={locked} disabled={locked}
> />
Accept
</Button>
<Button <Button
style={ButtonStyles.SUCCESS} style={ButtonStyles.SUCCESS}
customID={`accept-friend-${id}`} customID={`accept-friend-${id}`}
emoji={{ emoji={{
name: "blobcatgreen", name: "hugcatcozy",
id: "1375806000312881233" id: "1375955808826687508"
}} }}
disabled={locked} disabled={locked}
> />
Accept + Friend <Button
</Button> style={ButtonStyles.SECONDARY}
customID={`interview-${id}`}
emoji={{
name: "💬"
}}
/>
</ActionRow> </ActionRow>
<ActionRow> <ActionRow>
<Button <Button
@ -71,9 +71,7 @@ export function PendingApplicationMessage(props: {
id: "1375806202470203513" id: "1375806202470203513"
}} }}
disabled={locked} disabled={locked}
> />
Deny
</Button>
<Button <Button
style={ButtonStyles.DANGER} style={ButtonStyles.DANGER}
customID={`reject-ban-${id}`} customID={`reject-ban-${id}`}
@ -82,31 +80,37 @@ export function PendingApplicationMessage(props: {
id: "1375806319621046313" id: "1375806319621046313"
}} }}
disabled={locked} disabled={locked}
> />
Deny + Ban
</Button>
</ActionRow>
<ActionRow>
<Button
style={ButtonStyles.SECONDARY}
customID={`interview-${id}`}
emoji={{
name: "💬"
}}
>
Interview
</Button>
<Button <Button
style={ButtonStyles.SECONDARY} style={ButtonStyles.SECONDARY}
customID={`${locked ? "un" : ""}lock-${id}`} customID={`${locked ? "un" : ""}lock-${id}`}
emoji={{ emoji={{
name: "🔑" name: "🔑"
}} }}
> />
{!locked ? "Lock" : "Unlock"}
</Button>
</ActionRow> </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> </ComponentMessage>
); );
} }

View file

@ -1,8 +1,8 @@
import { AllIntents, Client, ComponentTypes, MessageFlags } from "oceanic.js"; import { AllIntents, Client, ComponentTypes, MessageFlags } from "oceanic.js";
import { selfappReq } from "./selfappReq"; import { selfappReq } from "./utils/selfappReq";
import { setupJoinRequestHandler } from "./joinRequestHandler"; import { setupJoinRequestHandler } from "./joinRequestHandler";
import { Constants } from "./Constants"; import { Constants } from "./Constants";
import { openDb } from "./database"; import { openDb } from "./utils/database";
export const client = new Client({ export const client = new Client({
auth: `Bot ${process.env.BOT_TOKEN}`, auth: `Bot ${process.env.BOT_TOKEN}`,
@ -22,6 +22,12 @@ client.on("ready", async () => {
setupJoinRequestHandler(client.shards.first()!); 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) => { process.on("uncaughtException", (e) => {
console.error(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 { client } from ".";
import { Constants } from "./Constants"; import { Constants } from "./Constants";
import { db } from "./database"; import { db } from "./utils/database";
import { import {
ActionRow, ActionRow,
Button, Button,
@ -12,13 +18,16 @@ import {
Separator, Separator,
TextDisplay TextDisplay
} from "~/components"; } from "~/components";
import { selfappReq } from "./selfappReq"; import { selfappReq } from "./utils/selfappReq";
import { User } from "./components/User"; import { User } from "./components/User";
import { Response } from "./types"; import { Response } from "./utils/types";
import { match } from "./utils"; import { match } from "./utils/utils";
import { PendingApplicationMessage } from "./components/PendingApplicationMessage"; import { PendingApplicationMessage } from "./components/PendingApplicationMessage";
import { ApplicationContent } from "./components/ApplicationContent"; import { ApplicationContent } from "./components/ApplicationContent";
import("./joinRequestActions/lockUnlock");
import("./joinRequestActions/interview");
export async function setupJoinRequestHandler(shard: Shard) { export async function setupJoinRequestHandler(shard: Shard) {
shard.ws?.on("message", async (d) => { shard.ws?.on("message", async (d) => {
const data = JSON.parse(d.toString("utf8")); const data = JSON.parse(d.toString("utf8"));
@ -51,6 +60,16 @@ export async function setupJoinRequestHandler(shard: Shard) {
const user = await client.rest.users.get( const user = await client.rest.users.get(
applicationData.request.user.id 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 = const pendingMsg =
await client.rest.channels.createMessage( await client.rest.channels.createMessage(
Constants.PENDING_CHANNEL_ID, Constants.PENDING_CHANNEL_ID,
@ -61,14 +80,10 @@ export async function setupJoinRequestHandler(shard: Shard) {
locked={ locked={
response.foundThrough === "sold" response.foundThrough === "sold"
} }
threadID={thread.id}
/> />
); );
const thread = await pendingMsg.startThread({
name: `${user.tag}'s application`,
autoArchiveDuration: 10080
});
const fullApplicantProfile: any = await selfappReq( const fullApplicantProfile: any = await selfappReq(
`/users/${user.id}/profile`, `/users/${user.id}/profile`,
"GET", "GET",
@ -127,7 +142,7 @@ export async function setupJoinRequestHandler(shard: Shard) {
} }
const response: Response = JSON.parse( 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( 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()}`; return `?${new URLSearchParams(data).toString()}`;
})(); })();
console.log(url);
return await fetch(url, { return await fetch(url, {
method, method,
headers: { 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;
}
}