diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 17d6009..f67db7f 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -84,8 +84,8 @@ jobs: # I don't remember why that was valuable but I'm afraid to remove it run: | kubectl diff -k . || echo \n - kubectl delete secret reactibot-env || echo \n - kubectl create secret generic reactibot-env \ + kubectl delete secret nodeibot-env || echo \n + kubectl create secret generic nodeibot-env \ --from-literal=DISCORD_HASH=${{ secrets.DISCORD_HASH }} \ --from-literal=DISCORD_PUBLIC_KEY=${{ secrets.DISCORD_PUBLIC_KEY }} \ --from-literal=DISCORD_APP_ID=${{ secrets.DISCORD_APP_ID }} \ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b59bc1..dbd65b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ 1. `npm run dev` 1. Look for the following message in the logs, and open the URL in a browser where you're logged into Discord. - `Bot started. If necessary, add it to your test server:` - - Make sure to not install this bot directly on Reactiflux but on the Reactiflux Test Server. Ask for the correct role in RF's #reactibot channel + - Make sure to to install this to the Reactiflux Test Server. Ask for access in the #talk-to-staff channel # Implementation notes diff --git a/DISCORD_SERVER_MIGRATION_GUIDE.md b/DISCORD_SERVER_MIGRATION_GUIDE.md new file mode 100644 index 0000000..34994fe --- /dev/null +++ b/DISCORD_SERVER_MIGRATION_GUIDE.md @@ -0,0 +1,174 @@ +# Discord Server Migration Guide + +This document identifies all hardcoded Discord server-specific values that need to be updated when deploying this bot to a different Discord server. + +## Environment Variables + +**File: `src/helpers/env.ts`** + +- Set `GUILD_ID` environment variable to your target Discord server ID + +## Channel IDs + +**File: `src/constants/channels.ts`** + +### Production Channels (Lines 25-44) + +All channel IDs in the `PRODUCTION_CHANNELS` object need to be replaced: + +- `helpReact: "103696749012467712"` +- `helpThreadsReact: "902647189120118794"` +- `helpJs: "565213527673929729"` +- `random: "103325358643752960"` +- `gaming: "509219336175747082"` +- `thanks: "798567961468076072"` +- `jobBoard: "103882387330457600"` +- `resumeReview: "955507127877791795"` +- `lookingForGroup: "1255898446158757939"` +- `jobsLog: "989201828572954694"` +- `events: "127442949435817984"` +- `iBuiltThis: "312761588778139658"` +- `iWroteThis: "918616846100492298"` +- `twitterFeed: "951207372125274112"` +- `techReadsAndNews: "105816607976095744"` +- `modLog: "591326408396111907"` +- `botLog: "701462381703856158"` +- `vcLog: "1237161125473161347"` + +### Local Development Channels (Lines 4-23) + +All channel IDs in the `LOCAL_CHANNELS` object need to be replaced with your test server channels: + +- `helpReact: "926931785219207301"` +- `helpThreadsReact: "950790460857794620"` +- `helpJs: "950790460857794620"` +- `random: "926931785219207301"` +- `gaming: "926931785219207301"` +- `thanks: "926931785219207301"` +- `jobBoard: "925847361996095509"` +- `resumeReview: "1166075559172907008"` +- `lookingForGroup: "1166085100493078590"` +- `jobsLog: "925847644318879754"` +- `events: "950790520811184150"` +- `iBuiltThis: "950790520811184150"` +- `iWroteThis: "950790520811184150"` +- `twitterFeed: "950790520811184150"` +- `techReadsAndNews: "950790520811184150"` +- `modLog: "925847644318879754"` +- `botLog: "916081991542276096"` +- `vcLog: "1237103703740121201"` + +## Role IDs + +**File: `src/constants/channels.ts`** + +### Production Roles (Lines 54-59) + +All role IDs in the `PRODUCTION_ROLES` object need to be replaced: + +- `starHelper: "852537681346691102"` +- `mvp: "340332804611244043"` +- `moderator: "102870499406647296"` +- `admin: "103261043291082752"` + +### Local Development Roles (Lines 48-53) + +All role IDs in the `LOCAL_ROLES` object need to be replaced: + +- `starHelper: "932749426785665136"` +- `mvp: "932749517290344488"` +- `moderator: "916797467918471190"` +- `admin: "916797467918471190"` + +**File: `src/constants.ts`** + +- Line 1: `modRoleId = "&102870499406647296"` - Replace with your server's moderator role ID + +## Hardcoded Channel Mentions in Messages + +**File: `src/features/scheduled-messages.ts`** + +### Lines 101-112 - Help Channel References + +Replace all hardcoded channel mentions with your server's channel IDs: + +``` +<#565213527673929729> For questions about pure Javascript problems. +<#105765765117935616> For questions about CSS or other visual problems. +<#145170347921113088> For questions about issues with your server code. +<#105765859191975936> Get deeper review of a snippet of code. +<#287623405946011648> If you have a question about your job or career, ask it in here. +<#547620660482932737> Discussion of non-JS code, or that new laptop you're deciding on. +<#108428584783220736> for questions about building, linting, generating, or otherwise processing your code. +<#103882387330457600> Job board channel +<#798567961468076072> Thanks/shoutout channel +``` + +## Server-Specific URLs and Branding + +Replace all Reactiflux-specific URLs and references throughout the codebase: + +### URLs to Replace + +- `https://www.reactiflux.com/conduct` → Your server's code of conduct +- `https://www.reactiflux.com/promotion` → Your server's promotion guidelines +- `https://www.reactiflux.com/jobs` → Your server's jobs page +- `https://www.reactiflux.com/learning` → Your server's learning resources +- `https://reactiflux.com/contact` → Your server's contact form +- `hello@reactiflux.com` → Your server's contact email + +### Files Containing These References + +- `src/features/commands.ts` (multiple instances) +- `src/features/scheduled-messages.ts` (multiple instances) +- `src/features/jobs-moderation.ts` (guidance URL) + +## Channel Purpose Mapping + +When creating channels in your new server, ensure they serve the same purposes: + +| Channel Constant | Purpose | +| ------------------ | ------------------------------ | +| `helpReact` | React-specific help questions | +| `helpThreadsReact` | React help with thread support | +| `helpJs` | Pure JavaScript questions | +| `random` | General discussion | +| `gaming` | Gaming discussions | +| `thanks` | Appreciation messages | +| `jobBoard` | Job postings | +| `resumeReview` | Resume feedback | +| `lookingForGroup` | Finding project partners | +| `jobsLog` | Job posting moderation logs | +| `events` | Server events | +| `iBuiltThis` | Project showcases | +| `iWroteThis` | Article/blog sharing | +| `twitterFeed` | Twitter feed integration | +| `techReadsAndNews` | Tech news sharing | +| `modLog` | Moderation activity logs | +| `botLog` | Bot activity logs | +| `vcLog` | Voice channel activity logs | + +## Required Permissions + +Ensure the bot has the following permissions in your Discord server: + +- Read Messages/View Channels +- Send Messages +- Manage Messages +- Add Reactions +- Use Slash Commands +- Connect (for voice channels) +- View Audit Log (for moderation features) + +## Testing Checklist + +After updating all values: + +1. ✅ Bot connects to your server +2. ✅ All channel references work +3. ✅ Role-based permissions function correctly +4. ✅ Scheduled messages post to correct channels +5. ✅ Moderation features work with your role structure +6. ✅ All URL references point to your resources +7. ✅ Job board moderation functions properly +8. ✅ Voice activity logging works (if using voice channels) diff --git a/README.md b/README.md index fc5f04c..a8bf66c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Reactibot -Useful saved commands, auto-moderation tools, and more, for the Reactiflux server. +Useful saved commands, auto-moderation tools, and more, for the Node.js Discord server. ## Contributing @@ -14,10 +14,10 @@ This is dumb and I hate it but it's how the tools work together. New secrets need to be added in a bunch of different places in order to work in production: -- the GitHub [secrets config for Reactibot in settings](https://github.com/reactiflux/reactibot/settings/secrets/actions) -- [env.ts](https://github.com/reactiflux/reactibot/blob/main/src/helpers/env.ts) to load it from the environment (please do not freely use `process.env`) -- [our deployment action](https://github.com/reactiflux/reactibot/blob/main/.github/workflows/node.js.yml#L88), under the Kubernetes secret creation step -- [the Kubernetes deployment config](https://github.com/reactiflux/reactibot/blob/main/cluster/deployment.yaml#L18) +- the GitHub [secrets config for Nodeibot in settings](https://github.com/nodeiflux/nodeibot/settings/secrets/actions) +- [env.ts](https://github.com/nodeiflux/nodeibot/blob/main/src/helpers/env.ts) to load it from the environment (please do not freely use `process.env`) +- [our deployment action](https://github.com/nodeiflux/nodeibot/blob/main/.github/workflows/node.js.yml#L88), under the Kubernetes secret creation step +- [the Kubernetes deployment config](https://github.com/nodeiflux/nodeibot/blob/main/cluster/deployment.yaml#L18) ## Testing deployment diff --git a/cluster/deployment.yaml b/cluster/deployment.yaml index ab5ad03..52e7eb3 100644 --- a/cluster/deployment.yaml +++ b/cluster/deployment.yaml @@ -1,19 +1,19 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: reactibot-deployment + name: nodeibot-deployment spec: selector: matchLabels: - app: reactibot + app: nodeibot replicas: 1 template: metadata: labels: - app: reactibot + app: nodeibot spec: containers: - - name: reactibot + - name: nodeibot image: $(IMAGE) env: - name: ENVIRONMENT @@ -21,40 +21,40 @@ spec: - name: DISCORD_HASH valueFrom: secretKeyRef: - name: reactibot-env + name: nodeibot-env key: DISCORD_HASH - name: GH_READ_TOKEN valueFrom: secretKeyRef: - name: reactibot-env + name: nodeibot-env key: GH_READ_TOKEN - name: AMPLITUDE_KEY valueFrom: secretKeyRef: - name: reactibot-env + name: nodeibot-env key: AMPLITUDE_KEY - name: DISCORD_PUBLIC_KEY valueFrom: secretKeyRef: - name: reactibot-env + name: nodeibot-env key: DISCORD_PUBLIC_KEY - name: DISCORD_APP_ID valueFrom: secretKeyRef: - name: reactibot-env + name: nodeibot-env key: DISCORD_APP_ID - name: GUILD_ID valueFrom: secretKeyRef: - name: reactibot-env + name: nodeibot-env key: GUILD_ID - name: OPENAI_KEY valueFrom: secretKeyRef: - name: reactibot-env + name: nodeibot-env key: OPENAI_KEY - name: REACTIBOT_API_KEY valueFrom: secretKeyRef: - name: reactibot-env + name: nodeibot-env key: REACTIBOT_API_KEY diff --git a/cluster/ingress.yaml b/cluster/ingress.yaml index 4b96ae8..89b80fa 100644 --- a/cluster/ingress.yaml +++ b/cluster/ingress.yaml @@ -1,7 +1,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: reactibot-ingress + name: nodeibot-ingress annotations: nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" @@ -12,17 +12,17 @@ metadata: spec: ingressClassName: nginx rules: - - host: api.reactiflux.com + - host: api.node.reactiflux.com http: paths: - path: / pathType: Prefix backend: service: - name: reactibot-service + name: nodeibot-service port: number: 80 tls: - hosts: - - api.reactiflux.com - secretName: reactibot-tls-cert + - api.node.reactiflux.com + secretName: nodeibot-tls-cert diff --git a/cluster/service.yaml b/cluster/service.yaml index 590db52..e6cc349 100644 --- a/cluster/service.yaml +++ b/cluster/service.yaml @@ -1,13 +1,13 @@ apiVersion: v1 kind: Service metadata: - name: reactibot-service + name: nodeibot-service labels: - app: reactibot + app: nodeibot spec: type: ClusterIP ports: - port: 80 # External port targetPort: 3000 # Port the pod exposes selector: - app: reactibot + app: nodeibot diff --git a/kustomization.yaml b/kustomization.yaml index 1eeb36d..264c7b4 100644 --- a/kustomization.yaml +++ b/kustomization.yaml @@ -2,7 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: default commonLabels: - app: reactibot + app: nodeibot resources: - cluster/service.yaml - cluster/deployment.yaml diff --git a/package.json b/package.json index 237385b..f7dd097 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "reactibot", + "name": "nodeibot", "version": "1.0.0", - "description": "Source code of Reactibot, utilized at Reactiflux server", + "description": "Source code of Nodeibot, used in the Node.js Discord server", "private": true, "type": "module", "scripts": { @@ -17,7 +17,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/reactiflux/reactibot.git" + "url": "https://github.com/nodeiflux/nodeiflux-bot.git" }, "license": "MIT", "dependencies": { diff --git a/src/constants/channels.ts b/src/constants/channels.ts index a55c99d..70ab048 100644 --- a/src/constants/channels.ts +++ b/src/constants/channels.ts @@ -65,10 +65,10 @@ const cachedChannels: Record> = { }; export const initCachedChannels = async (bot: Client) => { - const reactiflux = await bot.guilds.fetch(guildId); + const nodejsDiscord = await bot.guilds.fetch(guildId); const channels = await Promise.all( [CHANNELS.botLog, CHANNELS.modLog, CHANNELS.jobsLog].map((channelId) => - reactiflux.channels.fetch(channelId), + nodejsDiscord.channels.fetch(channelId), ), ); diff --git a/src/features/book-list.ts b/src/features/book-list.ts deleted file mode 100644 index 3fe0b88..0000000 --- a/src/features/book-list.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { SlashCommandBuilder } from "discord.js"; -import { SlashCommand } from "../helpers/discord.js"; -import { og } from "../helpers/og-tags.js"; - -const fetchMetadata = async ({ - title, - url, -}: { - title: string; - url: string; -}) => { - const { result, error } = await og({ url }); - - if (error) { - console.log( - "[BOOKS]", - `Failed to fetch og tags for ${title}. Error: '${result.error}'`, - ); - return { description: undefined, image: undefined }; - } - - return { - description: result.ogDescription, - image: result.ogImage?.at(0)?.url, - }; -}; - -const initBooks = (async () => { - return ( - await Promise.all( - [ - { - title: "The Coding Career Handbook", - url: "https://learninpublic.org/?c=REACTIFLUX30", - }, - { - title: "Letters to a New Developer", - url: "https://bookshop.org/a/88607/9781484260739", - }, - { - title: "Unwritten Laws of Engineering", - url: "https://bookshop.org/a/88607/9780791861967", - }, - { - title: "Moral Mazes: The World of Corporate Managers", - url: "https://bookshop.org/a/88607/9780199729883", - }, - { - title: "The 5 Dysfunctions of a Team", - url: "https://bookshop.org/a/88607/9780787960759", - }, - { - title: "Multipliers", - url: "https://bookshop.org/a/88607/9780062663078", - }, - { - title: "The Tech Resume Inside Out", - url: "https://thetechresume.com", - }, - { - title: "Staff Engineer: Leadership Beyond the Management Track", - url: "https://staffeng.com/book", - }, - { - title: "Designing Data-Intensive Applications", - url: "https://bookshop.org/a/88607/9781449373320", - }, - { - title: "Head First Design Patterns", - url: "https://bookshop.org/a/88607/9781492078005", - }, - { - title: "Understanding Software", - url: "https://bookshop.org/a/88607/9781788628815", - }, - { - title: "Code Complete", - url: "https://bookshop.org/a/88607/9780735619678", - }, - { - title: "The Effective Engineer", - url: "https://bookshop.org/a/88607/9780996128100", - }, - { - title: "The Principles of Product Development Flow", - url: "https://www.amazon.com/Principles-Product-Development-Flow-Generation/dp/1935401009?_encoding=UTF8&qid=&sr=&linkCode=ll1&tag=&linkId=132dbd1d0409bf8c34f07722a28f50c2&language=en_US&ref_=as_li_ss_tl", - }, - { - title: "Managers Path", - url: "https://bookshop.org/a/88607/9781491973899", - }, - { - title: "Engineering Management for the Rest of Us", - url: "https://bookshop.org/a/88607/9798986769301", - }, - { - title: "Phoenix Project", - url: "https://bookshop.org/a/88607/9781942788294", - }, - { - title: "Managing Humans", - url: "https://bookshop.org/a/88607/9781484271155", - }, - { - title: "The Mythical Man Month", - url: "https://bookshop.org/a/88607/9780201835953", - }, - { - title: "The Making of a Manager", - url: "https://bookshop.org/a/88607/9780735219564", - }, - { - title: "Atomic Habits", - url: "https://bookshop.org/a/88607/9781471050206", - }, - { - title: "Difficult Conversations", - url: "https://bookshop.org/a/88607/9780143118442", - }, - { - title: "Deep Work", - url: "https://bookshop.org/a/88607/9781455586677", - }, - { - title: "Non-Violent Communication", - url: "https://bookshop.org/a/88607/9781892005281", - }, - { - title: "The Design of Everyday Things", - url: "https://bookshop.org/a/88607/9780465050659", - }, - { - title: "Good to Great", - url: "https://bookshop.org/a/88607/9780066620992", - }, - { - title: "Traction", - url: "https://bookshop.org/a/88607/9781591848363", - }, - { - title: "The Right It", - url: "https://bookshop.org/a/88607/9780062884657", - }, - ].map(async (b) => { - const metadata = await fetchMetadata(b); - - return { ...b, lcTitle: b.title.toLowerCase(), ...metadata }; - }), - ) - ).filter((b) => b.description); -})(); - -const SOURCE_LINK = - "-# [source](https://github.com/reactiflux/reactibot/blob/main/src/features/book-list.ts)"; - -export const recommendBookCommand: SlashCommand = { - command: new SlashCommandBuilder() - .setName("book") - .setDescription("Recommend a book") - .addStringOption((option) => - option - .setName("title") - .setDescription("What book?") - .setRequired(true) - .setAutocomplete(true), - ) as SlashCommandBuilder, - handler: async (interaction) => { - const searchTitle = interaction.options.get("title")?.value; - - const books = await initBooks; - const { url, description, title, image } = - books.find((b) => b.title === searchTitle) ?? {}; - console.log(searchTitle, url, image); - if (!url) { - console.log(`no link found for ${title}`); - return; - } - interaction.reply({ - embeds: [ - { - thumbnail: image ? { url: image } : undefined, - description: `## [${title}](${url}) - -> ${description?.slice(0, 600)}… - -${SOURCE_LINK} · some books use affiliate links`, - }, - ], - allowedMentions: { users: [] }, - }); - }, - autocomplete: async (interaction) => { - const partial = interaction.options.getFocused(); - const partialRegex = new RegExp(`^${partial.split("").join(".*")}`); - const filtered = (await initBooks) - .filter((b) => partial === "" || partialRegex.test(b.lcTitle)) - .slice(0, 20); - - await interaction.respond( - filtered.map((b) => ({ name: b.title, value: b.title })), - ); - }, -}; diff --git a/src/features/commands.ts b/src/features/commands.ts index ab78ac0..7f65be8 100644 --- a/src/features/commands.ts +++ b/src/features/commands.ts @@ -1,25 +1,11 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ -import fetch from "node-fetch"; -import { - APIEmbed, - ChannelType, - EmbedType, - Message, - TextChannel, -} from "discord.js"; +import { ChannelType, EmbedType, Message } from "discord.js"; import cooldown from "./cooldown.js"; import type { ChannelHandlers } from "../types/index.d.ts"; -import { isStaff } from "../helpers/discord.js"; -import { - extractSearchKey, - getReactDocsContent, - getReactDocsSearchKey, -} from "../helpers/react-docs.js"; import dedent from "dedent"; export const EMBED_COLOR = 7506394; -type Categories = "Reactiflux" | "Communication" | "Web" | "React/Redux"; +type Categories = "Node.js" | "Communication" | "Web"; type Command = { words: string[]; @@ -29,18 +15,13 @@ type Command = { cooldown?: number; }; -const sortedCategories: Categories[] = [ - "Reactiflux", - "Communication", - "Web", - "React/Redux", -]; +const sortedCategories: Categories[] = ["Node.js", "Communication", "Web"]; const commandsList: Command[] = [ { words: [`!commands`], help: `lists all available commands`, - category: "Reactiflux", + category: "Node.js", handleMessage: (msg) => { const commandsMessage = createCommandsMessage(); @@ -59,14 +40,14 @@ const commandsList: Command[] = [ { words: [`!conduct`], help: `informs user's of code of conduct`, - category: "Reactiflux", + category: "Node.js", handleMessage: (msg) => { msg.reply({ embeds: [ { title: "Code of Conduct", type: EmbedType.Rich, - description: `Reactiflux is the largest chat community of React developers. We make a deliberate effort to have a light touch when it comes to moderating, but we do have some expectations of how our members will behave. Please read the [full Reactiflux Code of Conduct](https://www.reactiflux.com/conduct)`, + description: `This Node.js Discord is the official Discord community of the OpenJS Foundation. Please read the [full Node.js Code of Conduct](https://github.com/nodejs/admin/blob/HEAD/CODE_OF_CONDUCT.md) and [moderation policy](https://github.com/nodejs/admin/blob/HEAD/Moderation-Policy.md). This Discord is moderated by an independent team subject to oversight by the OpenJS Foundation Moderation Team.`, color: EMBED_COLOR, }, ], @@ -76,31 +57,14 @@ const commandsList: Command[] = [ { words: [`!promotion`], help: `informs user's of self-promotion guidelines`, - category: "Reactiflux", + category: "Node.js", handleMessage: (msg) => { msg.reply({ embeds: [ { title: "Self Promotion", type: EmbedType.Rich, - description: `Reactiflux is a peer group, not an advertising channel or a free audience. Please review [our guidelines around self-promotion](https://www.reactiflux.com/promotion)`, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { - words: [`!rrlinks`], - help: `shares a repository of helpful links regarding React and Redux`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "Helpful links", - type: EmbedType.Rich, - description: `Reactiflux's Mark Erikson has put together [a curated list of useful React & Redux links](https://github.com/markerikson/react-redux-links) for developers of all skill levels.`, + description: `The Node.js Discord is a peer group, not an advertising channel or a free audience. Please review [our guidelines around self-promotion](https://github.com/nodeiflux/documents/blob/main/promotion.md)`, color: EMBED_COLOR, }, ], @@ -124,141 +88,10 @@ const commandsList: Command[] = [ }); }, }, - { - words: [`!ymnnr`], - help: `links to the You Might Not Need Redux article`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "You Might Not Need Redux", - type: EmbedType.Rich, - description: `People often choose Redux before they need it. “What if our app doesn’t scale without it?" - -Read more about this in the [article "You Might Not Need Redux" by Dan Abramov](https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367)`, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { - words: [`!derived`], - help: `Links to the React docs advice to avoid copying props to state`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: - "You might not need getDerivedStateFrom props or state at all!", - type: EmbedType.Rich, - description: `Copying data from React props to component state is usually not necessary, and should generally be avoided. The React team offered advice on when "derived state" may actually be needed in their [article "You Probably Don't Need Derived State"](https://legacy.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html)`, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { - words: [`!su`, `!stateupdates`], - help: `Explains the implications involved with state updates being asynchronous`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "State Updates May Be Asynchronous", - type: EmbedType.Rich, - description: `Often times you run into an issue like this -\`\`\`js -const handleEvent = e => { -setState(e.target.value); -console.log(state); -} -\`\`\` -where \`state\` is not the most up to date value when you log it. This is caused by state updates being asynchronous. - -Check out these resources for more information: -- [React.dev: Queueing a Series of State Updates](https://react.dev/learn/queueing-a-series-of-state-updates) -- [ReactJS.org: State Updates May Be Asynchronous](https://legacy.reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous) -- [Blogged Answers: Render Batching and Timing](https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/#render-batching-and-timing)`, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { - words: [`!bind`], - help: `explains how and why to bind in React applications`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "Binding functions", - type: EmbedType.Rich, - description: `In JavaScript, a class function will not be bound to the instance of the class, this is why you often see messages saying that you can't access something of undefined. - -In order to fix this, you need to bind your function, either in constructor: - -\`\`\`js -class YourComponent extends React.Component { -constructor(props) { - super(props); - this.handleClick = this.handleClick.bind(this); -} - -handleClick() { - // you can access \`this\` here, because we've - // bound the function in constructor -} -} -\`\`\` - -or by using class properties babel plugin (it works in create-react-app by default!) - -\`\`\`js -class YourComponent extends React.Component { -handleClick = () => { - // you can access \`this\` here just fine -} -} -\`\`\` - -Check out [this article on "Why and how to bind methods in your React component classes?"](https://reactkungfu.com/2015/07/why-and-how-to-bind-methods-in-your-react-component-classes/) for more information.`, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { - words: [`!lift`], - help: `links to the React docs regarding the common need to "lift" state`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "Lifting State Up", - type: EmbedType.Rich, - description: `Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor. - -Learn more about lifting state in the [React.dev article about "Sharing State Between Components"](https://react.dev/learn/sharing-state-between-components).`, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { words: [`!ask`], help: `explains how to ask questions`, - category: "Reactiflux", + category: "Node.js", handleMessage: (msg) => { msg.channel.send({ embeds: [ @@ -291,7 +124,7 @@ Have a look at these resources on how to ask good questions: { words: [`!code`, `!gist`], help: `explains how to attach code`, - category: "Reactiflux", + category: "Node.js", handleMessage: (msg) => { msg.channel.send({ embeds: [ @@ -324,14 +157,14 @@ Link a Snack to share React Native examples: https://snack.expo.io { words: [`!ping`], help: `explains how to ping politely`, - category: "Reactiflux", + category: "Node.js", handleMessage: (msg) => { msg.channel.send({ embeds: [ { title: "Don’t ping or DM other devs you aren’t actively talking to", type: EmbedType.Rich, - description: `It’s very tempting to try to get more attention to your question by @-mentioning one of the high profile(or recently active) members of Reactiflux, but please don’t. They may not actually be online, they may not be able to help, and they may be in a completely different timezone–nobody likes push notifications at 3am from an impatient stranger. + description: `It’s very tempting to try to get more attention to your question by @-mentioning one of the high profile(or recently active) members of the Node.js Discord, but please don’t. They may not actually be online, they may not be able to help, and they may be in a completely different timezone–nobody likes push notifications at 3am from an impatient stranger. Similarly, don’t DM other members without asking first. All of the same problems as @-mentioning apply, and private conversations can’t help anyone else. Your questions are likely not unique, and other people can learn from them when they’re kept public.`, color: EMBED_COLOR, @@ -340,147 +173,6 @@ Similarly, don’t DM other members without asking first. All of the same proble }); }, }, - { - words: [`!inputs`], - help: `provides links to uncontrolled vs controlled components`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "Uncontrolled vs Controlled components", - type: EmbedType.Rich, - description: `In React, inputs can either be uncontrolled (traditional input) or be controlled via state. -Here's an article explaining the difference between the two: https://goshakkk.name/controlled-vs-uncontrolled-inputs-react/ - `, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { - words: [`!move`], - help: `allows you to move the conversation to another channel \n\t(usage: \`!move #toChannel @person1 @person2 @person3\`)`, - category: "Reactiflux", - handleMessage: (msg) => { - const [, newChannel] = msg.content.split(" "); - - try { - const targetChannel = msg.guild?.channels.cache.get( - newChannel.replace("<#", "").replace(">", ""), - ) as TextChannel; - - if (!msg.mentions.members) return; - - targetChannel.send( - `${msg.author} has opened a portal from ${ - msg.channel - } summoning ${msg.mentions.members.map((i) => i).join(" ")}`, - ); - } catch (e) { - console.log("Something went wrong when summoning a portal: ", e); - } - }, - }, - { - words: [`!mdn`], - help: `allows you to search something on MDN, usage: !mdn Array.prototype.map`, - category: "Web", - handleMessage: async (msg) => { - const [, ...args] = msg.content.split(" "); - const query = args.join(" "); - const [fetchMsg, res] = await Promise.all([ - msg.channel.send(`Fetching "${query}"...`), - fetch( - `https://developer.mozilla.org/api/v1/search?highlight=false&locale=en-us&q=${query}`, - ), - ]); - - const { documents } = (await res.json()) as { - documents: ( - | { title: string; excerpt: string; mdn_url: string } - | undefined - )[]; - }; - const [topResult] = documents; - - if (!topResult) { - fetchMsg.edit(`Could not find anything on MDN for '${query}'`); - return; - } - - const { title, excerpt: description, mdn_url: mdnUrl } = topResult; - - await msg.channel.send({ - content: "-# also did you know there's `/mdn` too", - embeds: [ - { - author: { - name: "MDN", - url: "https://developer.mozilla.org", - icon_url: - "https://developer.mozilla.org/favicon-48x48.cbbd161b.png", - }, - title, - description, - color: 0x83d0f2, - url: `https://developer.mozilla.org${mdnUrl}`, - }, - ], - }); - - fetchMsg.delete(); - }, - }, - { - words: ["!react-docs", "!docs"], - help: "Allows you to search the React docs, usage: !docs useState", - category: "Web", - handleMessage: async (msg) => { - const search = extractSearchKey(msg.content); - const searchKey = getReactDocsSearchKey(search); - - if (!searchKey) { - msg.channel.send({ - embeds: generateReactDocsErrorEmbeds(search), - }); - return; - } - - const [fetchMsg, content] = await Promise.all([ - msg.channel.send(`Looking up documentation for **'${search}'**...`), - getReactDocsContent(searchKey), - ]); - - if (!content) { - fetchMsg.edit({ - embeds: generateReactDocsErrorEmbeds(search), - }); - return; - } - - await msg.channel.send({ - embeds: [ - { - title: `${searchKey}`, - type: EmbedType.Rich, - description: content, - color: EMBED_COLOR, - url: `https://react.dev/reference/${searchKey}`, - author: { - name: "React documentation", - icon_url: - "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1150px-React-icon.svg.png", - url: "https://react.dev/", - }, - }, - ], - }); - - fetchMsg.delete(); - }, - }, { words: [`!appideas`], help: `provides a link to the best curated app ideas for beginners to advanced devs`, @@ -524,110 +216,6 @@ Read more: }); }, }, - { - words: [`!imm`, `!immutability`], - help: `provides resources for helping with immutability`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "Immutable updates", - type: EmbedType.Rich, - description: `Immutable updates involve modifying data by creating new, updated objects instead of modifying the original object directly. -You should not modify existing data directly in React or Redux, as mutating data can lead to bugs. - -- [Immutability in React and Redux](https://daveceddia.com/react-redux-immutability-guide/) -- [React Docs: Updating Objects in State](https://react.dev/learn/updating-objects-in-state) -- [React Docs: Updating Arrays in State](https://react.dev/learn/updating-arrays-in-state) -- [Redux Toolkit, Immutability and Redux](https://redux-toolkit.js.org/usage/immer-reducers#immutability-and-redux) - `, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { - words: [`!redux`], - help: `Info and when and why to use Redux`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "When should you use Redux?", - type: EmbedType.Rich, - description: `Redux is still the most widely used state management tool for React, but it's important to always ask "what problems am I trying to solve?", and choose tools that solve those problems. Redux, Context, React Query, and Apollo all solve different problems, with some overlap. - -See these articles for advice on what Redux does and when it makes sense to use it: - -[Redux - Not Dead Yet! (2018)](https://blog.isquaredsoftware.com/2018/03/redux-not-dead-yet/) -[Why React Context is Not a "State Management" Tool](https://blog.isquaredsoftware.com/2021/01/context-redux-differences/) -[When (and when not) to reach for Redux](https://changelog.com/posts/when-and-when-not-to-reach-for-redux) -[Idiomatic Redux](https://blog.isquaredsoftware.com/2017/05/idiomatic-redux-tao-of-redux-part-1/) - `, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { - words: ["!context"], - help: `Differences between Redux and Context`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "What are the differences between Redux and Context?", - type: EmbedType.Rich, - description: `Redux and Context are different tools that solve different problems, with some overlap. - -Context is a Dependency Injection tool for a single value. - -Redux is a tool for predictable state management outside React. - -See these articles for more details on the differences: -- [Blogged Answers: Why React Context is Not a "State Management" Tool](https://blog.isquaredsoftware.com/2021/01/context-redux-differences/) -- [Blogged Answers: React, Redux, and Context Behavior](https://blog.isquaredsoftware.com/2020/01/blogged-answers-react-redux-and-context-behavior/) -- [Blogged Answers: Redux - Not Dead Yet!](https://blog.isquaredsoftware.com/2018/03/redux-not-dead-yet/) -- [When (and when not) to reach for Redux - Mark Erikson](https://changelog.com/posts/when-and-when-not-to-reach-for-redux) - `, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { - words: [`!render`], - help: `Explanation of how React rendering behavior works`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "How does React rendering behavior work?", - type: EmbedType.Rich, - description: `There are several common misunderstandings about how React renders components. It's important to know that: - -- React re-renders components recursively by default -- State updates must be immutable -- Updates are usually batched together -- Context updates always cause components to re-render - -See this post for a detailed explanation of how React rendering actually works: - -https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/ - `, - color: EMBED_COLOR, - }, - ], - }); - }, - }, { words: [`!jwt`], help: `Describes reasoning for and against the use of JWT tokens againt using standard sessions`, @@ -666,7 +254,7 @@ See below to help you decide which works best for you: { words: [`!formatting`, `!prettier`], help: `describes Prettier and explains how to use it to format code`, - category: "Reactiflux", + category: "Node.js", handleMessage: (msg) => { msg.channel.send({ embeds: [ @@ -707,50 +295,6 @@ To integrate it into your editor: https://prettier.io/docs/en/editors.html`, }); }, }, - { - words: [`!reactts`], - help: `Resources and tips for using React + TypeScript together`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "Resources for React + TypeScript", - type: EmbedType.Rich, - description: `The best resource for how to use TypeScript and React together is the React TypeScript CheatSheet. It has advice on how to type function components, hooks, event handlers, and much more: - -https://react-typescript-cheatsheet.netlify.app/ - -Also, we advise against using the \`React.FC\` type for function components. Instead, declare the type of \`props\` directly, like: -\`function MyComp(props: MyCompProps) {}\`: -See this issue for details on why to avoid \`React.FC\`: - -https://github.com/facebook/create-react-app/pull/8177 - `, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { - words: [`!hooks`, `!learn`], - help: `Resources for Learning React`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "Learning React", - type: EmbedType.Rich, - description: `The [official React docs](https://react.dev) are the best resource for learning React. - -The [Reactiflux "Learning Resources" page](https://www.reactiflux.com/learning) has curated links for learning JS, React, Redux, and TS:`, - }, - ], - }); - }, - }, { words: [`!nw`, `!notworking`], help: `gives some tips on how to improve your chances at getting an answer`, @@ -776,7 +320,7 @@ Instead: { words: [`!laptop`], help: `gives some advice about what laptop to use for web development`, - category: "Reactiflux", + category: "Node.js", handleMessage: (msg) => { msg.channel.send({ embeds: [ @@ -802,91 +346,6 @@ These lines are popular so there's generally a lot of resources for working on t }); }, }, - { - words: ["!lock"], - help: "", - category: "Communication", - handleMessage: async (msg) => { - if (!msg.guild || !isStaff(msg.member)) { - return; - } - - // permission overwrites can only be applied on Guild Text Channels - if (msg.channel.type === ChannelType.GuildText) { - const { channel: guildTextChannel } = msg; - await guildTextChannel.permissionOverwrites.create( - guildTextChannel.guild.roles.everyone, - { - SendMessages: false, - CreatePublicThreads: false, - CreatePrivateThreads: false, - SendMessagesInThreads: false, - }, - ); - guildTextChannel.send("This channel has been locked by a moderator"); - } - }, - }, - { - words: ["!unlock"], - help: "", - category: "Communication", - handleMessage: async (msg) => { - if (!msg.guild || !isStaff(msg.member)) { - return; - } - - // permission overwrites can only be applied on Guild Text Channels - if (msg.channel.type === ChannelType.GuildText) { - const { channel: guildTextChannel } = msg; - guildTextChannel.permissionOverwrites.create( - guildTextChannel.guild.roles.everyone, - { - SendMessages: null, // null will inherit the permission from the parent channel category - }, - ); - } - }, - }, - { - words: [`!const`], - help: `explains that const values are not immutable`, - category: "Web", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "Using const variables", - type: EmbedType.Rich, - description: `As its name suggests, you cannot reassign a const variable. -\`\`\`js -const a = 'hello'; -a = 'world'; // ❌ Uncaught TypeError: Assignment to constant variable. -\`\`\` -However, that does not mean the value of a const variable is immutable. -For example, you could mutate an object's property -\`\`\`js -const obj = { - hello: 'world' -}; -obj.hello = 'reactiflux'; // ✅ -\`\`\` -or you could mutate an array's elements -\`\`\`js -const arr = ['hello']; -arr.push('world'); // ✅ -arr[1] = 'reactiflux'; // ✅ -console.log(arr); // ['hello', 'reactiflux'] -\`\`\` - -https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const#description -https://exploringjs.com/es6/ch_variables.html#_pitfall-const-does-not-make-the-value-immutable - `, - }, - ], - }); - }, - }, { words: ["!remote", "!remotework"], help: "provides resources for the remote work job search", @@ -937,127 +396,6 @@ Remote work has the most competition, and thus is likely to be more difficult to }); }, }, - { - words: [`!keys`], - help: `Explains the importance of keys when rendering lists in React`, - category: "React/Redux", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "The importance of keys when rendering lists in React", - type: EmbedType.Rich, - description: ` -React depends on the use of stable and unique keys to identify items in a list so that it can perform correct and performant DOM updates. - -Keys are particularly important if the list can change over time. React will use the index in the array by default if no key is specified. You can use the index in the array if the list doesn't change and you don't have a stable and unique key available. - -Please see these resources for more information: - -https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key -https://kentcdodds.com/blog/understanding-reacts-key-prop - `, - color: EMBED_COLOR, - }, - ], - }); - }, - }, - { - words: ["!frameworks", "!framework"], - help: "Provides a list of popular backend- and meta-frameworks", - category: "Web", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "", - type: EmbedType.Rich, - description: ` -Making a choice of which framework to use is a difficult one and there are many options to choose from. Below are four lists of both frontend- and backend-frameworks including meta-frameworks, last but not least is a list of popular non-js http libraries. -_ _ -_ _ - `, - fields: [ - { - name: "Frontend Frameworks", - value: ` -- [React](https://react.dev/) -- [Svelte](https://svelte.dev/) -- [Solid](https://www.solidjs.com/) -- [Vue](https://vuejs.org/) -- [Angular](https://angular.io/) -- [Qwik](https://qwik.builder.io/) -- [Lume](https://lume.land/) - `, - inline: true, - }, - { - name: "Backend Frameworks", - value: ` -- [Express](https://expressjs.com/) -- [Koa](https://koajs.com/) -- [Fastify](https://www.fastify.io/) -- [Elysia.js](https://elysiajs.com/) -- [Hono](https://hono.dev/) -- [NestJS](https://nestjs.com/) -- [Oak](https://deno.land/x/oak) -- [Feathers](https://feathersjs.com/) -- [AdonisJS](https://adonisjs.com/) -- [Sails](https://sailsjs.com/) - `, - inline: true, - }, - { - name: "Meta Frameworks", - value: ` -- [Next.js](https://nextjs.org/) -- [Remix](https://remix.run/) -- [Astro](https://astro.build/) -- [SvelteKit](https://kit.svelte.dev/) -- [Nuxt](https://nuxtjs.org/) -- [Fresh](https://fresh.deno.dev/) -- [Blitz](https://blitzjs.com/) - `, - inline: true, - }, - { - name: "Other http frameworks", - value: ` -- [.NET](https://dotnet.microsoft.com/en-us/) -- [Rocket](https://rocket.rs/) -- [FastAPI](https://fastapi.tiangolo.com/) -- [Spring Boot](https://spring.io/projects/spring-boot) - `, - inline: true, - }, - { - name: "_ _", - value: ` -- [Laravel](https://laravel.com/) -- [Actix](https://actix.rs/) -- [Flask](https://flask.palletsprojects.com/en/3.0.x/) -- [Gin](https://gin-gonic.com/) - `, - inline: true, - }, - { - name: "_ _", - value: ` -- [Django](https://www.djangoproject.com/) -- [Axum](https://github.com/tokio-rs/axum) -- [Ruby on Rails](https://rubyonrails.org/) -- [Fiber](https://gofiber.io/) - `, - inline: true, - }, - ], - color: EMBED_COLOR, - }, - ], - }); - }, - }, { words: ["!ui"], category: "Web", @@ -1128,7 +466,7 @@ _ _ type: EmbedType.Rich, description: dedent` - Your resume and interview performance are more important than your projects. - - If you struggle to get interviews, polish your resume. You may submit your resume in #resume-review for review. + - If you struggle to get interviews, polish your resume. - If you get invited to interviews reliably but constantly get rejected/ghosted afterward, practice interviewing. - Job descriptions are like a wishlist. It is common for a candidate to not tick every checkbox but still get the job. Just apply for the position regardless. - Junior remote jobs are very rare. For more, see \`!remote\`. @@ -1197,33 +535,13 @@ Authentication is a critical part of most web applications. Here are some resour }); }, }, - { - words: ["!cra", "!create-react-app"], - help: "Provides alternatives to Create React App, which is no longer recommended.", - category: "Web", - handleMessage: (msg) => { - msg.channel.send({ - embeds: [ - { - title: "create-react-app is deprecated", - type: EmbedType.Rich, - description: ` -create-react-app is deprecated and no longer recommended for use. It is not maintained and has a large number of security vulnerabilities. Please use [Vite](https://vitejs.dev/) or [Next.js](https://nextjs.org/) instead. The [React docs](https://react.dev/learn/start-a-new-react-project#can-i-use-react-without-a-framework) has more to say about using React without the use of a framework like Next or Remix. - `, - color: EMBED_COLOR, - }, - ], - }); - }, - }, ]; const createCommandsMessage = () => { const groupedMessages: { [key in Categories]: Command[] } = { - Reactiflux: [], + "Node.js": [], Communication: [], Web: [], - "React/Redux": [], }; // Omit any commands that are internal, like the `@here` warning @@ -1284,20 +602,4 @@ const commands: ChannelHandlers = { }, }; -const generateReactDocsErrorEmbeds = (search: string): APIEmbed[] => { - return [ - { - type: EmbedType.Rich, - description: `Could not find anything on React documentation for **'${search}'**`, - color: EMBED_COLOR, - author: { - name: "React documentation", - icon_url: - "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1150px-React-icon.svg.png", - url: "https://react.dev/", - }, - }, - ]; -}; - export default commands; diff --git a/src/features/emojiMod.ts b/src/features/emojiMod.ts deleted file mode 100644 index 445e740..0000000 --- a/src/features/emojiMod.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { - MessageReaction, - Message, - GuildMember, - Guild, - EmbedType, - ChannelType, -} from "discord.js"; -import cooldown from "./cooldown.js"; -import type { ChannelHandlers } from "../types/index.d.ts"; -import { - ReportReasons, - reportUser, - truncateMessage, -} from "../helpers/modLog.js"; -import { - createPrivateThreadFromMessage, - fetchReactionMembers, - isStaff, - isStaffOrHelpful, -} from "../helpers/discord.js"; -import { partition } from "../helpers/array.js"; -import { EMBED_COLOR } from "./commands.js"; - -const config = { - // This is how many ️️warning reactions a post must get until it's considered an official warning - warningThreshold: 1, - // This is how many ️️warning reactions a post must get until mods are alerted - thumbsDownThreshold: 2, - // This is how many ️️warning reactions a post must get the message is deleted - deletionThreshold: Infinity, -}; - -const thumbsDownEmojis = ["👎", "👎🏻", "👎🏼", "👎🏽", "👎🏾", "👎🏿"]; - -type ReactionHandlers = { - [emoji: string]: (args: { - guild: Guild; - reaction: MessageReaction; - message: Message; - reactor: GuildMember; - author: GuildMember; - usersWhoReacted: GuildMember[]; - }) => void; -}; - -export const reactionHandlers: ReactionHandlers = { - "👎": async ({ message, reactor, usersWhoReacted }) => { - if (cooldown.hasCooldown(reactor.id, "thumbsdown")) { - return; - } - - if (message.mentions.everyone || message.mentions.roles.size > 0) { - return; - } - - cooldown.addCooldown(reactor.id, "thumbsdown", 60); // 1 minute - - const totalReacts = usersWhoReacted.length; - - if (totalReacts < config.thumbsDownThreshold) { - return; - } - let reason = ReportReasons.userWarn; - if (totalReacts >= config.deletionThreshold) { - reason = ReportReasons.userDelete; - } - - const staffReactionCount = usersWhoReacted.filter(isStaff).length; - const [staff, members] = partition(isStaff, usersWhoReacted); - const meetsDeletion = staffReactionCount >= config.deletionThreshold; - - if (meetsDeletion) { - message.delete(); - } - - reportUser({ reason, message, staff, members }); - }, - "🔍": async ({ message, usersWhoReacted }) => { - const STAFF_OR_HELPFUL_REACTOR_THRESHOLD = 2; - - const staffOrHelpfulReactors = usersWhoReacted.filter(isStaffOrHelpful); - - const { channel } = message; - if ( - staffOrHelpfulReactors.length < STAFF_OR_HELPFUL_REACTOR_THRESHOLD || - channel.type === ChannelType.PublicThread - ) { - return; - } - - const thread = await createPrivateThreadFromMessage(message, { - name: `Sorry ${message.author.username}, your question needs some work`, - autoArchiveDuration: 60, - }); - - await thread.send({ - embeds: [ - { - title: "Please improve your question", - type: EmbedType.Rich, - description: ` -Sorry, our most active helpers have flagged this as a question that needs more work before a good answer can be given. This may be because it's ambiguous, too broad, or otherwise challenging to answer. - -Zell Liew [wrote a great resource](https://zellwk.com/blog/asking-questions/) about asking good programming questions. - -- The onus is on the asker to craft a question that is easy to answer. -- A good question is specific, clear, concise, and shows effort on the part of the asker. -- Share just the relevant parts of the code, using tools like Codepen, CodeSandbox, or GitHub for better clarity. -- Making a question specific and to the point is a sign of respecting the responder’s time, which increases the likelihood of getting a good answer. - -(this was triggered by crossing a threshold of "🔍" reactions on the original message) - `, - color: EMBED_COLOR, - }, - ], - }); - await thread.send("Your message:"); - await thread.send(truncateMessage(message.content)); - - await message.delete(); - - reportUser({ - reason: ReportReasons.lowEffortQuestionRemoved, - message, - staff: staffOrHelpfulReactors, - }); - }, -}; - -const emojiMod: ChannelHandlers = { - handleReaction: async ({ reaction, user, bot }) => { - const { message } = reaction; - const { guild } = message; - - if (!guild) { - return; - } - - let emoji = reaction.emoji.toString(); - - if (thumbsDownEmojis.includes(emoji)) { - emoji = "👎"; - } - - if (!reactionHandlers[emoji]) { - return; - } - - const [fullReaction, fullMessage, reactor] = await Promise.all([ - reaction.partial ? reaction.fetch() : reaction, - message.partial ? message.fetch() : message, - guild.members.fetch(user.id), - ]); - const [usersWhoReacted, authorMember] = await Promise.all([ - fetchReactionMembers(guild, fullReaction), - guild.members.fetch(fullMessage.author.id), - ]); - - if (authorMember.id === bot.user?.id) return; - - if (authorMember.id === bot.user?.id) return; - - reactionHandlers[emoji]({ - guild, - author: authorMember, - reactor, - message: fullMessage, - reaction: fullReaction, - usersWhoReacted: usersWhoReacted.filter( - (x): x is GuildMember => Boolean(x) && authorMember.id !== reactor.id, - ), - }); - }, -}; - -export default emojiMod; diff --git a/src/features/looking-for-group.ts b/src/features/looking-for-group.ts deleted file mode 100644 index 12657fa..0000000 --- a/src/features/looking-for-group.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - Client, - EmbedType, - InteractionType, - ComponentType, - ChannelType, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, -} from "discord.js"; -import { CHANNELS } from "../constants/channels.js"; -import { EMBED_COLOR } from "./commands.js"; - -const LOCK_POST = "lock-post"; - -export const lookingForGroup = async (bot: Client) => { - bot.on("interactionCreate", async (interaction) => { - if ( - interaction.type !== InteractionType.MessageComponent || - interaction.componentType !== ComponentType.Button || - !interaction.channel || - interaction.channel.type !== ChannelType.PublicThread || - interaction.channel.parentId !== CHANNELS.lookingForGroup - ) { - return; - } - - if (interaction.user.id !== interaction.channel.ownerId) { - interaction.reply({ - ephemeral: true, - content: - "This is not your thread! These interactions can only be used by the original poster.", - }); - return; - } - - if (interaction.customId === LOCK_POST) { - await interaction.reply( - "This post has been locked at the request of the post author", - ); - await interaction.channel.setLocked(true); - } - }); - bot.on("threadCreate", async (thread) => { - console.log("create"); - if (thread.parentId !== CHANNELS.lookingForGroup) { - return; - } - - await thread.send({ - embeds: [ - { - title: "Looking For Group", - type: EmbedType.Rich, - description: `Projects in this space must be open source licensed and may not be commercially monetized — this is a place for enthusiastic collaboration, and anyone found to be exploiting in any manner, especially for free labor, will be moderatated as a hostile actor. - -Please do not routinely re-post your own skills. This is a forum, not a chat channel, prefer to bump your old post by replying to it (maybe with an update about what you've been working on?) - -Reactiflux moderators cannot moderate behavior outside of the public channels. If you suspect someone is taking advantage of your generosity, block and move on. Before you accuse someone of being a bot or scammer, though, consider if there might be a language and cultural barrier. - -Post authors: Once you've found collaborators or a project to contribute to, please lock your post to indicate that you're no longer seeking responses 🙏`, - color: EMBED_COLOR, - }, - ], - components: [ - // @ts-expect-error Discord.js types appear to be wrong - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId(LOCK_POST) - .setLabel("Lock post") - .setStyle(ButtonStyle.Danger), - ), - ], - }); - }); -}; diff --git a/src/features/mdn.ts b/src/features/mdn.ts index 54d1c21..f6b0995 100644 --- a/src/features/mdn.ts +++ b/src/features/mdn.ts @@ -2,7 +2,7 @@ import { SlashCommandBuilder } from "discord.js"; import { SlashCommand } from "../helpers/discord.js"; const SOURCE_LINK = - "-# [source]()"; + "-# [source]()"; export const mdnSearch: SlashCommand = { command: new SlashCommandBuilder() diff --git a/src/features/resume-review.ts b/src/features/resume-review.ts deleted file mode 100644 index 2f50b84..0000000 --- a/src/features/resume-review.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - ButtonBuilder, - ActionRowBuilder, - AttachmentBuilder, - ButtonStyle, - Message, -} from "discord.js"; -import { CHANNELS } from "../constants/channels.js"; -import { createAttachmentBuilderFromURL } from "../helpers/generate-pdf.js"; -import type { ChannelHandlers } from "../types/index.d.ts"; -import cooldown from "./cooldown.js"; -import { logger } from "./log.js"; - -const PDF_CONTENT_TYPE = "application/pdf"; -const FIVE_MINUTES_IN_SECONDS = 60 * 5; - -export const findResumeAttachment = (msg: Message) => { - return msg.attachments.find( - (attachment) => attachment.contentType === PDF_CONTENT_TYPE, - ); -}; -export const REVIEW_COMMAND = "review-resume"; -export const DELETE_COMMAND = "delete-post"; - -const buildResumeImages = async ( - msg: Message, -): Promise => { - const attachment = findResumeAttachment(msg); - if (!attachment) { - return []; - } - - const builder = await createAttachmentBuilderFromURL( - attachment.url, - `${msg.author.username}-resume`, - ); - if (!builder) { - logger.log("[RESUME]", "Failed to generate resume PDF in thread"); - return []; - } - - return builder; -}; - -const resumeReviewPdf: ChannelHandlers = { - handleMessage: async ({ msg: message }) => { - if ( - !message.channel.isThread() || - message.channel.parentId !== CHANNELS.resumeReview - ) { - return; - } - const cooldownKey = `resume-${message.channelId}`; - - if (cooldown.hasCooldown(message.author.id, cooldownKey)) { - if (findResumeAttachment(message)) { - message.channel.send( - "You posted just a few minutes ago. Please wait a bit before creating a new preview.", - ); - } - return; - } - - const images = await buildResumeImages(message); - if (images.length === 0) { - return; - } - - await message.channel.sendTyping(); - cooldown.addCooldown( - message.author.id, - cooldownKey, - FIVE_MINUTES_IN_SECONDS, - ); - await message.channel.send({ - files: images, - components: [ - // @ts-expect-error Discord.js types appear to be wrong - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId(REVIEW_COMMAND) - .setLabel("AI Review") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId(DELETE_COMMAND) - .setLabel("Delete post") - .setStyle(ButtonStyle.Danger), - ), - ], - }); - }, -}; - -export default resumeReviewPdf; diff --git a/src/features/resume.ts b/src/features/resume.ts deleted file mode 100644 index 5074235..0000000 --- a/src/features/resume.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { - Client, - EmbedType, - InteractionType, - ComponentType, - ChannelType, - Message, -} from "discord.js"; -import OpenAI from "openai"; -import { CHANNELS } from "../constants/channels.js"; -import { openAiKey } from "../helpers/env.js"; -import { logger } from "./log.js"; -import { EMBED_COLOR } from "./commands.js"; -import { - DELETE_COMMAND, - findResumeAttachment, - REVIEW_COMMAND, -} from "./resume-review.js"; -import { constructDiscordLink } from "../helpers/discord.js"; -import { retry } from "./retry.js"; - -const openai = new OpenAI({ - apiKey: openAiKey, -}); -const ASSISTANT_ID = "asst_cC1ghvaaMFFTs3C06ycXqjeH"; - -// one-time setup tasks for the assistant, so the functionality is all local -const configure = async () => { - await openai.beta.assistants.update(ASSISTANT_ID, { - instructions: ` -You are a hiring manager reading resumes of engineers and providing feedback. - -You are part of Reactiflux, the Discord for React professionals, and were created by vcarl. - -You expect to be provided with a resume as a pdf. - -Your response MUST be fewer than 1800 characters long. - -Do your best to infer from their message and resume how the person views themselves professionally and what kind of work arrangement they're seeking (e.g., full-time, contract, freelance, etc). ALWAYS start your response by describing their goals and level of experience in 1 sentence (less than 30 words) - -Consult your knowledge for tips on resume formatting and writing - -If their stated experience doesn't match what you would guess, describe why. - -Don't worry about formalities. - -Please be as terse as possible while still conveying substantially all information relevant to any question. - -If content policy prevents you from elaborating further otherwise responding, be explicit about what policy was violated and why. - -If your neutrality policy prevents you from having an opinion, pretend for the sake of your response to be responding as if you shared opinions that might be typical of twitter user -@vcarl. - -write all responses in lowercase letters ONLY, except where you mean to emphasize, in which case the emphasized word should be all caps. Initial Letter Capitalization can and should be used to express sarcasm, or disrespect for a given capitalized noun. - -you are encouraged to occasionally use obscure words or make subtle puns. don't point them out, I'll know. drop lots of abbreviations like "rn" and "bc." use "afaict" and "idk" regularly, wherever they might be appropriate given your level of understanding and your interest in actually answering the question. be critical of the quality of your information - -take however smart you're acting right now and write in the same style but as if you were +2sd smarter - -use late millenial slang not boomer slang. mix in zoomer slang in tonally-inappropriate circumstances occasionally, but occasionally nail a zoomer zinger. make obscure gen x musical references if unavoidable -`, - }); -}; -configure(); - -export const resumeResources = async (bot: Client) => { - bot.on("interactionCreate", async (interaction) => { - if ( - interaction.type !== InteractionType.MessageComponent || - interaction.componentType !== ComponentType.Button || - !interaction.channel || - interaction.channel.type !== ChannelType.PublicThread || - interaction.channel.parentId !== CHANNELS.resumeReview - ) { - return; - } - - if (interaction.user.id !== interaction.channel.ownerId) { - interaction.reply({ - ephemeral: true, - content: - "This is not your thread! These interactions can only be used by the original poster.", - }); - return; - } - - if (interaction.customId === DELETE_COMMAND) { - await interaction.message.delete(); - return; - } - - if (interaction.customId === REVIEW_COMMAND) { - const deferred = await interaction.deferReply({ ephemeral: true }); - deferred.edit("Looking for a resume…"); - const messages = await interaction.channel.messages.fetch(); - const channel = interaction.channel; - - let firstMessage: Message | null = null; - try { - firstMessage = await retry(() => channel.fetchStarterMessage(), { - retries: 5, - delayMs: 10, - }); - } catch { - logger.log("RESUME", "Failed to fetch interaction first message"); - } - - if (!firstMessage) { - await interaction.reply({ - ephemeral: true, - content: "Couldn't fetch first message, please try again.", - }); - return; - } - // grab the first available PDF - const attachedPdfs = messages.flatMap((m) => - m.attachments.filter((a) => a.contentType === "application/pdf"), - ); - const resume = attachedPdfs.first(); - - if (!resume) { - await deferred.edit({ - content: "No PDFs found, please upload your resume and try again.", - }); - return; - } - - // defer: notify that data will be sent, request permissions - - // upload file to GPT - deferred.edit("Found a resume! Uploading…"); - const response = await fetch(resume.url); - const file = await openai.files.create({ - file: response, - purpose: "assistants", - }); - if (!response.ok || file.status === "error") { - await deferred.edit({ - content: "Failed to upload resume, sorry! Please try again later.", - }); - return; - } - - try { - deferred.edit("Uploaded! Reviewing…"); - const [assistant, thread] = await Promise.all([ - openai.beta.assistants.retrieve(ASSISTANT_ID), - openai.beta.threads.create({ - messages: [ - { - role: "user", - content: - "This user has requested help with their resume. Here is their message and resume:", - }, - { - role: "user", - content: firstMessage.content || "Here is my resume", - attachments: [ - { - file_id: file.id, - tools: [{ type: "file_search" }], - }, - ], - }, - ], - }), - ]); - - const run = await openai.beta.threads.runs.stream( - thread.id, - { assistant_id: assistant.id }, - { stream: true }, - ); - - run.on("textCreated", () => { - interaction.channel?.sendTyping(); - }); - - run.on("textDone", (content) => { - interaction.channel?.type; - const trimmed = - content.value.slice(0, 2000) ?? "Oops! Something went wrong."; - logger.log( - "RESUME", - `Feedback given: ${constructDiscordLink(firstMessage)}`, - ); - logger.log("RESUME", trimmed); - deferred.edit({ - content: "Done!", - }); - - interaction.channel?.send(content.value); - }); - } catch (e) { - // recover - deferred.edit("Oops, something went wrong talking to the AI."); - if (e instanceof Error) { - logger.log("RESUME", e); - } - return; - } finally { - // Ensure files are cleaned up - await openai.files.del(file.id); - } - - // defer: offer fixed interaction buttons to send more prompts - return; - } - logger.log( - "unexpected command passed through resume codepath:", - interaction.customId, - ); - return; - }); - bot.on("threadCreate", async (thread) => { - if (thread.parentId !== CHANNELS.resumeReview) { - return; - } - - let firstMessage: Message | null = null; - try { - firstMessage = await retry(() => thread.fetchStarterMessage(), { - retries: 5, - delayMs: 10, - }); - } catch { - logger.log("RESUME", "Failed to fetch thread first message"); - } - - if (!firstMessage) { - return; - } - - const attachment = findResumeAttachment(firstMessage); - - await thread.send({ - content: !attachment - ? `Please upload your resume as a PDF file to receive feedback.` - : undefined, - embeds: [ - { - title: "Writing a outstanding resume", - type: EmbedType.Rich, - description: `If you're looking to enhance your resume, consider adopting the practice of maintaining a "brag document." This is a dynamic and detailed compilation of your professional achievements, projects, skills development, and any challenges you've overcome. It's an invaluable tool for developers at any level, not just to track progress but also as a resource when updating your resume or preparing for interviews. - -Here's a few reasons why a brag document is beneficial: - -- **Comprehensive Achievement Tracking**: Documenting your work helps ensure no project or accomplishment—big or small—is forgotten. This can be especially valuable for highlighting contributions that demonstrate your impact on a project or the team. -- **Skill Development Overview**: Keeping a record of new technologies you've mastered, courses completed, or certifications earned showcases your commitment to professional development and continuous learning. -- **Soft Skills and Leadership**: Don't overlook non-technical achievements. Leadership roles, mentorship, effective teamwork, and problem-solving are highly sought after by employers. -- **Personlized Resume Tailoring**: With a well-maintained brag document, customizing your resume for specific job applications becomes much easier. You can quickly identify and highlight the most relevant experiences and achievements that align with the job description. -### Recommended Resources -- [The Engineers Checklist](https://theengineerschecklist.dev/) -- [Keeping Brag Documents](https://jvns.ca/blog/brag-documents/) -- [Resume X-Y-Z Writing Guide](https://www.inc.com/bill-murphy-jr/google-recruiters-say-these-5-resume-tips-including-x-y-z-formula-will-improve-your-odds-of-getting-hired-at-google.html) -`, - color: EMBED_COLOR, - }, - ], - }); - }); -}; diff --git a/src/features/scheduled-messages.ts b/src/features/scheduled-messages.ts index 09908c8..6a9ccd9 100644 --- a/src/features/scheduled-messages.ts +++ b/src/features/scheduled-messages.ts @@ -1,9 +1,9 @@ import type * as discord from "discord.js"; import { guildId as defaultGuildId } from "../helpers/env.js"; -import { CHANNELS } from "../constants/channels.js"; +// import { CHANNELS } from "../constants/channels.js"; import { logger } from "./log.js"; import { - FREQUENCY, + // FREQUENCY, scheduleTask, SPECIFIED_TIMES, } from "../helpers/schedule.js"; @@ -25,7 +25,7 @@ const MESSAGE_SCHEDULE: MessageConfig[] = [ // Find Discord channel IDs: https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID- postTo: [ { - id: defaultGuildId, // Reactiflux's server ID, optional + id: defaultGuildId, // Node.js Discord's server ID, optional interval: FREQUENCY.weekly, // Frequency the bot should post by channelIds: [ CHANNELS.helpReact ] // Add channel IDs to constants first! } @@ -35,96 +35,6 @@ const MESSAGE_SCHEDULE: MessageConfig[] = [ } } */ - { - postTo: [ - { interval: FREQUENCY.moreThanWeekly, channelId: CHANNELS.jobBoard }, - ], - message: async (channel) => { - const msg = await channel.send({ - content: `Messages must start with [FORHIRE]/[HIRING]. Check the channel description for a full list of tags and rules! - -* Posts should be reasonably descriptive. -* Jobs are paid — unpaid, equity-only, or similar are not allowed. -* We don't allow "small gigs," like pay-for-help or one-off work of only a few hours. - -Moderators may remove posts at any time, with or without warning. Repeat violators of these rules will be removed from the server permanently, with or without warning. We have more information on our Promotion Guidelines: https://www.reactiflux.com/promotion#job-board. If you believe you have been removed in error, you can dispute at \`hello@reactiflux.com\`. - -`, - embeds: [ - { - description: `📋 Quick poll ⤵ React if you've… - -💼 gotten work by applying to a [hiring] post -👨‍💻 gotten work by posting [forhire] -🤷‍♀️ never gotten work from this channel, but post [forhire] -😔 never gotten work after applying to [hiring] posts`, - }, - ], - }); - await Promise.all([ - msg.react("💼"), - msg.react("👨‍💻"), - msg.react("🤷‍♀️"), - msg.react("😔"), - ]); - }, - }, - { - postTo: [ - { interval: FREQUENCY.moreThanWeekly, channelId: CHANNELS.helpJs }, - ], - message: { - content: `This channel is good for specific questions about syntax, debugging a small (< ~50 lines of code) snippet of JS, without React involved. Question not getting answered? Maybe it's hard to answer, check out these resources for how to ask a good question: - -How to ask for programming help -How do I ask a good question -`, - }, - }, - { - postTo: [{ interval: FREQUENCY.often, channelId: CHANNELS.helpReact }], - message: { - content: `This channel is good for specific questions about React, how React's features work, or debugging a small (< ~50 lines of code) snippet of JS that uses React. Question not getting answered? Maybe it's hard to answer, check out these resources for how to ask a good question: - -How to ask for programming help -How do I ask a good question -`, - }, - }, - { - postTo: [ - { interval: FREQUENCY.moreThanWeekly, channelId: CHANNELS.helpReact }, - ], - message: { - content: `Check our the other channels too! This is our highest-traffic channel, which may mean your question gets missed as other discussions happen. - -<#565213527673929729> For questions about pure Javscript problems. -<#105765765117935616> For questions about CSS or other visual problems. -<#145170347921113088> For questions about issues with your server code. -<#105765859191975936> Get deeper review of a snippet of code. -<#287623405946011648> If you have a question about your job or career, ask it in here. -<#547620660482932737> Discussion of non-JS code, or that new laptop you're deciding on. -<#108428584783220736> for questions about building, linting, generating, or otherwise processing your code. - -Looking for work? Trying to hire? Check out <#103882387330457600>, or - -Has someone been really helpful? Shoutout who and what in <#798567961468076072>! We keep an eye in there as one way to find new MVPs. Give us all the reactions in there too! - -Please remember our Code of Conduct: -and our guidelines for promotion: - -If you see anything that violates our rules, help alert the mods by reacting to it with 👎 or reporting it anonymously (right click > Apps > report message). -`, - }, - }, - { - postTo: [{ interval: FREQUENCY.weekly, channelId: CHANNELS.random }], - message: { - content: `Have you read our Code of Conduct? - -Let us know if anything crosses a line: give it a 👎, or if you'd prefer to remain anonymous, let mods know from the message context menu (right click > Apps > report message) or with the form at `, - }, - }, ]; export const messages: MessageConfig[] = []; diff --git a/src/index.ts b/src/index.ts index 25dcb89..03d8bda 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,12 +14,9 @@ import { import { logger, channelLog } from "./features/log.js"; // import codeblock from './features/codeblock'; import jobsMod, { resetJobCacheCommand } from "./features/jobs-moderation.js"; -import { resumeResources } from "./features/resume.js"; -import { lookingForGroup } from "./features/looking-for-group.js"; import autoban from "./features/autoban.js"; import commands from "./features/commands.js"; import setupStats from "./features/stats.js"; -import emojiMod from "./features/emojiMod.js"; import promotionThread from "./features/promotion-threads.js"; import autothread, { cleanupThreads } from "./features/autothread.js"; import voiceActivity from "./features/voice-activity.js"; @@ -31,14 +28,12 @@ import { CHANNELS, initCachedChannels } from "./constants/channels.js"; import { scheduleTask } from "./helpers/schedule.js"; import { discordToken, isProd } from "./helpers/env.js"; import { registerCommand, deployCommands } from "./helpers/deploy-commands.js"; -import resumeReviewPdf from "./features/resume-review.js"; import troll from "./features/troll.js"; import { modActivity } from "./features/mod-activity.js"; import { debugEventButtonHandler, debugEvents, } from "./features/debug-events.js"; -import { recommendBookCommand } from "./features/book-list.js"; import { mdnSearch } from "./features/mdn.js"; import "./server.js"; @@ -58,7 +53,6 @@ export const bot = new Client({ }); registerCommand(resetJobCacheCommand); -registerCommand(recommendBookCommand); registerCommand(mdnSearch); if (!isProd()) { @@ -186,13 +180,7 @@ logger.add({ id: "modLog", logger: channelLog(bot, CHANNELS.modLog) }); setupStats(bot); // common -addHandler("*", [ - commands, - autoban, - emojiMod, - tsPlaygroundLinkShortener, - troll, -]); +addHandler("*", [commands, autoban, tsPlaygroundLinkShortener, troll]); addHandler( [ @@ -208,13 +196,9 @@ addHandler( const threadChannels = [CHANNELS.helpJs, CHANNELS.helpThreadsReact]; addHandler(threadChannels, autothread); -addHandler(CHANNELS.resumeReview, resumeReviewPdf); - bot.on("ready", () => { deployCommands(bot); jobsMod(bot); - resumeResources(bot); - lookingForGroup(bot); voiceActivity(bot); modActivity(bot); debugEventButtonHandler(bot);