├── .husky ├── .gitignore └── pre-commit ├── apps ├── ranking │ ├── src │ │ ├── index.ts │ │ ├── getTopTweets.ts │ │ ├── deleteRankedTweetsById.ts │ │ ├── getTweetIdChunks.ts │ │ ├── __mocks__ │ │ │ ├── createFakeTweetBatch.ts │ │ │ ├── createFakeTemporaryTweets.ts │ │ │ ├── createFakeRankedTweets.ts │ │ │ └── createFakeTweetData.ts │ │ ├── schema │ │ │ ├── TemporaryTweet.ts │ │ │ └── RankedTweet.ts │ │ ├── getTweetScore.ts │ │ ├── isPublicMetricsChanged.ts │ │ ├── deleteTemporaryTweets.ts │ │ ├── parseToRankedTweet.ts │ │ ├── saveRankedTweets.ts │ │ ├── getTweetBatchFromApi.ts │ │ ├── countNewAuthors.ts │ │ ├── getTemporaryTweets.ts │ │ ├── getTweetsByIdChunks.ts │ │ ├── saveTemporaryTweet.ts │ │ ├── getRankingPeriod.ts │ │ ├── postTweetRanking.ts │ │ ├── getRankedTweetsFromApi.ts │ │ ├── getRankedTweets.ts │ │ ├── __tests__ │ │ │ ├── countNewAuthors.test.ts │ │ │ ├── saveTemporaryTweet.test.ts │ │ │ └── getStartDateFunctionMap.test.ts │ │ ├── findRankedTweetsForSync.ts │ │ ├── getTwitterClient.ts │ │ ├── createPostFromRankedTweets.ts │ │ ├── getStartDate.ts │ │ └── getStats.ts │ ├── test │ │ ├── disconnectMongoose.ts │ │ ├── clearDbAndRestartCounters.ts │ │ └── connectMongoose.ts │ ├── jest-mongodb-config.js │ ├── tsconfig.json │ ├── jest.config.js │ ├── package.json │ └── setupFiles.js ├── cli │ ├── .npmrc │ ├── src │ │ └── bin │ │ │ └── cli.ts │ ├── babel.config.js │ ├── .eslintrc.js │ ├── jest.config.js │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── web │ ├── .npmrc │ ├── .gitignore │ ├── public │ │ ├── sib1.png │ │ ├── sib2.png │ │ ├── sib3.png │ │ ├── sibs.png │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── static │ │ │ └── fallback.png │ │ ├── apple-touch-icon.png │ │ ├── icons │ │ │ ├── apple-icon-180.png │ │ │ ├── apple-splash-1125-2436.png │ │ │ ├── apple-splash-1136-640.png │ │ │ ├── apple-splash-1170-2532.png │ │ │ ├── apple-splash-1242-2208.png │ │ │ ├── apple-splash-1242-2688.png │ │ │ ├── apple-splash-1284-2778.png │ │ │ ├── apple-splash-1334-750.png │ │ │ ├── apple-splash-1536-2048.png │ │ │ ├── apple-splash-1620-2160.png │ │ │ ├── apple-splash-1668-2224.png │ │ │ ├── apple-splash-1668-2388.png │ │ │ ├── apple-splash-1792-828.png │ │ │ ├── apple-splash-2048-1536.png │ │ │ ├── apple-splash-2048-2732.png │ │ │ ├── apple-splash-2160-1620.png │ │ │ ├── apple-splash-2208-1242.png │ │ │ ├── apple-splash-2224-1668.png │ │ │ ├── apple-splash-2388-1668.png │ │ │ ├── apple-splash-2436-1125.png │ │ │ ├── apple-splash-2532-1170.png │ │ │ ├── apple-splash-2688-1242.png │ │ │ ├── apple-splash-2732-2048.png │ │ │ ├── apple-splash-2778-1284.png │ │ │ ├── apple-splash-640-1136.png │ │ │ ├── apple-splash-750-1334.png │ │ │ ├── apple-splash-828-1792.png │ │ │ ├── manifest-icon-192.maskable.png │ │ │ └── manifest-icon-512.maskable.png │ │ ├── robots.txt │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-384x384.png │ │ ├── android-chrome-512x512.png │ │ ├── android-chrome-256x256.png.png │ │ ├── sitemap.xml │ │ ├── vercel.svg │ │ └── manifest.json │ ├── prettier.config.js │ ├── .babelrc │ ├── jest.setup.js │ ├── src │ │ ├── types │ │ │ ├── User.ts │ │ │ ├── Ranking.ts │ │ │ ├── Score.ts │ │ │ └── Tweet.ts │ │ ├── pages │ │ │ ├── 404.tsx │ │ │ ├── _offline.js │ │ │ ├── _app.tsx │ │ │ ├── api │ │ │ │ ├── score │ │ │ │ │ └── [username].ts │ │ │ │ ├── user │ │ │ │ │ └── [username].ts │ │ │ │ ├── getTwitterUsers.ts │ │ │ │ ├── auth │ │ │ │ │ └── [...nextauth].ts │ │ │ │ ├── tweet │ │ │ │ │ └── [id].ts │ │ │ │ └── ranking.ts │ │ │ ├── sibs-day │ │ │ │ └── blobs │ │ │ │ │ └── index.tsx │ │ │ └── ranking.tsx │ │ ├── components │ │ │ ├── pix │ │ │ │ ├── emv.ts │ │ │ │ ├── PixQrCode.tsx │ │ │ │ ├── DonatePix.tsx │ │ │ │ └── PixModal.tsx │ │ │ ├── home │ │ │ │ ├── cryptoAddress.tsx │ │ │ │ ├── ScoreButton.tsx │ │ │ │ ├── useRandom.tsx │ │ │ │ ├── SponsorButton.tsx │ │ │ │ └── TwitterLogin.tsx │ │ │ ├── Head.tsx │ │ │ ├── ColorPalette.tsx │ │ │ ├── ChakraNextLinkButton.tsx │ │ │ ├── __tests__ │ │ │ │ ├── ForkMeCorner.test.tsx │ │ │ │ ├── TwitterLogin.test.tsx │ │ │ │ └── Ranking.test.tsx │ │ │ ├── ranking │ │ │ │ ├── RankingTrophy.tsx │ │ │ │ └── RankingMetrics.tsx │ │ │ ├── score │ │ │ │ └── DiscordClaimButton.tsx │ │ │ └── tweet │ │ │ │ └── tweetContent.ts │ │ ├── getHttpProtocol.tsx │ │ ├── modules │ │ │ ├── twitter │ │ │ │ ├── getTwitterAuthorization.ts │ │ │ │ ├── __mocks__ │ │ │ │ │ └── twitterUserGet.ts │ │ │ │ ├── twitterUserGet.ts │ │ │ │ └── twitterFollowersGet.ts │ │ │ ├── score │ │ │ │ ├── getEmptyScore.ts │ │ │ │ ├── getElegibleTweets.test.ts │ │ │ │ ├── calculateUserScore.ts │ │ │ │ ├── getElegibleTweets.ts │ │ │ │ └── getRankingScore.ts │ │ │ ├── db │ │ │ │ ├── mongob.ts │ │ │ │ └── schema │ │ │ │ │ └── RankedTweet.ts │ │ │ ├── discord │ │ │ │ └── discordGuildJoinPut.ts │ │ │ └── ranking │ │ │ │ └── getRanking.ts │ │ ├── usePrevious.tsx │ │ ├── middlewares │ │ │ ├── validations │ │ │ │ └── __tests__ │ │ │ │ │ ├── validationsPost.test.ts │ │ │ │ │ ├── validations-cron-header-key.test.ts │ │ │ │ │ ├── validations-cron-header-value.test.ts │ │ │ │ │ ├── validations-cron-job-secret-key.test.ts │ │ │ │ │ └── validations.test.ts │ │ │ └── error-handler.ts │ │ ├── canvasUtil.tsx │ │ └── config.tsx │ ├── next-env.d.ts │ ├── .eslintrc.js │ ├── sentry.server.config.js │ ├── sentry.client.config.js │ ├── test │ │ └── testUtils.tsx │ ├── tsconfig.json │ ├── jest.config.js │ └── .env.example ├── sseraresume │ ├── .gitignore │ ├── src │ │ ├── constants.ts │ │ ├── github │ │ │ ├── client.ts │ │ │ ├── services │ │ │ │ └── getFile.ts │ │ │ └── index.ts │ │ ├── common │ │ │ └── utils │ │ │ │ ├── appendLinesToFile.ts │ │ │ │ ├── getArticles.ts │ │ │ │ └── utils.ts │ │ ├── types.ts │ │ ├── resume-file-generator.ts │ │ ├── tweet.ts │ │ └── config.ts │ ├── .env.example │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── chrome-ext │ ├── .prettierignore │ ├── dist │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-48.png │ │ ├── popup.html │ │ └── manifest.json │ ├── .gitignore │ ├── src │ │ ├── content │ │ │ └── index.tsx │ │ ├── backgroundPage.ts │ │ ├── popup │ │ │ ├── index.tsx │ │ │ └── TweetComposer.tsx │ │ └── __mocks__ │ │ │ └── webextension-polyfill-ts.ts │ ├── webpack.prod.js │ ├── webpack.dev.js │ ├── .prettierrc │ ├── babel.config.js │ ├── jest.config.js │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── package.json │ └── webpack.common.js ├── ui │ ├── src │ │ ├── index.tsx │ │ └── TweetComposer.tsx │ ├── package.json │ └── tsconfig.json ├── sseramemes │ ├── jest.config.js │ ├── .gitignore │ ├── static │ │ ├── logo.png │ │ ├── logo-opacity.png │ │ └── Montserrat │ │ │ ├── static │ │ │ ├── Montserrat-Bold.ttf │ │ │ ├── Montserrat-Thin.ttf │ │ │ ├── Montserrat-Black.ttf │ │ │ ├── Montserrat-Italic.ttf │ │ │ ├── Montserrat-Light.ttf │ │ │ ├── Montserrat-Medium.ttf │ │ │ ├── Montserrat-Regular.ttf │ │ │ ├── Montserrat-ExtraBold.ttf │ │ │ ├── Montserrat-SemiBold.ttf │ │ │ ├── Montserrat-BlackItalic.ttf │ │ │ ├── Montserrat-BoldItalic.ttf │ │ │ ├── Montserrat-ExtraLight.ttf │ │ │ ├── Montserrat-LightItalic.ttf │ │ │ ├── Montserrat-MediumItalic.ttf │ │ │ ├── Montserrat-ThinItalic.ttf │ │ │ ├── Montserrat-SemiBoldItalic.ttf │ │ │ ├── Montserrat-ExtraBoldItalic.ttf │ │ │ └── Montserrat-ExtraLightItalic.ttf │ │ │ ├── Montserrat-VariableFont_wght.ttf │ │ │ ├── Montserrat-Italic-VariableFont_wght.ttf │ │ │ └── fonts.conf │ ├── src │ │ ├── image-scripts │ │ │ ├── addTextToImage │ │ │ │ └── index.ts │ │ │ ├── addLogoToGif.ts │ │ │ ├── index.ts │ │ │ └── resizeImage.ts │ │ ├── types.ts │ │ ├── score.ts │ │ ├── removeData.ts │ │ ├── readyMessage.ts │ │ ├── addMetadata.ts │ │ ├── getMessageContent.ts │ │ ├── __tests__ │ │ │ └── getMessageContent.spec.ts │ │ └── modules │ │ │ └── Publish │ │ │ └── publishMemes.ts │ ├── babel.config.js │ ├── tsconfig.json │ ├── .env.example │ ├── scripts │ │ ├── testAddLogoToGif.ts │ │ ├── testResizeImage.ts │ │ ├── testAddLogo.ts │ │ ├── testAddLogoToVideo.ts │ │ └── testAddTextToImage.ts │ ├── ecosystem.config.js │ └── package.json ├── vscode-ext │ ├── .gitignore │ ├── media │ │ ├── icon.png │ │ ├── main.js │ │ └── reset.css │ ├── src │ │ ├── constants.ts │ │ └── getNonce.ts │ ├── webviews │ │ ├── pages │ │ │ └── sidebar.ts │ │ ├── tsconfig.json │ │ ├── global.d.ts │ │ └── components │ │ │ └── Sidebar.svelte │ ├── .vscode │ │ ├── tasks.json │ │ ├── extensions.json │ │ ├── settings.json │ │ └── launch.json │ ├── .vscodeignore │ ├── .eslintrc.json │ ├── tsconfig.json │ └── README.md ├── bot │ ├── test │ │ ├── disconnectMongoose.ts │ │ ├── clearDbAndRestartCounters.ts │ │ └── connectMongoose.ts │ ├── src │ │ ├── score.ts │ │ ├── debugConsole.ts │ │ ├── discord │ │ │ ├── index.ts │ │ │ └── oauth.ts │ │ ├── readyMessage.ts │ │ ├── mongodb.ts │ │ ├── tweetRanking │ │ │ └── jobs │ │ │ │ ├── index.ts │ │ │ │ ├── rankingJob.ts │ │ │ │ ├── syncRankedTweetJob.ts │ │ │ │ └── getRuleFromConfig.ts │ │ ├── handleRT.ts │ │ ├── handleThreadCreation.ts │ │ └── config.ts │ ├── jest-mongodb-config.js │ ├── tsconfig.json │ ├── scripts │ │ ├── isMainScript.ts │ │ └── discord │ │ │ ├── discordOAuth.ts │ │ │ ├── discordMe.ts │ │ │ ├── discordGuilds.ts │ │ │ ├── discordGuild.ts │ │ │ ├── discordListen.ts │ │ │ └── discordChannel.ts │ ├── jest.config.js │ ├── .env.example │ ├── setupFiles.js │ ├── README.md │ └── package.json └── sserartigos │ ├── src │ ├── score.ts │ ├── notification.ts │ ├── github │ │ ├── client.ts │ │ ├── services │ │ │ └── getFile.ts │ │ └── index.ts │ ├── fansfy │ │ ├── postArticle.ts │ │ └── index.ts │ ├── pollHandler.ts │ ├── common │ │ └── utils │ │ │ ├── appendLinesToFile.ts │ │ │ ├── getArticles.ts │ │ │ └── utils.ts │ ├── types.ts │ ├── index.ts │ ├── tweet.ts │ └── config.ts │ ├── tsconfig.json │ ├── .env.example │ ├── package.json │ └── README.md ├── pnpm-workspace.yaml ├── .npmrc ├── .github ├── FUNDING.yml ├── _dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── test.yml │ └── npm-publish.yml ├── turbo.json ├── jest.config.js ├── patches └── uuid+8.3.2.patch ├── README.md ├── .gitignore └── package.json /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /apps/ranking/src/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/cli/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /apps/web/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' -------------------------------------------------------------------------------- /apps/sseraresume/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.pem 3 | pk.pem -------------------------------------------------------------------------------- /apps/chrome-ext/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | *.snap -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Sentry 3 | .sentryclirc 4 | -------------------------------------------------------------------------------- /apps/ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | export { TweetComposer } from './TweetComposer'; -------------------------------------------------------------------------------- /apps/sseramemes/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | }; 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | link-workspace-packages=true 3 | prefer-workspace-packages=true -------------------------------------------------------------------------------- /apps/vscode-ext/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /apps/sseramemes/.gitignore: -------------------------------------------------------------------------------- 1 | test.png 2 | raw.mp4 3 | test-video.mp4 4 | test.gif 5 | test-logo.gif 6 | -------------------------------------------------------------------------------- /apps/web/public/sib1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/sib1.png -------------------------------------------------------------------------------- /apps/web/public/sib2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/sib2.png -------------------------------------------------------------------------------- /apps/web/public/sib3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/sib3.png -------------------------------------------------------------------------------- /apps/web/public/sibs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/sibs.png -------------------------------------------------------------------------------- /apps/web/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'all', 3 | singleQuote: true, 4 | }; 5 | -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: sibelius 4 | patreon: sibelius 5 | -------------------------------------------------------------------------------- /apps/vscode-ext/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/vscode-ext/media/icon.png -------------------------------------------------------------------------------- /apps/vscode-ext/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const suffix = '\ncc @sseraphini'; 2 | export const maxTweetLength = 264; 3 | -------------------------------------------------------------------------------- /apps/web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [["styled-components", { "ssr": true }]] 4 | } -------------------------------------------------------------------------------- /apps/chrome-ext/dist/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/chrome-ext/dist/icon-128.png -------------------------------------------------------------------------------- /apps/chrome-ext/dist/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/chrome-ext/dist/icon-16.png -------------------------------------------------------------------------------- /apps/chrome-ext/dist/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/chrome-ext/dist/icon-48.png -------------------------------------------------------------------------------- /apps/sseramemes/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/logo.png -------------------------------------------------------------------------------- /apps/web/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/favicon-16x16.png -------------------------------------------------------------------------------- /apps/web/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/favicon-32x32.png -------------------------------------------------------------------------------- /apps/sseramemes/src/image-scripts/addTextToImage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './addTextToImage'; 3 | -------------------------------------------------------------------------------- /apps/web/public/static/fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/static/fallback.png -------------------------------------------------------------------------------- /apps/cli/src/bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { main: mainFn } = require('../index'); 3 | mainFn(process.argv.slice(2)); 4 | -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/sseramemes/static/logo-opacity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/logo-opacity.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-icon-180.png -------------------------------------------------------------------------------- /apps/web/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | 3 | Allow: / 4 | 5 | Disallow: /404 6 | 7 | Sitemap: https://sseraphini.cc/sitemap.xml 8 | -------------------------------------------------------------------------------- /apps/cli/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['next/babel'], 3 | plugins: [['styled-components', { ssr: true }]], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/sseramemes/src/types.ts: -------------------------------------------------------------------------------- 1 | type memeConditions = boolean[] & { includes?: (value: boolean) => boolean } 2 | 3 | export { memeConditions } -------------------------------------------------------------------------------- /apps/web/jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { enableFetchMocks } from 'jest-fetch-mock'; 3 | enableFetchMocks(); 4 | -------------------------------------------------------------------------------- /apps/web/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/web/public/android-chrome-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/android-chrome-384x384.png -------------------------------------------------------------------------------- /apps/web/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/bot/test/disconnectMongoose.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export const disconnectMongoose = () => mongoose.disconnect(); 4 | -------------------------------------------------------------------------------- /apps/ranking/test/disconnectMongoose.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export const disconnectMongoose = () => mongoose.disconnect(); 4 | -------------------------------------------------------------------------------- /apps/sserartigos/src/score.ts: -------------------------------------------------------------------------------- 1 | export const MIN_POINTS_TO_PUSH = 1; 2 | 3 | export const EMOJIS_POINTS = { 4 | '💯': 1, 5 | '👎': -3, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/public/android-chrome-256x256.png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/android-chrome-256x256.png.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1125-2436.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1125-2436.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1136-640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1136-640.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1170-2532.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1170-2532.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1242-2208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1242-2208.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1242-2688.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1242-2688.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1284-2778.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1284-2778.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1334-750.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1334-750.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1536-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1536-2048.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1620-2160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1620-2160.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1668-2224.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1668-2224.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1668-2388.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1668-2388.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-1792-828.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-1792-828.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-2048-1536.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-2048-1536.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-2048-2732.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-2048-2732.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-2160-1620.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-2160-1620.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-2208-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-2208-1242.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-2224-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-2224-1668.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-2388-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-2388-1668.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-2436-1125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-2436-1125.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-2532-1170.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-2532-1170.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-2688-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-2688-1242.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-2732-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-2732-2048.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-2778-1284.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-2778-1284.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-640-1136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-640-1136.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-750-1334.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-750-1334.png -------------------------------------------------------------------------------- /apps/web/public/icons/apple-splash-828-1792.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/apple-splash-828-1792.png -------------------------------------------------------------------------------- /apps/web/src/types/User.ts: -------------------------------------------------------------------------------- 1 | export type User = { 2 | id: string; 3 | name: string; 4 | username: string; 5 | profile_image_url: string; 6 | }; 7 | -------------------------------------------------------------------------------- /apps/sseraresume/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const urlRegex = /(https?:\/\/[^\s]+)/g; 2 | 3 | export const queuesNames = { 4 | SUMMARY: 'summary', 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web/public/icons/manifest-icon-192.maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/manifest-icon-192.maskable.png -------------------------------------------------------------------------------- /apps/web/public/icons/manifest-icon-512.maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/web/public/icons/manifest-icon-512.maskable.png -------------------------------------------------------------------------------- /apps/sseraresume/.env.example: -------------------------------------------------------------------------------- 1 | DISCORD_BOT_TOKEN= 2 | GUILD_ID= 3 | GENERAL_CHANNEL_ID= 4 | OPEN_SOURCE_CHANNEL_ID= 5 | STARTUPS_CHANNEL_ID= 6 | 7 | -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-Thin.ttf -------------------------------------------------------------------------------- /apps/chrome-ext/.gitignore: -------------------------------------------------------------------------------- 1 | dist/js/* 2 | .vscode 3 | storybook-static/* 4 | 5 | /node_modules 6 | npm-debug.log 7 | yarn-error.log 8 | 9 | .DS_Store 10 | .eslintcache 11 | -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-Black.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-Italic.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-Light.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-Medium.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /apps/chrome-ext/src/content/index.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import { browser } from 'webextension-polyfill-ts'; 3 | 4 | window.onload = () => { 5 | // do something 6 | }; 7 | -------------------------------------------------------------------------------- /apps/sseramemes/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-ExtraBold.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-SemiBold.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/Montserrat-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/Montserrat-VariableFont_wght.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-BlackItalic.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-BoldItalic.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-ExtraLight.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-LightItalic.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-MediumItalic.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-ThinItalic.ttf -------------------------------------------------------------------------------- /apps/vscode-ext/webviews/pages/sidebar.ts: -------------------------------------------------------------------------------- 1 | import App from '../components/Sidebar.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | }); 6 | 7 | export default app; 8 | -------------------------------------------------------------------------------- /apps/web/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Error } from 'components/Error'; 2 | 3 | export default function Custom404() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/bot/src/score.ts: -------------------------------------------------------------------------------- 1 | export const MIN_POINTS_TO_TWEET = 1; 2 | 3 | export const EMOJIS_POINTS = { 4 | '💯': 1, 5 | '👎': -3, 6 | }; 7 | 8 | export const emojiRetweet = '🔁'; // '👁👄👁'; 9 | -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/Montserrat-Italic-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/Montserrat-Italic-VariableFont_wght.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/static/Montserrat-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibelius/ccsseraphini/HEAD/apps/sseramemes/static/Montserrat/static/Montserrat-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /apps/chrome-ext/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "production", 6 | }); 7 | -------------------------------------------------------------------------------- /apps/vscode-ext/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | ] 7 | } -------------------------------------------------------------------------------- /.github/_dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | time: '01:00' 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /apps/vscode-ext/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /apps/vscode-ext/media/main.js: -------------------------------------------------------------------------------- 1 | // This script will be run within the webview itself 2 | // It cannot access the main VS Code APIs directly. 3 | (function () { 4 | const vscode = acquireVsCodeApi(); 5 | })(); 6 | -------------------------------------------------------------------------------- /apps/sseramemes/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/web/src/components/pix/emv.ts: -------------------------------------------------------------------------------- 1 | export const emv = 2 | '00020126580014br.gov.bcb.pix0136c78052c6-43e5-4732-a9ef-90fd615ed6385204000053039865802BR5918Sibelius Seraphini6006Cidade62160512ccsseraphini630452D6'; 3 | -------------------------------------------------------------------------------- /apps/sseramemes/src/image-scripts/addLogoToGif.ts: -------------------------------------------------------------------------------- 1 | import { addLogoToImage } from './index'; 2 | export const addLogoToGif = async (buffer: Buffer): Promise => { 3 | return addLogoToImage(buffer, true); 4 | }; 5 | -------------------------------------------------------------------------------- /apps/sserartigos/src/notification.ts: -------------------------------------------------------------------------------- 1 | import { DiscordMessage } from './types'; 2 | 3 | export function handleError(e: any, notification: DiscordMessage) { 4 | console.error(e); 5 | notification.react('😭'); 6 | } 7 | -------------------------------------------------------------------------------- /apps/vscode-ext/webviews/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "include": ["./**/*", ], 4 | "exclude": ["../node_modules/*"], 5 | "compilerOptions": {"strict": true, } 6 | } -------------------------------------------------------------------------------- /apps/sseramemes/src/image-scripts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './addLogoToImage'; 2 | export * from './resizeImage'; 3 | export * from './addTextToImage'; 4 | const TWITTER_IMAGE_SIZE = 504; 5 | export { TWITTER_IMAGE_SIZE }; 6 | -------------------------------------------------------------------------------- /apps/sseramemes/src/score.ts: -------------------------------------------------------------------------------- 1 | export const MIN_POINTS_TO_TWEET_MEME = 2; 2 | 3 | export const EMOJIS_POINTS = { 4 | '💯': 1, 5 | '👎': -5, 6 | }; 7 | 8 | export const RETWEET_MEME_TIMEOUT = 1000 * 60 * 60; // 1 hour 9 | -------------------------------------------------------------------------------- /apps/web/src/components/pix/PixQrCode.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import QRCode from 'qrcode.react'; 3 | import { emv } from './emv'; 4 | 5 | export const PixQrCode = () => { 6 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /apps/web/src/getHttpProtocol.tsx: -------------------------------------------------------------------------------- 1 | export function getHttpProtocol(host: string) { 2 | const httpProtocol = ['localhost', ':3000'].some((t) => host.includes(t)) 3 | ? 'http' 4 | : 'https'; 5 | return httpProtocol; 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/bot/jest-mongodb-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongodbMemoryServerOptions: { 3 | binary: { 4 | version: '5.0.4', 5 | skipMD5: true, 6 | }, 7 | autoStart: false, 8 | instance: {}, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/chrome-ext/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "development", 6 | devtool: "inline-source-map", 7 | }); 8 | -------------------------------------------------------------------------------- /apps/ranking/jest-mongodb-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongodbMemoryServerOptions: { 3 | binary: { 4 | version: '5.0.4', 5 | skipMD5: true, 6 | }, 7 | autoStart: false, 8 | instance: {}, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/web/src/components/home/cryptoAddress.tsx: -------------------------------------------------------------------------------- 1 | export const ccsseraphiniAddressEthereum = 2 | '0xb9A5d5BA75339B1fF4cCdAefa0e3da1543A65d52'; 3 | 4 | export const ccsseraphiniAddressSolana = 5 | '24gVn4dinDmqHeCmXHhySet3swxMc3Jup7iYSFTbgztm'; 6 | -------------------------------------------------------------------------------- /apps/bot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": ["es6", "es2015", "dom"], 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/sseraresume/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "outDir": "build" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/sserartigos/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "outDir": "build" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@ccsseraphini/ui", 4 | "version": "0.0.1", 5 | "main": "./src/index.tsx", 6 | "module": "./src/index.tsx", 7 | "scripts": { 8 | "check-types": "tsc --noEmit" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/src/modules/twitter/getTwitterAuthorization.ts: -------------------------------------------------------------------------------- 1 | export const getTwitterAuthorization = (authorization: string) => { 2 | if (authorization.includes('Bearer')) { 3 | return authorization; 4 | } 5 | 6 | return `Bearer ${authorization}`; 7 | }; 8 | -------------------------------------------------------------------------------- /apps/vscode-ext/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "eamodio.tsl-problem-matcher" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | branch="$(git rev-parse --abbrev-ref HEAD)" 4 | 5 | if [ "$branch" = "main" ]; then 6 | echo "You can't commit directly to main branch" 7 | exit 1 8 | fi 9 | 10 | . "$(dirname "$0")/_/husky.sh" 11 | 12 | npx lint-staged 13 | -------------------------------------------------------------------------------- /apps/chrome-ext/dist/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cc @sseraphini 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /apps/chrome-ext/src/backgroundPage.ts: -------------------------------------------------------------------------------- 1 | import { browser } from 'webextension-polyfill-ts'; 2 | 3 | // TODO - check if we need some background work 4 | chrome.runtime.onInstalled.addListener(function () { 5 | chrome.tabs.create({ url: 'https://google.com' }); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /apps/web/src/usePrevious.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export const usePrevious = (value: any) => { 4 | const ref = useRef(); 5 | 6 | useEffect(() => { 7 | ref.current = value; 8 | }); 9 | 10 | return ref.current; 11 | }; 12 | -------------------------------------------------------------------------------- /apps/sseramemes/src/removeData.ts: -------------------------------------------------------------------------------- 1 | import { removeAltText, removeMemeCommands } from './image-scripts'; 2 | 3 | export const removeMetadata = (message: string) => { 4 | message = removeMemeCommands(message); 5 | message = removeAltText(message); 6 | return message; 7 | }; 8 | -------------------------------------------------------------------------------- /apps/vscode-ext/webviews/global.d.ts: -------------------------------------------------------------------------------- 1 | import * as _vscode from 'vscode'; 2 | 3 | declare global { 4 | const tsvscode: { 5 | postMessage: ({ type: string, value: any }) => void; 6 | getState: () => any; 7 | setState: (state: any) => any; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /apps/bot/test/clearDbAndRestartCounters.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export async function clearDatabase() { 4 | await mongoose.connection.db.dropDatabase(); 5 | } 6 | 7 | export async function clearDbAndRestartCounters() { 8 | await clearDatabase(); 9 | } 10 | -------------------------------------------------------------------------------- /apps/ranking/test/clearDbAndRestartCounters.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export async function clearDatabase() { 4 | await mongoose.connection.db.dropDatabase(); 5 | } 6 | 7 | export async function clearDbAndRestartCounters() { 8 | await clearDatabase(); 9 | } 10 | -------------------------------------------------------------------------------- /apps/ranking/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es6", 7 | "es2015", 8 | "dom" 9 | ], 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | } 13 | } -------------------------------------------------------------------------------- /apps/ranking/src/getTopTweets.ts: -------------------------------------------------------------------------------- 1 | import { RankedTweet } from './types/index'; 2 | 3 | const getTopTweets = (rankedTweets: RankedTweet[], size: number = 5) => 4 | rankedTweets 5 | .sort((a: RankedTweet, b: RankedTweet) => b.score - a.score) 6 | .slice(0, size); 7 | 8 | export default getTopTweets; 9 | -------------------------------------------------------------------------------- /apps/web/src/modules/score/getEmptyScore.ts: -------------------------------------------------------------------------------- 1 | import { UserScore } from 'types/Score'; 2 | 3 | export function getEmptyScore(): UserScore { 4 | return { 5 | tweet_count: 0, 6 | retweet_count: 0, 7 | reply_count: 0, 8 | like_count: 0, 9 | quote_count: 0, 10 | total: 0, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /apps/chrome-ext/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"], 5 | "options": { 6 | "parser": "json" 7 | } 8 | } 9 | ], 10 | "singleQuote": true, 11 | "trailingComma": "none", 12 | "arrowParens": "avoid" 13 | } 14 | -------------------------------------------------------------------------------- /apps/sseramemes/.env.example: -------------------------------------------------------------------------------- 1 | DISCORD_TOKEN= 2 | DISCORD_MEMES_CHANNEL_ID= 3 | TWITTER_ACCESS_TOKEN= 4 | TWITTER_ACCESS_TOKEN_SECRET= 5 | TWITTER_API_KEY= 6 | TWITTER_API_KEY_SECRET= 7 | INSTAGRAM_ACCOUNT_ID= 8 | INSTAGRAM_ACCESS_TOKEN= 9 | AWS_REGION= 10 | AWS_ACCESS_KEY_ID= 11 | AWS_SECRET_ACCESS_KEY= 12 | AWS_BUCKET_NAME= -------------------------------------------------------------------------------- /apps/vscode-ext/src/getNonce.ts: -------------------------------------------------------------------------------- 1 | export function getNonce() { 2 | let text = ''; 3 | const possible = 4 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 5 | for (let i = 0; i < 32; i++) { 6 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 7 | } 8 | return text; 9 | } 10 | -------------------------------------------------------------------------------- /apps/bot/scripts/isMainScript.ts: -------------------------------------------------------------------------------- 1 | export const isMainScript = (require, module) => { 2 | if (!module.parent) { 3 | return true; 4 | } 5 | 6 | if (process.env.DEBUG === 'true') { 7 | // eslint-disable-next-line 8 | console.log('not main script, check your script code'); 9 | } 10 | 11 | return false; 12 | }; 13 | -------------------------------------------------------------------------------- /apps/bot/src/debugConsole.ts: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | 3 | export const debugConsole = (obj: Record) => { 4 | // eslint-disable-next-line 5 | console.log( 6 | util.inspect(obj, { 7 | showHidden: false, 8 | depth: null, 9 | colors: true, 10 | showProxy: false, 11 | }), 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/src/components/Head.tsx: -------------------------------------------------------------------------------- 1 | import NextHead from 'next/head'; 2 | 3 | export const Head = () => { 4 | return ( 5 | 6 | cc @sseraphini 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /apps/web/src/pages/_offline.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | const Offline = () => ( 4 | <> 5 | 6 | cc @sseraphini 7 | 8 |

This is offline fallback page

9 |

When offline, any page route will fallback to this page

10 | 11 | ); 12 | 13 | export default Offline; 14 | -------------------------------------------------------------------------------- /apps/web/src/components/ColorPalette.tsx: -------------------------------------------------------------------------------- 1 | export const txtPalette = { 2 | primary: '#88b2fe', 3 | secondary: '#718096', 4 | base: '#fff', 5 | baseVar: '#000', 6 | }; 7 | 8 | export const bgPalette = { 9 | primary: '#88b2fe', 10 | secondary: '#1E1E1E', 11 | secondaryVar: '#121212', 12 | base: '#000', 13 | details: '#383838', 14 | }; 15 | -------------------------------------------------------------------------------- /apps/bot/jest.config.js: -------------------------------------------------------------------------------- 1 | const customJestConfig = { 2 | preset: '@shelf/jest-mongodb', 3 | moduleFileExtensions: ['js', 'ts'], 4 | transformIgnorePatterns: ['/node_modules/'], 5 | transform: { 6 | '^.+\\.(setupFiles.js|ts)?$': 'ts-jest', 7 | }, 8 | setupFiles: ['/setupFiles.js'], 9 | }; 10 | 11 | module.exports = customJestConfig; 12 | -------------------------------------------------------------------------------- /apps/bot/src/discord/index.ts: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch'; 2 | import { createServer } from 'http'; 3 | import app from './app'; 4 | import { config } from '../config'; 5 | 6 | (async () => { 7 | const server = createServer(app.callback()); 8 | 9 | server.listen(config.PORT, () => { 10 | console.log(`server running at http://localhost:${config.PORT}`); 11 | }); 12 | })(); 13 | -------------------------------------------------------------------------------- /apps/sseraresume/src/github/client.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/rest'; 2 | import { config } from '../config'; 3 | 4 | export const ocktokit = new Octokit({ auth: config.GITHUB_TOKEN }); 5 | 6 | export const gitRepoInfo = { 7 | owner: config.GIT_REPO_OWNER, 8 | repo: config.GIT_REPO_NAME, 9 | branch: config.GIT_REPO_BRANCH, 10 | path: config.ZETTELKASTEN_FILE_PATH, 11 | }; 12 | -------------------------------------------------------------------------------- /apps/sserartigos/.env.example: -------------------------------------------------------------------------------- 1 | DISCORD_BOT_TOKEN= 2 | LISTENED_USERS_ID=,, 3 | GITHUB_TOKEN= 4 | GIT_REPO_OWNER= 5 | GIT_REPO_NAME= 6 | GIT_REPO_BRANCH= 7 | ZETTELKASTEN_FILE_PATH= 8 | FANSFY_API_KEY= 9 | 10 | TWITTER_API_KEY= 11 | TWITTER_API_KEY_SECRET= 12 | TWITTER_ACCESS_TOKEN= 13 | TWITTER_ACCESS_TOKEN_SECRET= -------------------------------------------------------------------------------- /apps/sserartigos/src/github/client.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/rest'; 2 | import { config } from '../config'; 3 | 4 | export const ocktokit = new Octokit({ auth: config.GITHUB_TOKEN }); 5 | 6 | export const gitRepoInfo = { 7 | owner: config.GIT_REPO_OWNER, 8 | repo: config.GIT_REPO_NAME, 9 | branch: config.GIT_REPO_BRANCH, 10 | path: config.ZETTELKASTEN_FILE_PATH, 11 | }; 12 | -------------------------------------------------------------------------------- /apps/chrome-ext/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-react', 5 | { 6 | runtime: 'automatic', 7 | }, 8 | ], 9 | '@babel/preset-typescript', 10 | [ 11 | '@babel/preset-env', 12 | { 13 | corejs: 3, 14 | modules: false, 15 | useBuiltIns: 'usage', 16 | }, 17 | ], 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /apps/chrome-ext/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'], 3 | moduleNameMapper: { 4 | '\\.(css|less|scss|sss|styl)$': '/node_modules/jest-css-modules' 5 | }, 6 | roots: ['/src'], 7 | testPathIgnorePatterns: ['/node_modules/', 'stories.tsx'], 8 | transform: { 9 | '\\.tsx?$': 'ts-jest' 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /apps/cli/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | commonjs: true, 5 | es2021: true, 6 | jest: true, 7 | }, 8 | extends: ['eslint:recommended'], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaVersion: 'latest', 12 | }, 13 | plugins: ['@typescript-eslint'], 14 | ignorePatterns: ['*.d.ts', '../cli/lib/**'], 15 | rules: {}, 16 | }; 17 | -------------------------------------------------------------------------------- /apps/ranking/jest.config.js: -------------------------------------------------------------------------------- 1 | const customJestConfig = { 2 | preset: '@shelf/jest-mongodb', 3 | moduleFileExtensions: ['js', 'ts'], 4 | transformIgnorePatterns: ['/node_modules/'], 5 | transform: { 6 | '^.+\\.(setupFiles.js|ts)?$': 'ts-jest', 7 | }, 8 | setupFiles: ['/setupFiles.js'], 9 | watchPathIgnorePatterns: ['globalConfig'], 10 | }; 11 | 12 | module.exports = customJestConfig; 13 | -------------------------------------------------------------------------------- /apps/vscode-ext/media/reset.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | font-size: 13px; 4 | } 5 | 6 | *, 7 | *:before, 8 | *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | body, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | ol, 21 | ul { 22 | margin: 0; 23 | padding: 0; 24 | font-weight: normal; 25 | } 26 | 27 | img { 28 | max-width: 100%; 29 | height: auto; 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/src/components/home/ScoreButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@chakra-ui/react'; 2 | import { FaMedal } from 'react-icons/fa'; 3 | 4 | export const ScoreButton = () => ( 5 | 16 | ); 17 | -------------------------------------------------------------------------------- /apps/bot/src/readyMessage.ts: -------------------------------------------------------------------------------- 1 | import { EMOJIS_POINTS, MIN_POINTS_TO_TWEET } from './score'; 2 | 3 | const pointsText = Object.entries(EMOJIS_POINTS) 4 | .map(([emoji, point]) => `${emoji} vale ${point} pontos`, '') 5 | .join(' '); 6 | 7 | export const readyMessage = [ 8 | `O bot ta on 🚀 Como funciona?`, 9 | `Votem no tweets. Se a mensagem tiver ${MIN_POINTS_TO_TWEET} pontos, o RT é feito. ${pointsText}.`, 10 | ].join('\n'); 11 | -------------------------------------------------------------------------------- /apps/ranking/src/deleteRankedTweetsById.ts: -------------------------------------------------------------------------------- 1 | import { RankedTweetModel } from './schema/RankedTweet'; 2 | 3 | const deleteRankedTweetsById = async (tweetIds: string[]): Promise => { 4 | try { 5 | await RankedTweetModel.deleteMany({ tweet_id: { $in: tweetIds } }); 6 | } catch (error) { 7 | console.error('Fail to delete ranked tweets', error, { tweetIds }); 8 | } 9 | }; 10 | 11 | export default deleteRankedTweetsById; 12 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseBranch": "origin/main", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": [ 6 | "^build" 7 | ] 8 | }, 9 | "test": { 10 | "dependsOn": [ 11 | "^build" 12 | ] 13 | }, 14 | "lint": { 15 | "outputs": [] 16 | }, 17 | "check-types": { 18 | "cache": false 19 | }, 20 | "dev": { 21 | "cache": false 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/chrome-ext/src/popup/index.tsx: -------------------------------------------------------------------------------- 1 | // import 'isomorphic-fetch'; 2 | import 'core-js/stable'; 3 | import 'regenerator-runtime/runtime'; 4 | 5 | import * as React from 'react'; 6 | import * as ReactDOM from 'react-dom'; 7 | import { browser } from 'webextension-polyfill-ts'; 8 | import App from './App'; 9 | 10 | browser.tabs.query({ active: true, currentWindow: true }).then(() => { 11 | ReactDOM.render(, document.getElementById('popup')); 12 | }); 13 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: auto-merge 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | auto-merge: 10 | runs-on: ubuntu-latest 11 | if: github.actor == 'dependabot[bot]' 12 | steps: 13 | - uses: ahmadnassri/action-dependabot-auto-merge@v2.4 14 | with: 15 | github-token: ${{ secrets.AUTOMERGE_TOKEN }} 16 | command: "squash and merge" 17 | target: minor 18 | -------------------------------------------------------------------------------- /apps/chrome-ext/dist/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "ccsseraphini", 4 | "description": "cc @sseraphini chrome extension, to make it easy learn in public", 5 | "version": "1.0.0", 6 | "action": { 7 | "default_icon": "icon-16.png", 8 | "default_popup": "popup.html" 9 | }, 10 | "icons": { 11 | "16": "icon-16.png", 12 | "48": "icon-48.png", 13 | "128": "icon-128.png" 14 | }, 15 | "permissions": [] 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'next/core-web-vitals', 4 | 'plugin:@typescript-eslint/eslint-recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | ], 7 | env: { 8 | jest: true, 9 | }, 10 | rules: { 11 | '@typescript-eslint/no-unused-vars': [ 12 | 'error', 13 | { 14 | ignoreRestSiblings: true, 15 | }, 16 | ], 17 | '@typescript-eslint/ban-ts-comment': 1, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /apps/bot/.env.example: -------------------------------------------------------------------------------- 1 | DISCORD_TOKEN= 2 | DISCORD_CLIENT_ID= 3 | DISCORD_CLIENT_SECRET= 4 | DISCORD_BOT_TOKEN= 5 | DISCORD_BOT_CHANNEL_ID= 6 | DISCORD_GENERAL_CHANNEL_ID= 7 | 8 | TWITTER_BEARER_TOKEN= 9 | TWITTER_ACCESS_TOKEN= 10 | TWITTER_ACCESS_TOKEN_SECRET= 11 | TWITTER_API_KEY= 12 | TWITTER_API_KEY_SECRET= 13 | 14 | TWITTER_RANKING_ACCESS_TOKEN= 15 | TWITTER_RANKING_ACCESS_TOKEN_SECRET= 16 | TWITTER_RANKING_API_KEY= 17 | TWITTER_RANKING_API_KEY_SECRET= 18 | 19 | MONGO_URI= -------------------------------------------------------------------------------- /apps/chrome-ext/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'next/core-web-vitals', 4 | 'plugin:@typescript-eslint/eslint-recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | ], 7 | env: { 8 | jest: true, 9 | }, 10 | rules: { 11 | '@typescript-eslint/no-unused-vars': [ 12 | 'error', 13 | { 14 | ignoreRestSiblings: true, 15 | }, 16 | ], 17 | '@typescript-eslint/ban-ts-comment': 1, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /apps/sseramemes/src/image-scripts/resizeImage.ts: -------------------------------------------------------------------------------- 1 | import { TWITTER_IMAGE_SIZE } from './index'; 2 | import sharp from 'sharp'; 3 | 4 | export const resizeImage = async (buffer: Buffer): Promise => { 5 | const { width, height } = await sharp(buffer).metadata(); 6 | const i = sharp(buffer); 7 | if (height > TWITTER_IMAGE_SIZE) i.resize(null, TWITTER_IMAGE_SIZE); 8 | if (width > TWITTER_IMAGE_SIZE) i.resize(TWITTER_IMAGE_SIZE, null); 9 | return await i.toBuffer(); 10 | }; 11 | -------------------------------------------------------------------------------- /apps/sserartigos/src/fansfy/postArticle.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../config'; 2 | import { ENDPOINT_URL } from './index'; 3 | import 'isomorphic-fetch'; 4 | 5 | export const postArticle = (articleUrl: string) => { 6 | const payload = { apiKey: config.FANSFY_API_KEY, url: articleUrl }; 7 | 8 | return fetch(ENDPOINT_URL, { 9 | method: 'POST', 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | body: JSON.stringify(payload), 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/ranking/src/getTweetIdChunks.ts: -------------------------------------------------------------------------------- 1 | import { TweetWithId } from './types/index'; 2 | 3 | const getTweetIdChunks = (tweets: TweetWithId[]): string[][] => { 4 | return tweets.reduce((acc: string[][], { tweet_id }, index) => { 5 | const chunkIndex = Math.floor(index / 100); 6 | 7 | if (!acc[chunkIndex]) { 8 | acc[chunkIndex] = []; 9 | } 10 | 11 | acc[chunkIndex].push(tweet_id); 12 | 13 | return acc; 14 | }, []); 15 | }; 16 | 17 | export default getTweetIdChunks; 18 | -------------------------------------------------------------------------------- /apps/bot/src/mongodb.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { config } from './config'; 3 | 4 | export default async function connectDB(): Promise { 5 | if (!config.MONGO_URI) { 6 | console.warn('MONGO_URI is not defined'); 7 | return; 8 | } 9 | 10 | if (mongoose.connections[0].readyState) { 11 | return; 12 | } 13 | 14 | mongoose.set('strictQuery', false); 15 | await mongoose.connect(config.MONGO_URI); 16 | console.log('Connected to MongoDB'); 17 | } 18 | -------------------------------------------------------------------------------- /apps/ranking/src/__mocks__/createFakeTweetBatch.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryTweet, TweetBatch } from '../types/index'; 2 | import createFakeTweetData from './createFakeTweetData'; 3 | 4 | const createFakeTweetBatch = ( 5 | temporaryTweets: TemporaryTweet[], 6 | ): TweetBatch => { 7 | const fakeBatch: TweetBatch = { 8 | data: createFakeTweetData(temporaryTweets), 9 | errors: [], 10 | includes: {}, 11 | }; 12 | 13 | return fakeBatch; 14 | }; 15 | 16 | export default createFakeTweetBatch; 17 | -------------------------------------------------------------------------------- /apps/sserartigos/src/pollHandler.ts: -------------------------------------------------------------------------------- 1 | import { checkVotingAbility } from './common/utils/utils'; 2 | import { DiscordMessage } from './types'; 3 | 4 | /** 5 | * Messages that already have a poll. 6 | */ 7 | const messagesWithPoll = []; 8 | 9 | export const createPoll = (message: DiscordMessage) => { 10 | if (messagesWithPoll.includes(message.id) || !checkVotingAbility(message)) 11 | return; 12 | 13 | messagesWithPoll.push(message.id); 14 | message.react('💯'); 15 | message.react('👎'); 16 | }; 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: "16" 21 | - run: yarn 22 | - run: pnpm lint 23 | - run: pnpm test 24 | - run: pnpm check-types 25 | -------------------------------------------------------------------------------- /apps/sseraresume/src/github/services/getFile.ts: -------------------------------------------------------------------------------- 1 | import { GitHubResponseData, GitRepoInfo } from '../../types'; 2 | import { ocktokit } from '../client'; 3 | 4 | export const getFile = async ( 5 | repoInfo: GitRepoInfo, 6 | ): Promise => { 7 | const { data } = await ocktokit.repos.getContent({ 8 | owner: repoInfo.owner, 9 | repo: repoInfo.repo, 10 | ref: repoInfo.branch, 11 | path: repoInfo.path, 12 | }); 13 | 14 | return data as unknown as GitHubResponseData; 15 | }; 16 | -------------------------------------------------------------------------------- /apps/sserartigos/src/github/services/getFile.ts: -------------------------------------------------------------------------------- 1 | import { GitHubResponseData, GitRepoInfo } from '../../types'; 2 | import { ocktokit } from '../client'; 3 | 4 | export const getFile = async ( 5 | repoInfo: GitRepoInfo, 6 | ): Promise => { 7 | const { data } = await ocktokit.repos.getContent({ 8 | owner: repoInfo.owner, 9 | repo: repoInfo.repo, 10 | ref: repoInfo.branch, 11 | path: repoInfo.path, 12 | }); 13 | 14 | return data as unknown as GitHubResponseData; 15 | }; 16 | -------------------------------------------------------------------------------- /apps/vscode-ext/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /apps/ranking/src/schema/TemporaryTweet.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { model, Schema } from 'mongoose'; 2 | import { TemporaryTweet, TemporaryType } from '../types'; 3 | 4 | const temporaryTweetSchema = new Schema({ 5 | tweet_id: { 6 | type: String, 7 | required: true, 8 | }, 9 | created_at: { 10 | type: Date, 11 | required: true, 12 | }, 13 | }); 14 | 15 | export const TemporaryTweetModel = 16 | (mongoose.models.tweet as TemporaryType) || 17 | model('tweet', temporaryTweetSchema); 18 | -------------------------------------------------------------------------------- /apps/sseramemes/static/Montserrat/fonts.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | apps/sseramemes/static/Montserrat/Montserrat-Italic-VariableFont_wght.ttf 5 | apps/sseramemes/static/Montserrat/Montserrat-VariableFont_wght.ttf 6 | apps/sseramemes/static/Montserrat/static 7 | 8 | Montserrat 9 | 10 | Montserrat 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/web/src/components/ChakraNextLinkButton.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { Button, ButtonProps } from '@chakra-ui/react'; 3 | import { ReactNode } from 'react'; 4 | 5 | type Props = { 6 | href: string; 7 | children: ReactNode; 8 | } & ButtonProps; 9 | export const ChakraNextLinkButton = ({ href, children, ...props }: Props) => { 10 | return ( 11 | 12 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/sseramemes/scripts/testAddLogoToGif.ts: -------------------------------------------------------------------------------- 1 | import { addLogoToGif } from '../src/image-scripts/addLogoToGif'; 2 | import { GifUtil } from 'gifwrap'; 3 | import * as fs from 'fs'; 4 | 5 | const testAddLogoToGif = async () => { 6 | const gif = await GifUtil.read('test.gif'); 7 | const gifWithWatermark = await addLogoToGif(gif.buffer); 8 | 9 | /** 10 | * Save bufferWithWatermark to file. 11 | */ 12 | await fs.promises.writeFile('test-logo.gif', gifWithWatermark); 13 | }; 14 | 15 | testAddLogoToGif().then(); 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ 3 | '/apps/chrome-ext/jest.config.js', 4 | '/apps/cli/jest.config.js', 5 | '/apps/web/jest.config.js', 6 | '/apps/sseramemes/jest.config.js', 7 | '/apps/ranking/jest.config.js', 8 | '/apps/bot/jest.config.js', 9 | ], 10 | transformIgnorePatterns: ['node_modules/(?!use-debounce|uuid)'], 11 | coverageProvider: 'v8', 12 | collectCoverage: true, 13 | coverageDirectory: 'coverage', 14 | }; 15 | -------------------------------------------------------------------------------- /apps/sseraresume/src/common/utils/appendLinesToFile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * gets the file content and returns a base64 string with the appended lines 3 | */ 4 | export const appendLinesToFile = ( 5 | oldFileContent: string, 6 | contentToAppend: string[], 7 | ) => { 8 | const fileBuffer = atob(oldFileContent); 9 | 10 | const newFileBuffer = contentToAppend.reduce((acc, currentValue) => { 11 | return acc + '\n' + currentValue; 12 | }, fileBuffer); 13 | 14 | return Buffer.from(newFileBuffer).toString('base64'); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/sserartigos/src/common/utils/appendLinesToFile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * gets the file content and returns a base64 string with the appended lines 3 | */ 4 | export const appendLinesToFile = ( 5 | oldFileContent: string, 6 | contentToAppend: string[], 7 | ) => { 8 | const fileBuffer = atob(oldFileContent); 9 | 10 | const newFileBuffer = contentToAppend.reduce((acc, currentValue) => { 11 | return acc + '\n' + currentValue; 12 | }, fileBuffer); 13 | 14 | return Buffer.from(newFileBuffer).toString('base64'); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/src/middlewares/validations/__tests__/validationsPost.test.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import flood from 'pages/api/discord/flood'; 3 | 4 | test('Should validate if not post', async () => { 5 | const req = { method: 'GET' } as NextApiRequest; 6 | 7 | const res = { 8 | status: jest.fn(() => res), 9 | send: jest.fn(), 10 | } as Partial as NextApiResponse; 11 | 12 | await flood(req, res); 13 | 14 | expect(res.status).toBeCalledWith(405); 15 | }); 16 | -------------------------------------------------------------------------------- /apps/sseramemes/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * More details about ecosystem file: 3 | * https://pm2.keymetrics.io/docs/usage/application-declaration/ 4 | */ 5 | module.exports = { 6 | apps: [ 7 | { 8 | name: 'sseramemes', 9 | script: 'npm', 10 | args: 'start', 11 | ignore_watch: ['temp*'], 12 | /** 13 | * Add time to the logs 14 | */ 15 | time: true, 16 | /** 17 | * Watch changes to "deploy" the bot 18 | */ 19 | watch: true, 20 | }, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /apps/vscode-ext/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/ranking/src/getTweetScore.ts: -------------------------------------------------------------------------------- 1 | import { TweetData } from './types/index'; 2 | 3 | const getTweetScore = (tweet: Partial): number => { 4 | const { 5 | public_metrics: { retweet_count, reply_count, like_count, quote_count }, 6 | } = tweet as Required; 7 | 8 | const tweet_value = 1; 9 | const score = 10 | tweet_value + 11 | (retweet_count ?? 0) + 12 | (reply_count ?? 0) + 13 | (like_count ?? 0) + 14 | (quote_count ?? 0); 15 | 16 | return score; 17 | }; 18 | 19 | export default getTweetScore; 20 | -------------------------------------------------------------------------------- /apps/sseramemes/src/readyMessage.ts: -------------------------------------------------------------------------------- 1 | import { EMOJIS_POINTS, MIN_POINTS_TO_TWEET_MEME } from './score'; 2 | 3 | const pointsText = Object.entries(EMOJIS_POINTS) 4 | .map(([emoji, point]) => `${emoji} vale ${point} pontos`, '') 5 | .join(' '); 6 | 7 | export const readyMessage = [ 8 | `O bot ta on 🚀 Como funciona?`, 9 | `Mandem os memes e votem! Se a mensagem tiver ${MIN_POINTS_TO_TWEET_MEME} pontos, o tuíte é feito. ${pointsText}.`, 10 | 'Memes enviados antes desta mensagem não são consideradas (deploy a cada 5 minutos).', 11 | ].join('\n'); 12 | -------------------------------------------------------------------------------- /apps/web/src/types/Ranking.ts: -------------------------------------------------------------------------------- 1 | export interface UserRanking { 2 | _id: string; 3 | name: string; 4 | username: string; 5 | profileImageUrl: string; 6 | likes: number; 7 | retweets: number; 8 | tweets: number; 9 | quotes: number; 10 | replies: number; 11 | score: number; 12 | lastTweetRanked: Date; 13 | } 14 | 15 | export type TwitterUser = { 16 | id: string; 17 | profile_image_url: string; 18 | username: string; 19 | name: string; 20 | }; 21 | 22 | export type TwitterResponse = { 23 | data: TwitterUser[]; 24 | } | null; 25 | -------------------------------------------------------------------------------- /apps/bot/src/discord/oauth.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../config'; 2 | 3 | export const DISCORD_REDIRECT_URI = `${config.APP_URI}/api/discord/oauth`; 4 | 5 | export const scope = [ 6 | 'identify', 7 | 'guilds.join', // join user to a guild 8 | ].join(' '); 9 | 10 | export const OAUTH_QS = new URLSearchParams({ 11 | client_id: config.DISCORD_CLIENT_ID, 12 | redirect_uri: DISCORD_REDIRECT_URI, 13 | response_type: 'code', 14 | scope, 15 | }).toString(); 16 | 17 | export const OAUTH_URI = `https://discord.com/api/oauth2/authorize?${OAUTH_QS}`; 18 | -------------------------------------------------------------------------------- /apps/bot/src/tweetRanking/jobs/index.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from 'luxon'; 2 | 3 | import { config } from '../../config'; 4 | import rankingJob from './rankingJob'; 5 | import syncRankedTweetJob from './syncRankedTweetJob'; 6 | 7 | const startJobs = () => { 8 | if (!config.MONGO_URI) { 9 | console.warn('MONGO_URI is not defined'); 10 | return; 11 | } 12 | 13 | Settings.defaultZone = 'America/Sao_Paulo'; 14 | Settings.defaultLocale = 'pt-BR'; 15 | 16 | rankingJob(); 17 | syncRankedTweetJob(); 18 | }; 19 | 20 | export default startJobs; 21 | -------------------------------------------------------------------------------- /apps/web/src/middlewares/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | const withErrorHandler = 4 | (fn: NextApiHandler) => async (req: NextApiRequest, res: NextApiResponse) => { 5 | try { 6 | return await fn(req, res); 7 | } catch (err: any) { 8 | const statusCode = err.statusCode || 500; 9 | const message = err.message || 'Oops, something went wrong!'; 10 | res.status(statusCode).json({ statusCode, message }); 11 | } 12 | }; 13 | 14 | export default withErrorHandler; 15 | -------------------------------------------------------------------------------- /apps/web/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | import { ChakraProvider } from '@chakra-ui/react'; 3 | import { SessionProvider } from 'next-auth/react'; 4 | import { Head } from 'components/Head'; 5 | 6 | function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default MyApp; 18 | -------------------------------------------------------------------------------- /apps/bot/scripts/discord/discordOAuth.ts: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch'; 2 | import open from 'open'; 3 | import { isMainScript } from '../isMainScript'; 4 | import { OAUTH_URI } from '../../src/discord/oauth'; 5 | 6 | const run = async () => { 7 | await open(OAUTH_URI); 8 | }; 9 | 10 | (async () => { 11 | if (!isMainScript(require, module)) { 12 | return; 13 | } 14 | try { 15 | await run(); 16 | } catch (err) { 17 | // eslint-disable-next-line 18 | console.log('err: ', err); 19 | process.exit(1); 20 | } 21 | process.exit(0); 22 | })(); 23 | -------------------------------------------------------------------------------- /apps/sserartigos/src/fansfy/index.ts: -------------------------------------------------------------------------------- 1 | import { postArticle } from './postArticle'; 2 | 3 | export const ENDPOINT_URL = 'https://api.fansfy.io/v1/publish'; 4 | 5 | export const postAllArticles = async (links: string[]) => { 6 | const promisses = links.map((link) => postArticle(link)); 7 | 8 | await Promise.all(promisses).then((responses) => { 9 | responses.forEach(verifyIfHasError); 10 | }); 11 | 12 | return; 13 | }; 14 | 15 | function verifyIfHasError(res: Response) { 16 | if (!res.ok) throw new Error(`response from API was: ${res.status}`); 17 | } 18 | -------------------------------------------------------------------------------- /apps/bot/src/handleRT.ts: -------------------------------------------------------------------------------- 1 | import { TwitterApi, TweetV1 } from 'twitter-api-v2'; 2 | import { config } from './config'; 3 | 4 | const client = new TwitterApi({ 5 | appKey: config.TWITTER_API_KEY, 6 | appSecret: config.TWITTER_API_KEY_SECRET, 7 | accessToken: config.TWITTER_ACCESS_TOKEN, 8 | accessSecret: config.TWITTER_ACCESS_TOKEN_SECRET, 9 | }); 10 | 11 | export const handleRT = async (id: string) => { 12 | try { 13 | await client.v1.post(`statuses/retweet/${id}.json`); 14 | } catch (err) { 15 | console.log('handleRT err: ', err); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /apps/sseramemes/src/addMetadata.ts: -------------------------------------------------------------------------------- 1 | import { TwitterApi } from 'twitter-api-v2'; 2 | 3 | type AddMetadataParams = { 4 | mediaId: string; 5 | alt: string; 6 | mimeType: string; 7 | }; 8 | 9 | export const addMetadata = async ( 10 | client: TwitterApi, 11 | params: AddMetadataParams, 12 | ) => { 13 | const { mediaId, alt, mimeType } = params; 14 | 15 | if (mimeType.includes('image') && alt?.trim().length > 0) { 16 | await client.v1.createMediaMetadata(mediaId, { 17 | alt_text: { 18 | text: alt, 19 | }, 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /apps/web/src/components/__tests__/ForkMeCorner.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { ForkMe } from 'fork-me-corner'; 3 | 4 | describe('ForkMeCorner', () => { 5 | it('should render the component properly', () => { 6 | const repo = 'https://github.com/sibelius/ccsseraphini'; 7 | render(); 8 | 9 | expect( 10 | screen.getByRole('link', { name: 'View source on GitHub' }), 11 | ).toBeInTheDocument(); 12 | expect(screen.getByRole('link').href).toBe(repo); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /apps/web/src/modules/twitter/__mocks__/twitterUserGet.ts: -------------------------------------------------------------------------------- 1 | const userData = { 2 | data: { 3 | id: '1070750548608147456', 4 | name: 'Pablo Jonatan', 5 | username: 'pjonatansr', 6 | }, 7 | }; 8 | 9 | export const userProfile = (username: string, accessToken: string) => { 10 | return new Promise((res) => { 11 | process.nextTick(() => { 12 | if (!accessToken) { 13 | throw '401'; 14 | } 15 | 16 | if (username !== 'pjonatansr') { 17 | res({ data: {} }); 18 | } 19 | 20 | res(userData); 21 | }); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /patches/uuid+8.3.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/uuid/package.json b/node_modules/uuid/package.json 2 | index 4a3cd3c..d1a37ad 100644 3 | --- a/node_modules/uuid/package.json 4 | +++ b/node_modules/uuid/package.json 5 | @@ -25,6 +25,10 @@ 6 | "require": "./dist/index.js", 7 | "import": "./wrapper.mjs" 8 | }, 9 | + "browser": { 10 | + "import": "./dist/esm-browser/index.js", 11 | + "require": "./dist/index.js" 12 | + }, 13 | "default": "./dist/esm-browser/index.js" 14 | }, 15 | "./package.json": "./package.json" 16 | -------------------------------------------------------------------------------- /apps/web/sentry.server.config.js: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs'; 6 | 7 | const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; 8 | 9 | Sentry.init({ 10 | environment: process.env.NODE_ENV, 11 | dsn: SENTRY_DSN, 12 | tracesSampleRate: 1, 13 | integrations: [new Sentry.Integrations.Http({ tracing: true })], 14 | }); 15 | -------------------------------------------------------------------------------- /apps/web/src/components/__tests__/TwitterLogin.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { TwitterLogin } from '../home/TwitterLogin'; 3 | import { SessionProvider } from 'next-auth/react'; 4 | 5 | describe('TwitterLogin', () => { 6 | it('should render the component properly', () => { 7 | render( 8 | 9 | 10 | , 11 | ); 12 | 13 | expect( 14 | screen.getByRole('button', { name: 'Login with Twitter' }), 15 | ).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /apps/web/src/components/ranking/RankingTrophy.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@chakra-ui/react'; 2 | import { FaTrophy } from 'react-icons/fa'; 3 | 4 | interface Props { 5 | index: number; 6 | } 7 | 8 | export const RankingTrophy = ({ index }: Props) => { 9 | if (index > 2) return <>; 10 | 11 | const trophyColor = { 12 | 1: 'yellow.400', 13 | 2: 'gray.400', 14 | 3: 'orange.400', 15 | }; 16 | const position = (index + 1) as 1 | 2 | 3; 17 | const color = trophyColor[position]; 18 | 19 | return ; 20 | }; 21 | -------------------------------------------------------------------------------- /apps/ranking/src/isPublicMetricsChanged.ts: -------------------------------------------------------------------------------- 1 | import { Public_metrics } from './types'; 2 | 3 | const isPublicMetricsChanged = ( 4 | publicMetrics: Public_metrics, 5 | publicMetricsToCompare: Public_metrics, 6 | ): boolean => { 7 | const keys = [ 8 | 'retweet_count', 9 | 'reply_count', 10 | 'like_count', 11 | 'quote_count', 12 | ] as const; 13 | 14 | for (const key of keys) { 15 | if (publicMetrics[key] !== publicMetricsToCompare[key]) { 16 | return true; 17 | } 18 | } 19 | 20 | return false; 21 | }; 22 | 23 | export default isPublicMetricsChanged; 24 | -------------------------------------------------------------------------------- /apps/sseraresume/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Message, PartialMessage } from 'discord.js'; 2 | 3 | export type GitRepoInfo = { 4 | owner: string; 5 | repo: string; 6 | branch: string; 7 | path: string; 8 | }; 9 | 10 | export type GitHubResponseData = { 11 | type: 'dir' | 'file' | 'submodule' | 'symlink'; 12 | size: number; 13 | name: string; 14 | path: string; 15 | content?: string; 16 | sha: string; 17 | url: string; 18 | git_url: string; 19 | html_url: string; 20 | download_url: string; 21 | }; 22 | 23 | export type DiscordMessage = Message | PartialMessage; 24 | -------------------------------------------------------------------------------- /apps/sserartigos/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Message, PartialMessage } from 'discord.js'; 2 | 3 | export type GitRepoInfo = { 4 | owner: string; 5 | repo: string; 6 | branch: string; 7 | path: string; 8 | }; 9 | 10 | export type GitHubResponseData = { 11 | type: 'dir' | 'file' | 'submodule' | 'symlink'; 12 | size: number; 13 | name: string; 14 | path: string; 15 | content?: string; 16 | sha: string; 17 | url: string; 18 | git_url: string; 19 | html_url: string; 20 | download_url: string; 21 | }; 22 | 23 | export type DiscordMessage = Message | PartialMessage; 24 | -------------------------------------------------------------------------------- /apps/sseramemes/scripts/testResizeImage.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import fs from 'fs'; 3 | import { resizeImage } from '../src/image-scripts'; 4 | 5 | const testMeme = async () => { 6 | const image = await fetch( 7 | 'https://pbs.twimg.com/media/FSbQR8oXsAIKkQz?format=png&name=900x900', 8 | ); 9 | 10 | const buffer = Buffer.from(await image.arrayBuffer()); 11 | const resizedBuffer = await resizeImage(buffer); 12 | 13 | /** 14 | * Save resizedBuffer to file. 15 | */ 16 | await fs.promises.writeFile('test.png', resizedBuffer); 17 | }; 18 | 19 | testMeme().then(); 20 | -------------------------------------------------------------------------------- /apps/web/src/types/Score.ts: -------------------------------------------------------------------------------- 1 | import { Session as NextAuthSession } from 'next-auth/core/types'; 2 | 3 | export type Tweet = { 4 | text: string; 5 | in_reply_to_user_id: string; 6 | public_metrics: PublicMetrics; 7 | }; 8 | 9 | export interface PublicMetrics { 10 | retweet_count: number; 11 | reply_count: number; 12 | like_count: number; 13 | quote_count: number; 14 | } 15 | 16 | export interface Total { 17 | tweet_count: number; 18 | total: number; 19 | } 20 | 21 | export type UserScore = PublicMetrics & Total; 22 | 23 | export type Session = NextAuthSession & Record; 24 | -------------------------------------------------------------------------------- /apps/sserartigos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sserartigos", 3 | "version": "1.0.0", 4 | "description": "", 5 | "license": "ISC", 6 | "author": "", 7 | "main": "index.js", 8 | "directories": { 9 | "test": "test" 10 | }, 11 | "scripts": { 12 | "build_n_run": "tsc; node build/index.js", 13 | "compile": "tsc", 14 | "start": "node build/index.js" 15 | }, 16 | "dependencies": { 17 | "@octokit/rest": "21.1.1", 18 | "discord.js": "14.18.0", 19 | "dotenv": "16.4.7", 20 | "dotenv-safe": "8.2.0", 21 | "twitter-api-v2": "1.20.2" 22 | }, 23 | "keywords": [] 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/sentry.client.config.js: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the browser. 2 | // The config you add here will be used whenever a page is visited. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs'; 6 | import { Integrations } from '@sentry/tracing'; 7 | 8 | const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; 9 | 10 | Sentry.init({ 11 | environment: process.env.NODE_ENV, 12 | dsn: SENTRY_DSN, 13 | tracesSampleRate: 1, 14 | integrations: [new Integrations.BrowserTracing()], 15 | }); 16 | -------------------------------------------------------------------------------- /apps/cli/jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require('next/jest'); 2 | const pkg = require('./package.json'); 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | // dir: './', 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | const customJestConfig = { 11 | displayName: pkg.name, 12 | testEnvironment: 'node', 13 | }; 14 | 15 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 16 | module.exports = createJestConfig(customJestConfig); 17 | -------------------------------------------------------------------------------- /apps/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": ["es6", "es2015", "dom"], 6 | "declaration": true, 7 | "outDir": "lib", 8 | "rootDir": "src", 9 | "strict": true, 10 | "types": [ 11 | "node", 12 | "jest" 13 | ], 14 | "typeRoots": ["./types", "./node_modules/@types"], 15 | "esModuleInterop": true, 16 | "resolveJsonModule": true, 17 | "noImplicitAny": true 18 | }, 19 | "exclude": ["node_modules", "**/*.spec.ts", "**/*.test.ts"], 20 | "include": ["../src/**/*.tsx", "./src/**/*.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /apps/bot/src/tweetRanking/jobs/rankingJob.ts: -------------------------------------------------------------------------------- 1 | import { JobCallback, scheduleJob } from 'node-schedule'; 2 | import { DateTime } from 'luxon'; 3 | 4 | import { tweetRanking } from './tweetRanking'; 5 | import { getRuleFromConfig } from './getRuleFromConfig'; 6 | 7 | const rankingJob = () => { 8 | const rule = getRuleFromConfig('TWEET_RANKING_RULE'); 9 | if (!rule) return; 10 | 11 | const executeTweetRanking: JobCallback = tweetRanking(DateTime.now()); 12 | 13 | scheduleJob(rule, executeTweetRanking); 14 | console.info('Ranking Job started, with the rule: ', rule); 15 | }; 16 | 17 | export default rankingJob; 18 | -------------------------------------------------------------------------------- /apps/web/src/pages/api/score/[username].ts: -------------------------------------------------------------------------------- 1 | import { withSentry } from '@sentry/nextjs'; 2 | import { getUserScore } from 'modules/score/getUserScore'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | 5 | const userScoreHandler = async (req: NextApiRequest, res: NextApiResponse) => { 6 | const { username } = req.query; 7 | 8 | getUserScore(username as string) 9 | .then((result) => { 10 | res.status(200).json(result); 11 | }) 12 | .catch((error) => { 13 | res.status(error.statusCode || 500).json(error); 14 | }); 15 | }; 16 | 17 | export default withSentry(userScoreHandler); 18 | -------------------------------------------------------------------------------- /apps/sseramemes/scripts/testAddLogo.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import * as fs from 'fs'; 3 | import { addLogoToImage } from '../src/image-scripts'; 4 | 5 | const testMeme = async () => { 6 | const image = await fetch( 7 | 'https://pbs.twimg.com/media/FSbQR8oXsAIKkQz?format=png&name=900x900', 8 | ); 9 | 10 | const buffer = Buffer.from(await image.arrayBuffer()); 11 | const bufferWithWatermark = await addLogoToImage(buffer); 12 | 13 | /** 14 | * Save bufferWithWatermark to file. 15 | */ 16 | await fs.promises.writeFile('test.png', bufferWithWatermark); 17 | }; 18 | 19 | testMeme().then(); 20 | -------------------------------------------------------------------------------- /apps/sseramemes/scripts/testAddLogoToVideo.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { addLogoToVideo } from '../src/image-scripts/addLogoToVideo'; 3 | /** 4 | * For testing, please do have a video file in the root of the project named raw.mp4 5 | */ 6 | const testAddLogoToVideo = async () => { 7 | const video = await fs.promises.readFile('raw.mp4'); 8 | 9 | const bufferWithWatermark = await addLogoToVideo(video, 'video/mp4'); 10 | 11 | /** 12 | * Save bufferWithWatermark to file. 13 | */ 14 | await fs.promises.writeFile('test-video.mp4', bufferWithWatermark); 15 | }; 16 | 17 | testAddLogoToVideo().then(); 18 | -------------------------------------------------------------------------------- /apps/ranking/src/__mocks__/createFakeTemporaryTweets.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryTweetModel } from '../schema/TemporaryTweet'; 2 | import { TemporaryTweet } from '../types/index'; 3 | 4 | const baseId = '11603237370356776'; 5 | const createFakeTemporaryTweets = ( 6 | created_at: Date, 7 | length: number = 10, 8 | ): TemporaryTweet[] => 9 | Array.from({ length }, (_, i) => { 10 | const idSufix = `${i}`.padStart(2, '0'); 11 | const tweet_id = `${baseId}${idSufix}`; 12 | return new TemporaryTweetModel({ 13 | tweet_id, 14 | created_at, 15 | }); 16 | }); 17 | 18 | export default createFakeTemporaryTweets; 19 | -------------------------------------------------------------------------------- /apps/ranking/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@ccsseraphini/ranking", 4 | "version": "1.0.0", 5 | "main": "./src/index.ts", 6 | "module": "./src/index.ts", 7 | "scripts": { 8 | "test": "jest" 9 | }, 10 | "dependencies": { 11 | "luxon": "3.4.0", 12 | "mongoose": "8.12.1", 13 | "twitter-api-v2": "1.20.2" 14 | }, 15 | "devDependencies": { 16 | "@shelf/jest-mongodb": "^5.1.0", 17 | "@types/luxon": "3.4.2", 18 | "@types/node-schedule": "2.1.7", 19 | "jest": "^29.7.0", 20 | "jest-fetch-mock": "3.0.3", 21 | "ts-jest": "^29.2.6", 22 | "typescript": "5.8.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/chrome-ext/src/__mocks__/webextension-polyfill-ts.ts: -------------------------------------------------------------------------------- 1 | // src/__mocks__/webextension-polyfill-ts 2 | // Update this file to include any mocks for the `webextension-polyfill-ts` package 3 | // This is used to mock these values for Storybook so you can develop your components 4 | // outside the Web Extension environment provided by a compatible browser 5 | export const browser: any = { 6 | tabs: { 7 | executeScript(currentTabId: number, details: any) { 8 | return Promise.resolve({ done: true }); 9 | }, 10 | }, 11 | }; 12 | 13 | export interface Tabs { 14 | Tab: { 15 | id: number; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /apps/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": "src", 18 | "downlevelIteration": true 19 | }, 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/test/testUtils.tsx: -------------------------------------------------------------------------------- 1 | import { render, RenderOptions } from '@testing-library/react'; 2 | import { ReactElement } from 'react'; 3 | 4 | type ProvidersOptions = { 5 | children: ReactElement; 6 | }; 7 | 8 | // Add in any providers here if necessary: 9 | // (ReduxProvider, ThemeProvider, etc) 10 | const Providers = ({ children }: ProvidersOptions) => { 11 | return children; 12 | }; 13 | 14 | const customRender = (ui: ReactElement, options: RenderOptions = {}) => 15 | render(ui, { wrapper: Providers, ...options }); 16 | 17 | // re-export everything 18 | export * from '@testing-library/react'; 19 | 20 | // override render method 21 | export { customRender as render }; 22 | -------------------------------------------------------------------------------- /apps/sseraresume/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sseraresume", 3 | "version": "1.0.0", 4 | "description": "", 5 | "license": "ISC", 6 | "author": "", 7 | "main": "index.js", 8 | "directories": { 9 | "test": "test" 10 | }, 11 | "scripts": { 12 | "build_n_run": "tsc; node build/index.js", 13 | "compile": "tsc", 14 | "start": "tsup src/index.ts && node ./dist/index.js" 15 | }, 16 | "dependencies": { 17 | "@octokit/rest": "21.1.1", 18 | "bull": "4.16.5", 19 | "discord.js": "14.18.0", 20 | "dotenv": "16.4.7", 21 | "dotenv-safe": "8.2.0", 22 | "tsup": "8.4.0", 23 | "twitter-api-v2": "1.20.2" 24 | }, 25 | "keywords": [] 26 | } 27 | -------------------------------------------------------------------------------- /apps/bot/setupFiles.js: -------------------------------------------------------------------------------- 1 | const fetchMock = require('jest-fetch-mock'); 2 | fetchMock.enableFetchMocks(); 3 | 4 | [ 5 | 'DISCORD_TOKEN', 6 | 'DISCORD_CLIENT_ID', 7 | 'DISCORD_CLIENT_SECRET', 8 | 'DISCORD_BOT_TOKEN', 9 | 'DISCORD_BOT_CHANNEL_ID', 10 | 'TWITTER_BEARER_TOKEN', 11 | 'TWITTER_ACCESS_TOKEN', 12 | 'TWITTER_ACCESS_TOKEN_SECRET', 13 | 'TWITTER_API_KEY', 14 | 'TWITTER_API_KEY_SECRET', 15 | 'MONGO_URI', 16 | 'DISCORD_GENERAL_CHANNEL_ID', 17 | 'TWITTER_RANKING_API_KEY', 18 | 'TWITTER_RANKING_API_KEY_SECRET', 19 | 'TWITTER_RANKING_ACCESS_TOKEN', 20 | 'TWITTER_RANKING_ACCESS_TOKEN_SECRET', 21 | ].forEach((key) => { 22 | process.env[key] = key; 23 | }); 24 | -------------------------------------------------------------------------------- /apps/ranking/setupFiles.js: -------------------------------------------------------------------------------- 1 | const fetchMock = require('jest-fetch-mock'); 2 | fetchMock.enableFetchMocks(); 3 | 4 | [ 5 | 'DISCORD_TOKEN', 6 | 'DISCORD_CLIENT_ID', 7 | 'DISCORD_CLIENT_SECRET', 8 | 'DISCORD_BOT_TOKEN', 9 | 'DISCORD_BOT_CHANNEL_ID', 10 | 'TWITTER_BEARER_TOKEN', 11 | 'TWITTER_ACCESS_TOKEN', 12 | 'TWITTER_ACCESS_TOKEN_SECRET', 13 | 'TWITTER_API_KEY', 14 | 'TWITTER_API_KEY_SECRET', 15 | 'MONGO_URI', 16 | 'DISCORD_GENERAL_CHANNEL_ID', 17 | 'TWITTER_RANKING_API_KEY', 18 | 'TWITTER_RANKING_API_KEY_SECRET', 19 | 'TWITTER_RANKING_ACCESS_TOKEN', 20 | 'TWITTER_RANKING_ACCESS_TOKEN_SECRET', 21 | ].forEach((key) => { 22 | process.env[key] = key; 23 | }); 24 | -------------------------------------------------------------------------------- /apps/ranking/src/__mocks__/createFakeRankedTweets.ts: -------------------------------------------------------------------------------- 1 | import createFakeTemporaryTweets from './createFakeTemporaryTweets'; 2 | import createFakeTweetData from './createFakeTweetData'; 3 | import parseToRankedTweet from '../parseToRankedTweet'; 4 | import { TemporaryTweet, RankedTweet } from '../types/index'; 5 | 6 | const createFakeRankedTweets = ( 7 | created_at: Date, 8 | length: number = 10, 9 | ): RankedTweet[] => { 10 | const fakeTweets: TemporaryTweet[] = createFakeTemporaryTweets( 11 | created_at, 12 | length, 13 | ); 14 | const data = createFakeTweetData(fakeTweets); 15 | 16 | return data.map(parseToRankedTweet); 17 | }; 18 | 19 | export default createFakeRankedTweets; 20 | -------------------------------------------------------------------------------- /apps/ranking/src/deleteTemporaryTweets.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | import { TemporaryTweetModel } from './schema/TemporaryTweet'; 4 | 5 | const deleteTemporaryTweets = async ( 6 | startDate: DateTime, 7 | endDate: DateTime = DateTime.now(), 8 | ): Promise => { 9 | const since = startDate.toJSDate(); 10 | const until = endDate.toJSDate(); 11 | try { 12 | await TemporaryTweetModel.deleteMany({ 13 | created_at: { $gte: since, $lte: until }, 14 | }); 15 | } catch (error) { 16 | console.error('Fail to delete many tweets', error, { 17 | startDate, 18 | endDate, 19 | }); 20 | } 21 | }; 22 | 23 | export default deleteTemporaryTweets; 24 | -------------------------------------------------------------------------------- /apps/ranking/src/parseToRankedTweet.ts: -------------------------------------------------------------------------------- 1 | import getTweetScore from './getTweetScore'; 2 | import { Public_metrics } from './types/index'; 3 | import { RankedTweet, TweetData } from './types/index'; 4 | import { DateTime } from 'luxon'; 5 | 6 | const parseToRankedTweet = (tweet: TweetData): RankedTweet => { 7 | const { id, created_at, public_metrics, author_id } = 8 | tweet as Required; 9 | 10 | const score = getTweetScore(tweet); 11 | return { 12 | tweet_id: id, 13 | created_at: DateTime.fromISO(created_at).toJSDate(), 14 | public_metrics: public_metrics as Public_metrics, 15 | score, 16 | author_id, 17 | }; 18 | }; 19 | 20 | export default parseToRankedTweet; 21 | -------------------------------------------------------------------------------- /apps/ranking/src/saveRankedTweets.ts: -------------------------------------------------------------------------------- 1 | import { RankedTweet } from './types/index'; 2 | import { RankedTweetModel } from './schema/RankedTweet'; 3 | 4 | const saveRankedTweets = async (tweets: RankedTweet[]): Promise => { 5 | try { 6 | await RankedTweetModel.bulkWrite( 7 | tweets.map((tweet) => ({ 8 | replaceOne: { 9 | filter: { tweet_id: tweet.tweet_id }, 10 | replacement: tweet, 11 | upsert: true, 12 | }, 13 | })), 14 | { ordered: false }, 15 | ); 16 | } catch (error) { 17 | console.error('Fail to save ranked tweets', error, { tweets }); 18 | throw error; 19 | } 20 | }; 21 | 22 | export default saveRankedTweets; 23 | -------------------------------------------------------------------------------- /apps/chrome-ext/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": "src", 18 | "downlevelIteration": true, 19 | "types": ["chrome"] 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": "src", 18 | "downlevelIteration": true 19 | }, 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src/pages/api/auth/[...nextauth].ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cc @sseraphini / monorepo 2 | 3 | [cc @sseraphini article](https://sibelius.substack.com/p/cc-sseraphini) 4 | 5 | ## Getting Started 6 | 7 | This project uses [classic pnpm 1.x Workspaces](https://classic.yarnpkg.com/en/docs/workspaces) 8 | 9 | To setup a local development, you can clone and run pnpm in the root of the project: 10 | 11 | ```bash 12 | git clone https://github.com/sibelius/ccsseraphini 13 | cd ccsseraphini 14 | pnpm install 15 | ``` 16 | 17 | This will install all dependencies for projects inside `apps/` 18 | 19 | Now you can use `pnpm workspace` API to start a project: 20 | 21 | ```bash 22 | pnpm workspace web dev # this will run 'apps/web/package.json' script called 'dev' 23 | ``` 24 | -------------------------------------------------------------------------------- /apps/sserartigos/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Client, Events, GatewayIntentBits } from 'discord.js'; 2 | import { config } from './config'; 3 | import { handleVoting } from './handleVoting'; 4 | import { createPoll } from './pollHandler'; 5 | 6 | const client = new Client({ 7 | intents: [ 8 | GatewayIntentBits.Guilds, 9 | GatewayIntentBits.GuildMessages, 10 | GatewayIntentBits.GuildMessageReactions, 11 | GatewayIntentBits.MessageContent, 12 | ], 13 | }); 14 | 15 | client.once(Events.ClientReady, () => { 16 | console.log('opa 🚀'); 17 | }); 18 | 19 | client.on(Events.MessageCreate, createPoll); 20 | client.on(Events.MessageReactionAdd, handleVoting); 21 | 22 | client.login(config.DISCORD_BOT_TOKEN); 23 | -------------------------------------------------------------------------------- /apps/web/src/middlewares/validations/__tests__/validations-cron-header-key.test.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import flood from 'pages/api/discord/flood'; 3 | 4 | test('Should validate CRONJOB_HEADER_KEY', async () => { 5 | const req = { 6 | method: 'POST', 7 | } as Partial; 8 | 9 | const res = { 10 | status: jest.fn(() => res), 11 | json: jest.fn(), 12 | } as Partial; 13 | 14 | await flood(req as NextApiRequest, res as NextApiResponse).catch( 15 | (error: Error) => error, 16 | ); 17 | 18 | expect(res.status).toBeCalledWith(500); 19 | expect(res.json).toBeCalledWith({ error: 'env CRONJOB_HEADER_KEY not set' }); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/ranking/src/getTweetBatchFromApi.ts: -------------------------------------------------------------------------------- 1 | import { Client } from 'twitter-api-sdk'; 2 | 3 | import { TweetBatch } from './types/index'; 4 | 5 | const getTweetBatchFromApi = async ( 6 | ids: string[], 7 | bearerToken: string, 8 | ): Promise => { 9 | if (!bearerToken) { 10 | console.error('Bearer token is not defined'); 11 | return {}; 12 | } 13 | 14 | const { 15 | tweets: { findTweetsById }, 16 | } = new Client(bearerToken); 17 | 18 | const result: TweetBatch = await findTweetsById({ 19 | ids, 20 | expansions: ['author_id'], 21 | 'tweet.fields': ['public_metrics', 'created_at'], 22 | }); 23 | 24 | return result; 25 | }; 26 | 27 | export default getTweetBatchFromApi; 28 | -------------------------------------------------------------------------------- /apps/bot/src/tweetRanking/jobs/syncRankedTweetJob.ts: -------------------------------------------------------------------------------- 1 | import { scheduleJob } from 'node-schedule'; 2 | import { getSyncRankedTweetsFn } from '@ccsseraphini/ranking/src/getSyncRankedTweetsFn'; 3 | 4 | import { config } from '../../config'; 5 | import { getRuleFromConfig } from './getRuleFromConfig'; 6 | 7 | const syncRankedTweetJob = () => { 8 | const rule = getRuleFromConfig('SYNC_TWEETS_RULE'); 9 | if (!rule) return; 10 | 11 | const bearerToken = config.TWITTER_BEARER_TOKEN; 12 | 13 | const syncRankedTweets = getSyncRankedTweetsFn(bearerToken); 14 | 15 | scheduleJob(rule, syncRankedTweets); 16 | console.info('Sync ranked tweet job started, with the rule: ', rule); 17 | }; 18 | export default syncRankedTweetJob; 19 | -------------------------------------------------------------------------------- /apps/sseraresume/src/common/utils/getArticles.ts: -------------------------------------------------------------------------------- 1 | export const getArticles = (messageContent: string): string[] => { 2 | const urlRegex = /(https?:\/\/[^\s]+)/g; 3 | const links = messageContent.match(urlRegex) ?? []; 4 | return filterBlockedDomains(links); 5 | }; 6 | /** 7 | * Websites whose the bot should ignore 8 | * */ 9 | const BLOCKED_DOMAINS = ['twitter.com', 'google.com', 'youtube.com']; 10 | 11 | function filterBlockedDomains(links: string[]) { 12 | return links.filter(isNotBlocked); 13 | } 14 | 15 | function isNotBlocked(link: string) { 16 | const hasBlockedDomain = BLOCKED_DOMAINS.reduce( 17 | (acc, domain) => acc || link.includes(domain), 18 | false, 19 | ); 20 | return !hasBlockedDomain; 21 | } 22 | -------------------------------------------------------------------------------- /apps/sserartigos/src/common/utils/getArticles.ts: -------------------------------------------------------------------------------- 1 | export const getArticles = (messageContent: string): string[] => { 2 | const urlRegex = /(https?:\/\/[^\s]+)/g; 3 | const links = messageContent.match(urlRegex) ?? []; 4 | return filterBlockedDomains(links); 5 | }; 6 | /** 7 | * Websites whose the bot should ignore 8 | * */ 9 | const BLOCKED_DOMAINS = ['twitter.com', 'google.com', 'youtube.com']; 10 | 11 | function filterBlockedDomains(links: string[]) { 12 | return links.filter(isNotBlocked); 13 | } 14 | 15 | function isNotBlocked(link: string) { 16 | const hasBlockedDomain = BLOCKED_DOMAINS.reduce( 17 | (acc, domain) => acc || link.includes(domain), 18 | false, 19 | ); 20 | return !hasBlockedDomain; 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://sseraphini.cc.com/weekly0.8 4 | https://sseraphini.cc.com/searchweekly0.8 5 | https://sseraphini.cc.com/scoreweekly0.8 6 | 7 | -------------------------------------------------------------------------------- /apps/ranking/src/countNewAuthors.ts: -------------------------------------------------------------------------------- 1 | import { RankedTweetModel } from './schema/RankedTweet'; 2 | import { Stats } from './types/index'; 3 | 4 | export async function countNewAuthors( 5 | since: Date, 6 | until: Date, 7 | ): Promise> { 8 | const [stats]: Partial[] = await RankedTweetModel.aggregate([ 9 | { 10 | $group: { 11 | _id: '$author_id', 12 | first_created_at: { 13 | $min: '$created_at', 14 | }, 15 | }, 16 | }, 17 | { 18 | $match: { 19 | first_created_at: { 20 | $gte: since, 21 | $lte: until, 22 | }, 23 | }, 24 | }, 25 | { 26 | $count: 'newAuthors', 27 | }, 28 | ]); 29 | 30 | return stats; 31 | } 32 | -------------------------------------------------------------------------------- /apps/vscode-ext/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "allowSyntheticDefaultImports": true, 12 | "strict": true /* enable all strict type-checking options */ 13 | /* Additional Checks */ 14 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 15 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 16 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | ".vscode-test", 21 | "webviews" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /apps/bot/test/connectMongoose.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | declare global { 4 | interface Global { 5 | __MONGO_URI__: string; 6 | __MONGO_DB_NAME__: string; 7 | } 8 | 9 | // eslint-disable-next-line 10 | var __MONGO_URI__: string; 11 | // eslint-disable-next-line 12 | var __MONGO_DB_NAME__: string; 13 | } 14 | 15 | export async function connectMongoose() { 16 | jest.setTimeout(20000); 17 | 18 | mongoose.set('strictQuery', false); 19 | return mongoose.connect(global.__MONGO_URI__, { 20 | dbName: global.__MONGO_DB_NAME__, 21 | autoIndex: true, 22 | connectTimeoutMS: 30000, 23 | minPoolSize: 1, 24 | socketTimeoutMS: 30000, 25 | keepAlive: true, 26 | keepAliveInitialDelay: 1000, 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /apps/ranking/test/connectMongoose.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | declare global { 4 | interface Global { 5 | __MONGO_URI__: string; 6 | __MONGO_DB_NAME__: string; 7 | } 8 | 9 | // eslint-disable-next-line 10 | var __MONGO_URI__: string; 11 | // eslint-disable-next-line 12 | var __MONGO_DB_NAME__: string; 13 | } 14 | 15 | export async function connectMongoose() { 16 | jest.setTimeout(20000); 17 | mongoose.set('strictQuery', false); 18 | return mongoose.connect(global.__MONGO_URI__, { 19 | dbName: global.__MONGO_DB_NAME__, 20 | autoIndex: true, 21 | connectTimeoutMS: 30000, 22 | minPoolSize: 1, 23 | socketTimeoutMS: 30000, 24 | keepAlive: true, 25 | keepAliveInitialDelay: 1000, 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/src/components/score/DiscordClaimButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@chakra-ui/react'; 2 | import { config } from 'config'; 3 | import { FaDiscord } from 'react-icons/fa'; 4 | 5 | export const DiscordClaimButton = ({ totalScore }: { totalScore: number }) => { 6 | const minScore = config.DISCORD_SCORE_THRESHOLD as number; 7 | const disabled = totalScore < minScore; 8 | const href = !disabled ? '/discord/claim' : undefined; 9 | 10 | return ( 11 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /apps/web/src/modules/db/mongob.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { config } from 'config'; 3 | 4 | export default async function connectDB(): Promise { 5 | try { 6 | const { MONGO_URI } = config; 7 | 8 | if (!MONGO_URI) { 9 | console.warn('MONGO_URI is not set'); 10 | return; 11 | } 12 | 13 | if (mongoose.connection.readyState === 1) { 14 | console.log('Already connected to database'); 15 | return mongoose; 16 | } 17 | 18 | console.log('Connecting to database'); 19 | 20 | mongoose.set('strictQuery', false); 21 | return await mongoose.connect(MONGO_URI); 22 | } catch (error) { 23 | console.error('Error connecting to database:', error); 24 | return; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/src/modules/discord/discordGuildJoinPut.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../../config'; 2 | 3 | export const discordGuildJoinPut = async ( 4 | guildID: string, 5 | userID: string, 6 | userAccessToken: string, 7 | ): Promise => { 8 | const payload = { 9 | access_token: userAccessToken, 10 | }; 11 | 12 | const url = `http://discord.com/api/guilds/${guildID}/members/${userID}`; 13 | const options = { 14 | headers: { 15 | Accept: 'application/json', 16 | 'Content-Type': 'application/json', 17 | Authorization: `Bot ${config.DISCORD_BOT_TOKEN}`, 18 | }, 19 | method: 'PUT', 20 | body: JSON.stringify(payload), 21 | }; 22 | 23 | const response = await fetch(url, options); 24 | 25 | return response; 26 | }; 27 | -------------------------------------------------------------------------------- /apps/ranking/src/getTemporaryTweets.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryTweet, TweetFilter } from './types/index'; 2 | import { TemporaryTweetModel } from './schema/TemporaryTweet'; 3 | 4 | const getTemporaryTweets = async ({ 5 | created_at, 6 | }: Partial = {}): Promise => { 7 | const tweetFilter: Record = {}; 8 | 9 | if (!!created_at) { 10 | tweetFilter['created_at'] = { $gte: created_at }; 11 | } 12 | 13 | try { 14 | const tweets: TemporaryTweet[] = await TemporaryTweetModel.find({ 15 | ...tweetFilter, 16 | }); 17 | 18 | return tweets; 19 | } catch (error) { 20 | console.error('Fail to get tweets', error, { created_at }); 21 | return []; 22 | } 23 | }; 24 | 25 | export default getTemporaryTweets; 26 | -------------------------------------------------------------------------------- /apps/sseraresume/src/common/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../../config'; 2 | import { DiscordMessage } from '../../types'; 3 | import { getArticles } from './getArticles'; 4 | 5 | export const checkVotingAbility = (message: DiscordMessage): boolean => { 6 | return ( 7 | shouldCreatePoll(message) && 8 | getArticles(message.content).length >= 1 && 9 | message.attachments.size === 0 10 | ); 11 | }; 12 | 13 | function shouldCreatePoll(message: DiscordMessage) { 14 | const reactions = message.reactions.valueOf(); 15 | const shouldForcePoll = reactions.some( 16 | (item) => item.emoji.toString() === '🔗', 17 | ); 18 | 19 | if (shouldForcePoll) return true; 20 | return config.LISTENED_USERS_ID.some((value) => value === message.author.id); 21 | } 22 | -------------------------------------------------------------------------------- /apps/sserartigos/src/common/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../../config'; 2 | import { DiscordMessage } from '../../types'; 3 | import { getArticles } from './getArticles'; 4 | 5 | export const checkVotingAbility = (message: DiscordMessage): boolean => { 6 | return ( 7 | shouldCreatePoll(message) && 8 | getArticles(message.content).length >= 1 && 9 | message.attachments.size === 0 10 | ); 11 | }; 12 | 13 | function shouldCreatePoll(message: DiscordMessage) { 14 | const reactions = message.reactions.valueOf(); 15 | const shouldForcePoll = reactions.some( 16 | (item) => item.emoji.toString() === '🔗', 17 | ); 18 | 19 | if (shouldForcePoll) return true; 20 | return config.LISTENED_USERS_ID.some((value) => value === message.author.id); 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/src/components/home/useRandom.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from 'react'; 2 | 3 | export type TRandomContext = { 4 | randomNumber: number; 5 | }; 6 | 7 | const RandomContext = React.createContext({} as TRandomContext); 8 | 9 | export function useRandom() { 10 | return useContext(RandomContext); 11 | } 12 | 13 | export function RandomProvider({ children }: { children: React.ReactNode }) { 14 | const [randomNumber, setRandomNumber] = React.useState(Math.random()); 15 | 16 | useEffect(() => { 17 | setRandomNumber(Math.random()); 18 | }, []); 19 | 20 | const value: TRandomContext = { 21 | randomNumber, 22 | }; 23 | 24 | return ( 25 | {children} 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/bot/src/tweetRanking/jobs/getRuleFromConfig.ts: -------------------------------------------------------------------------------- 1 | import { RecurrenceRule } from 'node-schedule'; 2 | 3 | import { config } from '../../config'; 4 | 5 | export const getRuleFromConfig = (varName: string): RecurrenceRule => { 6 | const hourTimeString = config[varName]; 7 | 8 | if (!hourTimeString) { 9 | console.error(`${varName} is not defined in config`); 10 | return; 11 | } 12 | 13 | const rule = new RecurrenceRule(); 14 | rule.tz = 'America/Sao_Paulo'; 15 | 16 | try { 17 | const [hour, minute] = hourTimeString.split(':'); 18 | rule.hour = parseInt(hour, 10); 19 | rule.minute = parseInt(minute, 10); 20 | } catch (error) { 21 | console.error(`Error parsing ${varName} from config`); 22 | return; 23 | } 24 | 25 | return rule; 26 | }; 27 | -------------------------------------------------------------------------------- /apps/ranking/src/getTweetsByIdChunks.ts: -------------------------------------------------------------------------------- 1 | import getTweetBatchFromApi from './getTweetBatchFromApi'; 2 | import { TweetBatch, TweetData } from './types/index'; 3 | 4 | const getTweetsByIdChunks = async ( 5 | tweetIdChunks: string[][], 6 | berarerToken: string, 7 | ): Promise => { 8 | const tweets: TweetData[] = []; 9 | 10 | for await (const ids of tweetIdChunks) { 11 | try { 12 | const { data }: TweetBatch = await getTweetBatchFromApi( 13 | ids, 14 | berarerToken, 15 | ); 16 | 17 | if (!!data?.length) tweets.push(...data); 18 | } catch (error) { 19 | console.error('Fail to get tweets from Twitter API', error, { ids }); 20 | } 21 | } 22 | 23 | return tweets; 24 | }; 25 | 26 | export default getTweetsByIdChunks; 27 | -------------------------------------------------------------------------------- /apps/web/src/modules/twitter/twitterUserGet.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'types/User'; 2 | import { getTwitterAuthorization } from './getTwitterAuthorization'; 3 | 4 | export type Result = { 5 | data: User; 6 | }; 7 | 8 | export const userProfile = async ( 9 | username: string, 10 | accessToken: string, 11 | ): Promise => { 12 | const url = `https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url`; 13 | const options = { 14 | headers: { 15 | Accept: 'application/json', 16 | 'Content-Type': 'application/json', 17 | Authorization: getTwitterAuthorization(accessToken), 18 | }, 19 | method: 'GET', 20 | }; 21 | const response = await fetch(url, options); 22 | const data = await response.json(); 23 | 24 | return data; 25 | }; 26 | -------------------------------------------------------------------------------- /apps/ranking/src/saveTemporaryTweet.ts: -------------------------------------------------------------------------------- 1 | import { Tweet } from './types/index'; 2 | import { TemporaryTweet } from './types/index'; 3 | import { TemporaryTweetModel } from './schema/TemporaryTweet'; 4 | 5 | type SavedTweet = { 6 | created_at: Date; 7 | tweet_id: string; 8 | _id: any; 9 | }; 10 | 11 | const saveTemporaryTweet = async ({ 12 | data: { id: tweet_id, created_at }, 13 | }: Tweet): Promise => { 14 | try { 15 | const tweetModel = new TemporaryTweetModel({ 16 | tweet_id, 17 | created_at, 18 | }); 19 | 20 | const tweetSaved = await tweetModel.save(); 21 | 22 | return tweetSaved; 23 | } catch (error) { 24 | console.error('Fail to save tweet', error, { tweet_id, created_at }); 25 | } 26 | }; 27 | 28 | export default saveTemporaryTweet; 29 | -------------------------------------------------------------------------------- /apps/ranking/src/getRankingPeriod.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | import { getStartDate } from './getStartDate'; 4 | import { RankingPeriod } from './types/index'; 5 | 6 | export const getRankingPeriod = (until: DateTime): RankingPeriod => { 7 | const since = getStartDate(until); 8 | 9 | if (!!since) { 10 | const endOfMonth = until.endOf('month').day; 11 | 12 | const label = until.day === endOfMonth ? 'monthly' : 'biweekly'; 13 | 14 | return { 15 | label, 16 | since, 17 | until, 18 | }; 19 | } 20 | 21 | if (until.weekday === 7) { 22 | return { 23 | label: 'weekly', 24 | since: until.minus({ days: 6 }), 25 | until, 26 | }; 27 | } 28 | 29 | return { 30 | label: 'daily', 31 | since: until, 32 | until, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /apps/web/jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require('next/jest'); 2 | const pkg = require('./package.json'); 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | // dir: './', 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | const customJestConfig = { 11 | displayName: pkg.name, 12 | setupFilesAfterEnv: ['./jest.setup.js'], 13 | testEnvironment: 'jsdom', 14 | roots: ['/src/'], 15 | moduleDirectories: ['node_modules', 'src'], 16 | transformIgnorePatterns: ['node_modules/(?!use-debounce|uuid)'], 17 | }; 18 | 19 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 20 | module.exports = createJestConfig(customJestConfig); 21 | -------------------------------------------------------------------------------- /apps/ranking/src/postTweetRanking.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | import createPostFromRankedTweets from './createPostFromRankedTweets'; 3 | import { getRankingPeriod } from './getRankingPeriod'; 4 | import { RankingPeriod } from './types/index'; 5 | 6 | const postTweetRanking = async ( 7 | endDate: DateTime, 8 | config: Record, 9 | ): Promise => { 10 | const { label, since, until }: RankingPeriod = getRankingPeriod(endDate); 11 | 12 | const sinceStart = since.startOf('day'); 13 | const untilEnd = until.endOf('day'); 14 | 15 | console.info( 16 | `Posting ${label} ranking since ${since} to ${until}. Running at ${DateTime.now()}`, 17 | ); 18 | 19 | await createPostFromRankedTweets(sinceStart, config, untilEnd); 20 | }; 21 | 22 | export default postTweetRanking; 23 | -------------------------------------------------------------------------------- /apps/web/src/components/home/SponsorButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Stack } from '@chakra-ui/react'; 2 | 3 | import { SponsorLogo } from './SponsorLogo'; 4 | 5 | export const SponsorButton = () => { 6 | return ( 7 | <> 8 | 9 | 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /apps/web/src/pages/sibs-day/blobs/index.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@chakra-ui/react'; 2 | import type { LayoutProps } from '@chakra-ui/react'; 3 | 4 | type InsetDirections = 'top' | 'right' | 'bottom' | 'left'; 5 | type SizeResponsiveValue = LayoutProps['height']; 6 | 7 | type BlobProps = { 8 | bg?: string; 9 | size: SizeResponsiveValue; 10 | inset: Partial>; 11 | borderRadius: string | number; 12 | }; 13 | 14 | const Blob = ({ bg, size, inset, borderRadius }: BlobProps) => ( 15 | 26 | ); 27 | 28 | export default Blob; 29 | -------------------------------------------------------------------------------- /apps/web/src/types/Tweet.ts: -------------------------------------------------------------------------------- 1 | export interface TwitterResponseTweetInfo { 2 | author_id: string; 3 | id: string; 4 | text: string; 5 | created_at: Date; 6 | public_metrics: { 7 | retweet_count: number; 8 | reply_count: number; 9 | like_count: number; 10 | quote_count: number; 11 | }; 12 | attachments: { 13 | media_keys: string[]; 14 | }; 15 | } 16 | 17 | export interface TwitterResponseUserInfo { 18 | id: string; 19 | name: string; 20 | profile_image_url: string; 21 | username: string; 22 | } 23 | 24 | export interface TwitterResponseMediaInfo { 25 | width: number; 26 | type: string; 27 | media_key: string; 28 | url: string; 29 | height: number; 30 | } 31 | 32 | export interface TweetData extends TwitterResponseTweetInfo { 33 | userInfo: TwitterResponseUserInfo; 34 | } 35 | -------------------------------------------------------------------------------- /apps/sseraresume/src/resume-file-generator.ts: -------------------------------------------------------------------------------- 1 | import { ocktokit } from './github/client'; 2 | 3 | export const resumeToFile = async (data: string, filename: string) => { 4 | try { 5 | const owner = 'EmanuelCampos'; 6 | const repo = 'ccseraphini-daily-resume'; 7 | const path = `files/${filename}.md`; 8 | const message = 'Resume from the day'; 9 | const content = Buffer.from(data).toString('base64'); 10 | 11 | // Cria ou atualiza o arquivo 12 | const response = await ocktokit.repos.createOrUpdateFileContents({ 13 | owner, 14 | repo, 15 | path, 16 | message, 17 | content, 18 | }); 19 | console.log(`File ${path} added succesfully`); 20 | return response; 21 | } catch (err) { 22 | console.error(`Error when adding the archive ${err}`); 23 | throw err; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /apps/sseraresume/src/tweet.ts: -------------------------------------------------------------------------------- 1 | import { TwitterApi } from 'twitter-api-v2'; 2 | 3 | const client = new TwitterApi({ 4 | appKey: process.env.TWITTER_API_KEY, 5 | appSecret: process.env.TWITTER_API_KEY_SECRET, 6 | accessToken: process.env.TWITTER_ACCESS_TOKEN, 7 | accessSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET, 8 | }); 9 | 10 | const tweet = async (resume: string) => { 11 | try { 12 | await client.v1.tweet(resume, {}); 13 | 14 | return { 15 | ok: true, 16 | }; 17 | } catch (error) { 18 | return error; 19 | } 20 | }; 21 | 22 | export const tweetResume = async (resume: string) => { 23 | const response = await tweet(resume); 24 | 25 | if (response.ok) { 26 | console.log('Tweeted successfully'); 27 | } else { 28 | console.log('Error tweeting', response); 29 | } 30 | 31 | return; 32 | }; 33 | -------------------------------------------------------------------------------- /apps/web/src/canvasUtil.tsx: -------------------------------------------------------------------------------- 1 | import html2canvas from 'html2canvas'; 2 | 3 | export const exportAsImage = async ( 4 | element: HTMLDivElement, 5 | imageFileName: string, 6 | ) => { 7 | const canvas = await html2canvas(element, { 8 | allowTaint: true, 9 | useCORS: true, 10 | }); 11 | const image = canvas.toDataURL('image/png', 1.0); 12 | 13 | downloadImage(image, imageFileName); 14 | }; 15 | 16 | const downloadImage = (blob: string, fileName: string) => { 17 | const fakeLink = window.document.createElement('a'); 18 | fakeLink.setAttribute('style', 'display:none;'); 19 | fakeLink.download = fileName; 20 | 21 | fakeLink.href = blob; 22 | 23 | document.body.appendChild(fakeLink); 24 | fakeLink.click(); 25 | document.body.removeChild(fakeLink); 26 | 27 | fakeLink.remove(); 28 | }; 29 | 30 | export default exportAsImage; 31 | -------------------------------------------------------------------------------- /apps/ranking/src/__mocks__/createFakeTweetData.ts: -------------------------------------------------------------------------------- 1 | import { TweetData, TemporaryTweet } from '../types/index'; 2 | 3 | const getRandomInt = (min: number, max: number) => 4 | Math.floor(Math.random() * (max - min + 1)) + min; 5 | 6 | const getAuthorId = (size: number = 10): string => 7 | Math.random().toString(size).substring(2, 12); 8 | 9 | const createFakeTweetData = (temporaryTweets: TemporaryTweet[]) => 10 | temporaryTweets.map( 11 | ({ tweet_id, created_at }): TweetData => ({ 12 | id: tweet_id, 13 | created_at: created_at.toISOString(), 14 | author_id: getAuthorId(), 15 | public_metrics: { 16 | retweet_count: getRandomInt(0, 100), 17 | reply_count: getRandomInt(0, 1000), 18 | like_count: getRandomInt(0, 1000), 19 | quote_count: getRandomInt(0, 100), 20 | }, 21 | }), 22 | ); 23 | 24 | export default createFakeTweetData; 25 | -------------------------------------------------------------------------------- /apps/web/src/middlewares/validations/__tests__/validations-cron-header-value.test.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'config'; 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import flood from 'pages/api/discord/flood'; 4 | 5 | test('Should validate CRONJOB_HEADER_VALUE', async () => { 6 | Object.defineProperties(config, { 7 | CRONJOB_HEADER_KEY: { 8 | value: '2313873457', 9 | }, 10 | }); 11 | 12 | const req = { 13 | method: 'POST', 14 | } as Partial; 15 | 16 | const res = { 17 | status: jest.fn(() => res), 18 | json: jest.fn(), 19 | } as Partial; 20 | 21 | await flood(req as NextApiRequest, res as NextApiResponse).catch( 22 | (error) => error, 23 | ); 24 | 25 | expect(res.json).toBeCalledWith({ 26 | error: 'env CRONJOB_HEADER_VALUE not set', 27 | }); 28 | expect(res.status).toBeCalledWith(500); 29 | }); 30 | -------------------------------------------------------------------------------- /apps/sseramemes/scripts/testAddTextToImage.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import * as fs from 'fs'; 3 | import { addLogoToImage, addTextToImage } from '../src/image-scripts'; 4 | import { Message } from 'discord.js'; 5 | 6 | const testMeme = async () => { 7 | const image = await fetch( 8 | 'https://pbs.twimg.com/media/FSbQR8oXsAIKkQz?format=png&name=900x900', 9 | ); 10 | 11 | const buffer = Buffer.from(await image.arrayBuffer()); 12 | const bufferWithText = await addTextToImage( 13 | { 14 | content: 15 | ';meme text="eai jeff" position="top-center" location="north" color="xlarge-black"', 16 | } as Message, 17 | buffer, 18 | ); 19 | const bufferWithWatermark = await addLogoToImage(bufferWithText); 20 | /** 21 | * Save bufferWithWatermark to file. 22 | */ 23 | await fs.promises.writeFile('test.png', bufferWithWatermark); 24 | }; 25 | 26 | testMeme().then(); 27 | -------------------------------------------------------------------------------- /apps/web/src/modules/score/getElegibleTweets.test.ts: -------------------------------------------------------------------------------- 1 | import { getElegibleTweets } from './getElegibleTweets'; 2 | import { userTweets } from 'modules/twitter/twitterFollowersGet'; 3 | 4 | jest.mock('modules/twitter/twitterUserGet'); 5 | jest.mock('modules/twitter/twitterFollowersGet'); 6 | 7 | describe('GetElegibleTweets', () => { 8 | test('Should return only one elegible tweet', async () => { 9 | const config = { 10 | TWITTER_PROFILE_ID: '1070750548608147456', 11 | TWITTER_BEARER_TOKEN: 'access_token', 12 | }; 13 | const { data: tweets } = await userTweets( 14 | config.TWITTER_PROFILE_ID, 15 | config.TWITTER_BEARER_TOKEN, 16 | ).then((data) => data); 17 | 18 | const result = getElegibleTweets({ 19 | tweets, 20 | twitter_profile_id: '2313873457', 21 | twitter_profile_user: 'sseraphini', 22 | }); 23 | 24 | expect(result.length).toBe(1); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /apps/ranking/src/getRankedTweetsFromApi.ts: -------------------------------------------------------------------------------- 1 | import getTweetIdChunks from './getTweetIdChunks'; 2 | import parseToRankedTweet from './parseToRankedTweet'; 3 | import getTweetsByIdChunks from './getTweetsByIdChunks'; 4 | import { TweetData, TweetWithId, RankedTweet } from './types/index'; 5 | 6 | const getRankedTweetsFromApi = async ( 7 | tweets: TweetWithId[], 8 | bearerToken: string, 9 | ): Promise => { 10 | if (!tweets.length) { 11 | console.info('Tweets are empty'); 12 | return []; 13 | } 14 | 15 | const idChunks: string[][] = getTweetIdChunks(tweets); 16 | const tweetsData: TweetData[] = await getTweetsByIdChunks( 17 | idChunks, 18 | bearerToken, 19 | ); 20 | if (!tweetsData.length) { 21 | console.error('Not found tweets in TwitterAPI'); 22 | return []; 23 | } 24 | 25 | return tweetsData.map(parseToRankedTweet); 26 | }; 27 | 28 | export default getRankedTweetsFromApi; 29 | -------------------------------------------------------------------------------- /apps/ranking/src/schema/RankedTweet.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, model } from 'mongoose'; 2 | import type { RankedTweet, RankedType } from '../types'; 3 | 4 | const rankedTweetSchema = new Schema({ 5 | tweet_id: { 6 | type: String, 7 | required: true, 8 | }, 9 | created_at: { 10 | type: Date, 11 | required: true, 12 | }, 13 | author_id: { 14 | type: String, 15 | required: true, 16 | }, 17 | public_metrics: { 18 | type: { 19 | retweet_count: Number, 20 | reply_count: Number, 21 | like_count: Number, 22 | quote_count: Number, 23 | }, 24 | required: true, 25 | }, 26 | score: { 27 | type: Number, 28 | required: true, 29 | }, 30 | last_updated: Date, 31 | changes_since_last_update: Boolean, 32 | }); 33 | 34 | export const RankedTweetModel = 35 | (mongoose.models.rankedTweet as RankedType) || 36 | model('rankedTweet', rankedTweetSchema); 37 | -------------------------------------------------------------------------------- /apps/web/src/components/pix/DonatePix.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@chakra-ui/react'; 2 | import { useState } from 'react'; 3 | import { PixModal } from './PixModal'; 4 | import { PixLogo } from './PixLogo'; 5 | 6 | export const DonatePix = () => { 7 | const [isModalOpen, setIsModalOpen] = useState(false); 8 | 9 | const openModal = () => { 10 | setIsModalOpen(true); 11 | }; 12 | 13 | const closeModal = () => { 14 | setIsModalOpen(false); 15 | }; 16 | 17 | return ( 18 | <> 19 | 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /apps/sserartigos/src/tweet.ts: -------------------------------------------------------------------------------- 1 | import { TwitterApi } from 'twitter-api-v2'; 2 | 3 | const client = new TwitterApi({ 4 | appKey: process.env.TWITTER_API_KEY, 5 | appSecret: process.env.TWITTER_API_KEY_SECRET, 6 | accessToken: process.env.TWITTER_ACCESS_TOKEN, 7 | accessSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET, 8 | }); 9 | 10 | const tweet = async (url: string) => { 11 | try { 12 | await client.v1.tweet(url, {}); 13 | 14 | return { 15 | ok: true, 16 | }; 17 | } catch (error) { 18 | return error; 19 | } 20 | }; 21 | 22 | export const tweetArticles = async (urls: string[]) => { 23 | const promisses = urls.map((url) => tweet(url)); 24 | 25 | await Promise.all(promisses).then((responses) => { 26 | responses.forEach(verifyIfHasError); 27 | }); 28 | 29 | return; 30 | }; 31 | 32 | function verifyIfHasError(res: Response) { 33 | if (!res.ok) throw new Error(`response from API was: ${res.status}`); 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/src/pages/api/user/[username].ts: -------------------------------------------------------------------------------- 1 | import { withSentry } from '@sentry/nextjs'; 2 | import { userProfile } from 'modules/twitter/twitterUserGet'; 3 | import { NextApiRequest, NextApiResponse } from 'next'; 4 | import { config } from '../../../config'; 5 | 6 | const userHandler = async (req: NextApiRequest, res: NextApiResponse) => { 7 | const { username } = req.query; 8 | 9 | const access_token = config.TWITTER_BEARER_TOKEN; 10 | if (!access_token) { 11 | return res.status(401).json({ 12 | message: 'Authorization required', 13 | }); 14 | } 15 | 16 | const result = await userProfile(username as string, access_token as string); 17 | 18 | if (!result.data) { 19 | return res.status(404).json({ 20 | message: 'User not found', 21 | }); 22 | } 23 | 24 | const { data: user } = result; 25 | 26 | return res.status(200).json({ 27 | user, 28 | }); 29 | }; 30 | 31 | export default withSentry(userHandler); 32 | -------------------------------------------------------------------------------- /apps/web/src/modules/db/schema/RankedTweet.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, model } from 'mongoose'; 2 | import type { RankedTweet, RankedType } from '@ccsseraphini/ranking/src/types'; 3 | 4 | const rankedTweetSchema = new Schema({ 5 | tweet_id: { 6 | type: String, 7 | required: true, 8 | }, 9 | created_at: { 10 | type: Date, 11 | required: true, 12 | }, 13 | author_id: { 14 | type: String, 15 | required: true, 16 | }, 17 | public_metrics: { 18 | type: { 19 | retweet_count: Number, 20 | reply_count: Number, 21 | like_count: Number, 22 | quote_count: Number, 23 | }, 24 | required: true, 25 | }, 26 | score: { 27 | type: Number, 28 | required: true, 29 | }, 30 | last_updated: Date, 31 | changes_since_last_update: Boolean, 32 | }); 33 | 34 | export const RankedTweetModel = 35 | (mongoose.models.rankedTweet as RankedType) || 36 | model('rankedTweet', rankedTweetSchema); 37 | -------------------------------------------------------------------------------- /apps/ranking/src/getRankedTweets.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | import getTemporaryTweets from './getTemporaryTweets'; 4 | import { RankedTweet, TemporaryTweet } from './types/index'; 5 | import getRankedTweetsFromApi from './getRankedTweetsFromApi'; 6 | 7 | const getRankedTweets = async ( 8 | date: DateTime, 9 | bearerToken: string, 10 | ): Promise => { 11 | const created_at = date.toJSDate(); 12 | 13 | try { 14 | const temporaryTweets: TemporaryTweet[] = await getTemporaryTweets({ 15 | created_at, 16 | }); 17 | 18 | if (!temporaryTweets?.length) { 19 | console.error('No tweets found in database'); 20 | return []; 21 | } 22 | 23 | const tweets = await getRankedTweetsFromApi(temporaryTweets, bearerToken); 24 | 25 | return tweets; 26 | } catch (error) { 27 | console.error('Fail to get tweets from Twitter API', error, { created_at }); 28 | return []; 29 | } 30 | }; 31 | 32 | export default getRankedTweets; 33 | -------------------------------------------------------------------------------- /apps/cli/README.md: -------------------------------------------------------------------------------- 1 | # cc @sseraphini 2 | 3 | ccsseraphini is a CLI made in TypeScript and NodeJS so you can create a tweet from your terminal by tagging the "cc @sseraphini". This will open a tab in your default browser ready for you to post your question! 4 | And it's part of [this project](https://github.com/sibelius/ccsseraphini) where you can find a website, and a chrome extension and this CLI, feel free to contribute to this project and help improve the apps. 5 | 6 | If you want to understand what is "cc @sseraphini" read the [cc @sseraphini article](https://sibelius.substack.com/p/cc-sseraphini) 7 | 8 | ## Usage 9 | 10 | You can run the CLI without installing with npx: 11 | 12 | ```bash 13 | npx ccsseraphini "How can I learn Relay?" 14 | ``` 15 | 16 | Or if you prefer, you can install it globally on your machine with the command: 17 | 18 | ```bash 19 | npm i -g ccsseraphini 20 | ``` 21 | 22 | And use as a common command in your terminal: 23 | 24 | ```bash 25 | ccsseraphini "How can I learn Relay?" 26 | ``` 27 | -------------------------------------------------------------------------------- /apps/web/src/config.tsx: -------------------------------------------------------------------------------- 1 | export const config = { 2 | NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET as string, 3 | TWITTER_BEARER_TOKEN: process.env.TWITTER_BEARER_TOKEN as string, 4 | TWITTER_CLIENT_ID: process.env.TWITTER_CLIENT_ID as string, 5 | TWITTER_CLIENT_SECRET: process.env.TWITTER_CLIENT_SECRET as string, 6 | TWITTER_PROFILE_ID: process.env.TWITTER_PROFILE_ID as string, 7 | TWITTER_PROFILE_USER: process.env.TWITTER_PROFILE_USER as string, 8 | DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID as string, 9 | DISCORD_CLIENT_SECRET: process.env.DISCORD_CLIENT_SECRET as string, 10 | DISCORD_BOT_TOKEN: process.env.DISCORD_BOT_TOKEN as string, 11 | GUILD_ID: process.env.GUILD_ID as string, 12 | WEBHOOK_DISCORD: process.env.WEBHOOK_DISCORD as string, 13 | CRONJOB_HEADER_KEY: process.env.CRONJOB_HEADER_KEY as string, 14 | CRONJOB_HEADER_VALUE: process.env.CRONJOB_HEADER_VALUE as string, 15 | DISCORD_SCORE_THRESHOLD: 100, 16 | MONGO_URI: process.env.MONGO_URI as string, 17 | TWITTER_RANKING_SIZE: 100, 18 | } as const; 19 | -------------------------------------------------------------------------------- /apps/sserartigos/src/config.ts: -------------------------------------------------------------------------------- 1 | import dotenvSafe from 'dotenv-safe'; 2 | import path from 'path'; 3 | 4 | const cwd = process.cwd(); 5 | 6 | const root = path.join.bind(cwd); 7 | 8 | dotenvSafe.config({ 9 | path: root('.env'), 10 | sample: root('.env.example'), 11 | }); 12 | 13 | export const config = { 14 | APP_URI: process.env.APP_URI || 'http://localhost:3000', 15 | 16 | DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID as string, 17 | DISCORD_BOT_TOKEN: process.env.DISCORD_BOT_TOKEN as string, 18 | GUILD_ID: process.env.GUILD_ID as string, 19 | LISTENED_USERS_ID: process.env.LISTENED_USERS_ID.split(',') as string[], 20 | GITHUB_TOKEN: process.env.GITHUB_TOKEN as string, 21 | GIT_REPO_OWNER: process.env.GIT_REPO_OWNER as string, 22 | GIT_REPO_NAME: process.env.GIT_REPO_NAME as string, 23 | ZETTELKASTEN_FILE_PATH: process.env.ZETTELKASTEN_FILE_PATH as string, 24 | GIT_REPO_BRANCH: process.env.GIT_REPO_BRANCH as string, 25 | 26 | FANSFY_API_KEY: process.env.FANSFY_API_KEY as string, 27 | 28 | PORT: 3000, 29 | } as const; 30 | -------------------------------------------------------------------------------- /apps/vscode-ext/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | }, 19 | { 20 | "name": "Extension Tests", 21 | "type": "extensionHost", 22 | "request": "launch", 23 | "args": [ 24 | "--extensionDevelopmentPath=${workspaceFolder}", 25 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 26 | ], 27 | "outFiles": [ 28 | "${workspaceFolder}/out/test/**/*.js" 29 | ], 30 | "preLaunchTask": "npm: test-watch" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /apps/bot/src/handleThreadCreation.ts: -------------------------------------------------------------------------------- 1 | import { AnyThreadChannel, TextChannel, Client, ChannelType } from 'discord.js'; 2 | import { config } from './config'; 3 | 4 | const isForumPost = (thread: AnyThreadChannel) => { 5 | return thread.parent.type === ChannelType.GuildForum; 6 | }; 7 | 8 | type ThreadCreationArgs = { 9 | thread: AnyThreadChannel; 10 | client: Client; 11 | }; 12 | 13 | export const handleThreadCreation = async ({ 14 | thread, 15 | client, 16 | }: ThreadCreationArgs) => { 17 | if (!isForumPost(thread)) return; 18 | 19 | try { 20 | const generalChannel = client.channels.cache.get( 21 | config.DISCORD_GENERAL_CHANNEL_ID, 22 | ) as TextChannel; 23 | 24 | generalChannel.send({ 25 | allowedMentions: { users: [thread.ownerId] }, 26 | content: `✨ <@${thread.ownerId}> created a new post, called <#${thread.id}>!`, 27 | }); 28 | } catch (error) { 29 | console.warn(`Could not fetch user of id: ${thread.ownerId}`); 30 | console.error(`[Error] - ${error}`); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | .pnp 7 | .pnp.js 8 | 9 | # testing 10 | coverage 11 | 12 | # next.js 13 | .next/ 14 | out/ 15 | 16 | # production 17 | build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | src/artifacts 24 | cache 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # local env files 32 | .env 33 | .env.local 34 | .env.development.local 35 | .env.test.local 36 | .env.production.local 37 | .env.prod 38 | 39 | # vercel 40 | .vercel 41 | 42 | **/public/workbox-*.js 43 | **/public/sw.js 44 | **/public/service-worker.js 45 | **/public/fallback-*.js 46 | 47 | 48 | __generated__/ 49 | 50 | tsconfig.tsbuildinfo 51 | .eslintcache 52 | 53 | .yarn/ 54 | .yarnrc.yml 55 | 56 | .turbo 57 | build/** 58 | dist/** 59 | .next/** 60 | 61 | apps/cli/lib 62 | 63 | package-lock.json 64 | 65 | test.png 66 | apps/web/public/sw.js.map 67 | globalConfig.json 68 | .vscode 69 | yarn.lock 70 | 71 | node-compile-cache/ -------------------------------------------------------------------------------- /apps/web/src/components/home/TwitterLogin.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@chakra-ui/react'; 2 | import { signIn, signOut, useSession } from 'next-auth/react'; 3 | 4 | export const TwitterLogin = () => { 5 | const { data: session } = useSession(); 6 | 7 | type ButtonProps = { 8 | [index: string]: { 9 | onClick: () => Promise; 10 | colorScheme: string; 11 | text: string; 12 | }; 13 | }; 14 | 15 | const buttonProps: ButtonProps = { 16 | login: { 17 | // @ts-ignore 18 | onClick: () => { 19 | signIn('twitter'); 20 | }, 21 | colorScheme: 'twitter', 22 | text: 'Login with Twitter', 23 | }, 24 | logout: { 25 | onClick: () => signOut(), 26 | colorScheme: 'red', 27 | text: 'Logout with Twitter', 28 | }, 29 | }; 30 | 31 | const sessionMode = !!session ? 'logout' : 'login'; 32 | const props = buttonProps[sessionMode]; 33 | 34 | return ( 35 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /apps/sseraresume/src/github/index.ts: -------------------------------------------------------------------------------- 1 | import { appendLinesToFile } from '../common/utils/appendLinesToFile'; 2 | import { gitRepoInfo as repoInfo, ocktokit } from './client'; 3 | import { getFile } from './services/getFile'; 4 | 5 | const commitChangedFile = async (commit: { 6 | newFile: string; 7 | commitMessage: string; 8 | commitSha: string; 9 | }) => { 10 | await ocktokit.repos.createOrUpdateFileContents({ 11 | ...repoInfo, 12 | message: commit.commitMessage, 13 | content: commit.newFile, 14 | sha: commit.commitSha, 15 | branch: repoInfo.branch, 16 | }); 17 | }; 18 | 19 | export const createCommitToZettelkastenFile = async ( 20 | commitMessage: string, 21 | linesToAppend: string[], 22 | ) => { 23 | const file = await getFile(repoInfo); 24 | const newFile = appendLinesToFile(file.content, linesToAppend); 25 | const commitInfo = { 26 | newFile, 27 | commitMessage, 28 | commitSha: file.sha, 29 | }; 30 | 31 | commitChangedFile(commitInfo) 32 | .then((_) => console.log('commited')) 33 | .catch((e) => console.error(e)); 34 | }; 35 | -------------------------------------------------------------------------------- /apps/sserartigos/src/github/index.ts: -------------------------------------------------------------------------------- 1 | import { appendLinesToFile } from '../common/utils/appendLinesToFile'; 2 | import { gitRepoInfo as repoInfo, ocktokit } from './client'; 3 | import { getFile } from './services/getFile'; 4 | 5 | const commitChangedFile = async (commit: { 6 | newFile: string; 7 | commitMessage: string; 8 | commitSha: string; 9 | }) => { 10 | await ocktokit.repos.createOrUpdateFileContents({ 11 | ...repoInfo, 12 | message: commit.commitMessage, 13 | content: commit.newFile, 14 | sha: commit.commitSha, 15 | branch: repoInfo.branch, 16 | }); 17 | }; 18 | 19 | export const createCommitToZettelkastenFile = async ( 20 | commitMessage: string, 21 | linesToAppend: string[], 22 | ) => { 23 | const file = await getFile(repoInfo); 24 | const newFile = appendLinesToFile(file.content, linesToAppend); 25 | const commitInfo = { 26 | newFile, 27 | commitMessage, 28 | commitSha: file.sha, 29 | }; 30 | 31 | commitChangedFile(commitInfo) 32 | .then((_) => console.log('commited')) 33 | .catch((e) => console.error(e)); 34 | }; 35 | -------------------------------------------------------------------------------- /apps/web/src/modules/score/calculateUserScore.ts: -------------------------------------------------------------------------------- 1 | import { UserScore, Tweet } from 'types/Score'; 2 | import { getEmptyScore } from './getEmptyScore'; 3 | 4 | export function calculateUserScore(tweets: Tweet[]): UserScore { 5 | const { length: tweet_count } = tweets; 6 | 7 | return tweets.reduce( 8 | ( 9 | accumulator: UserScore, 10 | { 11 | public_metrics: { retweet_count, reply_count, like_count, quote_count }, 12 | }: Tweet, 13 | ): UserScore => { 14 | const retweets = retweet_count + accumulator.retweet_count; 15 | const replies = reply_count + accumulator.reply_count; 16 | const likes = like_count + accumulator.like_count; 17 | const quotes = quote_count + accumulator.quote_count; 18 | const total = tweet_count + retweets + replies + likes + quotes; 19 | 20 | return { 21 | tweet_count, 22 | retweet_count: retweets, 23 | reply_count: replies, 24 | like_count: likes, 25 | quote_count: quotes, 26 | total, 27 | }; 28 | }, 29 | getEmptyScore(), 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/src/middlewares/validations/__tests__/validations-cron-job-secret-key.test.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { config } from 'config'; 3 | import flood from 'pages/api/discord/flood'; 4 | 5 | test('CronjobOrg should provide secretkey in the header', async () => { 6 | Object.defineProperties(config, { 7 | CRONJOB_HEADER_KEY: { 8 | value: 'X-Cronjob-Org-Secretkey', 9 | }, 10 | CRONJOB_HEADER_VALUE: { 11 | value: '2313873457', 12 | }, 13 | }); 14 | 15 | const req = { 16 | method: 'POST', 17 | headers: { 18 | 'X-Cronjob-Org-Secretkey': undefined, 19 | }, 20 | } as Partial; 21 | 22 | const res = { 23 | status: jest.fn(() => res), 24 | send: jest.fn(), 25 | } as Partial; 26 | 27 | await flood(req as NextApiRequest, res as NextApiResponse).catch( 28 | (error: Error) => error, 29 | ); 30 | 31 | expect(res.send).toBeCalledWith( 32 | 'CronjobOrg should provide secretkey in the header', 33 | ); 34 | expect(res.status).toBeCalledWith(400); 35 | }); 36 | -------------------------------------------------------------------------------- /apps/ranking/src/__tests__/countNewAuthors.test.ts: -------------------------------------------------------------------------------- 1 | import { clearDbAndRestartCounters } from '../../test/clearDbAndRestartCounters'; 2 | import { connectMongoose } from '../../test/connectMongoose'; 3 | import { disconnectMongoose } from '../../test/disconnectMongoose'; 4 | import { countNewAuthors } from '../countNewAuthors'; 5 | import { RankedTweetModel } from '../schema/RankedTweet'; 6 | import createFakeRankedTweets from '../__mocks__/createFakeRankedTweets'; 7 | 8 | beforeAll(connectMongoose); 9 | beforeEach(clearDbAndRestartCounters); 10 | afterAll(disconnectMongoose); 11 | 12 | it('should have only one new author', async () => { 13 | const created_at = new Date('2022-08-11T19:08:00.000Z'); 14 | const rankedTweets1 = createFakeRankedTweets(created_at, 1); 15 | const rankedTweets2 = createFakeRankedTweets( 16 | new Date('2022-07-30T19:08:00.000Z'), 17 | 1, 18 | ); 19 | 20 | await RankedTweetModel.insertMany([rankedTweets1, rankedTweets2].flat()); 21 | 22 | const { newAuthors } = await countNewAuthors(created_at, new Date()); 23 | 24 | expect(newAuthors).toBe(1); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/ranking/src/findRankedTweetsForSync.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon'; 2 | 3 | import { RankedTweet } from './types/index'; 4 | import { RankedTweetModel } from './schema/RankedTweet'; 5 | 6 | const findRankedTweetsForSync = async ( 7 | date: DateTime = DateTime.now(), 8 | ): Promise => { 9 | const twoDaysAgo = date.endOf('day').minus({ days: 2 }).toJSDate(); 10 | const oneMonthAgo = date.endOf('day').minus({ months: 1 }).toJSDate(); 11 | 12 | const tweets: RankedTweet[] = await RankedTweetModel.find({ 13 | changes_since_last_update: { 14 | $ne: false, 15 | }, 16 | $or: [ 17 | { 18 | last_updated: { 19 | $lte: twoDaysAgo, 20 | }, 21 | created_at: { 22 | $gte: oneMonthAgo, 23 | }, 24 | }, 25 | { 26 | last_updated: { 27 | $eq: null, 28 | }, 29 | created_at: { 30 | $lte: twoDaysAgo, 31 | $gte: oneMonthAgo, 32 | }, 33 | }, 34 | ], 35 | }); 36 | 37 | return tweets; 38 | }; 39 | 40 | export default findRankedTweetsForSync; 41 | -------------------------------------------------------------------------------- /apps/web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /apps/web/src/modules/twitter/twitterFollowersGet.ts: -------------------------------------------------------------------------------- 1 | import { getTwitterAuthorization } from './getTwitterAuthorization'; 2 | 3 | type Result = { 4 | data: Tweet[]; 5 | meta: { 6 | result_count: number; 7 | next_token?: string; 8 | }; 9 | }; 10 | 11 | type Tweet = { 12 | text: string; 13 | in_reply_to_user_id: string; 14 | public_metrics: { 15 | retweet_count: number; 16 | reply_count: number; 17 | like_count: number; 18 | quote_count: number; 19 | }; 20 | }; 21 | 22 | export const userTweets = async ( 23 | userId: string, 24 | accessToken: string, 25 | ): Promise => { 26 | const url = `https://api.twitter.com/2/users/${userId}/tweets?tweet.fields=public_metrics,in_reply_to_user_id&max_results=100`; 27 | const options = { 28 | headers: { 29 | Accept: 'application/json', 30 | 'Content-Type': 'application/json', 31 | Authorization: getTwitterAuthorization(accessToken), 32 | }, 33 | method: 'GET', 34 | }; 35 | const response = await fetch(url, options); 36 | const data = await response.json(); 37 | 38 | return data; 39 | }; 40 | -------------------------------------------------------------------------------- /apps/bot/README.md: -------------------------------------------------------------------------------- 1 | # cc sseraphini bot 2 | 3 | ## How to setup cc sseraphini bot 4 | 5 | 1. Create a bot and retrieve the token. [How to Get a Discord Bot Token](https://www.writebots.com/discord-bot-token/). It should have the following permissions: 6 | 7 | - Send Messages 8 | - Read Message History 9 | 10 | 1. Add token to `.env` file with name `DISCORD_TOKEN`. 11 | 12 | 1. Get channel id from Discord and add it to `.env` file with name `DISCORD_CHANNEL_ID`. 13 | 14 | 1. Get general channel id from Discord and add it to `.env` file with name `DISCORD_GENERAL_CHANNEL_ID`. 15 | 16 | 1. Go to Twitter developer portal and retrieve the tokens and save them to `.env`: 17 | 18 | - `TWITTER_ACCESS_TOKEN` 19 | - `TWITTER_ACCESS_TOKEN_SECRET` 20 | - `TWITTER_API_KEY` 21 | - `TWITTER_API_KEY_SECRET` 22 | 23 | 1. You can use same twitter credentials to fill tweet ranking envs too 24 | - `TWITTER_RANKING_ACCESS_TOKEN` 25 | - `TWITTER_RANKING_ACCESS_TOKEN_SECRET` 26 | - `TWITTER_RANKING_API_KEY` 27 | - `TWITTER_RANKING_API_KEY_SECRET` 28 | 29 | ## How to run 30 | 31 | ``` 32 | pnpm start 33 | ``` 34 | -------------------------------------------------------------------------------- /apps/ranking/src/getTwitterClient.ts: -------------------------------------------------------------------------------- 1 | import { TwitterApi } from 'twitter-api-v2'; 2 | 3 | const configValidator = ( 4 | config: Record, 5 | keys: string[], 6 | ): boolean => 7 | keys.reduce((valid, key, index) => { 8 | if (!config[key]) { 9 | console.error(`${keys[index]} is missing`); 10 | return false; 11 | } 12 | 13 | return valid; 14 | }, true); 15 | 16 | const getTwitterClient = async ( 17 | config: Record, 18 | ): Promise => { 19 | const neededKeys = [ 20 | 'TWITTER_RANKING_API_KEY', 21 | 'TWITTER_RANKING_API_KEY_SECRET', 22 | 'TWITTER_RANKING_ACCESS_TOKEN', 23 | 'TWITTER_RANKING_ACCESS_TOKEN_SECRET', 24 | ]; 25 | 26 | if (!configValidator(config, neededKeys)) return; 27 | 28 | const client = new TwitterApi({ 29 | appKey: config.TWITTER_API_KEY, 30 | appSecret: config.TWITTER_API_KEY_SECRET, 31 | accessToken: config.TWITTER_ACCESS_TOKEN, 32 | accessSecret: config.TWITTER_ACCESS_TOKEN_SECRET, 33 | }); 34 | 35 | return client; 36 | }; 37 | 38 | export default getTwitterClient; 39 | -------------------------------------------------------------------------------- /apps/sseramemes/src/getMessageContent.ts: -------------------------------------------------------------------------------- 1 | import { Message, PartialMessage } from 'discord.js'; 2 | 3 | export const getMessageContent = async (message: Message | PartialMessage) => { 4 | const discordContent = message.content; 5 | 6 | /** 7 | * Return all strings with <@123345456789> pattern. 8 | */ 9 | const discordUsersPattern = discordContent.match(/<\@\d{1,}>/gm) || []; 10 | 11 | let finalContent = discordContent; 12 | 13 | for (const discordUserPattern of discordUsersPattern) { 14 | const discordUserId = discordUserPattern.replace(/[<@>]/g, ''); 15 | 16 | let replacer: string; 17 | 18 | try { 19 | const discordUser = await message.client.users.fetch(discordUserId); 20 | 21 | replacer = discordUser?.username ? `@${discordUser.username}` : ''; 22 | } catch { 23 | replacer = ''; 24 | } 25 | 26 | finalContent = finalContent.replace(discordUserPattern, replacer); 27 | } 28 | 29 | return finalContent.substring(0, 280); 30 | }; 31 | 32 | export const getAltText = (message: string) => { 33 | return message.match(/(?<=\(alt:)(.*)(?=\))/g)?.[0]?.trim(); 34 | }; 35 | -------------------------------------------------------------------------------- /apps/web/src/pages/api/getTwitterUsers.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'config'; 2 | import { TwitterResponse, TwitterUser } from 'types/Ranking'; 3 | 4 | const BASE_URL = 'https://api.twitter.com/2'; 5 | const USERS_URL = 'users'; 6 | const USER_FIELDS = 'profile_image_url,username,name'; 7 | 8 | export const getTwitterUsers = async ( 9 | ids: string[], 10 | ): Promise => { 11 | const bearerToken = config.TWITTER_BEARER_TOKEN; 12 | const idString = ids.join(','); 13 | const url = `${BASE_URL}/${USERS_URL}?ids=${idString}&user.fields=${USER_FIELDS}`; 14 | 15 | try { 16 | const response = await fetch(url, { 17 | method: 'GET', 18 | headers: { 19 | Authorization: `Bearer ${bearerToken}`, 20 | }, 21 | }); 22 | 23 | if (!response.ok) { 24 | return []; 25 | } 26 | 27 | const twitterData = (await response.json()) as TwitterResponse; 28 | if (twitterData === null || !Array.isArray(twitterData.data)) { 29 | return []; 30 | } 31 | 32 | return twitterData.data; 33 | } catch (error) { 34 | console.error('Error getting ranking:', error); 35 | return []; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /apps/ranking/src/__tests__/saveTemporaryTweet.test.ts: -------------------------------------------------------------------------------- 1 | import { TemporaryTweet } from '../types'; 2 | import { clearDbAndRestartCounters } from '../../test/clearDbAndRestartCounters'; 3 | import { connectMongoose } from '../../test/connectMongoose'; 4 | import { disconnectMongoose } from '../../test/disconnectMongoose'; 5 | import { Data, Tweet } from '../types/index'; 6 | import saveTemporaryTweet from '../saveTemporaryTweet'; 7 | 8 | beforeAll(connectMongoose); 9 | beforeEach(clearDbAndRestartCounters); 10 | afterAll(disconnectMongoose); 11 | 12 | it('should save temporary tweet', async () => { 13 | const data: Data = { 14 | attachments: {}, 15 | author_id: '2244994945', 16 | public_metrics: { 17 | retweet_count: 304, 18 | reply_count: 152, 19 | like_count: 1012, 20 | quote_count: 117, 21 | }, 22 | created_at: '2021-11-15T19:08:05.000Z', 23 | id: '1460323737035677698', 24 | }; 25 | 26 | const tweet: Partial = { 27 | data, 28 | }; 29 | 30 | const { _id } = (await saveTemporaryTweet( 31 | tweet as Tweet, 32 | )) as TemporaryTweet & { _id: any }; 33 | 34 | expect(_id).toBeDefined(); 35 | }); 36 | -------------------------------------------------------------------------------- /apps/bot/scripts/discord/discordMe.ts: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch'; 2 | import { isMainScript } from '../isMainScript'; 3 | 4 | const run = async () => { 5 | const authorization = process.env.DISCORD_AUTHORIZATION; 6 | 7 | if (!authorization) { 8 | // eslint-disable-next-line 9 | console.log('DISCORD_AUTHORIZATION is not set'); 10 | // eslint-disable-next-line 11 | console.log('export DISCORD_AUTHORIZATION="Bearer "'); 12 | return; 13 | } 14 | 15 | const url = `http://discord.com/api/users/@me`; 16 | const options = { 17 | headers: { 18 | Accept: 'application/json', 19 | 'Content-Type': 'application/json', 20 | Authorization: authorization, 21 | }, 22 | method: 'GET', 23 | }; 24 | const response = await fetch(url, options); 25 | const data = await response.json(); 26 | 27 | console.log({ 28 | data, 29 | }); 30 | }; 31 | 32 | (async () => { 33 | if (!isMainScript(require, module)) { 34 | return; 35 | } 36 | try { 37 | await run(); 38 | } catch (err) { 39 | // eslint-disable-next-line 40 | console.log('err: ', err); 41 | process.exit(1); 42 | } 43 | process.exit(0); 44 | })(); 45 | -------------------------------------------------------------------------------- /apps/ui/src/TweetComposer.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Box, Button, Textarea, Badge } from '@chakra-ui/react'; 3 | import { FaTwitter } from 'react-icons/fa'; 4 | 5 | export const TweetComposer = () => { 6 | const [text, setText] = useState(''); 7 | const suffix = '\ncc @sseraphini'; 8 | const counter = 279 - suffix.length - text.length; 9 | const tweet = encodeURIComponent(`${text}${suffix}`); 10 | 11 | return ( 12 | <> 13 |