├── .husky ├── commit-msg └── pre-commit ├── apps └── website │ ├── .env.development │ ├── tsconfig.eslint.json │ ├── postcss.config.js │ ├── public │ └── assets │ │ ├── chatsift.png │ │ ├── favicon.ico │ │ └── fonts │ │ ├── Author-Bold.eot │ │ ├── Author-Bold.ttf │ │ ├── Author-Bold.woff │ │ ├── Author-Bold.woff2 │ │ ├── Author-Italic.eot │ │ ├── Author-Italic.ttf │ │ ├── Author-Light.eot │ │ ├── Author-Light.ttf │ │ ├── Author-Light.woff │ │ ├── Author-Medium.eot │ │ ├── Author-Medium.ttf │ │ ├── Author-Italic.woff │ │ ├── Author-Italic.woff2 │ │ ├── Author-Light.woff2 │ │ ├── Author-Medium.woff │ │ ├── Author-Medium.woff2 │ │ ├── Author-Regular.eot │ │ ├── Author-Regular.ttf │ │ ├── Author-Regular.woff │ │ ├── Author-Semibold.eot │ │ ├── Author-Semibold.ttf │ │ ├── Author-Variable.eot │ │ ├── Author-Variable.ttf │ │ ├── Author-BoldItalic.eot │ │ ├── Author-BoldItalic.ttf │ │ ├── Author-BoldItalic.woff │ │ ├── Author-Extralight.eot │ │ ├── Author-Extralight.ttf │ │ ├── Author-Extralight.woff │ │ ├── Author-LightItalic.eot │ │ ├── Author-LightItalic.ttf │ │ ├── Author-Regular.woff2 │ │ ├── Author-Semibold.woff │ │ ├── Author-Semibold.woff2 │ │ ├── Author-Variable.woff │ │ ├── Author-Variable.woff2 │ │ ├── Author-BoldItalic.woff2 │ │ ├── Author-Extralight.woff2 │ │ ├── Author-LightItalic.woff │ │ ├── Author-LightItalic.woff2 │ │ ├── Author-MediumItalic.eot │ │ ├── Author-MediumItalic.ttf │ │ ├── Author-MediumItalic.woff │ │ ├── Author-ExtralightItalic.eot │ │ ├── Author-ExtralightItalic.ttf │ │ ├── Author-MediumItalic.woff2 │ │ ├── Author-SemiboldItalic.eot │ │ ├── Author-SemiboldItalic.ttf │ │ ├── Author-SemiboldItalic.woff │ │ ├── Author-SemiboldItalic.woff2 │ │ ├── Author-VariableItalic.eot │ │ ├── Author-VariableItalic.ttf │ │ ├── Author-VariableItalic.woff │ │ ├── Author-VariableItalic.woff2 │ │ ├── Author-ExtralightItalic.woff │ │ └── Author-ExtralightItalic.woff2 │ ├── src │ ├── app │ │ ├── page.tsx │ │ ├── dashboard │ │ │ ├── layout.tsx │ │ │ ├── [id] │ │ │ │ ├── ama │ │ │ │ │ ├── amas │ │ │ │ │ │ ├── [amaId] │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ ├── CreateAMACard.tsx │ │ │ │ │ │ │ ├── AMASessionCard.tsx │ │ │ │ │ │ │ ├── IncludeEndedToggle.tsx │ │ │ │ │ │ │ └── AMASessionsList.tsx │ │ │ │ │ │ ├── new │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ └── _components │ │ │ │ │ │ │ │ ├── RefreshServerDataButton.tsx │ │ │ │ │ │ │ │ ├── PromptModeToggle.tsx │ │ │ │ │ │ │ │ ├── SnowflakeInput.tsx │ │ │ │ │ │ │ │ └── RawPromptField.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── _components │ │ │ │ │ │ └── AMADashboardCrumbs.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── settings │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── _components │ │ │ │ │ │ ├── GrantsList.tsx │ │ │ │ │ │ ├── AddGrantCard.tsx │ │ │ │ │ │ └── GrantCard.tsx │ │ │ │ └── layout.tsx │ │ │ ├── _components │ │ │ │ ├── RefreshGuildsButton.tsx │ │ │ │ ├── GuildList.tsx │ │ │ │ └── GuildCard.tsx │ │ │ └── page.tsx │ │ ├── not-found.tsx │ │ └── layout.tsx │ ├── utils │ │ ├── urls.ts │ │ ├── bots.tsx │ │ ├── util.ts │ │ └── channels.tsx │ ├── hooks │ │ └── isMounted.ts │ ├── components │ │ ├── user │ │ │ ├── LoginButton.tsx │ │ │ ├── UserErrorHandler.tsx │ │ │ ├── UserAvatarMe.tsx │ │ │ ├── UserDesktop.tsx │ │ │ ├── LogoutButton.tsx │ │ │ ├── UserAvatar.tsx │ │ │ └── UserMobile.tsx │ │ ├── icons │ │ │ ├── SvgHamburger.tsx │ │ │ ├── SvgClose.tsx │ │ │ ├── SvgPlus.tsx │ │ │ ├── SvgChevronDown.tsx │ │ │ ├── SvgChatSift.tsx │ │ │ ├── SvgAutoModerator.tsx │ │ │ ├── channels │ │ │ │ ├── SvgChannelForum.tsx │ │ │ │ ├── SvgChannelText.tsx │ │ │ │ ├── SvgChannelThread.tsx │ │ │ │ └── SvgChannelCategory.tsx │ │ │ ├── SvgRefresh.tsx │ │ │ ├── SvgDarkTheme.tsx │ │ │ ├── SvgLightTheme.tsx │ │ │ ├── SvgGitHub.tsx │ │ │ ├── SvgAMA.tsx │ │ │ └── SvgDiscord.tsx │ │ ├── common │ │ │ ├── Skeleton.tsx │ │ │ ├── Logo.tsx │ │ │ ├── Heading.tsx │ │ │ ├── GenericAvatar.tsx │ │ │ ├── GenericAvatarImages.tsx │ │ │ ├── Button.tsx │ │ │ ├── ScrollArea.tsx │ │ │ ├── Avatar.tsx │ │ │ ├── Providers.tsx │ │ │ ├── GuildIcon.tsx │ │ │ └── SearchBar.tsx │ │ ├── nav │ │ │ ├── navbarItems.ts │ │ │ ├── Navbar.tsx │ │ │ └── NavbarDesktop.tsx │ │ └── footer │ │ │ ├── ThemeSwitchButton.tsx │ │ │ └── Footer.tsx │ ├── styles │ │ └── globals.css │ └── middleware.ts │ ├── next-env.d.ts │ ├── tsconfig.json │ ├── next.config.mjs │ ├── tailwind.config.ts │ └── package.json ├── packages ├── public │ ├── discord-utils │ │ ├── vitest.config.ts │ │ ├── src │ │ │ ├── index.ts │ │ │ └── __tests__ │ │ │ │ ├── sortChannels.test.ts │ │ │ │ └── embed.test.ts │ │ ├── tsconfig.eslint.json │ │ ├── tsup.config.ts │ │ ├── tsconfig.json │ │ ├── README.md │ │ └── package.json │ ├── pino-rotate-file │ │ ├── vitest.config.ts │ │ ├── tsconfig.eslint.json │ │ ├── tsup.config.ts │ │ ├── tsconfig.json │ │ ├── README.md │ │ └── package.json │ └── parse-relative-time │ │ ├── vitest.config.ts │ │ ├── tsconfig.eslint.json │ │ ├── tsup.config.ts │ │ ├── tsconfig.json │ │ ├── README.md │ │ ├── package.json │ │ └── src │ │ └── __tests__ │ │ └── index.test.ts └── private │ ├── core │ ├── tsconfig.eslint.json │ ├── src │ │ ├── lib │ │ │ ├── constants.ts │ │ │ ├── discordPermissions.ts │ │ │ ├── util.ts │ │ │ └── promiseAllObject.ts │ │ ├── index.ts │ │ └── types │ │ │ └── entities.ts │ ├── tsconfig.json │ └── package.json │ └── backend-core │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ ├── src │ ├── index.ts │ └── lib │ │ ├── redis.ts │ │ ├── data │ │ ├── bots.ts │ │ ├── _entity.ts │ │ └── _store.ts │ │ ├── logger.ts │ │ ├── database.ts │ │ ├── context.ts │ │ └── env.ts │ └── package.json ├── services ├── api │ ├── tsconfig.eslint.json │ ├── tsconfig.json │ ├── src │ │ ├── util │ │ │ ├── setEquals.ts │ │ │ ├── schemas.ts │ │ │ ├── constants.ts │ │ │ ├── sendBoom.ts │ │ │ ├── discordAPI.ts │ │ │ ├── crypt.ts │ │ │ ├── stateCookie.ts │ │ │ ├── __tests__ │ │ │ │ └── crypt.test.ts │ │ │ └── channels.ts │ │ ├── bin.ts │ │ ├── routes │ │ │ ├── _types │ │ │ │ ├── routeTypes.ts │ │ │ │ └── index.ts │ │ │ ├── routes.ts │ │ │ ├── auth │ │ │ │ ├── logout.ts │ │ │ │ ├── me.ts │ │ │ │ └── discord.ts │ │ │ ├── guilds │ │ │ │ ├── deleteGrant.ts │ │ │ │ ├── getGrants.ts │ │ │ │ ├── get.ts │ │ │ │ └── createGrant.ts │ │ │ └── ama │ │ │ │ ├── updateAMA.ts │ │ │ │ └── getAMAs.ts │ │ ├── middleware │ │ │ ├── validate.ts │ │ │ ├── __tests__ │ │ │ │ ├── validate.test.ts │ │ │ │ └── jsonParser.test.ts │ │ │ ├── jsonParser.ts │ │ │ └── attachHttpUtils.ts │ │ └── index.ts │ └── package.json └── ama-bot │ ├── tsconfig.eslint.json │ ├── src │ ├── lib │ │ ├── rest.ts │ │ ├── queues.ts │ │ ├── collector.ts │ │ ├── client.ts │ │ ├── gateway.ts │ │ └── components.ts │ ├── index.ts │ └── bin.ts │ ├── tsconfig.json │ └── package.json ├── .github ├── auto_assign.yml ├── workflows │ ├── pr-automation.yml │ ├── sync-labels.yml │ ├── test.yml │ ├── deploy-manual.yml │ └── deploy.yml └── labels.yml ├── prisma ├── migrations │ └── migration_lock.toml └── schema.prisma ├── .prettierignore ├── tsconfig.json ├── README.md ├── .prettierrc.json ├── .vscode └── settings.json ├── compose ├── .yarnrc.yml ├── .env.private.example ├── .commitlintrc.json ├── tsconfig.eslint.json ├── .dockerignore ├── .env.public ├── LICENSE ├── vitest.config.ts ├── turbo.json ├── .gitattributes ├── tsup.config.ts ├── .gitignore ├── tsconfig.base.json ├── Dockerfile ├── actions └── yarnCache │ └── action.yml ├── docker-compose.yml └── package.json /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | yarn commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn build && yarn lint && yarn test:nocov 2 | -------------------------------------------------------------------------------- /apps/website/.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL=http://localhost:7004 2 | -------------------------------------------------------------------------------- /packages/public/discord-utils/vitest.config.ts: -------------------------------------------------------------------------------- 1 | export { default } from '../../../vitest.config'; 2 | -------------------------------------------------------------------------------- /packages/public/pino-rotate-file/vitest.config.ts: -------------------------------------------------------------------------------- 1 | export { default } from '../../../vitest.config'; 2 | -------------------------------------------------------------------------------- /packages/public/parse-relative-time/vitest.config.ts: -------------------------------------------------------------------------------- 1 | export { default } from '../../../vitest.config'; 2 | -------------------------------------------------------------------------------- /packages/public/discord-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './embed.js'; 2 | export * from './sortChannels.js'; 3 | -------------------------------------------------------------------------------- /apps/website/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.eslint.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /services/api/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.eslint.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | addReviewers: true 2 | reviewers: 3 | - didinele 4 | numberOfReviewers: 0 5 | runOnDraft: true 6 | -------------------------------------------------------------------------------- /services/ama-bot/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.eslint.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/website/public/assets/chatsift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/chatsift.png -------------------------------------------------------------------------------- /apps/website/public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/favicon.ico -------------------------------------------------------------------------------- /packages/private/core/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.eslint.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/website/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | export default function HomePage() { 2 | return

:)

; 3 | } 4 | -------------------------------------------------------------------------------- /packages/private/backend-core/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.eslint.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/public/discord-utils/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.eslint.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/public/pino-rotate-file/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.eslint.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/public/parse-relative-time/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.eslint.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Bold.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Bold.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Bold.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Bold.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Italic.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Italic.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Light.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Light.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Light.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Medium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Medium.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Medium.ttf -------------------------------------------------------------------------------- /packages/public/discord-utils/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { createTsupConfig } from '../../../tsup.config'; 2 | 3 | export default createTsupConfig(); 4 | -------------------------------------------------------------------------------- /packages/public/pino-rotate-file/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { createTsupConfig } from '../../../tsup.config'; 2 | 3 | export default createTsupConfig(); 4 | -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Italic.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Italic.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Light.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Medium.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Medium.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Regular.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Regular.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Regular.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Semibold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Semibold.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Semibold.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Variable.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Variable.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Variable.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Variable.ttf -------------------------------------------------------------------------------- /packages/public/parse-relative-time/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { createTsupConfig } from '../../../tsup.config'; 2 | 3 | export default createTsupConfig(); 4 | -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-BoldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-BoldItalic.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-BoldItalic.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-BoldItalic.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Extralight.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Extralight.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Extralight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Extralight.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Extralight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Extralight.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-LightItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-LightItalic.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-LightItalic.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Regular.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Semibold.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Semibold.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Variable.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Variable.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Variable.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Variable.woff2 -------------------------------------------------------------------------------- /apps/website/src/utils/urls.ts: -------------------------------------------------------------------------------- 1 | export const URLS = { 2 | API: { 3 | LOGIN: `${process.env['NEXT_PUBLIC_API_URL']}/v3/auth/discord`, 4 | }, 5 | } as const; 6 | -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-BoldItalic.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-Extralight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-Extralight.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-LightItalic.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-LightItalic.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-MediumItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-MediumItalic.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-MediumItalic.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-MediumItalic.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-ExtralightItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-ExtralightItalic.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-ExtralightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-ExtralightItalic.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-MediumItalic.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-SemiboldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-SemiboldItalic.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-SemiboldItalic.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-SemiboldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-SemiboldItalic.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-SemiboldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-SemiboldItalic.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-VariableItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-VariableItalic.eot -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-VariableItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-VariableItalic.ttf -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-VariableItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-VariableItalic.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-VariableItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-VariableItalic.woff2 -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-ExtralightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-ExtralightItalic.woff -------------------------------------------------------------------------------- /apps/website/public/assets/fonts/Author-ExtralightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChatSift/chatsift/HEAD/apps/website/public/assets/fonts/Author-ExtralightItalic.woff2 -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/.turbo 2 | **/node_modules 3 | **/dist 4 | **/coverage 5 | **/.next 6 | **/build 7 | **/out 8 | .yarn 9 | 10 | packages/private/core/src/types/entities.ts 11 | -------------------------------------------------------------------------------- /packages/private/core/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const BOTS = ['AMA'] as const; 2 | 3 | export type BotId = (typeof BOTS)[number]; 4 | 5 | export const NewAccessTokenHeader = 'X-Update-Access-Token' as const; 6 | -------------------------------------------------------------------------------- /services/ama-bot/src/lib/rest.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from '@chatsift/backend-core'; 2 | import { REST } from '@discordjs/rest'; 3 | 4 | export const rest = new REST({ version: '10' }).setToken(getContext().env.AMA_BOT_TOKEN); 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Root", 4 | "extends": "./tsconfig.base.json", 5 | "compilerOptions": { 6 | "noEmit": true 7 | }, 8 | "include": [] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chatsift 2 | 3 | Monorepo for all of our bots and their common utilities, along with some NPM packages. 4 | 5 | ## Licensing 6 | 7 | This project is lincensed under the GNU AGPLv3 license. View the full file [here](./LICENSE). 8 | -------------------------------------------------------------------------------- /services/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["dist", "node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc.json", 3 | "printWidth": 120, 4 | "useTabs": true, 5 | "singleQuote": true, 6 | "quoteProps": "as-needed", 7 | "trailingComma": "all", 8 | "endOfLine": "lf" 9 | } 10 | -------------------------------------------------------------------------------- /packages/private/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/constants.js'; 2 | export * from './lib/discordPermissions.js'; 3 | export * from './lib/promiseAllObject.js'; 4 | export * from './lib/util.js'; 5 | 6 | export type * from './types/entities.js'; 7 | -------------------------------------------------------------------------------- /packages/private/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["dist", "node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/website/src/utils/bots.tsx: -------------------------------------------------------------------------------- 1 | import type { BotId } from '@chatsift/core'; 2 | import { SvgAMA } from '@/components/icons/SvgAMA'; 3 | 4 | export const Bots = { 5 | AMA: { Icon: SvgAMA }, 6 | } as const satisfies Record; 7 | -------------------------------------------------------------------------------- /packages/private/backend-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["dist", "node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/public/discord-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["dist", "node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/public/pino-rotate-file/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["dist", "node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "workbench.editor.customLabels.patterns": { 4 | // Next JS 5 | "**/src/app/**/page.tsx": "${dirname} - Page", 6 | "**/src/app/**/layout.tsx": "${dirname} - Layout", 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/public/parse-relative-time/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "rootDir": "./src" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["dist", "node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /compose: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -f .env.public ] 4 | then 5 | export $(cat .env.public | xargs) 6 | fi 7 | 8 | if [ -f .env.private ] 9 | then 10 | export $(cat .env.private | xargs) 11 | fi 12 | 13 | docker compose \ 14 | -f docker-compose.yml \ 15 | ${@%$0} 16 | -------------------------------------------------------------------------------- /apps/website/src/hooks/isMounted.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export function useIsMounted(): boolean { 4 | const [mounted, setMounted] = useState(false); 5 | 6 | useEffect(() => { 7 | setMounted(true); 8 | }, []); 9 | 10 | return mounted; 11 | } 12 | -------------------------------------------------------------------------------- /services/api/src/util/setEquals.ts: -------------------------------------------------------------------------------- 1 | export function setEquals(a: Set, b: Set): boolean { 2 | if (a.size !== b.size) { 3 | return false; 4 | } 5 | 6 | for (const aItem of a) { 7 | if (!b.has(aItem)) { 8 | return false; 9 | } 10 | } 11 | 12 | return true; 13 | } 14 | -------------------------------------------------------------------------------- /apps/website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 7 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react'; 2 | import { NavGateProvider } from '@/components/common/NavGate'; 3 | 4 | export default function DashboardLayout({ children }: PropsWithChildren) { 5 | return {children}; 6 | } 7 | -------------------------------------------------------------------------------- /apps/website/src/components/user/LoginButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/common/Button'; 2 | import { URLS } from '@/utils/urls'; 3 | 4 | export function LoginButton() { 5 | return ( 6 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /services/ama-bot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src", 6 | "declaration": false, 7 | "declarationMap": false, 8 | }, 9 | "include": ["src/**/*"], 10 | "exclude": ["dist", "node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /services/api/src/util/schemas.ts: -------------------------------------------------------------------------------- 1 | import { SnowflakeRegex } from '@sapphire/discord-utilities'; 2 | import z from 'zod'; 3 | 4 | export const snowflakeSchema = z.string().regex(SnowflakeRegex); 5 | 6 | export const queryWithFreshSchema = z.strictObject({ 7 | force_fresh: z.stringbool().optional().default(false), 8 | }); 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableGlobalCache: false 2 | 3 | logFilters: 4 | - code: YN0002 5 | level: discard 6 | - code: YN0013 7 | level: discard 8 | - code: YN0032 9 | level: discard 10 | - code: YN0060 11 | level: discard 12 | 13 | nodeLinker: node-modules 14 | 15 | yarnPath: .yarn/releases/yarn-4.9.4.cjs 16 | -------------------------------------------------------------------------------- /.github/workflows/pr-automation.yml: -------------------------------------------------------------------------------- 1 | name: 'PR Automation' 2 | 3 | on: 4 | pull_request_target: 5 | 6 | jobs: 7 | triage: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Automatically assign reviewers 11 | if: github.event.action == 'opened' 12 | uses: kentaro-m/auto-assign-action@v1.2.1 13 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/SvgHamburger.tsx: -------------------------------------------------------------------------------- 1 | export function SvgHamburger() { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /services/ama-bot/src/index.ts: -------------------------------------------------------------------------------- 1 | import { startGuildSyncing } from './lib/client.js'; 2 | import { registerHandlers } from './lib/components.js'; 3 | import { gateway } from './lib/gateway.js'; 4 | 5 | export async function bin(): Promise { 6 | await registerHandlers(); 7 | 8 | await gateway.connect(); 9 | startGuildSyncing(); 10 | } 11 | -------------------------------------------------------------------------------- /services/api/src/util/constants.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from '@chatsift/backend-core'; 2 | import type { SerializeOptions } from 'cookie'; 3 | 4 | export const cookieWithDomain = (cookie: Cookie): Cookie => ({ 5 | ...cookie, 6 | domain: getContext().env.IS_PRODUCTION ? getContext().env.ROOT_DOMAIN : undefined, 7 | }); 8 | -------------------------------------------------------------------------------- /apps/website/src/components/common/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utils/util'; 2 | 3 | export function Skeleton({ className, ...props }: React.HTMLAttributes) { 4 | return ( 5 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /.env.private.example: -------------------------------------------------------------------------------- 1 | IS_PRODUCTION=false # obviously, set to true in prod. 2 | 3 | ENCRYPTION_KEY=boop # generate using node -e "console.log(require('crypto').randomBytes(32).toString('base64'));" 4 | OAUTH_DISCORD_CLIENT_SECRET=boop # from your discord application page 5 | 6 | LOCAL_DATABASE_PORT=5432 7 | LOCAL_DOZZLE_PORT=8080 8 | 9 | AMA_BOT_TOKEN=boop 10 | -------------------------------------------------------------------------------- /packages/private/core/src/lib/discordPermissions.ts: -------------------------------------------------------------------------------- 1 | import type { ValueResolvable } from '@sapphire/bitfield'; 2 | import { BitField } from '@sapphire/bitfield'; 3 | import { PermissionFlagsBits } from 'discord-api-types/v10'; 4 | 5 | export const PermissionsBitField = new BitField(PermissionFlagsBits); 6 | 7 | export type PermissionsResolvable = ValueResolvable; 8 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/commitlintrc.json", 3 | "extends": ["@commitlint/config-angular"], 4 | "rules": { 5 | "type-enum": [ 6 | 2, 7 | "always", 8 | ["chore", "build", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test", "types"] 9 | ], 10 | "scope-case": [0], 11 | "subject-exclamation-mark": [0] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/private/backend-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@chatsift/core'; 2 | 3 | export type * from './lib/data/_entity.js'; 4 | export * from './lib/data/_store.js'; 5 | export * from './lib/data/bots.js'; 6 | 7 | export * from './lib/context.js'; 8 | export * from './lib/database.js'; 9 | export * from './lib/env.js'; 10 | export * from './lib/logger.js'; 11 | export * from './lib/redis.js'; 12 | -------------------------------------------------------------------------------- /apps/website/src/components/nav/navbarItems.ts: -------------------------------------------------------------------------------- 1 | interface NavbarItem { 2 | readonly href: string; 3 | readonly name: string; 4 | } 5 | 6 | export const navbarItems = [ 7 | { 8 | name: 'Dashboard', 9 | href: '/dashboard', 10 | }, 11 | { 12 | name: 'GitHub', 13 | href: '/github', 14 | }, 15 | { 16 | name: 'Support', 17 | href: '/support', 18 | }, 19 | ] as const satisfies readonly NavbarItem[]; 20 | -------------------------------------------------------------------------------- /apps/website/src/components/user/UserErrorHandler.tsx: -------------------------------------------------------------------------------- 1 | import { LoginButton } from './LoginButton'; 2 | import { APIError } from '@/utils/fetcher'; 3 | 4 | // TODO? 5 | export function UserErrorHandler({ error }: { readonly error: Error }) { 6 | if (error instanceof APIError && error.payload.statusCode === 401) { 7 | return ; 8 | } 9 | 10 | console.error(error); 11 | return <>Error; 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "ESLint", 4 | "extends": "./tsconfig.base.json", 5 | "compilerOptions": { 6 | "allowJs": true, 7 | "checkJs": false, 8 | "noEmit": true 9 | }, 10 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"], 11 | "exclude": ["**/node_modules", "**/dist", "**/.turbo", "**/coverage"] 12 | } 13 | -------------------------------------------------------------------------------- /services/api/src/bin.ts: -------------------------------------------------------------------------------- 1 | import { createDatabase, createLogger, createRedis, initContext } from '@chatsift/backend-core'; 2 | 3 | const logger = createLogger('api'); 4 | const db = createDatabase(logger); 5 | const redis = await createRedis(logger); 6 | initContext({ db, logger, redis }); 7 | 8 | // Make sure to import anything else AFTER initializing the context 9 | const { bin } = await import('./index.js'); 10 | await bin(); 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .idea 4 | .vscode 5 | .husky 6 | 7 | **/node_modules 8 | **/types 9 | **/.next 10 | **/dist 11 | 12 | **/vitest.config.ts 13 | **/eslint.config.js 14 | **/.prettierrc.json 15 | **/.prettierrc.cjs 16 | **/.prettierignore 17 | **/tsconfig.eslint.json 18 | 19 | .commitlinrrc.json 20 | .gitattributes 21 | .gitignore 22 | .env.private.example 23 | 24 | LICENSE 25 | **/README.md 26 | 27 | .DS_Store 28 | -------------------------------------------------------------------------------- /packages/private/core/src/lib/util.ts: -------------------------------------------------------------------------------- 1 | export interface ModuleWithDefault { 2 | default: Type; 3 | } 4 | 5 | export function isModuleWithDefault( 6 | mod: any, 7 | typePredicate?: (value: any) => value is Type, 8 | ): mod is ModuleWithDefault { 9 | const predicateIsTrue = typePredicate ? typePredicate(mod?.default) : true; 10 | return mod && typeof mod === 'object' && 'default' in mod && predicateIsTrue; 11 | } 12 | -------------------------------------------------------------------------------- /apps/website/src/components/common/Logo.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | import { SvgChatSift } from '@/components/icons/SvgChatSift'; 4 | 5 | export function Logo() { 6 | return ( 7 | 8 | 9 |

ChatSift

10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /services/ama-bot/src/bin.ts: -------------------------------------------------------------------------------- 1 | import { createDatabase, createLogger, createRedis, initContext } from '@chatsift/backend-core'; 2 | 3 | const logger = createLogger('ama-bot'); 4 | const db = createDatabase(logger); 5 | const redis = await createRedis(logger); 6 | initContext({ db, logger, redis }); 7 | 8 | // Make sure to import anything else AFTER initializing the context 9 | const { bin } = await import('./index.js'); 10 | await bin(); 11 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/SvgClose.tsx: -------------------------------------------------------------------------------- 1 | export function SvgClose() { 2 | return ( 3 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/SvgPlus.tsx: -------------------------------------------------------------------------------- 1 | export function SvgPlus() { 2 | return ( 3 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/private/backend-core/src/lib/redis.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import type { Logger } from 'pino'; 3 | import { createClient, RESP_TYPES } from 'redis'; 4 | 5 | export async function createRedis(logger: Logger) { 6 | return createClient({ url: 'redis://redis:6379' }) 7 | .withTypeMapping({ 8 | [RESP_TYPES.BLOB_STRING]: Buffer, 9 | }) 10 | .on('error', (err) => logger.error(err, 'redis error')) 11 | .connect(); 12 | } 13 | -------------------------------------------------------------------------------- /packages/private/backend-core/src/lib/data/bots.ts: -------------------------------------------------------------------------------- 1 | import type { BotId } from '@chatsift/core'; 2 | import { createRecipe, DataType } from 'bin-rw'; 3 | import { RedisStore } from './_store.js'; 4 | 5 | interface BotInfo { 6 | guilds: string[]; 7 | } 8 | 9 | export const GuildList = new RedisStore({ 10 | TTL: null, 11 | recipe: createRecipe({ 12 | guilds: [DataType.String], 13 | }), 14 | makeKey: (id: BotId) => `bot:${id}`, 15 | storeOld: false, 16 | }); 17 | -------------------------------------------------------------------------------- /apps/website/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url('author.css'); 6 | 7 | body { 8 | font-family: Author-Variable, sans-serif; 9 | font-feature-settings: 10 | 'pnum' on, 11 | 'lnum' on; 12 | } 13 | 14 | .hide-for-mobile-override { 15 | & > *:nth-child(1) { 16 | display: none; 17 | } 18 | 19 | /* tailwind md: */ 20 | @media (max-width: 768px) { 21 | & > * { 22 | display: none; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/website/src/components/common/Heading.tsx: -------------------------------------------------------------------------------- 1 | interface HeadingProps { 2 | readonly subtitle?: string | undefined; 3 | readonly title: string; 4 | } 5 | 6 | export function Heading({ title, subtitle }: HeadingProps) { 7 | return ( 8 |
9 |

{title}

10 | {subtitle &&

{subtitle}

} 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /.env.public: -------------------------------------------------------------------------------- 1 | ROOT_DOMAIN=automoderator.app 2 | 3 | PRISMA_DATABASE_URL=postgres://chatsift:admin@127.0.0.1:${LOCAL_DATABASE_PORT}/chatsift 4 | REDIS_URL=redis://redis:6379 5 | 6 | API_PORT=7004 7 | OAUTH_DISCORD_CLIENT_ID=1005791929075769344 8 | CORS="http:\/\/localhost:3000|https:\/\/canary\.automoderator\.app" 9 | 10 | API_URL_DEV=http://localhost:7004 11 | API_URL_PROD=https://api-canary.automoderator.app 12 | 13 | FRONTEND_URL_DEV=http://localhost:3000 14 | FRONTEND_URL_PROD=https://canary.automoderator.app 15 | -------------------------------------------------------------------------------- /apps/website/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import type { NextRequest } from 'next/server'; 3 | import { URLS } from './utils/urls'; 4 | 5 | export async function middleware(request: NextRequest) { 6 | const cookies = request.cookies; 7 | 8 | if (!cookies.has('refresh_token')) { 9 | return NextResponse.redirect(new URL(URLS.API.LOGIN, request.url)); 10 | } 11 | 12 | return NextResponse.next(); 13 | } 14 | 15 | export const config = { 16 | matcher: '/dashboard/:path*', 17 | }; 18 | -------------------------------------------------------------------------------- /apps/website/src/components/user/UserAvatarMe.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { UserAvatar } from './UserAvatar'; 4 | import { client } from '@/data/client'; 5 | 6 | interface UserAvatarMeProps { 7 | readonly className: string; 8 | } 9 | 10 | export function UserAvatarMe({ className }: UserAvatarMeProps) { 11 | const { isLoading, data: user } = client.auth.useMe(); 12 | 13 | if (user === null) { 14 | return null; 15 | } 16 | 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /apps/website/src/components/nav/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { NavbarDesktop } from './NavbarDesktop'; 2 | import { NavbarMobile } from './NavbarMobile'; 3 | 4 | export function Navbar() { 5 | return ( 6 |
7 | 8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/amas/[amaId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { AMADashboardCrumbs } from '../../_components/AMADashboardCrumbs'; 2 | import { AMADetails } from './_components/AMADetails'; 3 | import { Heading } from '@/components/common/Heading'; 4 | 5 | export default function AMADetailPage() { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync Labels 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - '.github/labels.yml' 12 | 13 | jobs: 14 | synclabels: 15 | name: Sync Labels 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | 21 | - name: Sync labels 22 | uses: crazy-max/ghaction-github-labeler@v3 23 | with: 24 | github-token: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/SvgChevronDown.tsx: -------------------------------------------------------------------------------- 1 | interface SvgChevronDownProps { 2 | readonly className?: string; 3 | readonly size?: number; 4 | } 5 | 6 | export function SvgChevronDown({ className, size = 16 }: SvgChevronDownProps) { 7 | return ( 8 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/SvgChatSift.tsx: -------------------------------------------------------------------------------- 1 | export function SvgChatSift() { 2 | return ( 3 | 4 | 11 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /services/api/src/routes/_types/routeTypes.ts: -------------------------------------------------------------------------------- 1 | export type * from '../ama/createAMA.js'; 2 | export type * from '../ama/getAMA.js'; 3 | export type * from '../ama/getAMAs.js'; 4 | export type * from '../ama/updateAMA.js'; 5 | export type * from '../ama/repostPrompt.js'; 6 | 7 | export type * from '../auth/discord.js'; 8 | export type * from '../auth/discordCallback.js'; 9 | export type * from '../auth/logout.js'; 10 | export type * from '../auth/me.js'; 11 | 12 | export type * from '../guilds/get.js'; 13 | export type * from '../guilds/createGrant.js'; 14 | export type * from '../guilds/deleteGrant.js'; 15 | export type * from '../guilds/getGrants.js'; 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021-2023 ChatSift 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU Affero General Public License as 5 | published by the Free Software Foundation, either version 3 of the 6 | License, or (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/_components/RefreshGuildsButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/common/Button'; 4 | import { SvgRefresh } from '@/components/icons/SvgRefresh'; 5 | import { client } from '@/data/client'; 6 | 7 | export function RefreshGuildsButton() { 8 | const { refetch, isLoading } = client.auth.useMe({ force_fresh: 'true' }); 9 | 10 | return ( 11 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/website/src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import { Button } from '@/components/common/Button'; 5 | 6 | export default function NotFound() { 7 | const router = useRouter(); 8 | 9 | return ( 10 |
11 |

12 | The page you are looking for could not be found 13 |

14 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/private/backend-core/src/lib/data/_entity.ts: -------------------------------------------------------------------------------- 1 | import type { Recipe } from 'bin-rw'; 2 | 3 | /** 4 | * Responsible for defining the behavior of a given entity. 5 | */ 6 | export interface IEntity { 7 | /** 8 | * How long an entity of this entity should remain in the store without any operations being performed on it. 9 | */ 10 | readonly TTL: number | null; 11 | /** 12 | * Generates a redis key for this entity. 13 | */ 14 | makeKey(id: string): string; 15 | /** 16 | * Recipe for encoding and decoding TData. 17 | */ 18 | readonly recipe: Recipe; 19 | /** 20 | * Whether or not to store the previous version of the entity when setting a new value. 21 | */ 22 | readonly storeOld: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | exclude: ['**/node_modules', '**/dist', '.idea', '.git', '.cache'], 6 | passWithNoTests: true, 7 | typecheck: { 8 | enabled: true, 9 | include: ['**/__tests__/types.test.ts'], 10 | tsconfig: 'tsconfig.json', 11 | }, 12 | coverage: { 13 | enabled: true, 14 | reporter: ['text', 'lcov', 'clover'], 15 | exclude: [ 16 | '**/dist', 17 | '**/__tests__', 18 | '**/__mocks__', 19 | '**/coverage', 20 | '**/tsup.config.ts', 21 | '**/vitest.config.ts', 22 | '**/.next', 23 | 'eslint.config.js', 24 | '.yarn', 25 | 'apps/website', 26 | ], 27 | }, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/SvgAutoModerator.tsx: -------------------------------------------------------------------------------- 1 | export function SvgAutoModerator({ width, height }: { readonly height?: number; readonly width?: number }) { 2 | return ( 3 | 4 | 10 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardCrumbs } from '../../_components/DashboardCrumbs'; 2 | import { GrantsList } from './_components/GrantsList'; 3 | import { Heading } from '@/components/common/Heading'; 4 | 5 | export default function SettingsPage() { 6 | return ( 7 | <> 8 |
9 | 10 | 14 |
15 | 16 |
17 | 18 |
19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**", ".next/**", "!.next/cache/**"] 7 | }, 8 | "lint": { 9 | "dependsOn": ["^build"] 10 | }, 11 | "lint:fix": { 12 | "dependsOn": ["^build"], 13 | "cache": false 14 | }, 15 | "test": { 16 | "dependsOn": ["^build"] 17 | }, 18 | "clean": { 19 | "cache": false 20 | }, 21 | "tag-docker": { 22 | "outputs": [], 23 | "inputs": ["src/**/*.ts", "Dockerfile"] 24 | } 25 | }, 26 | "globalDependencies": [ 27 | "eslint.config.js", 28 | ".prettierrc.json", 29 | "tsconfig.json", 30 | "tsconfig.*.json", 31 | "vitest.config.ts", 32 | "prisma/schema.prisma" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/channels/SvgChannelForum.tsx: -------------------------------------------------------------------------------- 1 | interface SvgChannelForumProps { 2 | readonly className?: string; 3 | readonly size?: number; 4 | } 5 | 6 | export function SvgChannelForum({ className, size = 20 }: SvgChannelForumProps) { 7 | return ( 8 | 16 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/private/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/package.json", 3 | "name": "@chatsift/core", 4 | "version": "0.1.0", 5 | "description": "Core utils and types for cross-service interaction", 6 | "type": "module", 7 | "exports": { 8 | ".": "./dist/index.js" 9 | }, 10 | "types": "./dist/index.d.ts", 11 | "scripts": { 12 | "build": "yarn run db:generate && tsc", 13 | "test": "vitest run", 14 | "test:watch": "vitest", 15 | "lint": "eslint src", 16 | "lint:fix": "eslint src --fix", 17 | "clean": "rimraf dist" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^24.9.1", 21 | "typescript": "~5.9.3" 22 | }, 23 | "dependencies": { 24 | "@sapphire/bitfield": "^1.2.4", 25 | "discord-api-types": "^0.38.30" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix-style line endings for all text files 2 | * text=auto eol=lf 3 | 4 | # Windows-specific files should have CRLF line endings 5 | *.bat text eol=crlf 6 | 7 | # Binary files 8 | *.png binary 9 | *.jpg binary 10 | *.jpeg binary 11 | *.gif binary 12 | *.ico binary 13 | *.mp4 binary 14 | *.mp3 binary 15 | *.flv binary 16 | *.fla binary 17 | *.swf binary 18 | *.gz binary 19 | *.zip binary 20 | *.7z binary 21 | *.ttf binary 22 | *.eot binary 23 | *.woff binary 24 | *.woff2 binary 25 | *.pdf binary 26 | 27 | # Archives 28 | *.7z binary 29 | *.jar binary 30 | *.rar binary 31 | *.zip binary 32 | *.gz binary 33 | *.bz2 binary 34 | *.tar binary 35 | *.tgz binary 36 | 37 | # Fonts 38 | *.ttf binary 39 | *.eot binary 40 | *.otf binary 41 | *.woff binary 42 | *.woff2 binary -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/amas/_components/CreateAMACard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { useParams } from 'next/navigation'; 5 | import { SvgPlus } from '@/components/icons/SvgPlus'; 6 | 7 | export function CreateAMACard() { 8 | const params = useParams<{ id: string }>(); 9 | 10 | return ( 11 | 15 | 16 | Create AMA 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /services/api/src/util/sendBoom.ts: -------------------------------------------------------------------------------- 1 | import type { Boom } from '@hapi/boom'; 2 | import type { Response } from 'polka'; 3 | import { treeifyError, ZodError } from 'zod'; 4 | 5 | /** 6 | * Send a Boom error to the client 7 | * 8 | * @param error - \@hapi/boom `Boom` instance 9 | * @param res - Response to send the error to 10 | */ 11 | export function sendBoom(error: Boom, res: Response) { 12 | res.statusCode = error.output.statusCode; 13 | for (const [header, value] of Object.entries(error.output.headers)) { 14 | res.setHeader(header, value!); 15 | } 16 | 17 | if (error.data instanceof ZodError) { 18 | error.output.payload = { 19 | ...error.output.payload, 20 | ...treeifyError(error.data), 21 | }; 22 | } 23 | 24 | return res.end(JSON.stringify(error.output.payload)); 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Quality Check 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | quality: 7 | name: Quality Check 8 | runs-on: ubuntu-latest 9 | env: 10 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 11 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Install node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 22 20 | 21 | - name: Install dependencies 22 | uses: ./actions/yarnCache 23 | 24 | - name: Ensure prisma schema is up to date 25 | run: yarn prisma generate 26 | 27 | - name: Build 28 | run: yarn build 29 | 30 | - name: ESLint 31 | run: yarn lint 32 | -------------------------------------------------------------------------------- /apps/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "ESNext", 10 | "moduleDetection": "force", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "baseUrl": ".", 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | }, 25 | "noUncheckedSideEffectImports": false 26 | }, 27 | "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], 28 | "exclude": ["node_modules", ".next", "dist"] 29 | } 30 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/amas/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { AMADashboardCrumbs } from '../../_components/AMADashboardCrumbs'; 2 | import { CreateAMAForm } from './_components/CreateAMAForm'; 3 | import { RefreshServerDataButton } from './_components/RefreshServerDataButton'; 4 | import { Heading } from '@/components/common/Heading'; 5 | 6 | export default function NewAMAPage() { 7 | return ( 8 |
9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/private/backend-core/src/lib/logger.ts: -------------------------------------------------------------------------------- 1 | import type { TransportTargetOptions } from 'pino'; 2 | import { pino as createPinoLogger, transport as pinoTransport, stdTimeFunctions } from 'pino'; 3 | 4 | export function createLogger(name: string) { 5 | // TODO: File rotations for prod? 6 | const targets: TransportTargetOptions[] = [ 7 | { 8 | target: 'pino/file', 9 | level: 'trace', 10 | options: { 11 | destination: 1, // stdout 12 | }, 13 | }, 14 | ]; 15 | 16 | const transport = pinoTransport({ 17 | targets, 18 | level: 'trace', 19 | }); 20 | 21 | return createPinoLogger( 22 | { 23 | level: 'trace', 24 | name, 25 | timestamp: stdTimeFunctions.isoTime, 26 | formatters: { 27 | level: (levelLabel, level) => ({ level, levelLabel }), 28 | }, 29 | }, 30 | transport, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { NavGateCheck } from '@/components/common/NavGate'; 3 | import { server } from '@/data/server'; 4 | 5 | export async function generateMetadata({ params }: LayoutProps<'/dashboard/[id]'>): Promise { 6 | const { id } = await params; 7 | let name; 8 | 9 | try { 10 | const { data: me } = await server.auth.me.fetch(); 11 | const guild = me?.guilds.find((g) => g.id === id); 12 | name = guild?.name; 13 | } catch (error) { 14 | console.error(error); 15 | name = null; 16 | } 17 | 18 | return { 19 | title: name ?? 'Server not found', 20 | }; 21 | } 22 | 23 | export default async function GuildLayout({ children }: LayoutProps<'/dashboard/[id]'>) { 24 | return {children}; 25 | } 26 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/amas/page.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardCrumbs } from '../../../_components/DashboardCrumbs'; 2 | import { AMASessionsList } from './_components/AMASessionsList'; 3 | import { IncludeEndedToggle } from './_components/IncludeEndedToggle'; 4 | import { Heading } from '@/components/common/Heading'; 5 | import { SearchBar } from '@/components/common/SearchBar'; 6 | 7 | export default function AMAMangementPage() { 8 | return ( 9 | <> 10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/private/backend-core/src/lib/database.ts: -------------------------------------------------------------------------------- 1 | import type { DB } from '@chatsift/core'; 2 | import { Kysely, PostgresDialect } from 'kysely'; 3 | import { Pool } from 'pg'; 4 | import type { Logger } from 'pino'; 5 | 6 | export function createDatabase(logger: Logger): Kysely { 7 | const pool = new Pool({ 8 | connectionString: 'postgres://chatsift:admin@postgres:5432/chatsift', 9 | }); 10 | 11 | const dialect = new PostgresDialect({ 12 | pool, 13 | }); 14 | 15 | return new Kysely({ 16 | dialect, 17 | log: (event) => { 18 | if (event.level === 'error') { 19 | logger.error({ query: event.query.sql, err: event.error }, 'Query responsible for error'); 20 | } else if (event.level === 'query') { 21 | logger.debug({ query: event.query.sql, duration: event.queryDurationMillis }, 'Executed query'); 22 | } 23 | }, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/SvgRefresh.tsx: -------------------------------------------------------------------------------- 1 | export function SvgRefresh() { 2 | return ( 3 | 4 | 5 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /services/ama-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatsift/ama-bot", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsc", 8 | "test": "vitest run", 9 | "test:watch": "vitest", 10 | "lint": "eslint src", 11 | "lint:fix": "eslint src --fix", 12 | "clean": "rimraf dist", 13 | "tag-docker": "docker image tag chatsift/chatsift-next:latest chatsift/chatsift-next:ama" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^24.9.1", 17 | "kysely": "^0.28.8", 18 | "typescript": "~5.9.3", 19 | "vitest": "^3.2.4" 20 | }, 21 | "dependencies": { 22 | "@chatsift/backend-core": "workspace:^", 23 | "@discordjs/core": "^3.0.0-dev.1759363313-f510b5ffa", 24 | "@discordjs/rest": "^3.0.0-dev.1759363313-f510b5ffa", 25 | "@discordjs/ws": "^3.0.0-dev.1759363313-f510b5ffa", 26 | "bin-rw": "^0.1.1", 27 | "nanoid": "^5.1.6" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/website/src/components/user/UserDesktop.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { LoginButton } from './LoginButton'; 4 | import { LogoutButton } from './LogoutButton'; 5 | import { UserAvatarMe } from './UserAvatarMe'; 6 | import { UserErrorHandler } from './UserErrorHandler'; 7 | import { Skeleton } from '@/components/common/Skeleton'; 8 | import { client } from '@/data/client'; 9 | 10 | export function UserDesktop() { 11 | const { data: user, error, isLoading } = client.auth.useMe(); 12 | 13 | if (error) { 14 | return ; 15 | } 16 | 17 | if (isLoading) { 18 | return ; 19 | } 20 | 21 | if (!user) { 22 | return ; 23 | } 24 | 25 | return ( 26 |
27 | 28 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { relative, resolve } from 'node:path'; 2 | import process from 'node:process'; 3 | import { defineConfig, type Options } from 'tsup'; 4 | 5 | type ConfigOptions = Pick< 6 | Options, 7 | 'entry' | 'esbuildOptions' | 'format' | 'globalName' | 'minify' | 'noExternal' | 'sourcemap' | 'target' 8 | >; 9 | 10 | export const createTsupConfig = ({ 11 | globalName, 12 | format = ['esm', 'cjs'], 13 | target = 'es2021', 14 | sourcemap = true, 15 | minify = false, 16 | entry = ['src/index.ts'], 17 | noExternal, 18 | esbuildOptions, 19 | }: ConfigOptions = {}) => 20 | defineConfig({ 21 | clean: true, 22 | entry, 23 | format, 24 | minify, 25 | skipNodeModulesBundle: true, 26 | sourcemap, 27 | target, 28 | tsconfig: relative(__dirname, resolve(process.cwd(), 'tsconfig.json')), 29 | keepNames: true, 30 | globalName, 31 | noExternal, 32 | esbuildOptions, 33 | }); 34 | -------------------------------------------------------------------------------- /apps/website/src/components/footer/ThemeSwitchButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from 'next-themes'; 4 | import { Button } from '@/components/common/Button'; 5 | import { Skeleton } from '@/components/common/Skeleton'; 6 | import { SvgDarkTheme } from '@/components/icons/SvgDarkTheme'; 7 | import { SvgLightTheme } from '@/components/icons/SvgLightTheme'; 8 | import { useIsMounted } from '@/hooks/isMounted'; 9 | 10 | export function ThemeSwitchButton() { 11 | const isMounted = useIsMounted(); 12 | const { theme, setTheme } = useTheme(); 13 | 14 | if (!isMounted) { 15 | return ; 16 | } 17 | 18 | return ( 19 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | lerna-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | user-assets 16 | 17 | # Build outputs 18 | dist/ 19 | build/ 20 | out/ 21 | .turbo/ 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage/ 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Environment variables 31 | .env 32 | .env.local 33 | .env.development.local 34 | .env.test.local 35 | .env.production.local 36 | .env.private 37 | 38 | # OS 39 | .DS_Store 40 | Thumbs.db 41 | 42 | # IDEs and editors 43 | .idea/ 44 | *.swp 45 | *.swo 46 | *~ 47 | 48 | # Logs 49 | logs 50 | *.log 51 | 52 | # Temporary folders 53 | tmp/ 54 | temp/ 55 | 56 | # Next.js 57 | .next/ 58 | .yarn/* 59 | !.yarn/releases/ 60 | .vercel 61 | 62 | # Changesets 63 | .changeset/pre.json 64 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { GuildList } from './_components/GuildList'; 3 | import { RefreshGuildsButton } from './_components/RefreshGuildsButton'; 4 | import { Heading } from '@/components/common/Heading'; 5 | import { SearchBar } from '@/components/common/SearchBar'; 6 | 7 | export const metadata: Metadata = { 8 | title: 'Dashboard', 9 | }; 10 | 11 | export default function DashboardPage() { 12 | return ( 13 | <> 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/private/backend-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/package.json", 3 | "name": "@chatsift/backend-core", 4 | "version": "0.1.0", 5 | "description": "Core backend utilities", 6 | "type": "module", 7 | "exports": { 8 | ".": "./dist/index.js" 9 | }, 10 | "types": "./dist/index.d.ts", 11 | "scripts": { 12 | "build": "tsc", 13 | "test": "vitest run", 14 | "test:watch": "vitest", 15 | "lint": "eslint src", 16 | "lint:fix": "eslint src --fix", 17 | "clean": "rimraf dist" 18 | }, 19 | "devDependencies": { 20 | "@chatsift/core": "workspace:^", 21 | "@types/node": "^24.9.1", 22 | "@types/pg": "^8.15.5", 23 | "typescript": "~5.9.3" 24 | }, 25 | "dependencies": { 26 | "@sapphire/discord-utilities": "^3.5.0", 27 | "bin-rw": "^0.1.1", 28 | "kysely": "^0.28.8", 29 | "pg": "^8.16.3", 30 | "pino": "^9.14.0", 31 | "redis": "^5.8.3", 32 | "zod": "^4.1.12" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/website/src/components/user/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import { Button } from '../common/Button'; 5 | import { client } from '@/data/client'; 6 | 7 | interface LogoutButtonProps { 8 | // eslint-disable-next-line @typescript-eslint/method-signature-style 9 | readonly additionally?: () => void; 10 | readonly className?: string; 11 | } 12 | 13 | export function LogoutButton({ className, additionally }: LogoutButtonProps) { 14 | const logoutMutation = client.auth.useLogout(); 15 | const router = useRouter(); 16 | 17 | return ( 18 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/SvgDarkTheme.tsx: -------------------------------------------------------------------------------- 1 | export function SvgDarkTheme() { 2 | return ( 3 | 4 | 8 | 12 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /services/ama-bot/src/lib/queues.ts: -------------------------------------------------------------------------------- 1 | import type { AMASession } from '@chatsift/backend-core'; 2 | import type { Selectable } from 'kysely'; 3 | 4 | export enum CurrentlyInQueue { 5 | mod, 6 | guest, 7 | answers, 8 | } 9 | 10 | interface GetNextQueueResult { 11 | kind: CurrentlyInQueue; 12 | queueId: string; 13 | } 14 | 15 | export function getNextQueue(currently: CurrentlyInQueue, session: Selectable): GetNextQueueResult | null { 16 | switch (currently) { 17 | case CurrentlyInQueue.answers: { 18 | return null; 19 | } 20 | 21 | case CurrentlyInQueue.guest: { 22 | return { kind: CurrentlyInQueue.answers, queueId: session.answersChannelId }; 23 | } 24 | 25 | case CurrentlyInQueue.mod: { 26 | if (session.guestQueueId) { 27 | return { kind: CurrentlyInQueue.guest, queueId: session.guestQueueId }; 28 | } 29 | 30 | return { kind: CurrentlyInQueue.answers, queueId: session.answersChannelId }; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/private/core/src/lib/promiseAllObject.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable tsdoc/syntax */ 2 | 3 | /** 4 | * Transforms an object of promises into a promise of an object where all the values are awaited, much like 5 | * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all | Promise.all}. 6 | * 7 | * @remarks 8 | * This is the flow we follow: 9 | * 10 | * { a: Promise, b: Promise } 11 | * 12 | * => [ ['a', Promise], ['b', Promise] ] 13 | * 14 | * => [ Promise<['a', X]>, Promise<'b', Y>] ] 15 | * 16 | * => (via awaited Promise.all) [ ['a', X], ['b', Y] ] 17 | * 18 | * => Promise<{ a: X, b: Y }> 19 | */ 20 | export async function promiseAllObject>>( 21 | obj: TRecord, 22 | ): Promise<{ [K in keyof TRecord]: Awaited }> { 23 | return Object.fromEntries(await Promise.all(Object.entries(obj).map(async ([key, value]) => [key, await value]))); 24 | } 25 | -------------------------------------------------------------------------------- /services/api/src/routes/routes.ts: -------------------------------------------------------------------------------- 1 | // This file should exclusively re-export the default exports from each route file 2 | 3 | export { default as CreateAMA } from './ama/createAMA.js'; 4 | export { default as GetAMA } from './ama/getAMA.js'; 5 | export { default as GetAMAs } from './ama/getAMAs.js'; 6 | export { default as UpdateAMA } from './ama/updateAMA.js'; 7 | export { default as RepostPrompt } from './ama/repostPrompt.js'; 8 | 9 | export { default as GetAuthDiscord } from './auth/discord.js'; 10 | export { default as GetAuthDiscordCallback } from './auth/discordCallback.js'; 11 | export { default as PostAuthLogout } from './auth/logout.js'; 12 | export { default as GetAuthMe } from './auth/me.js'; 13 | 14 | export { default as GetGuild } from './guilds/get.js'; 15 | export { default as CreateGrant } from './guilds/createGrant.js'; 16 | export { default as DeleteGrant } from './guilds/deleteGrant.js'; 17 | export { default as GetGrants } from './guilds/getGrants.js'; 18 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/settings/_components/GrantsList.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useParams } from 'next/navigation'; 4 | import { AddGrantCard } from './AddGrantCard'; 5 | import { GrantCard } from './GrantCard'; 6 | import { Skeleton } from '@/components/common/Skeleton'; 7 | import { client } from '@/data/client'; 8 | 9 | export function GrantsList() { 10 | const { id: guildId } = useParams<{ id: string }>(); 11 | const { data, isLoading } = client.guilds.grants.useGrants(guildId); 12 | 13 | if (isLoading) { 14 | return ( 15 | <> 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | return ( 24 | <> 25 | 26 | {data!.users.map((user, index) => ( 27 | 28 | ))} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/website/src/components/user/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import type { DefaultUserAvatarAssets, APIUser } from 'discord-api-types/v10'; 2 | import { CDNRoutes, ImageFormat, RouteBases } from 'discord-api-types/v10'; 3 | import { GenericAvatar } from '../common/GenericAvatar'; 4 | 5 | interface UserAvatarProps { 6 | readonly className: string; 7 | readonly isLoading: boolean; 8 | readonly user: APIUser | undefined; 9 | } 10 | 11 | export function UserAvatar({ className, isLoading, user }: UserAvatarProps) { 12 | const assetURL = user?.avatar 13 | ? `${RouteBases.cdn}${CDNRoutes.userAvatar(user.id, user.avatar, ImageFormat.PNG)}` 14 | : `${RouteBases.cdn}${CDNRoutes.defaultUserAvatar(Number((BigInt(user?.id ?? '0') >> 22n) % 6n) as DefaultUserAvatarAssets)}`; 15 | 16 | return ( 17 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Base", 4 | "compilerOptions": { 5 | "target": "es2024", 6 | "lib": ["ES2024"], 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler", 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "allowJs": false, 12 | "checkJs": false, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "skipLibCheck": true, 16 | "declaration": true, 17 | "declarationMap": true, 18 | "sourceMap": true, 19 | "noUncheckedIndexedAccess": true, 20 | "exactOptionalPropertyTypes": true, 21 | "noImplicitReturns": true, 22 | "noImplicitOverride": true, 23 | "noPropertyAccessFromIndexSignature": true, 24 | "noFallthroughCasesInSwitch": true, 25 | "noUncheckedSideEffectImports": true, 26 | "useDefineForClassFields": true, 27 | "resolveJsonModule": true, 28 | "isolatedModules": true 29 | }, 30 | "exclude": ["**/node_modules", "**/dist", "**/.turbo", "**/coverage"] 31 | } 32 | -------------------------------------------------------------------------------- /services/api/src/util/discordAPI.ts: -------------------------------------------------------------------------------- 1 | import { getContext, type BotId } from '@chatsift/backend-core'; 2 | import type { Snowflake } from '@discordjs/core'; 3 | import { API } from '@discordjs/core'; 4 | import { REST } from '@discordjs/rest'; 5 | import type { MeGuild } from './me.js'; 6 | 7 | const oauthREST = new REST({ version: '10' }); 8 | export const discordAPIOAuth = new API(oauthREST); 9 | 10 | const amaREST = new REST({ version: '10' }).setToken(getContext().env.AMA_BOT_TOKEN); 11 | export const discordAPIAma = new API(amaREST); 12 | 13 | export const APIMapping: Record = { 14 | AMA: discordAPIAma, 15 | }; 16 | 17 | const latest = new Map(); 18 | export function roundRobinAPI(guild: MeGuild): API { 19 | if (guild.bots.length === 1) { 20 | return APIMapping[guild.bots[0]!]; 21 | } 22 | 23 | const index = latest.get(guild.id) ?? -1; 24 | const nextIndex = (index + 1) % guild.bots.length; 25 | latest.set(guild.id, nextIndex); 26 | 27 | return APIMapping[guild.bots[nextIndex]!]; 28 | } 29 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/_components/AMADashboardCrumbs.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import type { AMASessionDetailed, AMASessionWithCount } from '@chatsift/api'; 4 | import { useParams } from 'next/navigation'; 5 | import { DashboardCrumbs } from '../../../_components/DashboardCrumbs'; 6 | import { client } from '@/data/client'; 7 | 8 | function useCurrentAMA(id: string, amaId?: string) { 9 | if (!amaId) return undefined; 10 | const { data: currentAMA } = client.guilds.ama.useAMA(id, amaId); 11 | return currentAMA as AMASessionDetailed | undefined; 12 | } 13 | 14 | export function AMADashboardCrumbs() { 15 | const params = useParams<{ amaId?: string; id: string }>(); 16 | 17 | const { data: amaSessions } = client.guilds.ama.useAMAs(params.id, { 18 | include_ended: 'false', 19 | }); 20 | const currentAMA = useCurrentAMA(params.id, params.amaId); 21 | 22 | return ( 23 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /apps/website/src/components/common/GenericAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar } from './Avatar'; 2 | import { GenericAvatarImage, GenericAvatarInitials } from './GenericAvatarImages'; 3 | import { Skeleton } from './Skeleton'; 4 | import { cn } from '@/utils/util'; 5 | 6 | interface GenericAvatarProps { 7 | readonly assetURL: string | undefined; 8 | readonly className: string; 9 | readonly disableLink?: boolean; 10 | readonly href: string; 11 | readonly initials: string; 12 | readonly isLoading: boolean; 13 | } 14 | 15 | export function GenericAvatar({ className, href, disableLink, isLoading, assetURL, initials }: GenericAvatarProps) { 16 | return ( 17 | 18 | 19 | {isLoading ? ( 20 | 21 | ) : assetURL ? ( 22 | 23 | ) : ( 24 | 25 | )} 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/public/discord-utils/src/__tests__/sortChannels.test.ts: -------------------------------------------------------------------------------- 1 | import type { APIChannel } from 'discord-api-types/v10'; 2 | import { ChannelType } from 'discord-api-types/v10'; 3 | import { test, expect } from 'vitest'; 4 | import { sortChannels } from '../sortChannels.js'; 5 | 6 | test('sorting a list of channels', () => { 7 | // Higher position than the category, but should come out on top 8 | const first = { 9 | id: '1', 10 | position: 1, 11 | type: ChannelType.GuildText, 12 | } as unknown as APIChannel; 13 | 14 | const second = { 15 | id: '0', 16 | position: 0, 17 | type: ChannelType.GuildCategory, 18 | } as unknown as APIChannel; 19 | 20 | const third = { 21 | id: '2', 22 | position: 0, 23 | type: ChannelType.GuildText, 24 | parent_id: '0', 25 | } as unknown as APIChannel; 26 | 27 | const fourth = { 28 | id: '3', 29 | position: 1, 30 | type: ChannelType.GuildText, 31 | parent_id: '0', 32 | } as unknown as APIChannel; 33 | 34 | expect(sortChannels([first, second, third, fourth])).toStrictEqual([first, second, third, fourth]); 35 | }); 36 | -------------------------------------------------------------------------------- /services/api/src/middleware/validate.ts: -------------------------------------------------------------------------------- 1 | import { badData } from '@hapi/boom'; 2 | import type { NextHandler, Request, Response } from 'polka'; 3 | import type { ZodType } from 'zod'; 4 | 5 | /** 6 | * Request properties that can be validated 7 | */ 8 | export type ValidateMiddlewareProp = 'body' | 'headers' | 'params' | 'query'; 9 | 10 | /** 11 | * Creates a request handler that validates a given request property - also potentially mutates the property, applying defaults and sane type conversions 12 | * 13 | * @param schema - A shapeshift schema to validate the property against - please refer to the zod documentation for more information 14 | * @param prop - The property to validate 15 | */ 16 | export function validate(schema: ZodType, prop: ValidateMiddlewareProp) { 17 | return async (req: Request, _: Response, next: NextHandler) => { 18 | const result = schema.safeParse(req[prop]); 19 | 20 | if (!result.success) { 21 | return next(badData(result.error?.message, result.error)); 22 | } 23 | 24 | req[prop] = result.data as unknown; 25 | return next(); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/amas/new/_components/RefreshServerDataButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import type { BotId } from '@chatsift/core'; 4 | import { useParams } from 'next/navigation'; 5 | import { Button } from '@/components/common/Button'; 6 | import { SvgRefresh } from '@/components/icons/SvgRefresh'; 7 | import { client } from '@/data/client'; 8 | 9 | interface RefreshServerDataButtonProps { 10 | readonly for_bot: BotId; 11 | } 12 | 13 | export function RefreshServerDataButton({ for_bot }: RefreshServerDataButtonProps) { 14 | const params = useParams<{ id: string }>(); 15 | const { id: guildId } = params; 16 | 17 | const { refetch, isLoading } = client.guilds.useInfo(guildId, { for_bot, force_fresh: 'true' }); 18 | 19 | return ( 20 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/SvgLightTheme.tsx: -------------------------------------------------------------------------------- 1 | export function SvgLightTheme() { 2 | return ( 3 | 4 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/website/src/components/common/GenericAvatarImages.tsx: -------------------------------------------------------------------------------- 1 | import { AvatarFallback, AvatarImage } from './Avatar'; 2 | import { cn } from '@/utils/util'; 3 | 4 | interface GenericAvatarInitialsProps { 5 | readonly className?: string; 6 | readonly initials: string; 7 | } 8 | 9 | export function GenericAvatarInitials({ className, initials }: GenericAvatarInitialsProps) { 10 | return ( 11 |
17 | {initials.toUpperCase()} 18 |
19 | ); 20 | } 21 | 22 | interface GenericAvatarImageProps { 23 | readonly assetURL: string; 24 | readonly className?: string; 25 | } 26 | 27 | export function GenericAvatarImage({ className, assetURL }: GenericAvatarImageProps) { 28 | return ( 29 | <> 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/public/discord-utils/README.md: -------------------------------------------------------------------------------- 1 | # `@chatsift/discord-utils` 2 | 3 | [![GitHub](https://img.shields.io/badge/License-GNU%20AGPLv3-yellow.svg)](https://github.com/chatsift/chatsift/blob/main/LICENSE) 4 | [![npm](https://img.shields.io/npm/v/@chatsift/discord-utils?color=crimson&logo=npm)](https://www.npmjs.com/package/@chatsift/discord-utils) 5 | [![TypeScript](https://github.com/chatsift/chatsift/actions/workflows/test.yml/badge.svg)](https://github.com/chatsift/chatsift/actions/workflows/test.yml) 6 | 7 | Niche utilities for working with Discord's API 8 | 9 | ## Installation 10 | 11 | - `npm install @chatsift/discord-utils` 12 | - `pnpm install @chatsift/discord-utils` 13 | - `yarn add @chatsift/discord-utils` 14 | 15 | ## Contributing 16 | 17 | Please see the main [README.md](https://github.com/chatsift/chatsift) for info on how to contribute to this package or the other `@chatsift` packages. 18 | 19 | ## LICENSE 20 | 21 | This project is licensed under the GNU AGPLv3 license. 22 | 23 | It should, however, be noted that some packages are forks of other open source projects, and are therefore, sub-licensed. 24 | -------------------------------------------------------------------------------- /packages/public/pino-rotate-file/README.md: -------------------------------------------------------------------------------- 1 | # `@chatsift/pino-rotate-file` 2 | 3 | [![GitHub](https://img.shields.io/badge/License-GNU%20AGPLv3-yellow.svg)](https://github.com/chatsift/chatsift/blob/main/LICENSE) 4 | [![npm](https://img.shields.io/npm/v/@chatsift/pino-rotate-file?color=crimson&logo=npm)](https://www.npmjs.com/package/@chatsift/pino-rotate-file) 5 | [![TypeScript](https://github.com/chatsift/chatsift/actions/workflows/test.yml/badge.svg)](https://github.com/chatsift/chatsift/actions/workflows/test.yml) 6 | 7 | Simple pino transport for rotating files 8 | 9 | ## Installation 10 | 11 | - `npm install @chatsift/pino-rotate-file` 12 | - `pnpm install @chatsift/pino-rotate-file` 13 | - `yarn add @chatsift/pino-rotate-file` 14 | 15 | ## Contributing 16 | 17 | Please see the main [README.md](https://github.com/chatsift/chatsift) for info on how to contribute to this package or the other `@chatsift` packages. 18 | 19 | ## LICENSE 20 | 21 | This project is licensed under the MIT license. 22 | 23 | It should, however, be noted that some packages are forks of other open source projects, and are therefore, sub-licensed. 24 | -------------------------------------------------------------------------------- /services/api/src/util/crypt.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'; 3 | import { getContext } from '@chatsift/backend-core'; 4 | 5 | /** 6 | * Returns a base64-encoded string containing the IV and the encrypted given `data`. 7 | */ 8 | export function encrypt(data: string): string { 9 | const key = Buffer.from(getContext().env.ENCRYPTION_KEY, 'base64'); 10 | const iv = randomBytes(16); 11 | 12 | const cipher = createCipheriv('aes-256-ctr', key, iv); 13 | return Buffer.concat([iv, cipher.update(data, 'utf8'), cipher.final()]).toString('base64'); 14 | } 15 | 16 | /** 17 | * Decodes a string created by `encrypt` and returns the original data. 18 | */ 19 | export function decrypt(data: string): string { 20 | const buffer = Buffer.from(data, 'base64'); 21 | 22 | const key = Buffer.from(getContext().env.ENCRYPTION_KEY, 'base64'); 23 | const iv = buffer.subarray(0, 16); 24 | 25 | const decipher = createDecipheriv('aes-256-ctr', key, iv); 26 | 27 | return Buffer.concat([decipher.update(buffer.subarray(16)), decipher.final()]).toString('utf8'); 28 | } 29 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: 'backlog' 2 | color: '7ef7ef' 3 | - name: 'bug' 4 | color: 'd73a4a' 5 | - name: 'chore' 6 | color: 'ffffff' 7 | - name: 'ci' 8 | color: '0075ca' 9 | - name: 'dependencies' 10 | color: '276bd1' 11 | - name: 'documentation' 12 | color: '0075ca' 13 | - name: 'duplicate' 14 | color: 'cfd3d7' 15 | - name: 'feature request' 16 | color: 'fcf95a' 17 | - name: 'good first issue' 18 | color: '7057ff' 19 | - name: 'has PR' 20 | color: '4b1f8e' 21 | - name: 'help wanted' 22 | color: '008672' 23 | - name: 'in progress' 24 | color: 'ffccd7' 25 | - name: 'in review' 26 | color: 'aed5fc' 27 | - name: 'invalid' 28 | color: 'e4e669' 29 | - name: 'need repro' 30 | color: 'c66037' 31 | - name: 'performance' 32 | color: '80c042' 33 | - name: 'priority:high' 34 | color: 'fc1423' 35 | - name: 'refactor' 36 | color: '1d637f' 37 | - name: 'regression' 38 | color: 'ea8785' 39 | - name: 'semver:major' 40 | color: 'c10f47' 41 | - name: 'semver:minor' 42 | color: 'e4f486' 43 | - name: 'semver:patch' 44 | color: 'e8be8b' 45 | - name: 'tests' 46 | color: 'f06dff' 47 | - name: 'wontfix' 48 | color: 'ffffff' 49 | -------------------------------------------------------------------------------- /apps/website/src/components/user/UserMobile.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { LoginButton } from './LoginButton'; 4 | import { LogoutButton } from './LogoutButton'; 5 | import { UserAvatarMe } from './UserAvatarMe'; 6 | import { UserErrorHandler } from './UserErrorHandler'; 7 | import { client } from '@/data/client'; 8 | 9 | interface UserMobileProps { 10 | // eslint-disable-next-line @typescript-eslint/method-signature-style 11 | readonly setMobileNavOpen: (open: boolean) => void; 12 | } 13 | 14 | export function UserMobile({ setMobileNavOpen }: UserMobileProps) { 15 | const { data: user, error } = client.auth.useMe(); 16 | 17 | if (error) { 18 | return ; 19 | } 20 | 21 | if (!user) { 22 | return ; 23 | } 24 | 25 | return ( 26 |
27 | 28 |

{user.username}

29 | setMobileNavOpen(false)} 31 | className="ml-auto text-secondary dark:text-secondary-dark" 32 | /> 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /services/api/src/util/stateCookie.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import { randomBytes } from 'node:crypto'; 3 | 4 | export class StateCookie { 5 | public static from(data: string): StateCookie { 6 | const bytes = Buffer.from(data, 'base64'); 7 | const nonce = bytes.subarray(0, 16); 8 | const createdAt = new Date(bytes.readUInt32LE(16) * 1_000); 9 | const redirectURI = bytes.subarray(20).toString(); 10 | 11 | return new this(redirectURI, nonce, createdAt); 12 | } 13 | 14 | public constructor(redirectURI: string); 15 | public constructor(redirectURI: string, nonce: Buffer, createdAt: Date); 16 | 17 | public constructor( 18 | public readonly redirectURI: string, 19 | private readonly nonce: Buffer = randomBytes(16), 20 | public readonly createdAt: Date = new Date(), 21 | ) {} 22 | 23 | public toBytes(): Buffer { 24 | const time = Buffer.allocUnsafe(4); 25 | time.writeUInt32LE(Math.floor(this.createdAt.getTime() / 1_000)); 26 | return Buffer.concat([this.nonce, time, Buffer.from(this.redirectURI)]); 27 | } 28 | 29 | public toCookie(): string { 30 | return this.toBytes().toString('base64'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/_components/GuildList.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useSearchParams } from 'next/navigation'; 4 | import { useMemo } from 'react'; 5 | import GuildCard from './GuildCard'; 6 | import { client } from '@/data/client'; 7 | import { sortGuilds } from '@/utils/util'; 8 | 9 | export function GuildList() { 10 | const { data: me } = client.auth.useMe(); 11 | const searchParams = useSearchParams(); 12 | 13 | const searchQuery = searchParams.get('search') ?? ''; 14 | 15 | const manageable = useMemo(() => me?.guilds.filter((g) => g.meCanManage) ?? [], [me]); 16 | const sorted = useMemo(() => { 17 | const lower = searchQuery.toLowerCase(); 18 | 19 | if (!manageable.length) { 20 | return []; 21 | } 22 | 23 | const filtered = manageable.filter((guild) => guild.name.toLowerCase().includes(lower)); 24 | return sortGuilds(filtered); 25 | }, [manageable, searchQuery]); 26 | 27 | return ( 28 |
    29 | {sorted.map((guild) => ( 30 |
  • 31 | 32 |
  • 33 | ))} 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy-manual.yml: -------------------------------------------------------------------------------- 1 | name: Deploy manual 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | deploy: 8 | name: Manual deploy 9 | runs-on: ubuntu-latest 10 | env: 11 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 12 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Install node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 22 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v1 25 | 26 | - name: Login to DockerHub 27 | run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }} 28 | 29 | - name: Install dependencies 30 | uses: ./actions/yarnCache 31 | 32 | - name: Build the images 33 | run: docker build -t chatsift/chatsift-next:latest -f ./Dockerfile . 34 | 35 | - name: Tag all 36 | run: yarn turbo run tag-docker --force --no-cache 37 | 38 | - name: Push to DockerHub 39 | run: docker image push --all-tags chatsift/chatsift-next 40 | -------------------------------------------------------------------------------- /services/api/src/util/__tests__/crypt.test.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer'; 2 | import { randomBytes } from 'node:crypto'; 3 | import { expect, test, vi } from 'vitest'; 4 | import { encrypt, decrypt } from '../crypt.js'; 5 | 6 | vi.mock('crypto', async () => { 7 | // eslint-disable-next-line @typescript-eslint/consistent-type-imports 8 | const original: typeof import('crypto') = await vi.importActual('crypto'); 9 | // eslint-disable-next-line unicorn/consistent-function-scoping 10 | const randomBytes = (len: number) => Buffer.from(Array.from({ length: len }).fill(1)); 11 | return { 12 | ...original, 13 | randomBytes, 14 | }; 15 | }); 16 | 17 | vi.mock('@chatsift/backend-core', () => ({ 18 | getContext: () => ({ 19 | env: { 20 | ENCRYPTION_KEY: randomBytes(32).toString('base64'), 21 | }, 22 | }), 23 | })); 24 | 25 | const PLAIN_DATA = 'this is very sensitive'; 26 | const SECRET_DATA = encrypt(PLAIN_DATA); 27 | 28 | test('encrypt', () => { 29 | expect(SECRET_DATA).toBe('AQEBAQEBAQEBAQEBAQEBAejE/bWU7BLYic/V/zbJLfwqp2c5B/8='); 30 | }); 31 | 32 | test('decrypt', () => { 33 | expect(decrypt(SECRET_DATA)).toBe(PLAIN_DATA); 34 | }); 35 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/amas/new/_components/PromptModeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/common/Button'; 2 | 3 | type PromptMode = 'normal' | 'raw'; 4 | 5 | interface PromptModeToggleProps { 6 | readonly mode: PromptMode; 7 | onModeChange(mode: PromptMode): void; 8 | } 9 | 10 | export function PromptModeToggle({ mode, onModeChange }: PromptModeToggleProps) { 11 | return ( 12 |
13 | 24 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/public/parse-relative-time/README.md: -------------------------------------------------------------------------------- 1 | # `@chatsift/parse-relative-time` 2 | 3 | [![GitHub](https://img.shields.io/badge/License-GNU%20AGPLv3-yellow.svg)](https://github.com/chatsift/chatsift/blob/main/LICENSE) 4 | [![npm](https://img.shields.io/npm/v/@chatsift/parse-relative-time?color=crimson&logo=npm)](https://www.npmjs.com/package/@chatsift/parse-relative-time) 5 | [![TypeScript](https://github.com/chatsift/chatsift/actions/workflows/test.yml/badge.svg)](https://github.com/chatsift/chatsift/actions/workflows/test.yml) 6 | 7 | Relative time parser, similar to [vercel/ms](https://github.com/vercel/ms) 8 | 9 | ## Installation 10 | 11 | - `npm install @chatsift/parse-relative-time` 12 | - `pnpm install @chatsift/parse-relative-time` 13 | - `yarn add @chatsift/parse-relative-time` 14 | 15 | ## Contributing 16 | 17 | Please see the main [README.md](https://github.com/chatsift/chatsift) for info on how to contribute to this package or the other `@chatsift` packages. 18 | 19 | ## LICENSE 20 | 21 | This project is licensed under the MIT license. 22 | 23 | It should, however, be noted that some packages are forks of other open source projects, and are therefore, sub-licensed. 24 | -------------------------------------------------------------------------------- /apps/website/src/utils/util.ts: -------------------------------------------------------------------------------- 1 | import type { MeGuild } from '@chatsift/api'; 2 | import { clsx, type ClassValue } from 'clsx'; 3 | import { twMerge } from 'tailwind-merge'; 4 | 5 | export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); 6 | 7 | export const retryWrapper = (retry: (retries: number, error: Error) => boolean) => (retries: number, error: Error) => { 8 | if (process.env.NODE_ENV === 'development') { 9 | return false; 10 | } 11 | 12 | return retry(retries, error); 13 | }; 14 | 15 | export const exponentialBackOff = (failureCount: number) => 2 ** failureCount * 1_000; 16 | 17 | export const sortGuilds = (guilds: MeGuild[]) => 18 | guilds 19 | .slice() 20 | .reverse() 21 | .sort((a, b) => b.bots.length - a.bots.length); 22 | 23 | export const getGuildAcronym = (guildName: string) => 24 | guildName 25 | .replaceAll("'s ", ' ') 26 | .replaceAll(/\w+/g, (substring) => substring[0]!) 27 | .replaceAll(/\s/g, ''); 28 | 29 | export const formatDate = (date: Date) => 30 | new Intl.DateTimeFormat('en-US', { 31 | year: 'numeric', 32 | month: 'long', 33 | day: 'numeric', 34 | hour: '2-digit', 35 | minute: '2-digit', 36 | }).format(date); 37 | -------------------------------------------------------------------------------- /packages/public/parse-relative-time/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatsift/parse-relative-time", 3 | "description": "Relative time parser, similar to vercel/ms", 4 | "version": "0.3.1", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "exports": { 8 | "types": "./dist/index.d.ts", 9 | "import": "./dist/index.mjs", 10 | "require": "./dist/index.js" 11 | }, 12 | "directories": { 13 | "lib": "src" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "build": "tsup && tsc", 20 | "test": "vitest run", 21 | "test:watch": "vitest", 22 | "lint": "eslint src", 23 | "lint:fix": "eslint src --fix", 24 | "clean": "rimraf dist" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/chatsift/chatsift.git", 29 | "directory": "packages/npm/parse-relative-time" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/chatsift/chatsift/issues" 33 | }, 34 | "homepage": "https://github.com/chatsift/chatsift", 35 | "devDependencies": { 36 | "@types/node": "^24.9.1", 37 | "tsup": "^8.5.0", 38 | "typescript": "~5.9.3", 39 | "vitest": "^2.1.9" 40 | }, 41 | "dependencies": { 42 | "tslib": "^2.8.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/website/src/components/footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeSwitchButton } from './ThemeSwitchButton'; 2 | import SvgDiscord from '@/components/icons/SvgDiscord'; 3 | import { SvgGitHub } from '@/components/icons/SvgGitHub'; 4 | 5 | export function Footer() { 6 | return ( 7 |
8 | © ChatSift, 2022 - Present 9 |
10 | 18 |
19 |

Theme:

20 | 21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/public/discord-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatsift/discord-utils", 3 | "description": "Niche utilities for working with Discord's raw API", 4 | "version": "0.5.1", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "exports": { 8 | "types": "./dist/index.d.ts", 9 | "import": "./dist/index.mjs", 10 | "require": "./dist/index.js" 11 | }, 12 | "directories": { 13 | "lib": "src" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "build": "tsup && tsc", 20 | "test": "vitest run", 21 | "test:watch": "vitest", 22 | "lint": "eslint src", 23 | "lint:fix": "eslint src --fix", 24 | "clean": "rimraf dist" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/chatsift/chatsift.git", 29 | "directory": "packages/npm/discord-utils" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/chatsift/chatsift/issues" 33 | }, 34 | "homepage": "https://github.com/chatsift/chatsift", 35 | "devDependencies": { 36 | "@types/node": "^24.9.1", 37 | "tsup": "^8.5.0", 38 | "typescript": "~5.9.3", 39 | "vitest": "^2.1.9" 40 | }, 41 | "dependencies": { 42 | "discord-api-types": "^0.38.30", 43 | "tslib": "^2.8.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/private/backend-core/src/lib/context.ts: -------------------------------------------------------------------------------- 1 | import type { DB } from '@chatsift/core'; 2 | import type { Kysely } from 'kysely'; 3 | import type { Logger } from 'pino'; 4 | import { ENV } from './env.js'; 5 | import type { createRedis } from './redis.js'; 6 | 7 | export interface Context { 8 | API_URL: string; 9 | BCRYPT_SALT_ROUNDS: number; 10 | FRONTEND_URL: string; 11 | UP_SINCE: number; 12 | 13 | db: Kysely; 14 | env: typeof ENV; 15 | logger: Logger; 16 | redis: Awaited>; 17 | } 18 | 19 | let context: Context | null = null; 20 | 21 | export function initContext(given: Pick): void { 22 | if (context !== null) { 23 | throw new Error('Context has already been initialized'); 24 | } 25 | 26 | context = { 27 | API_URL: ENV.IS_PRODUCTION ? ENV.API_URL_PROD : ENV.API_URL_DEV, 28 | BCRYPT_SALT_ROUNDS: 14, 29 | FRONTEND_URL: ENV.IS_PRODUCTION ? ENV.FRONTEND_URL_PROD : ENV.FRONTEND_URL_DEV, 30 | UP_SINCE: Date.now(), 31 | 32 | env: ENV, 33 | ...given, 34 | }; 35 | } 36 | 37 | export function getContext(): Context { 38 | if (!context) { 39 | throw new Error('Context has not been initialized yet'); 40 | } 41 | 42 | return context; 43 | } 44 | -------------------------------------------------------------------------------- /services/ama-bot/src/lib/collector.ts: -------------------------------------------------------------------------------- 1 | import { setTimeout, clearTimeout } from 'node:timers'; 2 | import type { APIInteraction, APIModalSubmitInteraction } from '@discordjs/core'; 3 | import { GatewayDispatchEvents, InteractionType } from '@discordjs/core'; 4 | import { client } from './client.js'; 5 | 6 | export async function collectModal(id: string, waitFor: number): Promise { 7 | return new Promise((resolve, reject) => { 8 | const cleanup = () => { 9 | /* eslint-disable @typescript-eslint/no-use-before-define */ 10 | client.off(GatewayDispatchEvents.InteractionCreate, handler); 11 | clearTimeout(timeout); 12 | /* eslint-enable @typescript-eslint/no-use-before-define */ 13 | }; 14 | 15 | const handler = ({ data: interaction }: { data: APIInteraction }) => { 16 | if (interaction.type === InteractionType.ModalSubmit && interaction.data.custom_id === id) { 17 | resolve(interaction); 18 | cleanup(); 19 | } 20 | }; 21 | 22 | const timeout = setTimeout(() => { 23 | reject(new Error('Modal submission timed out')); 24 | cleanup(); 25 | }, waitFor).unref(); 26 | 27 | client.on(GatewayDispatchEvents.InteractionCreate, handler); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/amas/new/_components/SnowflakeInput.tsx: -------------------------------------------------------------------------------- 1 | interface SnowflakeInputProps { 2 | readonly error?: string | undefined; 3 | readonly id: string; 4 | readonly label: string; 5 | onChange(value: string): void; 6 | readonly placeholder?: string; 7 | readonly required?: boolean; 8 | readonly value: string; 9 | } 10 | 11 | export function SnowflakeInput({ 12 | id, 13 | label, 14 | value, 15 | onChange, 16 | error, 17 | placeholder = '123456789012345678', 18 | required = false, 19 | }: SnowflakeInputProps) { 20 | return ( 21 |
22 | 25 | onChange(e.target.value)} 29 | placeholder={placeholder} 30 | type="text" 31 | value={value} 32 | /> 33 | {error &&

{error}

} 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /apps/website/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | export default { 3 | reactStrictMode: true, 4 | images: { 5 | contentDispositionType: 'attachment', 6 | contentSecurityPolicy: "default-src 'self'; frame-src 'none'; sandbox;", 7 | remotePatterns: [ 8 | { 9 | protocol: 'https', 10 | hostname: 'cdn.discordapp.com', 11 | pathname: '/icons/**', 12 | }, 13 | ], 14 | }, 15 | productionBrowserSourceMaps: true, 16 | logging: { 17 | fetches: { 18 | fullUrl: true, 19 | }, 20 | }, 21 | eslint: { 22 | ignoreDuringBuilds: true, 23 | }, 24 | typescript: { 25 | ignoreBuildErrors: false, 26 | }, 27 | experimental: { 28 | reactCompiler: true, 29 | }, 30 | async redirects() { 31 | return [ 32 | { 33 | source: '/github', 34 | destination: 'https://github.com/chatsift', 35 | permanent: true, 36 | }, 37 | { 38 | source: '/support', 39 | destination: 'https://discord.gg/tgZ2pSgXXv', 40 | permanent: true, 41 | }, 42 | { 43 | source: '/invites/ama', 44 | destination: 45 | 'https://discord.com/oauth2/authorize?client_id=1427232824854970409&permissions=274878024704&scope=applications.commands%20bot', 46 | permanent: true, 47 | }, 48 | ]; 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /services/api/src/routes/auth/logout.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from '@chatsift/backend-core'; 2 | import type { NextHandler, Response } from 'polka'; 3 | import { isAuthed } from '../../middleware/isAuthed.js'; 4 | import { discordAPIOAuth } from '../../util/discordAPI.js'; 5 | import { noopAccessToken, noopRefreshToken } from '../../util/tokens.js'; 6 | import type { TRequest } from '../route.js'; 7 | import { Route, RouteMethod } from '../route.js'; 8 | 9 | export default class PostAuthLogout extends Route { 10 | public readonly info = { 11 | method: RouteMethod.post, 12 | path: '/v3/auth/logout', 13 | } as const; 14 | 15 | public override readonly middleware = [ 16 | ...isAuthed({ fallthrough: false, isGlobalAdmin: false, isGuildManager: false }), 17 | ]; 18 | 19 | public override async handle(req: TRequest, res: Response, next: NextHandler) { 20 | await discordAPIOAuth.oauth2.revokeToken( 21 | getContext().env.OAUTH_DISCORD_CLIENT_ID, 22 | getContext().env.OAUTH_DISCORD_CLIENT_SECRET, 23 | { token: req.tokens!.refresh.discordRefreshToken, token_type_hint: 'refresh_token' }, 24 | ); 25 | 26 | noopAccessToken(res); 27 | noopRefreshToken(res); 28 | 29 | res.statusCode = 200; 30 | return res.end(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/website/src/components/common/Button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import type { ButtonProps } from 'react-aria-components'; 5 | import { Button as AriaButton } from 'react-aria-components'; 6 | import { cn } from '@/utils/util'; 7 | 8 | export function Button(props: ButtonProps) { 9 | const { className, ...rest } = props; 10 | const [isLoading, setIsLoading] = useState(false); 11 | 12 | return ( 13 | { 21 | if (props.onPress) { 22 | try { 23 | setIsLoading(true); 24 | // eslint-disable-next-line @typescript-eslint/await-thenable, @typescript-eslint/no-confusing-void-expression 25 | await props.onPress?.(event); 26 | } finally { 27 | setIsLoading(false); 28 | } 29 | } 30 | }} 31 | > 32 | {props.children} 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /services/api/src/middleware/__tests__/validate.test.ts: -------------------------------------------------------------------------------- 1 | import { Boom } from '@hapi/boom'; 2 | import type { Request, Response } from 'polka'; 3 | import { afterEach, expect, test, vi } from 'vitest'; 4 | import z from 'zod'; 5 | import { validate } from '../validate.js'; 6 | 7 | const next = vi.fn(); 8 | 9 | afterEach(() => { 10 | vi.resetAllMocks(); 11 | }); 12 | 13 | const makeMockedRequest = (data: any) => data as Request; 14 | const mockedResponse = {} as unknown as Response; 15 | 16 | test('invalid schema', () => { 17 | const validator = validate( 18 | z 19 | .object({ 20 | foo: z.string(), 21 | }) 22 | .strict(), 23 | 'body', 24 | ); 25 | 26 | void validator(makeMockedRequest({ body: { foo: 1 } }), mockedResponse, next); 27 | expect(next).toHaveBeenCalledWith(expect.any(Boom)); 28 | }); 29 | 30 | test('valid schema', () => { 31 | const validator = validate( 32 | z 33 | .object({ 34 | foo: z.string(), 35 | bar: z.number().default(5), 36 | }) 37 | .strict(), 38 | 'body', 39 | ); 40 | 41 | const req = { body: { foo: 'bar' } }; 42 | 43 | void validator(makeMockedRequest(req), mockedResponse, next); 44 | expect(next).toHaveBeenCalledWith(); 45 | expect(req).toHaveProperty('body', { foo: 'bar', bar: 5 }); 46 | }); 47 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/SvgGitHub.tsx: -------------------------------------------------------------------------------- 1 | export function SvgGitHub() { 2 | return ( 3 | 4 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/amas/_components/AMASessionCard.tsx: -------------------------------------------------------------------------------- 1 | import type { AMASessionWithCount } from '@chatsift/api'; 2 | import Link from 'next/link'; 3 | 4 | interface AMASessionCardProps { 5 | readonly data: AMASessionWithCount; 6 | } 7 | 8 | export function AMASessionCard({ data }: AMASessionCardProps) { 9 | return ( 10 | 14 |
15 |

16 | {data.title} 17 |

18 |

19 | {data.questionCount} {data.questionCount === 1 ? 'question' : 'questions'} 20 |

21 |
22 |
23 | 26 | {data.ended ? 'Ended' : 'Active'} 27 | 28 |
29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/website/src/components/nav/NavbarDesktop.tsx: -------------------------------------------------------------------------------- 1 | import * as NavigationMenu from '@radix-ui/react-navigation-menu'; 2 | import { navbarItems } from './navbarItems'; 3 | import { Logo } from '@/components/common/Logo'; 4 | import { UserDesktop } from '@/components/user/UserDesktop'; 5 | 6 | export function NavbarDesktop() { 7 | return ( 8 |
    9 |
  • 10 | 11 |
  • 12 | 13 |
    14 | 15 | 16 | {navbarItems.map((item) => ( 17 | 18 | 22 | {item.name} 23 | 24 | 25 | ))} 26 | 27 | 28 |
    29 | 30 |
  • 31 | 32 |
  • 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /packages/public/pino-rotate-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatsift/pino-rotate-file", 3 | "description": "Simple pino transport for rotating files", 4 | "version": "0.5.1", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "exports": { 8 | "types": "./dist/index.d.ts", 9 | "import": "./dist/index.mjs", 10 | "require": "./dist/index.js" 11 | }, 12 | "directories": { 13 | "lib": "src" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "build": "tsup && tsc", 20 | "test": "vitest run", 21 | "test:watch": "vitest", 22 | "lint": "eslint src", 23 | "lint:fix": "eslint src --fix", 24 | "clean": "rimraf dist" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/chatsift/chatsift.git", 29 | "directory": "packages/npm/pino-rotate-file" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/chatsift/chatsift/issues" 33 | }, 34 | "homepage": "https://github.com/chatsift/chatsift", 35 | "devDependencies": { 36 | "@types/node": "^24.9.1", 37 | "sonic-boom": "^4.2.0", 38 | "tsup": "^8.5.0", 39 | "typescript": "~5.9.3", 40 | "vitest": "^2.1.9" 41 | }, 42 | "dependencies": { 43 | "pino": "^9.14.0", 44 | "pino-abstract-transport": "^1.2.0", 45 | "pino-pretty": "^13.1.2", 46 | "tslib": "^2.8.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/SvgAMA.tsx: -------------------------------------------------------------------------------- 1 | export function SvgAMA({ width, height }: { readonly height?: number; readonly width?: number }) { 2 | return ( 3 | 4 | 10 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/private/backend-core/src/lib/env.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import { SnowflakeRegex } from '@sapphire/discord-utilities'; 3 | import z from 'zod'; 4 | 5 | const envSchema = z.object({ 6 | // General 7 | IS_PRODUCTION: z.stringbool().default(false), 8 | ROOT_DOMAIN: z.string(), 9 | ADMINS: z 10 | .string() 11 | .optional() 12 | .transform((value) => value?.split(', ')) 13 | .pipe(z.array(z.string().regex(SnowflakeRegex)).optional()) 14 | .transform((value) => (value ? new Set(value) : new Set())), 15 | 16 | // API 17 | API_PORT: z.string().pipe(z.coerce.number()), 18 | OAUTH_DISCORD_CLIENT_ID: z.string().regex(SnowflakeRegex), 19 | OAUTH_DISCORD_CLIENT_SECRET: z.string(), 20 | CORS: z.string().transform((value, ctx) => { 21 | try { 22 | return new RegExp(value); 23 | } catch { 24 | ctx.addIssue({ 25 | code: 'custom', 26 | message: 'Not a valid regular expression', 27 | }); 28 | return z.NEVER; 29 | } 30 | }), 31 | // Length of a base64-encoded 32-byte key. Used for JWT signing and encryption 32 | ENCRYPTION_KEY: z.string().length(44), 33 | API_URL_DEV: z.url(), 34 | API_URL_PROD: z.url(), 35 | FRONTEND_URL_DEV: z.url(), 36 | FRONTEND_URL_PROD: z.url(), 37 | 38 | // AMA 39 | AMA_BOT_TOKEN: z.string(), 40 | }); 41 | 42 | export const ENV = envSchema.parse(process.env); 43 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { DashboardCrumbs } from '../../_components/DashboardCrumbs'; 3 | import { Heading } from '@/components/common/Heading'; 4 | 5 | export default async function AMAPage({ params }: PageProps<'/dashboard/[id]/ama/amas'>) { 6 | const { id } = await params; 7 | 8 | return ( 9 |
10 |
11 | 12 | 13 | 18 | {/* TODO */} 19 |
20 | Q 21 |
22 |
23 |

Manage AMAs

24 |

Create, and manage AMAs in your community

25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /services/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatsift/api", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "exports": { 7 | ".": "./dist/index.js" 8 | }, 9 | "types": "./dist/index.d.ts", 10 | "scripts": { 11 | "build": "tsc", 12 | "test": "vitest run", 13 | "test:watch": "vitest", 14 | "lint": "eslint src", 15 | "lint:fix": "eslint src --fix", 16 | "clean": "rimraf dist", 17 | "tag-docker": "docker image tag chatsift/chatsift-next:latest chatsift/chatsift-next:api" 18 | }, 19 | "devDependencies": { 20 | "@types/bcrypt": "^6.0.0", 21 | "@types/busboy": "^1.5.4", 22 | "@types/cors": "^2.8.19", 23 | "@types/jsonwebtoken": "^9.0.10", 24 | "@types/node": "^24.9.1", 25 | "kysely": "^0.28.8", 26 | "typescript": "~5.9.3", 27 | "vitest": "^3.2.4" 28 | }, 29 | "dependencies": { 30 | "@chatsift/backend-core": "workspace:^", 31 | "@discordjs/core": "^3.0.0-dev.1759363313-f510b5ffa", 32 | "@discordjs/rest": "^3.0.0-dev.1759363313-f510b5ffa", 33 | "@hapi/boom": "^10.0.1", 34 | "@sapphire/discord-utilities": "^3.5.0", 35 | "bcrypt": "^6.0.0", 36 | "busboy": "^1.6.0", 37 | "cookie": "^1.0.2", 38 | "cors": "^2.8.5", 39 | "helmet": "^8.1.0", 40 | "jsonwebtoken": "^9.0.2", 41 | "nanoid": "^5.1.6", 42 | "polka": "^1.0.0-next.28", 43 | "sharp": "^0.34.4", 44 | "zod": "^4.1.12" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/amas/_components/IncludeEndedToggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { usePathname, useRouter, useSearchParams } from 'next/navigation'; 4 | import { useCallback } from 'react'; 5 | import { Button } from '@/components/common/Button'; 6 | 7 | export function IncludeEndedToggle() { 8 | const searchParams = useSearchParams(); 9 | const pathname = usePathname(); 10 | const router = useRouter(); 11 | 12 | const includeEnded = searchParams.get('include_ended') === 'true'; 13 | 14 | const handleToggle = useCallback(() => { 15 | const params = new URLSearchParams(searchParams); 16 | if (includeEnded) { 17 | params.delete('include_ended'); 18 | } else { 19 | params.set('include_ended', 'true'); 20 | } 21 | 22 | const newUrl = params.toString() ? `${pathname}?${params.toString()}` : pathname; 23 | router.push(newUrl); 24 | }, [includeEnded, pathname, router, searchParams]); 25 | 26 | return ( 27 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - 'Quality Check' 7 | branches: 8 | - main 9 | types: 10 | - completed 11 | 12 | jobs: 13 | deploy: 14 | name: Deploy 15 | runs-on: ubuntu-latest 16 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 17 | env: 18 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 19 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | with: 25 | fetch-depth: 2 26 | 27 | - name: Install node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 22 31 | 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v1 34 | 35 | - name: Login to DockerHub 36 | run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }} 37 | 38 | - name: Install dependencies 39 | uses: ./actions/yarnCache 40 | 41 | - name: Build the images 42 | run: docker build -t chatsift/chatsift-next:latest -f ./Dockerfile . 43 | 44 | - name: Tag changes 45 | run: yarn turbo run tag-docker --filter '...[HEAD~1...HEAD~0]' 46 | 47 | - name: Push to DockerHub 48 | run: docker image push --all-tags chatsift/chatsift-next 49 | -------------------------------------------------------------------------------- /apps/website/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import typographyPlugin from '@tailwindcss/typography'; 2 | import type { Config } from 'tailwindcss'; 3 | import tailwindAnimate from 'tailwindcss-animate'; 4 | 5 | const config: Config = { 6 | content: [ 7 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 8 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 9 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 10 | ], 11 | darkMode: 'class', 12 | theme: { 13 | colors: { 14 | base: { 15 | DEFAULT: '#F1F2F5', 16 | dark: '#151519', 17 | }, 18 | primary: { 19 | DEFAULT: '#1d274e', 20 | dark: '#F6F6FB', 21 | }, 22 | secondary: { 23 | DEFAULT: 'rgba(29, 39, 78, 0.75)', 24 | dark: '#F6F6FBB2', 25 | }, 26 | accent: '#ffffff', 27 | disabled: { 28 | DEFAULT: '#1E284F80', 29 | dark: '#F5F5FC66', 30 | }, 31 | on: { 32 | primary: { 33 | DEFAULT: '#1E284F40', 34 | dark: '#F4F4FD33', 35 | }, 36 | secondary: { 37 | DEFAULT: 'rgba(29, 39, 78, 0.15)', 38 | dark: '#F4F4FD1A', 39 | }, 40 | tertiary: { 41 | DEFAULT: '#1E284F0D', 42 | dark: '#F4F4FD0D', 43 | }, 44 | }, 45 | misc: { 46 | accent: '#2f8fee', 47 | danger: '#ff5052', 48 | }, 49 | card: { 50 | DEFAULT: '#FFFFFF', 51 | dark: '#1C1C21', 52 | }, 53 | }, 54 | }, 55 | plugins: [typographyPlugin, tailwindAnimate], 56 | }; 57 | 58 | export default config; 59 | -------------------------------------------------------------------------------- /services/api/src/routes/auth/me.ts: -------------------------------------------------------------------------------- 1 | import type { NextHandler, Response } from 'polka'; 2 | import type z from 'zod'; 3 | import { isAuthed } from '../../middleware/isAuthed.js'; 4 | import type { Me } from '../../util/me.js'; 5 | import { fetchMe } from '../../util/me.js'; 6 | import { queryWithFreshSchema } from '../../util/schemas.js'; 7 | import type { TRequest } from '../route.js'; 8 | import { Route, RouteMethod } from '../route.js'; 9 | 10 | export type { Me, MeGuild } from '../../util/me.js'; 11 | 12 | const querySchema = queryWithFreshSchema; 13 | export type GetAuthMeQuery = z.input; 14 | 15 | export default class GetAuthMe extends Route { 16 | public readonly info = { 17 | method: RouteMethod.get, 18 | path: '/v3/auth/me', 19 | } as const; 20 | 21 | public override readonly queryValidationSchema = querySchema; 22 | 23 | public override readonly middleware = [ 24 | ...isAuthed({ fallthrough: false, isGlobalAdmin: false, isGuildManager: false }), 25 | ]; 26 | 27 | public override async handle(req: TRequest, res: Response, next: NextHandler) { 28 | const { force_fresh } = req.query; 29 | 30 | const result: Me = await fetchMe(req.tokens!.access.discordAccessToken, force_fresh); 31 | 32 | res.statusCode = 200; 33 | res.setHeader('Content-Type', 'application/json'); 34 | return res.end(JSON.stringify(result)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /services/api/src/middleware/jsonParser.ts: -------------------------------------------------------------------------------- 1 | import { badData, badRequest } from '@hapi/boom'; 2 | import type { NextHandler, Request, Response } from 'polka'; 3 | 4 | declare module 'polka' { 5 | interface Request { 6 | /** 7 | * Raw (unparsed) JSON body of the request - present if `wantRaw` is set to `false` in {@link jsonParser} 8 | */ 9 | rawBody?: string; 10 | } 11 | } 12 | 13 | /** 14 | * Creates a request handler that parses the request body as JSON 15 | * 16 | * @param wantRaw - Whether to set the value of {@link Request.rawBody} 17 | */ 18 | export function jsonParser(wantRaw = false) { 19 | return async (req: Request, _: Response, next: NextHandler) => { 20 | if (!req.headers['content-type']?.startsWith('application/json')) { 21 | return next(badRequest('unexpected content type')); 22 | } 23 | 24 | req.setEncoding('utf8'); 25 | 26 | try { 27 | let data = ''; 28 | for await (const chunk of req) { 29 | data += chunk; 30 | } 31 | 32 | if (wantRaw) { 33 | req.rawBody = data; 34 | } 35 | 36 | if (data === '') { 37 | // eslint-disable-next-line n/callback-return 38 | await next(); 39 | return; 40 | } 41 | 42 | req.body = JSON.parse(data) as unknown; 43 | 44 | // eslint-disable-next-line n/callback-return 45 | await next(); 46 | } catch (error_) { 47 | const error = error_ as Error; 48 | return next(badData(error.message)); 49 | } 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /apps/website/src/components/common/ScrollArea.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as ScrollAreaBase from '@radix-ui/react-scroll-area'; 4 | import type { ReactNode } from 'react'; 5 | 6 | interface ScrollAreaProps { 7 | readonly children: ReactNode; 8 | readonly className?: string; 9 | readonly rootClassName?: string; 10 | } 11 | 12 | export function ScrollArea({ children, className, rootClassName }: ScrollAreaProps) { 13 | return ( 14 | 15 | {children} 16 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/channels/SvgChannelText.tsx: -------------------------------------------------------------------------------- 1 | interface SvgChannelTextProps { 2 | readonly className?: string; 3 | readonly size?: number; 4 | } 5 | 6 | export function SvgChannelText({ className, size = 20 }: SvgChannelTextProps) { 7 | return ( 8 | 16 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /services/ama-bot/src/lib/client.ts: -------------------------------------------------------------------------------- 1 | import { setInterval } from 'node:timers'; 2 | import { getContext, GuildList } from '@chatsift/backend-core'; 3 | import type { Snowflake } from '@discordjs/core'; 4 | import { InteractionType, Client, GatewayDispatchEvents } from '@discordjs/core'; 5 | import { handleComponentInteraction } from './components.js'; 6 | import { gateway } from './gateway.js'; 7 | import { rest } from './rest.js'; 8 | 9 | // keep a copy of the guild ids we manage here to easily patch redis 10 | const guildIds = new Set(); 11 | 12 | export function startGuildSyncing(): void { 13 | setInterval(async () => { 14 | void GuildList.set('AMA', { guilds: [...guildIds] }); 15 | }, 10_000).unref(); 16 | } 17 | 18 | export const client = new Client({ rest, gateway }); 19 | 20 | client 21 | .on(GatewayDispatchEvents.GuildCreate, ({ data: guild }) => { 22 | guildIds.add(guild.id); 23 | }) 24 | .on(GatewayDispatchEvents.GuildDelete, ({ data: guild }) => { 25 | if (!guild.unavailable) { 26 | guildIds.delete(guild.id); 27 | } 28 | }) 29 | .on(GatewayDispatchEvents.InteractionCreate, async ({ data: interaction }) => { 30 | if (interaction.type === InteractionType.MessageComponent) { 31 | await handleComponentInteraction(interaction); 32 | } else { 33 | getContext().logger.warn({ interactionType: interaction.type }, 'Unhandled interaction type'); 34 | } 35 | }) 36 | .once(GatewayDispatchEvents.Ready, () => { 37 | getContext().logger.info('Logged in successfully'); 38 | }); 39 | -------------------------------------------------------------------------------- /apps/website/src/components/common/Avatar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 4 | import * as React from 'react'; 5 | import { cn } from '@/utils/util'; 6 | 7 | export const Avatar = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 16 | )); 17 | Avatar.displayName = AvatarPrimitive.Root.displayName; 18 | 19 | export const AvatarImage = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )); 25 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 26 | 27 | export const AvatarFallback = React.forwardRef< 28 | React.ElementRef, 29 | React.ComponentPropsWithoutRef 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )); 37 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 38 | -------------------------------------------------------------------------------- /services/ama-bot/src/lib/gateway.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from '@chatsift/backend-core'; 2 | import type { RESTGetAPIGatewayBotResult } from '@discordjs/core'; 3 | import { GatewayIntentBits, Routes } from '@discordjs/core'; 4 | import { CompressionMethod, WebSocketManager, WebSocketShardEvents } from '@discordjs/ws'; 5 | import { rest } from './rest.js'; 6 | 7 | export const gateway = new WebSocketManager({ 8 | token: getContext().env.AMA_BOT_TOKEN, 9 | intents: GatewayIntentBits.Guilds, 10 | fetchGatewayInformation: async () => rest.get(Routes.gatewayBot()) as Promise, 11 | compression: CompressionMethod.ZlibNative, 12 | }); 13 | 14 | gateway 15 | .on(WebSocketShardEvents.Closed, (code, shardId) => getContext().logger.info({ shardId, code }, 'Shard CLOSED')) 16 | .on(WebSocketShardEvents.HeartbeatComplete, ({ ackAt, heartbeatAt, latency }, shardId) => 17 | getContext().logger.debug({ shardId, ackAt, heartbeatAt, latency }, 'Shard HEARTBEAT'), 18 | ) 19 | .on(WebSocketShardEvents.Error, (shardId, error) => getContext().logger.error({ shardId, error }, 'Shard ERROR')) 20 | .on(WebSocketShardEvents.Debug, (message, shardId) => getContext().logger.debug({ shardId }, message)) 21 | .on(WebSocketShardEvents.Hello, (shardId) => getContext().logger.debug({ shardId }, 'Shard HELLO')) 22 | .on(WebSocketShardEvents.Ready, (data, shardId) => getContext().logger.debug({ data, shardId }, 'Shard READY')) 23 | .on(WebSocketShardEvents.Resumed, (shardId) => getContext().logger.debug({ shardId }, 'Shard RESUMED')); 24 | -------------------------------------------------------------------------------- /apps/website/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { HydrationBoundary } from '@tanstack/react-query'; 2 | import type { Metadata } from 'next'; 3 | import type { PropsWithChildren } from 'react'; 4 | import { Providers } from '@/components/common/Providers'; 5 | import { ScrollArea } from '@/components/common/ScrollArea'; 6 | import { Footer } from '@/components/footer/Footer'; 7 | import { Navbar } from '@/components/nav/Navbar'; 8 | import { server } from '@/data/server'; 9 | 10 | import '@/styles/globals.css'; 11 | 12 | export const metadata: Metadata = { 13 | title: { 14 | template: '%s | ChatSift', 15 | default: 'ChatSift', 16 | }, 17 | icons: { 18 | other: [{ rel: 'icon', url: '/assets/favicon.ico' }], 19 | }, 20 | }; 21 | 22 | export default async function RootLayout({ children }: PropsWithChildren) { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 |
33 | {children} 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine 2 | LABEL name="chatsift" 3 | 4 | WORKDIR /usr/chatsift 5 | 6 | RUN apk add --update \ 7 | && apk add --no-cache ca-certificates \ 8 | && apk add --no-cache --virtual .build-deps curl git python3 alpine-sdk 9 | 10 | COPY turbo.json package.json tsconfig.base.json tsconfig.json tsup.config.ts yarn.lock .yarnrc.yml ./ 11 | COPY .yarn ./.yarn 12 | 13 | COPY packages/public/discord-utils/package.json ./packages/public/discord-utils/package.json 14 | COPY packages/public/parse-relative-time/package.json ./packages/public/parse-relative-time/package.json 15 | COPY packages/public/pino-rotate-file/package.json ./packages/public/pino-rotate-file/package.json 16 | COPY packages/private/backend-core/package.json ./packages/private/backend-core/package.json 17 | COPY packages/private/core/package.json ./packages/private/core/package.json 18 | 19 | COPY services/ama-bot/package.json ./services/ama-bot/package.json 20 | COPY services/api/package.json ./services/api/package.json 21 | 22 | RUN yarn workspaces focus --all 23 | 24 | COPY prisma ./prisma 25 | 26 | COPY packages/public/discord-utils ./packages/public/discord-utils 27 | COPY packages/public/parse-relative-time ./packages/public/parse-relative-time 28 | COPY packages/public/pino-rotate-file ./packages/public/pino-rotate-file 29 | COPY packages/private/backend-core ./packages/private/backend-core 30 | COPY packages/private/core ./packages/private/core 31 | 32 | COPY services/ama-bot ./services/ama-bot 33 | COPY services/api ./services/api 34 | 35 | RUN yarn turbo run build 36 | RUN yarn workspaces focus --all --production 37 | -------------------------------------------------------------------------------- /actions/yarnCache/action.yml: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/discordjs/discord.js/blob/7196fe36e8089dde7bcaf0db4dd09cf524125e0c/packages/actions/src/yarnCache/action.yml 2 | 3 | # Full credits to discord.js and its contributors, below is the original Apache 2.0 LICENSE file: 4 | # https://github.com/discordjs/discord.js/blob/7196fe36e8089dde7bcaf0db4dd09cf524125e0c/LICENSE 5 | 6 | name: 'yarn install' 7 | description: 'Run yarn install with node_modules linker and cache enabled' 8 | runs: 9 | using: 'composite' 10 | steps: 11 | - name: Expose yarn config as "$GITHUB_OUTPUT" 12 | id: yarn-config 13 | shell: bash 14 | run: | 15 | echo "CACHE_FOLDER=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT 16 | 17 | - name: Restore yarn cache 18 | uses: actions/cache@v3 19 | id: yarn-download-cache 20 | with: 21 | path: ${{ steps.yarn-config.outputs.CACHE_FOLDER }} 22 | key: yarn-download-cache-${{ hashFiles('yarn.lock') }} 23 | restore-keys: | 24 | yarn-download-cache- 25 | 26 | - name: Restore yarn install state 27 | id: yarn-install-state-cache 28 | uses: actions/cache@v3 29 | with: 30 | path: .yarn/ci-cache/ 31 | key: ${{ runner.os }}-yarn-install-state-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} 32 | 33 | - name: Install dependencies 34 | shell: bash 35 | run: | 36 | yarn install --immutable --inline-builds 37 | env: 38 | YARN_ENABLE_GLOBAL_CACHE: 'false' 39 | YARN_NM_MODE: 'hardlinks-local' 40 | YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz 41 | -------------------------------------------------------------------------------- /apps/website/src/components/common/Providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import type { QueryClientConfig } from '@tanstack/react-query'; 4 | import { isServer, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 6 | import { Provider as JotaiProvider } from 'jotai'; 7 | import { ThemeProvider } from 'next-themes'; 8 | import type { PropsWithChildren } from 'react'; 9 | import { APIError } from '@/utils/fetcher'; 10 | 11 | let browserQueryClient: QueryClient | undefined; 12 | 13 | function getQueryClient() { 14 | const base: QueryClientConfig = { 15 | defaultOptions: { 16 | queries: { 17 | staleTime: 60 * 1_000, 18 | }, 19 | }, 20 | }; 21 | 22 | if (isServer) { 23 | return new QueryClient(base); 24 | } 25 | 26 | return (browserQueryClient ??= new QueryClient({ 27 | ...base, 28 | queryCache: new QueryCache({ 29 | // TODO: Handle in some way 30 | onError: (error) => { 31 | if (error instanceof APIError) { 32 | console.error('Query error:', error.payload); 33 | } else { 34 | console.error('Network Error:', error); 35 | } 36 | }, 37 | }), 38 | })); 39 | } 40 | 41 | export function Providers({ children }: PropsWithChildren) { 42 | const queryClient = getQueryClient(); 43 | 44 | return ( 45 | 46 | 47 | {children} 48 | 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /services/api/src/routes/_types/index.ts: -------------------------------------------------------------------------------- 1 | import type { InferRouteBodyOrQuery, InferRouteMethod, InferRouteResult, RouteMethod } from '../route.js'; 2 | import type * as routes from '../routes.js'; 3 | 4 | type Narrow = Narrowed extends Narowee ? Narrowed : never; 5 | type ConstructorToType = TConstructor extends new (...args: any[]) => infer T ? T : never; 6 | type RoutesByClass = { 7 | [K in keyof typeof routes]: ConstructorToType<(typeof routes)[K]>; 8 | }; 9 | type RoutesByPath = { 10 | [Path in RoutesByClass[keyof RoutesByClass]['info']['path']]: Narrow< 11 | RoutesByClass[keyof RoutesByClass], 12 | { info: { path: Path } } 13 | >; 14 | }; 15 | 16 | interface RouteMethodMap { 17 | [RouteMethod.get]: 'GET'; 18 | [RouteMethod.post]: 'POST'; 19 | [RouteMethod.put]: 'PUT'; 20 | [RouteMethod.delete]: 'DELETE'; 21 | [RouteMethod.patch]: 'PATCH'; 22 | } 23 | 24 | export type { ParseHTTPParameters } from '../route.js'; 25 | 26 | export type APIRoutes = { 27 | [Path in keyof RoutesByPath]: { 28 | [Method in RouteMethodMap[InferRouteMethod]]: Narrow< 29 | RoutesByPath[Path], 30 | { info: { method: Lowercase } } 31 | >; 32 | }; 33 | }; 34 | 35 | // TODO: Look into Date -> string 36 | 37 | export type InferAPIRouteBodyOrQuery< 38 | TPath extends keyof APIRoutes, 39 | TMethod extends keyof APIRoutes[TPath], 40 | > = InferRouteBodyOrQuery; 41 | 42 | export type InferAPIRouteResult< 43 | TPath extends keyof APIRoutes, 44 | TMethod extends keyof APIRoutes[TPath], 45 | > = InferRouteResult; 46 | 47 | export type * from './routeTypes.js'; 48 | -------------------------------------------------------------------------------- /apps/website/src/components/icons/channels/SvgChannelThread.tsx: -------------------------------------------------------------------------------- 1 | interface SvgChannelThreadProps { 2 | readonly className?: string; 3 | readonly size?: number; 4 | } 5 | 6 | export function SvgChannelThread({ className, size = 20 }: SvgChannelThreadProps) { 7 | return ( 8 | 16 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /apps/website/src/app/dashboard/[id]/ama/amas/new/_components/RawPromptField.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/common/Button'; 2 | 3 | interface RawPromptFieldProps { 4 | onFormatClick(): void; 5 | onPaste(e: React.ClipboardEvent): void; 6 | onValueChange(value: string): void; 7 | readonly value: string; 8 | } 9 | 10 | export function RawPromptField({ value, onValueChange, onFormatClick, onPaste }: RawPromptFieldProps) { 11 | return ( 12 |
13 |
14 | 17 | 23 |
24 |