├── .env.example ├── .github ├── CODEOWNERS ├── SECURITY.md ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .yamllint ├── CONTRIBUTING.md ├── Procfile ├── README.md ├── app.js ├── commands ├── call.js ├── not-found.js ├── reason.js └── restart.js ├── db.js ├── endpoints ├── index.js ├── ping.js ├── slack-invite.js ├── slack-tutorial.js └── start-from-clippy.js ├── express-receiver.js ├── image_pan_slice.sh ├── index.js ├── interactions ├── cleanup-cave.js ├── ensure-channels.js ├── handle-rummage.js ├── init-rummage.js ├── join-cave.js ├── post-welcome-committee.js └── startup.js ├── manifest.yml ├── package.json ├── prisma ├── migrations │ ├── 20220406171246_init │ │ └── migration.sql │ ├── 20220406184950_add_timestamps_to_invite │ │ └── migration.sql │ ├── 20220406190104_default_invite_updated_at │ │ └── migration.sql │ ├── 20220725195106_add_user_model │ │ └── migration.sql │ ├── 20220801141028_add_invite_user_id │ │ └── migration.sql │ ├── 20240228193737_identify_users │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── setup └── cave-channel.js ├── util ├── alert.js ├── channel-is-active.js ├── get-invite.js ├── invite-types │ ├── default.js │ ├── hcb.js │ └── onboard.js ├── invite-user-to-channel.js ├── invite-user.js ├── metrics.js ├── mirror-message.js ├── notify-channel.js ├── sleep.js ├── transcript.js ├── transcript.yml └── upgrade-user.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | AUTH_TOKEN= 2 | DATABASE_URL= 3 | HEROKU_POSTGRESQL_BLUE_URL= 4 | SLACK_BOT_TOKEN= 5 | OLD_SLACK_INVITE_TOKEN= 6 | SLACK_BROWSER_TOKEN= 7 | SLACK_COOKIE= 8 | SLACK_LEGACY_TOKEN= 9 | SLACK_SIGNING_SECRET= 10 | OPENAI_KEY= 11 | SHADOW_DATABASE_URL= -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @radioblahaj @aboutdavid 2 | 3 | prisma/* @maxwofford @radioblahaj 4 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | Hack Club takes the security and privacy of Hack Clubbers very seriously, regardless of jurisdiction, as in, we believe everyone has the right to control their data. 4 | 5 | If you have any security concerns or would like to report security issues, please email toriel@hackclub.com. You should receive a response within 24 hours. If not, or if it is very urgent, you can [join our Slack](https://hackclub.com/slack) and mention that you found a security issue in Toriel. 6 | 7 | Please provide as much information as you can, including: 8 | - Commit version (`git rev-parse --short HEAD`) (if self-hosting) 9 | - Step by step instructions on how to reproduce it 10 | - Proof of concept (if possible) 11 | - Impact/security risk of the issue 12 | 13 | We believe in the [Coordinated Vulnerability Disclosure](https://www.cisa.gov/coordinated-vulnerability-disclosure-process) process. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'weekly' 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | # yamllint: 12 | # name: YAML Lint 13 | # runs-on: ubuntu-latest 14 | # steps: 15 | # - name: Check out code 16 | # uses: actions/checkout@v2 17 | # - name: Run yamllint 18 | # uses: actionshub/yamllint@main 19 | 20 | # mdl: 21 | # name: Markdown Lint 22 | # runs-on: ubuntu-latest 23 | # steps: 24 | # - name: Check out code 25 | # uses: actions/checkout@v2 26 | # - name: Run Markdown Lint 27 | # uses: actionshub/markdownlint@main 28 | # with: 29 | # filesToIgnoreRegex: '((old|node_modules)\/.*)' 30 | 31 | prettier: 32 | name: Prettier 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v2 37 | - name: Run prettier 38 | uses: actionsx/prettier@v2 39 | with: 40 | # Prettier CLI arguments 41 | args: --check "**/*.js" 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.env 2 | node_modules/ 3 | package-lock.json 4 | pnpm-lock.yaml 5 | transcript.dev.yml -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | old/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "printWidth": 80, 5 | "semi": false, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | line-length: enable 6 | ignore: | 7 | old/ 8 | node_modules/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for wanting to contribute to TORIEL! After all, anyone is welcome to contribute 4 | 5 | ## Code of Conduct 6 | 7 | This project, as well as others, are governed by the [Hack Club Code of Conduct](https://hackclub.com/conduct/). You are encourages to report any violations of the Code of Conduct to conduct@hackclub.com, and we will try and follow up/start taking care of it within 48-hours. 8 | 9 | ## Questions 10 | 11 | If you have a question, you are encouraged to ask them in [#toriel-dev](https://hackclub.slack.com/archives/C02B7CWDD0E) on our [Slack](https://hackclub.com/slack) 12 | 13 | ## Bugs 14 | You can use [GitHub issues](https://github.com/hackclub/toriel/issues) to report bugs, or ask them in [#toriel-dev](https://hackclub.slack.com/archives/C02B7CWDD0E). 15 | 16 | Before submitting a report, please make sure you checked and have done the following: 17 | 18 | ### Using it in Slack 19 | - Included screenshots/screen recordings of the bug. 20 | - Explain what you were doing right before the bug occured 21 | 22 | ### Self-Hosting 23 | - Checked your Node.js version 24 | - Checked if you are using a recent commit 25 | - Included the Operating System and version, 26 | - Included logs given by the bot. 27 | 28 | ## Security Issues 29 | Please **do not submit security issues via GitHub issues**. This is because Toriel has access to very sensitive data, such as IP addresses, E-mails, and other personally identifiable information (PII). Please notify the Toriel development team privately at toriel@hackclub.com. Read more in our [security policy](https://github.com/hackclub/toriel/blob/main/.github/SECURITY.md) 30 | 31 | ## Setting up a development environment 32 | There are [instructions in the README.md](https://github.com/hackclub/toriel#running-locally) on how to set it up. -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: npx prisma migrate deploy 2 | web: npm start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Toriel 2 | 3 | ![Bot status](https://img.shields.io/website?url=https%3A%2F%2Ftoriel.hackclub.com%2Fping&label=bot) 4 | 5 | > Toriel is currently undergoing a new revamp! You can view the stuff being worked on [here](https://docs.google.com/document/d/13AYCps0_hWMG6lolBcIg3xGiTOch2rk5uViSghfRzZE/edit?usp=sharing), and take part in #toriel-dev, if you're on the slack 6 | 7 | _Just a lil' ol greeter bot to direct new members joining the Hack Club Slack. You wonder what she might say..._ 8 | 9 | _Toriel is a fork of [Clippy](https://github.com/hackclub/clippy)._ 10 | 11 | ## Running locally 12 | 13 | Contributions are encouraged and welcome! 14 | 15 | In order to run Toriel locally, you'll need to [join the Hack Club Slack](https://hackclub.com/slack). From there, ask @creds to be added to the Toriel app on Slack. 16 | 17 | 1. Clone this repository 18 | `git clone https://github.com/hackclub/toriel && cd toriel` 19 | 2. Install [ngrok](https://dashboard.ngrok.com/get-started/setup) (if you haven't already) 20 | 3. Install dependencies 21 | `npm install` 22 | 4. Create `.env` file 23 | - `touch .env` 24 | - Send a message mentioning `@creds` in [Hack Club's Slack](https://hackclub.com/slack/) asking for the `.env` file 25 | 5. Start server 26 | `npm run dev` 27 | 6. Forward your local server to [underpass](https://github.com/cjdenio/underpass), an open source alternative made by a Hack Club team member. 28 | 7. Update the settings in the [manifest.yml](https://github.com/hackclub/toriel/blob/main/manifest.yml) 29 | - Change the slash command and event endpoints by replacing `https://toriel.hackclub.com` with your ngrok URL 30 | - You can find your ngrok URL in the terminal where you ran `ngrok http 3000`. It would look similar to this (your ngrok URL will be different): 31 | ![Screenshot of ngrok running](https://cloud-mt3q3pxrm-hack-club-bot.vercel.app/0ngrok.png) 32 | - Take this URL and replace it with all `https://toriel.hackclub.com/slack/events` URLs (the [first slash command url](https://github.com/hackclub/toriel/blob/922eb46862a472bc36d90a45cdb804741ff60d2e/manifest.yml#L14), [second slash command url](https://github.com/hackclub/toriel/blob/922eb46862a472bc36d90a45cdb804741ff60d2e/manifest.yml#L18), [event url](https://github.com/hackclub/toriel/blob/922eb46862a472bc36d90a45cdb804741ff60d2e/manifest.yml#L39), and [interactivity url](https://github.com/hackclub/toriel/blob/922eb46862a472bc36d90a45cdb804741ff60d2e/manifest.yml#L46). Include `/slack/events` so your new URLs would be `your-ngrok-URL/slack/events`. 33 | - Change the app and bot name from `TORIEL` to something that doesn't start with `T` (ex. `msw-dev-toriel`). This prevents Slack's auto-suggestions from confusing new users. 34 | ![Screenshot of where to change name](https://cloud-mrhdyhr0u-hack-club-bot.vercel.app/0name.png) 35 | - Change the slash commands from `/toriel-COMMAND` to something that doesn't start with `t` (ex. `/toriel-restart` -> `/msw-dev-restart`). 36 | ![Screenshot of the location of slash command](https://cloud-hmei7opsz-hack-club-bot.vercel.app/0slash.png) 37 | 8. Update slash commands 38 | - Go to [index.js](index.js) and find the following line 39 | ![Screenshot of /toriel-restart and /toriel-call](https://cloud-ceuonqm0d-hack-club-bot.vercel.app/0screenshot_2022-04-18_at_10.38.26_pm.png) 40 | - Change `/toriel-restart` and `/toriel-call` to your own slash commands like `msw-dev-restart` 41 | 9. Go to [api.slack.com](https://api.slack.com/apps?new_app=1) to create a new slack app 42 | - Select "from an [app manifest](https://api.slack.com/reference/manifests)" 43 | ![Screenshot of slack app options](https://cloud-kqknb2w6y-hack-club-bot.vercel.app/0screenshot_2022-04-18_at_6.15.25_pm.png) 44 | When prompted, make sure you select the Hack Club Workspace (you might need to sign in). 45 | - On the next modal, replace the demo manifest with your edited [manifest.yml](manifest.yml) 46 | ![Screenshot of manifest modal](https://user-images.githubusercontent.com/621904/164060319-e79851ac-f29b-463e-a32b-9bc5968ce8db.png) 47 | 10. Click "Install to Workspace" under the "Basic Information" tab in the settings bar 48 | ![Screenshot of installing to workspace](https://user-images.githubusercontent.com/621904/164061251-2f7fc9ef-3c07-482d-83f7-86f5798d77ad.png) 49 | 11. Edit the env variables in [.env](.env) file. 50 | ``` 51 | SLACK_SIGNING_SECRET=SIGNING_SECRET 52 | SLACK_BOT_TOKEN=Bot_User_OAuth_Token 53 | ``` 54 | where `SIGNING_SECRET` is your app's Signing Secret, which you can find by clicking on "Basic Information" in the settings bar and scrolling down (click show to and copy it) 55 | ![Screenshot of where to click show and copy the signing secret](https://cloud-j9zzknpea-hack-club-bot.vercel.app/0screenshot_2022-04-18_at_6.49.53_pm.png) 56 | and `Bot_User_OAuth_Token` is found under "OAuth & Permissions" (You will need click "Install to Workspace" before you can view the token) 57 | ![Screenshot of where the token is](https://cloud-twxncowk1-hack-club-bot.vercel.app/0screenshot_2022-04-18_at_7.00.44_pm.png) 58 | 12. [Create a private channel](https://slack.com/help/articles/201402297-Create-a-channel) where your app can run the welcome flow. Similar to the role that #in-the-cave plays for Toriel (run `toriel-restart` in slack to see what this means). 59 | - Invite your bot to that channel (you can @mention it to add) 60 | 13. Edit [transcript.yml](/util/transcript.yml). If you want to make your own local transcript.yml with your own channels for testing, call it `transcript.dev.yaml`, and it will load from that instead, however, it won't be synced to production. 61 | 62 | - The key:value pairs under `channels:` represent `channel-name:channel-id` and these are referred to elsewhere in the codebase with `{channels.channel-name}` 63 | 64 | ![Screenshot of channel list](https://cloud-5prq93r05-hack-club-bot.vercel.app/0screenshot_2022-04-18_at_9.12.10_pm.png) 65 | 66 | - You can add your own bot to these channels for testing **or** you can delete or comment out channels you don't need for testing 67 | 68 | > IMPORTANT: you might need access to these channels later on, like to add a [new user to channels](/util/invite-user.js), which means not giving your bot access now could break the app. 69 | 70 | - Add your own private channel to the channel list. For example, if your channel is `msw-test-cave` with the channel id `CDF1A5EG865`, you would add: 71 | ``` 72 | msw-test-cave: CDF1A5EG865 73 | ``` 74 | - You can find the channel ID by opening the channel and clicking on the channel name in the top left. Scroll down the modal and you should see it in the bottom left corner 75 | ![Screenshot of channel ID](https://user-images.githubusercontent.com/621904/164070484-d3d4f57a-546f-4d60-b800-7c052a3bcbcf.png) 76 | 77 | 14. Do a global find and replace in the codebase to update `channels.cave` to `channels.name-of-your-private-channel` 78 | - As `{channels.cave}` refers to `#in-the-cave`, we need to replace it with the private channel (ex. `{channels.msw-test-cave}`) that you created for your bot 79 | 15. Run `npm run dev` again and also reinstall your app to the workspace (under Basic Information) 80 | ![Screenshot of reinstall your app page](https://cloud-8uduk6deq-hack-club-bot.vercel.app/0screenshot_2022-04-18_at_9.38.48_pm.png) 81 | 82 | If you run into an error where the message reads `Toriel is not invited to these channels` or `channel_not_found`, just invite your bot to that channel (you can check the channel with its ID by referring back to [transcript.yml](/util/transcript.yml). If the channel is private, you can create a new private channel as its substitute, but remember to update the references in the code. Ex. `#toriels-diary` is a private channel, so you can create `#msw-toriels-diary`, add it to [transcript.yml](/util/transcript.yml), and change all `{channels.toriels-diary}` to `{channels.msw-toriels-diary}`. 83 | 84 | _Note: you have to re-update the Underpass tunnel URL on the Slack app manifest and verify the URL each time to restart the server, since the tunnel URL changes_ 85 | 86 | **Formatting** is important, please run `npm run fmt` on contribution. 87 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const { App } = require('@slack/bolt') 2 | const { receiver } = require('./express-receiver') 3 | 4 | const app = new App({ 5 | token: process.env.SLACK_BOT_TOKEN, 6 | signingSecret: process.env.SLACK_SIGNING_SECRET, 7 | receiver, 8 | }) 9 | 10 | module.exports = { app, client: app.client } 11 | -------------------------------------------------------------------------------- /commands/call.js: -------------------------------------------------------------------------------- 1 | const { sleep } = require('../util/sleep') 2 | const { transcript } = require('../util/transcript') 3 | const { metrics } = require('../util/metrics') 4 | 5 | async function call({ respond }) { 6 | metrics.increment('events.commands.run.call', 1) 7 | const messageToSend = transcript('command.cell') 8 | const messageLines = messageToSend.split('\n') 9 | for (let i = 0; i < messageLines.length; i++) { 10 | let line = messageLines[i] 11 | if (line != '') { 12 | await sleep(line.length * 15) 13 | 14 | await respond({ 15 | text: line, 16 | }) 17 | } 18 | } 19 | } 20 | module.exports = call 21 | -------------------------------------------------------------------------------- /commands/not-found.js: -------------------------------------------------------------------------------- 1 | const { transcript } = require('../util/transcript') 2 | const { metrics } = require('../util/metrics') 3 | async function notFound({ respond }) { 4 | metrics.increment('events.commands.run.notfound', 1) 5 | await respond({ 6 | text: transcript('command.not-found'), 7 | }) 8 | } 9 | module.exports = notFound 10 | -------------------------------------------------------------------------------- /commands/reason.js: -------------------------------------------------------------------------------- 1 | const { prisma } = require('../db') 2 | const { transcript } = require('../util/transcript') 3 | 4 | async function reason(args) { 5 | const { payload, respond } = args 6 | const { text, channel_id } = payload 7 | // check that we're in the welcome committee channel 8 | if (channel_id != transcript('channels.welcome-committee')) { 9 | await respond({ text: transcript('command.reason.wrong-channel') }) 10 | return 11 | } 12 | 13 | if (!text) { 14 | await respond({ text: transcript('command.reason.no-reason') }) 15 | return 16 | } 17 | 18 | const userRegex = /<@([A-Za-z0-9]+)\|.+>/i 19 | const userMatches = text.match(userRegex) 20 | const foundUser = userMatches ? userMatches[1] : null 21 | if (!foundUser || foundUser == '') { 22 | await respond({ text: transcript('command.reason.no-user') }) 23 | } 24 | 25 | const invite = await prisma.invite.findFirst({ 26 | where: { user_id: foundUser }, 27 | }) 28 | const reason = invite?.welcome_message 29 | 30 | await respond({ text: transcript('command.reason.success', { reason }) }) 31 | } 32 | 33 | module.exports = reason 34 | -------------------------------------------------------------------------------- /commands/restart.js: -------------------------------------------------------------------------------- 1 | const { joinCaveInteraction } = require('../interactions/join-cave') 2 | const { transcript } = require('../util/transcript') 3 | const { metrics } = require('../util/metrics') 4 | 5 | async function restart(args) { 6 | metrics.increment('events.commands.run.restart', 1) 7 | 8 | const { payload, client, respond } = args 9 | const { user_id, text } = payload 10 | 11 | let userToReset = user_id 12 | 13 | const userRegex = /<@([A-Za-z0-9]+)\|.+>/i 14 | const userMatches = text.match(userRegex) 15 | const foundUser = userMatches ? userMatches[1] : null 16 | if (foundUser && foundUser != '') { 17 | const callingUser = await client.users.info({ 18 | user: user_id, 19 | }) 20 | 21 | if (callingUser.user.is_admin || callingUser.user.is_owner) { 22 | userToReset = foundUser 23 | 24 | respond({ 25 | text: `resetting <@${userToReset}>`, 26 | }) 27 | } else { 28 | // no permissions– skip 29 | respond({ 30 | text: 'Only admins and owners can reset another user', 31 | }) 32 | return null 33 | } 34 | } else { 35 | respond({ 36 | text: `resetting your tutorial... head to <#${transcript( 37 | 'channels.cave' 38 | )}>`, 39 | }) 40 | } 41 | 42 | await client.chat.postMessage({ 43 | text: 'restarting...', 44 | channel: userToReset, 45 | // icon_url: transcript('startup.avatar') 46 | }) 47 | 48 | const joinArgs = args 49 | joinArgs.payload.channel = transcript('channels.cave') 50 | joinArgs.payload.user = userToReset 51 | joinCaveInteraction(args) 52 | } 53 | module.exports = restart 54 | -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client') 2 | const { metrics } = require('./util/metrics') 3 | 4 | const operationToType = { 5 | findUnique: 'find', 6 | findUniqueOrThrow: 'find', 7 | findFirst: 'find', 8 | findFirstOrThrow: 'find', 9 | findMany: 'find', 10 | create: 'create', 11 | createMany: 'create', 12 | update: 'update', 13 | updateMany: 'update', 14 | upsert: 'update', 15 | delete: 'delete', 16 | deleteMany: 'delete', 17 | aggregate: 'find', 18 | groupBy: 'find', 19 | count: 'find', 20 | findRaw: 'find', 21 | } 22 | 23 | const prisma = new PrismaClient().$extends({ 24 | // extend prisma client 25 | // to send query metrics such as latency & failures 26 | query: { 27 | async $allOperations({ operation, model, args, query }) { 28 | const metricKey = `${operation}_${model}` 29 | console.log(metricKey, operationToType[operation]) 30 | try { 31 | const start = performance.now() 32 | const queryResult = await query(args) 33 | const time = performance.now() - start 34 | 35 | metrics.timing(`prisma.latency.${metricKey}`, time) 36 | metrics.increment(`prisma.success.${operationToType[operation]}`, 1) 37 | metrics.increment(`prisma.success.model.${model}`, 1) 38 | return queryResult 39 | } catch (err) { 40 | console.log(err) 41 | metrics.increment(`prisma.errors.${operationToType[operation]}`, 1) 42 | metrics.increment(`prisma.errors.model.${model}`, 1) 43 | } 44 | return 45 | }, 46 | }, 47 | }) 48 | 49 | module.exports = { prisma } 50 | -------------------------------------------------------------------------------- /endpoints/index.js: -------------------------------------------------------------------------------- 1 | module.exports = async function index(req, res) { 2 | res.redirect('https://github.com/hackclub/toriel') 3 | } 4 | -------------------------------------------------------------------------------- /endpoints/ping.js: -------------------------------------------------------------------------------- 1 | module.exports = async function ping(req, res) { 2 | res.json({ pong: true }) 3 | } 4 | -------------------------------------------------------------------------------- /endpoints/slack-invite.js: -------------------------------------------------------------------------------- 1 | const { client } = require('../app') 2 | const { inviteUser } = require('../util/invite-user') 3 | const { transcript } = require('../util/transcript') 4 | 5 | module.exports = async function slackInvite(req, res) { 6 | // this endpoint is hit by the form on hackclub.com/slack 7 | try { 8 | if (!req.headers.authorization) { 9 | return res.status(403).json({ error: 'No credentials sent!' }) 10 | } 11 | if (req.headers.authorization != `Bearer ${process.env.AUTH_TOKEN}`) { 12 | return res.status(403).json({ error: 'Invalid credentials sent!' }) 13 | } 14 | 15 | const email = req?.body?.email 16 | const result = { email } 17 | if (email) { 18 | const res = await inviteUser(req.body) 19 | result.ok = res.ok 20 | result.error = res.error 21 | let invites = res?.invites 22 | if (invites) { 23 | result.error = invites[0]?.error 24 | result.ok = result.ok 25 | } 26 | if (result.error === 'already_in_team') { 27 | // User is already in Slack - send them an email via Loops telling them how to login 28 | let email = res?.invites[0]?.email 29 | let userInfo = await client.users.lookupByEmail({ email }) 30 | let isMcg = userInfo?.user?.is_restricted 31 | let isScg = userInfo?.user?.is_ultra_restricted 32 | let dataVariables 33 | let transactionalId 34 | if (isMcg) { 35 | // Check if they're in cave channel, invite if not 36 | await client.conversations 37 | .invite({ 38 | channel: transcript('channels.cave'), 39 | users: userInfo.user.id, 40 | token: process.env.SLACK_USER_TOKEN, 41 | }) 42 | .catch((err) => { 43 | if (err.data.error === 'already_in_channel') { 44 | // User is already in channel, do nothing 45 | } else { 46 | console.log(err) 47 | } 48 | }) 49 | let caveChannelData = await client.conversations.info({ 50 | channel: transcript('channels.cave'), 51 | }) 52 | let caveChannelName = caveChannelData.channel.name 53 | 54 | dataVariables = { 55 | email, 56 | caveChannelName: caveChannelName, 57 | caveChannelUrl: `https://hackclub.slack.com/archives/${transcript('channels.cave')}`, 58 | } 59 | transactionalId = process.env.LOOPS_MCG_TRANSACTIONAL_ID 60 | } else if (isScg) { 61 | // We shouldn't really have any of these and if we do, we likely don't want to promote them 62 | return res.status(500).json({ 63 | ok: false, 64 | error: 'User is already in Slack but is an SCG', 65 | }) 66 | } else { 67 | // Full user, don't invite to chanel - just set the full user template 68 | dataVariables = { 69 | email, 70 | } 71 | transactionalId = process.env.LOOPS_FULL_USER_TRANSACTIONAL_ID 72 | } 73 | 74 | const lres = await fetch('https://app.loops.so/api/v1/transactional', { 75 | method: 'POST', 76 | headers: { 77 | 'Content-Type': 'application/json', 78 | Authorization: `Bearer ${process.env.LOOPS_API_KEY}`, 79 | }, 80 | body: JSON.stringify({ 81 | email, 82 | transactionalId, 83 | dataVariables, 84 | }), 85 | }) 86 | } 87 | } 88 | return res.json(result) 89 | } catch (e) { 90 | console.log(e) 91 | return res.status(500).json({ ok: false, error: 'a fatal error occurred' }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /endpoints/slack-tutorial.js: -------------------------------------------------------------------------------- 1 | const { client } = require('../app') 2 | const { prisma } = require('../db') 3 | 4 | module.exports = async function slackTutorial(req, res) { 5 | // this endpoint is hit by @clippy in the Slack to check if @toriel is handling the onboarding 6 | // if we return false, @clippy will step in and onboard the user 7 | 8 | // EDIT: this endpoint is now also used by @orpheus when running the /airtable command 9 | const { user } = req.params 10 | const slackuser = await client.users.info({ user }) 11 | const email = slackuser?.user?.profile?.email 12 | const invite = await prisma.invite.findFirst({ 13 | where: { email }, 14 | orderBy: { createdAt: 'desc' }, 15 | }) 16 | res.json({ 17 | invite: Boolean(invite), 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /endpoints/start-from-clippy.js: -------------------------------------------------------------------------------- 1 | // the user is directed to the cave 2 | const { transcript } = require('../util/transcript') 3 | const { client } = require('../app') 4 | 5 | module.exports = async function startFromClippy(req, res) { 6 | const { user_id } = req.query 7 | try { 8 | await Promise.all([ 9 | client.chat.postEphemeral({ 10 | text: transcript('announcements-to-cave'), 11 | channel: transcript('channels.announcements'), 12 | user: user_id, 13 | }), 14 | ]) 15 | } catch (e) {} 16 | res.json({ pong: true }) 17 | } 18 | -------------------------------------------------------------------------------- /express-receiver.js: -------------------------------------------------------------------------------- 1 | const { ExpressReceiver } = require('@slack/bolt') 2 | 3 | const receiver = new ExpressReceiver({ 4 | signingSecret: process.env.SLACK_SIGNING_SECRET, 5 | }) 6 | 7 | module.exports = { receiver } 8 | -------------------------------------------------------------------------------- /image_pan_slice.sh: -------------------------------------------------------------------------------- 1 | # input.png found at https://cloud-o67g3jvnt-hack-club-bot.vercel.app/0chara_sprite_intro.png 2 | # this will create a file for each frame– filesystems beware 3 | wget --output-document input.png https://cloud-o67g3jvnt-hack-club-bot.vercel.app/0chara_sprite_intro.png 4 | pan_size=300 5 | rm slide_*.png 6 | for i in {0..$pan_size..5} 7 | do 8 | top_cut=$(expr $i) 9 | bot_cut=$(expr $pan_size - $i) 10 | convert input.png -gravity South -chop 0x$bot_cut -gravity North -chop 0x$top_cut slide_$(printf "%05d" $i).png 11 | done 12 | convert -delay 30 -loop 1 slide_*.png animation.gif 13 | rm slide_*.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const { inviteUserToChannel } = require('./util/invite-user-to-channel') 3 | const { mirrorMessage } = require('./util/mirror-message') 4 | const { transcript } = require('./util/transcript') 5 | /*const { 6 | postWelcomeCommittee, 7 | } = require('./interactions/post-welcome-committee')*/ 8 | const express = require('express') 9 | 10 | const { app, client } = require('./app.js') 11 | const { receiver } = require('./express-receiver') 12 | const { getInvite } = require('./util/get-invite') 13 | const { sleep } = require('./util/sleep') 14 | const { prisma } = require('./db') 15 | const { metrics } = require('./util/metrics') 16 | const { upgradeUser } = require('./util/upgrade-user.js') 17 | const { destroyHelpMeMessage } = require('./util/notify-channel.js') 18 | const { scheduleHelpMeMessage } = require('./util/notify-channel') 19 | const { sendInfo } = require('./util/alert') 20 | receiver.router.use(express.json()) 21 | 22 | receiver.router.get('/', require('./endpoints/index')) 23 | 24 | receiver.router.get('/ping', require('./endpoints/ping')) 25 | 26 | receiver.router.get( 27 | '/start-from-clippy', 28 | require('./endpoints/start-from-clippy') 29 | ) 30 | 31 | // Spit out global metrics every 5s 32 | setInterval(() => { 33 | metrics.increment('events.pulse', 1) 34 | }, 1000 * 5) 35 | 36 | receiver.router.get( 37 | '/slack-tutorial/:user', 38 | require('./endpoints/slack-tutorial') 39 | ) 40 | 41 | receiver.router.post('/slack-invite', require('./endpoints/slack-invite')) 42 | 43 | const defaultChannels = [ 44 | 'lounge', 45 | 'scrapbook', 46 | 'ship', 47 | 'welcome', 48 | 'library', 49 | 'happenings', 50 | ] 51 | 52 | const getSuggestion = () => { 53 | const suggestions = [ 54 | `tell us how you're doing in <#${transcript('channels.lounge')}>`, 55 | `post your proudest ship in <#${transcript('channels.ship')}>`, 56 | `post a project you're currently working on in <#${transcript( 57 | 'channels.scrapbook' 58 | )}>`, 59 | `post the next number in <#${transcript('channels.counttoamillion')}>`, 60 | `answer the latest question in <#${transcript( 61 | 'channels.question-of-the-day' 62 | )}>`, 63 | `ask 8-ball your fortune for the coming week in <#${transcript( 64 | 'channels.8-ball' 65 | )}>`, 66 | `share a photo of your surroundings in <#${transcript( 67 | 'channels.surroundings' 68 | )}>`, 69 | `tell us what you're listening to in <#${transcript('channels.music')}>`, 70 | ] 71 | return suggestions[Math.floor(Math.random() * suggestions.length)] 72 | } 73 | 74 | app.event('message', async (args) => { 75 | // begin the firehose 76 | const { body, client } = args 77 | const { event } = body 78 | const { type, subtype, user, channel, ts, text } = event 79 | 80 | if (text === 'RUMMAGE') { 81 | mirrorMessage({ 82 | message: text, 83 | user, 84 | channel, 85 | type: 'Rummage', 86 | }) 87 | const { 88 | handleRummageInteraction, 89 | } = require('./interactions/handle-rummage') 90 | await handleRummageInteraction(args) 91 | // } else if (text == 'trigger rummage') { 92 | // mirrorMessage({ 93 | // message: text, 94 | // user, 95 | // channel, 96 | // type: "Rummage Init", 97 | // }) 98 | // const { initRummageInteraction } = require('./interactions/init-rummage') 99 | // await initRummageInteraction(args) 100 | } 101 | 102 | if ( 103 | text?.toLowerCase()?.includes('toriel') || 104 | text?.includes(transcript('selfUserID')) 105 | ) { 106 | mirrorMessage({ 107 | message: text, 108 | user, 109 | channel, 110 | type, 111 | }) 112 | } 113 | 114 | const protectedChannels = [transcript('channels.cave')] 115 | if (type == 'message' && protectedChannels.includes(channel)) { 116 | console.log(`Attempting to remove ${subtype} message in #cave channel`) 117 | metrics.increment('events.protectedChannel.deletions', 1) 118 | await client.chat 119 | .delete({ 120 | token: process.env.SLACK_LEGACY_TOKEN, // sudo 121 | channel, 122 | ts, 123 | }) 124 | .catch((e) => { 125 | console.warn(e) 126 | }) 127 | } 128 | 129 | defaultAddsId = defaultChannels.map((e) => { 130 | return transcript(`channels.${e}`) 131 | }) // map all default channels into ids as channel prop is given as id 132 | 133 | if ( 134 | subtype === 'channel_join' && 135 | text === `<@${user}> has joined the channel` && 136 | defaultAddsId.includes(channel) 137 | ) { 138 | console.log('Deleting "user has joined" message') 139 | 140 | const jobs = [] 141 | jobs.push( 142 | client.chat 143 | .delete({ 144 | token: process.env.SLACK_LEGACY_TOKEN, // sudo 145 | channel, 146 | ts, 147 | }) 148 | .catch((e) => { 149 | console.warn(e) 150 | }) 151 | ) 152 | jobs.push( 153 | mirrorMessage({ 154 | message: `<@${user}> has been dropped into cave`, 155 | user, 156 | channel, 157 | type, 158 | }) 159 | ) 160 | jobs.push(scheduleHelpMeMessage(client, user)) 161 | 162 | await Promise.all(jobs) 163 | } // delete "user has joined" message if it is sent in one of the default channels that TORIEL adds new members to 164 | }) 165 | 166 | const addToChannels = async (user, event) => { 167 | await upgradeUser(user) 168 | await sleep(1000) // timeout to prevent race-condition during channel invites 169 | const invite = await getInvite({ user }) 170 | let channelsToInvite = defaultChannels 171 | if (event) { 172 | channelsToInvite.push(event) 173 | defaultChannels.push(event) 174 | } 175 | await Promise.all( 176 | channelsToInvite.map((c) => 177 | inviteUserToChannel(user, transcript(`channels.${c}`)) 178 | ) 179 | ) 180 | 181 | const suggestion = getSuggestion() 182 | await client.chat.postMessage({ 183 | text: transcript('house.added-to-channels', { suggestion }), 184 | blocks: [ 185 | transcript('block.text', { 186 | text: transcript('house.added-to-channels', { suggestion }), 187 | }), 188 | ], 189 | channel: user, 190 | }) 191 | 192 | // TODO weigh by reactions or just do something else entirely 193 | const history = await client.conversations.history({ 194 | channel: transcript('channels.ship'), 195 | limit: 10, 196 | }) 197 | const message = history.messages[Math.floor(Math.random() * 10)] 198 | const link = ( 199 | await client.chat.getPermalink({ 200 | channel: transcript('channels.ship'), 201 | message_ts: message.ts, 202 | }) 203 | ).permalink 204 | } 205 | 206 | app.command(/.*?/, async (args) => { 207 | const { ack, payload, respond } = args 208 | const { command, text, user_id, channel_id } = payload 209 | 210 | try { 211 | mirrorMessage({ 212 | message: `${command} ${text}`, 213 | user: user_id, 214 | channel: channel_id, 215 | type: 'slash-command', 216 | }) 217 | 218 | await ack() 219 | 220 | await respond({ 221 | blocks: [ 222 | { 223 | type: 'context', 224 | elements: [ 225 | { 226 | type: 'mrkdwn', 227 | text: `${command} ${text}`, 228 | }, 229 | ], 230 | }, 231 | ], 232 | }) 233 | 234 | switch (command) { 235 | case '/toriel-restart': 236 | await require(`./commands/restart`)(args) 237 | metrics.increment('events.restart', 1) 238 | break 239 | 240 | case '/toriel-call': 241 | await require(`./commands/call`)(args) 242 | break 243 | 244 | case '/toriel-reason': 245 | await require(`./commands/reason`)(args) 246 | break 247 | 248 | default: 249 | await require('./commands/not-found')(args) 250 | break 251 | } 252 | } catch (e) { 253 | console.error(e) 254 | } 255 | }) 256 | 257 | app.action(/.*?/, async (args) => { 258 | const { ack, respond, payload, client, body } = args 259 | const user = body.user.id 260 | 261 | mirrorMessage({ 262 | message: `_<@${user}> clicked '${payload.text.text}'_`, 263 | user: user, 264 | channel: body.container.channel_id, 265 | type: body.type, 266 | }) 267 | 268 | await ack() 269 | 270 | switch (payload.value) { 271 | case 'cave_start': 272 | const { joinCaveInteraction } = require('./interactions/join-cave') 273 | const dbUser = await prisma.user.findFirst({ where: { user_id: user } }) 274 | await joinCaveInteraction({ ...args, payload: { user } }) 275 | 276 | if (!dbUser) { 277 | await postWelcomeCommittee(user) 278 | } 279 | break 280 | 281 | case 'coc_complete': 282 | const slackuser = await client.users.info({ user }) 283 | const email = slackuser?.user?.profile?.email 284 | const invite = await prisma.invite.findFirst({ where: { email } }) 285 | 286 | await prisma.user.update({ 287 | where: { 288 | user_id: user, 289 | }, 290 | data: { 291 | toriel_stage: 'ACCEPTED_COC', 292 | }, 293 | }) 294 | 295 | metrics.increment('events.acceptcoc', 1) 296 | 297 | if (invite?.event) { 298 | const event = invite?.event 299 | await prisma.user.update({ 300 | where: { user_id: user }, 301 | data: { club_leader: false }, 302 | }) 303 | await addToChannels(user, event) 304 | } 305 | await client.chat.postMessage({ 306 | text: transcript('house.club-leader'), 307 | blocks: [ 308 | transcript('block.text', { text: transcript('house.club-leader') }), 309 | transcript('block.double-button', [ 310 | { text: 'yes', value: 'club_leader_yes' }, 311 | { text: 'no', value: 'club_leader_no' }, 312 | ]), 313 | ], 314 | channel: user, 315 | }) 316 | break 317 | case 'club_leader_yes': 318 | await prisma.user.update({ 319 | where: { user_id: user }, 320 | data: { club_leader: true }, 321 | }) 322 | await client.chat.postMessage({ 323 | text: transcript('club-leader.text'), 324 | channel: transcript('club-leader.notifiee'), 325 | }) 326 | await addToChannels(user) 327 | // user upgrading from multi-channel to full user takes some time, so wait to prevent race conditions 328 | await sleep(5000) 329 | await inviteUserToChannel(user, transcript('channels.leaders')) 330 | await prisma.user.update({ 331 | where: { 332 | user_id: user, 333 | }, 334 | data: { 335 | toriel_stage: 'FINISHED', 336 | }, 337 | }) 338 | 339 | metrics.increment('events.flow.finish', 1) 340 | 341 | await destroyHelpMeMessage(client, user) 342 | 343 | break 344 | case 'club_leader_no': 345 | await prisma.user.update({ 346 | where: { user_id: user }, 347 | data: { club_leader: false }, 348 | }) 349 | await addToChannels(user) 350 | 351 | await prisma.user.update({ 352 | where: { 353 | user_id: user, 354 | }, 355 | data: { 356 | toriel_stage: 'FINISHED', 357 | }, 358 | }) 359 | 360 | metrics.increment('events.flow.finish', 1) 361 | 362 | await destroyHelpMeMessage(client, user) 363 | 364 | break 365 | default: 366 | await respond({ 367 | replace_original: false, 368 | text: transcript('errors.not-found'), 369 | }) 370 | console.log({ args }) 371 | break 372 | } 373 | }) 374 | 375 | app.start(process.env.PORT || 3001).then(async () => { 376 | console.log(transcript('startupLog')) 377 | 378 | const { ensureSlackChannels } = require('./interactions/ensure-channels') 379 | await ensureSlackChannels() 380 | 381 | const { cleanupCaveChannel } = require('./interactions/cleanup-cave') 382 | await cleanupCaveChannel() 383 | 384 | metrics.increment('events.startup', 1) 385 | 386 | if (process.env.NODE_ENV === 'production') { 387 | const { startupInteraction } = require('./interactions/startup') 388 | await startupInteraction() 389 | } 390 | 391 | /* DEVELOPMENT UTILITIES (uncomment to use) */ 392 | const { setupCaveChannel } = require('./setup/cave-channel') 393 | // await setupCaveChannel(app) 394 | }) 395 | 396 | setInterval(async function () { 397 | const noId = ( 398 | await prisma.invite.findMany({ 399 | where: { 400 | user_id: null, 401 | }, 402 | }) 403 | ).length 404 | metrics.gauge('flow.users.no_account', noId) 405 | 406 | const totalInvitesThisWeek = ( 407 | await prisma.invite.findMany({ 408 | where: { 409 | createdAt: { 410 | lte: new Date(), 411 | gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), 412 | }, 413 | }, 414 | }) 415 | ).length 416 | 417 | const totalUsersThisWeek = ( 418 | await prisma.invite.findMany({ 419 | where: { 420 | createdAt: { 421 | lte: new Date(), 422 | gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), 423 | }, 424 | user_id: { 425 | not: null, 426 | }, 427 | }, 428 | }) 429 | ).length 430 | 431 | metrics.gauge('flow.users.invites.this_week', totalInvitesThisWeek) 432 | metrics.gauge('flow.users.joins.this_week', totalUsersThisWeek) 433 | }, 1000 * 10) // update to a longer span in not testing 434 | 435 | process.on('unhandledRejection', (error) => { 436 | sendInfo({ 437 | summary: 'An unhandled rejection was captured just now', 438 | detailed: error?.stack, 439 | }) 440 | console.error(error) 441 | }) 442 | 443 | module.exports = { app } 444 | -------------------------------------------------------------------------------- /interactions/cleanup-cave.js: -------------------------------------------------------------------------------- 1 | const { client } = require('../app') 2 | const { transcript } = require('../util/transcript') 3 | const alert = require('../util/alert') 4 | 5 | async function cleanupCaveChannel(dryRun = true) { 6 | const channel = transcript('channels.cave') 7 | const data = await client.conversations.history({ 8 | channel, 9 | }) 10 | const { messages } = data 11 | 12 | const selfUserID = transcript('selfUserID') 13 | const messagesToRemove = messages.filter((message) => { 14 | return message.user != selfUserID 15 | }) 16 | 17 | if (dryRun) { 18 | if (messagesToRemove.length != 0) 19 | await alert.sendInfo({ 20 | summary: `(dry) Removing ${messagesToRemove.length} message(s) in cave channel`, 21 | detailed: `(dry) Removing ${messagesToRemove.length} message(s) in cave channel`, 22 | }) 23 | console.log( 24 | `[DRY RUN] Found ${messagesToRemove.length} message(s) from other users in #cave channel, run with dryRun=false to remove` 25 | ) 26 | } else { 27 | if (messagesToRemove.length != 0) 28 | await alert.sendInfo({ 29 | summary: `Removing ${messagesToRemove.length} message(s) in cave channel`, 30 | detailed: `Removing ${messagesToRemove.length} message(s) in cave channel`, 31 | }) 32 | console.log( 33 | `Found ${messagesToRemove.length} message(s) from other users in #cave channel, cleaning up...` 34 | ) 35 | 36 | await Promise.all( 37 | messagesToRemove.map((message) => { 38 | // Note from David: It appears tombstone messages belong to Data Loss Prevention 39 | // Which, it appears it can't delete (message_not_found) 40 | // https://slack.com/help/articles/12914005852819-Slack-data-loss-prevention 41 | if (message.subtype == 'tombstone') return 42 | client.chat 43 | .delete({ 44 | token: process.env.SLACK_LEGACY_TOKEN, // sudo 45 | channel, 46 | ts: message?.ts, 47 | thread_ts: message?.thread_ts, 48 | }) 49 | .catch((e) => { 50 | console.warn(e) 51 | }) 52 | }) 53 | ) 54 | } 55 | } 56 | 57 | module.exports = { cleanupCaveChannel } 58 | -------------------------------------------------------------------------------- /interactions/ensure-channels.js: -------------------------------------------------------------------------------- 1 | const { client } = require('../app') 2 | const { transcript } = require('../util/transcript') 3 | 4 | async function ensureChannels() { 5 | const data = await client.users.conversations() 6 | const currentChannelIDs = data.channels.map((c) => c.id) 7 | const testChannelIDs = [ 8 | transcript('channels.toriels-diary'), 9 | transcript('channels.cave'), 10 | transcript('channels.bot-spam'), 11 | transcript('channels.toriel-dev'), 12 | // default channels: 13 | transcript('channels.lounge'), 14 | transcript('channels.code'), 15 | transcript('channels.hack-night'), 16 | transcript('channels.neighbourhood'), 17 | transcript('channels.ship'), 18 | transcript('channels.scrapbook'), 19 | transcript('channels.counttoamillion'), 20 | transcript('channels.leaders'), 21 | transcript('channels.hq'), 22 | ] 23 | 24 | let missingChannels = [] 25 | testChannelIDs.forEach((testID) => { 26 | let found = currentChannelIDs.indexOf(testID) > -1 27 | if (!found) { 28 | missingChannels.push(testID) 29 | } 30 | }) 31 | if (missingChannels.length === 0) { 32 | console.log('Toriel is in all channels she should have access to') 33 | } else { 34 | console.warn('⚠️Toriel is not invited to these channels:', missingChannels) 35 | } 36 | } 37 | 38 | module.exports = { ensureSlackChannels: ensureChannels } 39 | -------------------------------------------------------------------------------- /interactions/handle-rummage.js: -------------------------------------------------------------------------------- 1 | const { transcript } = require('../util/transcript') 2 | 3 | function increaseRummageCount() { 4 | const rummageCount = parseInt(process.env.RUMMAGE_COUNT) || 0 5 | process.env.RUMMAGE_COUNT = rummageCount + 1 6 | return rummageCount 7 | } 8 | 9 | const rummageChannel = 10 | process.env.RUMMAGE_CHANNEL || transcript('channels.announcements') 11 | 12 | async function handleRummage(args) { 13 | const { client, payload } = args 14 | const { user, text, channel } = payload 15 | 16 | if (channel !== rummageChannel) { 17 | console.log('not in announcements channel') 18 | return 19 | } 20 | 21 | const rummageThread = process.env.RUMMAGE_THREAD || '1713055476.037209' 22 | console.log('saved rummage thread is ', rummageThread) 23 | 24 | if (!rummageThread) { 25 | console.log('no rummage thread') 26 | return 27 | } 28 | 29 | if (text !== 'RUMMAGE') { 30 | console.log('not rummage') 31 | return 32 | } 33 | 34 | const messageCount = increaseRummageCount() 35 | 36 | let message = '' 37 | if (messageCount < 10) { 38 | message = transcript('rummage.first-ten') 39 | } else if (messageCount < 150) { 40 | message = transcript('rummage.first-hundred') 41 | } else if (messageCount < 400) { 42 | message = transcript('rummage.second-hundred') 43 | } else if (messageCount < 600) { 44 | message = transcript('rummage.third-hundred') 45 | } else { 46 | message = transcript('rummage.end') 47 | } 48 | 49 | if (messageCount > 1 && messageCount % 50 == 0) { 50 | message += transcript('rummage.progress', { count: messageCount }) 51 | } 52 | 53 | await client.chat.postMessage({ 54 | text: `<@${user}>: ${message}`, 55 | channel: rummageChannel, 56 | thread_ts: rummageThread, 57 | username: `A ${transcript('rummage.raccoon-types')} racoon`.toUpperCase(), 58 | icon_url: transcript('rummage.avatar.raccoon'), 59 | }) 60 | } 61 | 62 | module.exports = { handleRummageInteraction: handleRummage } 63 | -------------------------------------------------------------------------------- /interactions/init-rummage.js: -------------------------------------------------------------------------------- 1 | const { sleep } = require('../util/sleep') 2 | const { transcript } = require('../util/transcript') 3 | 4 | const rummageChannel = 5 | process.env.RUMMAGE_CHANNEL || transcript('channels.announcements') 6 | 7 | async function initRummage(args) { 8 | const { client, payload } = args 9 | const { user } = payload 10 | 11 | // ensure only triggered by the right user 12 | 13 | if (user !== 'U0C7B14Q3') { 14 | return 15 | } 16 | 17 | const topLevelMessage = await client.chat.postMessage({ 18 | text: transcript('rummage.announcements.init'), 19 | channel: rummageChannel, 20 | username: 'Trash can', 21 | icon_url: transcript('rummage.avatar.trashcan'), 22 | }) 23 | 24 | const { ok, ts } = topLevelMessage 25 | if (!ok) { 26 | // oh no... 27 | console.log({ topLevelMessage }) 28 | return 29 | } 30 | 31 | const threadID = ts 32 | 33 | process.env.RUMMAGE_THREAD = threadID 34 | console.log('RUMMAGE_THREAD', process.env.RUMMAGE_THREAD) 35 | 36 | await sleep(10000) 37 | 38 | await client.chat.postMessage({ 39 | text: transcript('rummage.announcements.raccoon-init'), 40 | channel: rummageChannel, 41 | thread_ts: process.env.RUMMAGE_THREAD, 42 | username: 'A YAPPY RACCOON', 43 | icon_url: transcript('rummage.avatar.raccoon'), 44 | }) 45 | 46 | await sleep(10000) 47 | 48 | await client.chat.postMessage({ 49 | blocks: [ 50 | transcript('block.text', { 51 | text: transcript('rummage.announcements.raccoon-rummage'), 52 | }), 53 | transcript('block.context', { 54 | text: transcript('rummage.announcements.raccoon-join'), 55 | }), 56 | ], 57 | channel: rummageChannel, 58 | thread_ts: process.env.RUMMAGE_THREAD, 59 | username: 'A LOUD RACCOON', 60 | icon_url: transcript('rummage.avatar.raccoon'), 61 | }) 62 | } 63 | 64 | module.exports = { initRummageInteraction: initRummage } 65 | -------------------------------------------------------------------------------- /interactions/join-cave.js: -------------------------------------------------------------------------------- 1 | // the user joins #the-cave, the starter channel 2 | const { sleep } = require('../util/sleep') 3 | const { transcript } = require('../util/transcript') 4 | const { prisma } = require('../db') 5 | const { getEmailFromUser } = require('../util/get-invite') 6 | const { metrics } = require('../util/metrics') 7 | 8 | async function joinCaveInteraction(args) { 9 | const { client, payload } = args 10 | const { user } = payload 11 | 12 | try { 13 | await prisma.user.create({ data: { user_id: user } }) 14 | } catch (e) { 15 | if (e.code !== 'P2002') throw e 16 | } 17 | 18 | await prisma.invite.updateMany({ 19 | where: { email: await getEmailFromUser({ user }) }, 20 | data: { user_id: user }, 21 | }) 22 | 23 | await prisma.user.update({ 24 | where: { 25 | user_id: user, 26 | }, 27 | data: { 28 | toriel_stage: 'STARTED_FLOW', 29 | }, 30 | }) 31 | 32 | metrics.increment('events.flow.cavestart', 1) 33 | 34 | await Promise.all([ 35 | client.chat.postEphemeral({ 36 | text: transcript('cave-join', { user }), 37 | channel: transcript('channels.cave'), 38 | user, 39 | }), 40 | ]) 41 | 42 | await Promise.all([ 43 | await sleep(1000), 44 | await client.chat.postMessage({ 45 | text: transcript('house.coc'), 46 | blocks: [ 47 | transcript('block.text', { 48 | text: transcript('house.coc'), 49 | }), 50 | transcript('block.single-button', { 51 | text: 'i do', 52 | value: 'coc_complete', 53 | }), 54 | ], 55 | // icon_url: transcript('avatar.default'), 56 | channel: user, 57 | unfurl_links: false, 58 | }), 59 | ]) 60 | } 61 | module.exports = { joinCaveInteraction } 62 | -------------------------------------------------------------------------------- /interactions/post-welcome-committee.js: -------------------------------------------------------------------------------- 1 | const { prisma } = require('../db') 2 | const { transcript } = require('../util/transcript') 3 | const { client } = require('../app') 4 | 5 | async function postWelcomeCommittee(user) { 6 | try { 7 | const slackuser = await client.users.info({ user }) 8 | const email = slackuser?.user?.profile?.email 9 | const invite = await prisma.invite.findFirst({ 10 | where: { email }, 11 | orderBy: { createdAt: 'desc' }, 12 | }) 13 | const message = 14 | invite?.['welcome_message'] || "I'm using the /toriel-restart command" 15 | const continent = invite?.['continent'] || 'DEFAULT_CONTINENT' 16 | const hs = invite ? invite.high_school : true 17 | const event = invite ? invite.event : null 18 | 19 | // This will go away once professor bloom is fully done 20 | await client.chat.postMessage({ 21 | channel: transcript('channels.welcome-committee'), 22 | text: transcript('welcome-committee', { 23 | user, 24 | message, 25 | continent, 26 | hs, 27 | event, 28 | }), 29 | }) 30 | 31 | // Can we add some error handling here so if the post request fails it dms me (@Jasper)? 32 | await fetch(`https://professorbloom.hackclub.com/toriel/newUser`, { 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | Auth: process.env.AUTH_TOKEN, 36 | }, 37 | method: 'POST', 38 | body: JSON.stringify({ 39 | user: user, 40 | continent: continent, 41 | joinReason: message, 42 | }), 43 | }) 44 | } catch (e) { 45 | console.error(e) 46 | } 47 | } 48 | 49 | module.exports = { postWelcomeCommittee } 50 | -------------------------------------------------------------------------------- /interactions/startup.js: -------------------------------------------------------------------------------- 1 | const { client } = require('../app') 2 | const { transcript } = require('../util/transcript') 3 | const { metrics } = require('../util/metrics') 4 | 5 | function getEnv() { 6 | return process.env.NODE_ENV === 'production' ? 'prod' : 'dev' 7 | } 8 | 9 | async function startup() { 10 | await client.chat.postMessage({ 11 | blocks: [transcript('block.text', { text: transcript('startup.message') })], 12 | channel: transcript('channels.bot-spam'), 13 | username: 'TUTORIEL', 14 | icon_url: transcript('startup.avatar'), 15 | unfurl_links: false, 16 | unfurl_media: false, 17 | }) 18 | } 19 | 20 | module.exports = { startupInteraction: startup } 21 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | # This file is the Slack Manifest file 2 | # READMORE: https://api.slack.com/reference/manifests 3 | display_information: 4 | name: TORIEL 5 | description: What a nice lady. She seems kinda lonely. 6 | background_color: '#160f1a' 7 | long_description: "Just a lil' greeter bot to direct new members in the Hack Club slack channel and collect some analytics data for that process.\r \r You think if you call her she might have something to say (/toriel-call). \r \r For staff: This is a WIP bot. Reach out to @msw with questions and comments." 8 | features: 9 | bot_user: 10 | display_name: TORIEL 11 | always_online: true 12 | slash_commands: 13 | - command: /toriel-restart 14 | url: https://toriel.hackclub.com/slack/events 15 | description: Start toriel from scratch 16 | should_escape: true 17 | - command: /toriel-call 18 | url: https://toriel.hackclub.com/slack/events 19 | description: Chat with toriel 20 | should_escape: true 21 | - command: /toriel-reason 22 | url: https://toriel.hackclub.com/slack/events 23 | description: Ask toriel the reason someone joined (welcome committee only) 24 | should_escape: true 25 | oauth_config: 26 | scopes: 27 | bot: 28 | - app_mentions:read 29 | - channels:history 30 | - channels:join 31 | - channels:manage 32 | - channels:read 33 | - chat:write 34 | - chat:write.customize 35 | - commands 36 | - files:write 37 | - groups:history # not used in prod… useful when developing locally & using a private group as the #cave channel 38 | - groups:read 39 | - im:write 40 | - mpim:write 41 | - users:read 42 | - users:read.email 43 | settings: 44 | event_subscriptions: 45 | request_url: https://toriel.hackclub.com/slack/events 46 | bot_events: 47 | - app_mention 48 | - member_joined_channel 49 | - message.channels 50 | - team_join 51 | interactivity: 52 | is_enabled: true 53 | request_url: https://toriel.hackclub.com/slack/events 54 | org_deploy_enabled: false 55 | socket_mode_enabled: false 56 | token_rotation_enabled: false 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TORIEL", 3 | "version": "0.1.0", 4 | "description": "Hack Club's welcomer bot. Welcomes new members to the community and keeps bad people out", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "npx prisma migrate deploy && npx prisma generate && node index.js", 9 | "dev": "npx nodemon index.js", 10 | "fmt": "npx prettier --write '**/*.js'" 11 | }, 12 | "authors": [ 13 | "MaxWofford", 14 | "MatthewStanciu" 15 | ], 16 | "license": "MIT", 17 | "dependencies": { 18 | "@prisma/client": "^5.14.0", 19 | "@slack/bolt": "^3.18.0", 20 | "dotenv": "^16.3.1", 21 | "express": "^4.18.2", 22 | "form-data": "^4.0.0", 23 | "js-yaml": "^4.1.0", 24 | "node-statsd": "^0.1.1" 25 | }, 26 | "devDependencies": { 27 | "prisma": "^5.14.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /prisma/migrations/20220406171246_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Continent" AS ENUM ('AFRICA', 'ASIA', 'AUSTRALIA', 'EUROPE', 'NORTH_AMERICA', 'SOUTH_AMERICA'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Invite" ( 6 | "id" TEXT NOT NULL, 7 | "email" TEXT NOT NULL, 8 | "ip_address" TEXT NOT NULL, 9 | "user_agent" TEXT NOT NULL, 10 | "high_school" BOOLEAN NOT NULL, 11 | "welcome_message" TEXT NOT NULL, 12 | "continent" "Continent" NOT NULL, 13 | 14 | CONSTRAINT "Invite_pkey" PRIMARY KEY ("id") 15 | ); 16 | -------------------------------------------------------------------------------- /prisma/migrations/20220406184950_add_timestamps_to_invite/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `updatedAt` to the `Invite` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Invite" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20220406190104_default_invite_updated_at/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Invite" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20220725195106_add_user_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "user_id" TEXT NOT NULL, 4 | "club_leader" BOOLEAN, 5 | 6 | CONSTRAINT "User_pkey" PRIMARY KEY ("user_id") 7 | ); 8 | -------------------------------------------------------------------------------- /prisma/migrations/20220801141028_add_invite_user_id/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Invite" ADD COLUMN "user_id" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240228193737_identify_users/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Stage" AS ENUM ('INITIALIZED', 'STARTED_FLOW', 'ACCEPTED_COC', 'FINISHED'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "Invite" ADD COLUMN "event" TEXT, 6 | ADD COLUMN "schedule_stuck_message_id" TEXT; 7 | 8 | -- AlterTable 9 | ALTER TABLE "User" ADD COLUMN "toriel_stage" "Stage" NOT NULL DEFAULT 'INITIALIZED'; 10 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | // If this timesout with error code p1001: 11 | // https://lifesaver.codes/answer/error-p1001-can-t-reach-database-server-at-aws-rds-mysql-5652 12 | url = env("DATABASE_URL") 13 | // https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database#cloud-hosted-shadow-databases-must-be-created-manually 14 | shadowDatabaseUrl = env("SHADOW_DATABASE_URL") 15 | } 16 | 17 | enum Continent { 18 | AFRICA 19 | ASIA 20 | AUSTRALIA 21 | EUROPE 22 | NORTH_AMERICA 23 | SOUTH_AMERICA 24 | } 25 | 26 | model Invite { 27 | createdAt DateTime @default(now()) 28 | updatedAt DateTime @default(now()) @updatedAt 29 | id String @id @default(uuid()) 30 | email String 31 | ip_address String 32 | user_agent String 33 | high_school Boolean 34 | welcome_message String 35 | event String? 36 | continent Continent 37 | user_id String? 38 | schedule_stuck_message_id String? 39 | } 40 | 41 | model User { 42 | user_id String @id 43 | club_leader Boolean? 44 | toriel_stage Stage @default(INITIALIZED) 45 | } 46 | 47 | enum Stage { 48 | INITIALIZED 49 | STARTED_FLOW 50 | ACCEPTED_COC 51 | FINISHED 52 | } 53 | -------------------------------------------------------------------------------- /setup/cave-channel.js: -------------------------------------------------------------------------------- 1 | const { transcript } = require('../util/transcript') 2 | const { client } = require('../app') 3 | 4 | async function setupCaveChannel() { 5 | await postImage() 6 | // await postAudio() 7 | await postMessage() 8 | } 9 | 10 | async function postImage() { 11 | const file = Buffer.from( 12 | await (await fetch(transcript('files.cave-image'))).arrayBuffer() 13 | ) 14 | const response = await client.files.uploadV2({ 15 | channels: transcript('channels.cave'), 16 | file: file, 17 | filename: 'you fall into a cave...', 18 | }) 19 | } 20 | 21 | async function postMessage() { 22 | client.chat.postMessage({ 23 | channel: transcript('channels.cave'), 24 | text: transcript('cave-intro'), 25 | icon_url: transcript('avatar.log'), 26 | blocks: [ 27 | transcript('block.text', { text: transcript('cave-intro') }), 28 | transcript('block.single-button', { 29 | text: 'Explore the cave', 30 | value: 'cave_start', 31 | }), 32 | ], 33 | }) 34 | } 35 | 36 | async function postAudio() { 37 | const file = Buffer.from( 38 | await (await fetch(transcript('files.cave-audio'))).arrayBuffer() 39 | ) 40 | console.log({ 41 | channel: transcript('channels.cave'), 42 | }) 43 | const response = await client.files.uploadV2({ 44 | channels: transcript('channels.cave'), 45 | file: file.data, 46 | filename: 'play me', 47 | }) 48 | } 49 | 50 | module.exports = { setupCaveChannel } 51 | -------------------------------------------------------------------------------- /util/alert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A utility module for sending alerts via Slack and Twilio. 3 | * @module alert 4 | */ 5 | 6 | module.exports = { 7 | /** 8 | * Sends an informational alert to Slack. 9 | * @param {Object} options - The options for the alert. 10 | * @param {string} options.summary - The summary of the alert (for notifications) 11 | * @param {string} options.detailed - The detailed information of the alert. 12 | */ 13 | sendInfo: function ({ summary, detailed }) { 14 | detailed = '```' + detailed.replaceAll('`', 'ˋ') + '```' 15 | fetch(process.env.SLACK_WEBHOOK_INFO, { 16 | method: 'POST', 17 | body: JSON.stringify({ 18 | text: detailed, 19 | }), 20 | }) 21 | }, 22 | /** 23 | * Sends an urgent alert to Slack and PagerDuty 24 | * @param {Object} options - The options for the alert. 25 | * @param {string} options.summary - The summary of the alert (for notifications) 26 | * @param {string} options.detailed - The detailed information of the alert. 27 | */ 28 | sendUrgent: function ({ summary, detailed }) { 29 | detailed = '```' + detailed.replaceAll('`', 'ˋ') + '```' 30 | fetch(process.env.SLACK_WEBHOOK_ERROR, { 31 | method: 'POST', 32 | body: JSON.stringify({ 33 | text: detailed, 34 | }), 35 | }) 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /util/channel-is-active.js: -------------------------------------------------------------------------------- 1 | const { client } = require('../app') 2 | 3 | // Check if a channel has activity within a set time, if not, return false 4 | // "since" should be a duration in ms and defaults to 1 day 5 | async function channelIsActive({ channelName, since = 1000 * 60 * 60 * 24 }) { 6 | const channel = transcript(`channels.${channelName}`) 7 | const data = await client.conversations.history({ 8 | channel, 9 | oldest: Date.now() - since, 10 | inclusive: true, 11 | }) 12 | const { messages } = data 13 | return messages.length > 0 14 | } 15 | module.exports = { channelIsActive } 16 | -------------------------------------------------------------------------------- /util/get-invite.js: -------------------------------------------------------------------------------- 1 | const { client } = require('../app') 2 | const { prisma } = require('../db') 3 | 4 | async function getEmailFromUser({ user }) { 5 | const slackUser = await client.users.info({ user }) 6 | const email = slackUser?.user?.profile?.email 7 | return email 8 | } 9 | 10 | async function getInvite({ user, email }) { 11 | if (!email) { 12 | email = await getEmailFromUser({ user }) 13 | } 14 | const invite = await prisma.invite.findFirst({ 15 | where: { email }, 16 | orderBy: { createdAt: 'desc' }, 17 | }) 18 | return invite 19 | } 20 | 21 | module.exports = { getEmailFromUser, getInvite } 22 | -------------------------------------------------------------------------------- /util/invite-types/default.js: -------------------------------------------------------------------------------- 1 | const { transcript } = require('../transcript') 2 | 3 | const defaultInvite = { 4 | channels: [transcript('channels.cave')], 5 | customMessage: 'While wandering through a forest, you stumble upon a cave...', 6 | } 7 | 8 | module.exports = { defaultInvite } 9 | -------------------------------------------------------------------------------- /util/invite-types/hcb.js: -------------------------------------------------------------------------------- 1 | const { transcript } = require('../transcript') 2 | 3 | const hcbInvite = { 4 | channels: [transcript('channels.bank')], 5 | customMessage: 'While wandering through a forest, you stumble upon a cave...', 6 | } 7 | 8 | module.exports = { hcbInvite } 9 | -------------------------------------------------------------------------------- /util/invite-types/onboard.js: -------------------------------------------------------------------------------- 1 | const { transcript } = require('../transcript') 2 | 3 | const onboardInvite = { 4 | channels: [transcript('channels.onboard'), transcript('channels.cave')], 5 | customMessage: 'Welcome onboard!', 6 | } 7 | 8 | module.exports = { onboardInvite } 9 | -------------------------------------------------------------------------------- /util/invite-user-to-channel.js: -------------------------------------------------------------------------------- 1 | const { client } = require('../app') 2 | const { metrics } = require('./metrics') 3 | 4 | async function inviteUserToChannel( 5 | user, 6 | channel, 7 | doAsAdmin = false, 8 | notInChannel = false 9 | ) { 10 | console.log('inviting', user, 'to', channel) 11 | 12 | if (notInChannel) { 13 | try { 14 | await client.conversations.join({ channel }) 15 | } catch (e) {} 16 | } 17 | 18 | const token = doAsAdmin 19 | ? process.env.SLACK_LEGACY_TOKEN 20 | : process.env.SLACK_BOT_TOKEN 21 | return await client.conversations 22 | .invite({ 23 | token: token, 24 | channel: channel, 25 | users: user, 26 | }) 27 | .catch((err) => { 28 | if (err.data.error === 'already_in_channel') { 29 | console.log(`${user} is already in ${channel}—skipping this step...`) 30 | } 31 | if (!notInChannel && err.data.error === 'not_in_channel') { 32 | metrics.increment('events.flow.addtochannel', 1) 33 | return inviteUserToChannel(user, channel, doAsAdmin, true) 34 | } 35 | console.log(err.data.error, 'while inviting', user, 'to', channel) 36 | }) 37 | } 38 | 39 | module.exports = { inviteUserToChannel } 40 | -------------------------------------------------------------------------------- /util/invite-user.js: -------------------------------------------------------------------------------- 1 | const { prisma } = require('../db') 2 | const { defaultInvite } = require('./invite-types/default') 3 | const { onboardInvite } = require('./invite-types/onboard') 4 | const { metrics } = require('./metrics') 5 | 6 | async function inviteGuestToSlack({ email, channels, _customMessage }) { 7 | // This is an undocumented API method found in https://github.com/ErikKalkoken/slackApiDoc/pull/70 8 | // Unlike the documention in that PR, we're driving it not with a legacy token but a browser storage+cookie pair 9 | 10 | // The SLACK_COOKIE is a xoxd-* token found in browser cookies under the key 'd' 11 | // The SLACK_BROWSER_TOKEN is a xoxc-* token found in browser local storage using this script: https://gist.github.com/maxwofford/5779ea072a5485ae3b324f03bc5738e1 12 | 13 | // I haven't yet found out how to add custom messages, so those are ignored for now 14 | const cookieValue = `d=${process.env.SLACK_COOKIE}` 15 | 16 | // Create a new Headers object 17 | const headers = new Headers() 18 | 19 | // Add the cookie to the headers 20 | headers.append('Cookie', cookieValue) 21 | headers.append('Content-Type', 'application/json') 22 | headers.append('Authorization', `Bearer ${process.env.SLACK_BROWSER_TOKEN}`) 23 | const data = JSON.stringify({ 24 | token: process.env.SLACK_BROWSER_TOKEN, 25 | invites: [ 26 | { 27 | email, 28 | type: 'restricted', 29 | mode: 'manual', 30 | }, 31 | ], 32 | restricted: true, 33 | channels: channels.join(','), 34 | }) 35 | 36 | const res = await fetch(`https://slack.com/api/users.admin.inviteBulk`, { 37 | headers, 38 | method: 'POST', 39 | body: data, 40 | }) 41 | metrics.increment('events.flow.invitetoslack', 1) 42 | return await res.json() 43 | } 44 | 45 | async function inviteUser({ 46 | email, 47 | ip, 48 | continent, 49 | teen, 50 | reason, 51 | userAgent, 52 | event, 53 | }) { 54 | await prisma.invite.create({ 55 | data: { 56 | email: email, 57 | user_agent: userAgent || 'user_agent is empty', 58 | ip_address: ip, 59 | high_school: teen, // we actually just care if they're a teenager, so middle school is included in high school 60 | welcome_message: reason, // record their reason for joining the slack as their welcome message 61 | continent: continent.toUpperCase().replace(/\W/g, '_'), 62 | event: event || null, // This is a field that is only filled if someone signed up with ?event= query 63 | }, 64 | }) 65 | 66 | let invite = defaultInvite 67 | if (event == 'onboard') { 68 | invite = onboardInvite 69 | } 70 | const { channels, customMessage } = invite 71 | 72 | return await inviteGuestToSlack({ email, channels, customMessage }) 73 | } 74 | 75 | module.exports = { inviteUser } 76 | -------------------------------------------------------------------------------- /util/metrics.js: -------------------------------------------------------------------------------- 1 | const { StatsD } = require('node-statsd') 2 | 3 | const env = process.env.NODE_ENV || 'development' 4 | const graphite = process.env.GRAPHITE_HOST 5 | 6 | if (env.toLowerCase() == 'production' && graphite == null) { 7 | throw new Error('Graphite is not working') 8 | } 9 | 10 | const options = { 11 | host: graphite, 12 | port: 8125, 13 | prefix: `${env}.toriel.`, 14 | } 15 | 16 | const metrics = new StatsD(options) 17 | 18 | module.exports = { metrics } 19 | -------------------------------------------------------------------------------- /util/mirror-message.js: -------------------------------------------------------------------------------- 1 | const { transcript } = require('./transcript') 2 | const { client } = require('../app') 3 | 4 | async function mirrorMessage({ message, user, channel, type }) { 5 | try { 6 | const context = `a ${type} from <@${user}> in <#${channel}>` 7 | await client.chat.postMessage({ 8 | channel: transcript('channels.toriels-diary'), 9 | text: context, 10 | blocks: [ 11 | { 12 | type: 'section', 13 | text: { 14 | type: 'mrkdwn', 15 | text: `> ${message}`, 16 | }, 17 | }, 18 | { 19 | type: 'context', 20 | elements: [ 21 | { 22 | type: 'mrkdwn', 23 | text: context, 24 | }, 25 | ], 26 | }, 27 | ], 28 | }) 29 | } catch (e) { 30 | console.error(e) 31 | } 32 | } 33 | 34 | module.exports = { mirrorMessage } 35 | -------------------------------------------------------------------------------- /util/notify-channel.js: -------------------------------------------------------------------------------- 1 | const { prisma } = require('../db') 2 | const { transcript } = require('../util/transcript') 3 | 4 | async function scheduleHelpMeMessage(client, user_id) { 5 | const postDate = new Date() 6 | //6 hours into the future 7 | 8 | postDate.setTime(postDate.getTime() + 6 * 60 * 60 * 1000) 9 | 10 | const result = await client.chat.scheduleMessage({ 11 | channel: transcript('channels.welcome-committee'), 12 | text: transcript('notify-stage', { 13 | user: user_id, 14 | }), 15 | post_at: Math.floor(postDate.getTime() / 1000), 16 | }) 17 | 18 | if (result.ok) { 19 | await prisma.invite.updateMany({ 20 | where: { user_id }, 21 | data: { 22 | schedule_stuck_message_id: result.scheduled_message_id, 23 | }, 24 | }) 25 | } else { 26 | await client.chat.postMessage({ 27 | channel: transcript('channels.welcome-committee'), 28 | text: `Error: cannot post schedule "help me" message for <@${user_id}>. Reason: ${result.error}`, 29 | }) 30 | 31 | console.log(`Error in notify-channel.js notifyStage(): ${result}`) 32 | } 33 | } 34 | 35 | async function destroyHelpMeMessage(client, user_id) { 36 | try { 37 | const userInvite = await prisma.invite.findFirst({ 38 | where: { user_id: user_id }, 39 | }) 40 | if (!userInvite || !userInvite.schedule_stuck_message_id) return 41 | await client.chat.deleteScheduledMessage({ 42 | channel: transcript('channels.welcome-committee'), 43 | scheduled_message_id: userInvite.schedule_stuck_message_id, 44 | }) 45 | 46 | await prisma.invite.updateMany({ 47 | where: { 48 | user_id: user_id, 49 | }, 50 | data: { 51 | schedule_stuck_message_id: null, 52 | }, 53 | }) 54 | } catch (error) { 55 | console.log(error, userInvite.schedule_stuck_message_id) 56 | } 57 | } 58 | 59 | module.exports = { scheduleHelpMeMessage, destroyHelpMeMessage } 60 | -------------------------------------------------------------------------------- /util/sleep.js: -------------------------------------------------------------------------------- 1 | module.exports = { sleep } 2 | function sleep(ms) { 3 | return new Promise((resolve) => setTimeout(resolve, ms)) 4 | } 5 | -------------------------------------------------------------------------------- /util/transcript.js: -------------------------------------------------------------------------------- 1 | const yaml = require('js-yaml') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const { metrics } = require('./metrics') 5 | const { sendUrgent } = require('./alert') 6 | 7 | const sample = (arr) => { 8 | return arr[Math.floor(Math.random() * arr.length)] 9 | } 10 | const loadTranscript = () => { 11 | try { 12 | var transcriptPath = path.resolve(__dirname, './transcript.yml') 13 | if (fs.existsSync(path.resolve(__dirname, './transcript.dev.yml'))) 14 | transcriptPath = path.resolve(__dirname, './transcript.dev.yml') 15 | const doc = yaml.load(fs.readFileSync(transcriptPath, 'utf8')) 16 | return doc 17 | } catch (e) { 18 | console.error(e) 19 | } 20 | } 21 | const recurseTranscript = (searchArr, transcriptObj) => { 22 | const searchCursor = searchArr.shift() 23 | const targetObj = transcriptObj[searchCursor] 24 | 25 | if (!targetObj) { 26 | metrics.increment('events.transcript.load.failure', 1) 27 | sendUrgent({ 28 | summary: 'Failed to load transcript.yml', 29 | detailed: 30 | 'There was a failure loading the transcript.yml, which is breaking the app.\n\n' + 31 | transcript('errors.transcript'), 32 | }) 33 | throw new Error(transcript('errors.transcript')) 34 | } 35 | if (searchArr.length > 0) { 36 | return recurseTranscript(searchArr, targetObj) 37 | } else { 38 | if (Array.isArray(targetObj)) { 39 | return sample(targetObj) 40 | } else { 41 | return targetObj 42 | } 43 | } 44 | } 45 | const replaceErrors = (key, value) => { 46 | // from https://stackoverflow.com/a/18391400 47 | if (value instanceof Error) { 48 | const error = {} 49 | Object.getOwnPropertyNames(value).forEach((key) => { 50 | error[key] = value[key] 51 | }) 52 | return error 53 | } 54 | return value 55 | } 56 | 57 | const transcript = (search, vars) => { 58 | if (vars) { 59 | console.log( 60 | `I'm searching for words in my yaml file under "${search}". These variables are set: ${JSON.stringify( 61 | vars, 62 | replaceErrors 63 | )}` 64 | ) 65 | } else { 66 | console.log(`I'm searching for words in my yaml file under "${search}"`) 67 | } 68 | const searchArr = search.split('.') 69 | const transcriptObj = loadTranscript() 70 | let dehydratedTarget 71 | try { 72 | dehydratedTarget = recurseTranscript(searchArr, transcriptObj) 73 | } catch (e) { 74 | console.log(e) 75 | dehydratedTarget = search 76 | } 77 | return hydrateObj(dehydratedTarget, vars) 78 | } 79 | module.exports = { transcript } 80 | const hydrateObj = (obj, vars = {}) => { 81 | if (obj == null) { 82 | return null 83 | } 84 | if (typeof obj === 'string') { 85 | return evalTranscript(obj, vars) 86 | } 87 | if (Array.isArray(obj)) { 88 | return obj.map((o) => hydrateObj(o, vars)) 89 | } 90 | if (typeof obj === 'object') { 91 | Object.keys(obj).forEach((key) => { 92 | obj[key] = hydrateObj(obj[key], vars) 93 | }) 94 | return obj 95 | } 96 | } 97 | const evalTranscript = (target, vars = {}) => 98 | function () { 99 | return eval('`' + target + '`') 100 | }.call({ 101 | ...vars, 102 | t: transcript, 103 | }) 104 | -------------------------------------------------------------------------------- /util/transcript.yml: -------------------------------------------------------------------------------- 1 | type-of-pie: 2 | - cinnamon 3 | - butterscotch 4 | - cinnamon and butterscotch 5 | - cinnamon 6 | - butterscotch 7 | - cinnamon and butterscotch 8 | - snail # apparently a favorite of hers in Undertale 9 | 10 | errors: 11 | general: errors.general 12 | transcript: errors.transcript 13 | 14 | startupLog: I'm awake, my child 15 | 16 | selfUserID: U036UQD2893 17 | 18 | channels: 19 | 8-ball: C03DNGQA6SY 20 | announcements: C0266FRGT 21 | bot-spam: C0P5NE354 22 | cave: C039PAG1AV7 23 | code: C0EA9S0A0 24 | commons: C021CQCHP09 25 | community: C01D7AHKMPF 26 | counttoamillion: CDJMS683D 27 | epoch: C044SRZR8MB 28 | gamedev: C6LHL48G2 29 | hackathon-organizers: C03QSGGCJN7 30 | hackathons: C0NP503L7 31 | hack-night: C0JDWKJVA 32 | haikus: C036GP78EC8 33 | hq: C0C78SG9L 34 | lounge: C0266FRGV 35 | leaders: C02PA5G01ND 36 | music: C0DCUUH7E 37 | neighbourhood: C01AS1YEM8A 38 | pasture: C01PF39CVAS 39 | poll-of-the-day: C01U8UCHZC1 40 | question-of-the-day: C013AGZKYCS 41 | scrapbook: C01504DCLVD 42 | scrapbook-dev: C035D6S6TFW 43 | ship: C0M8PUPU6 44 | slack-themes: CKKLW54QG 45 | sprig: C02UN35M7LG 46 | surroundings: C02EWM09ACE 47 | the-basement: C037TG53X9U 48 | toriels-diary: C03APMU234G 49 | uno-games: C01ABQB8S6A 50 | welcome: C75M7C0SY 51 | whack-a-mole: C01TW2CAK55 52 | wordle: C02TWKX227J 53 | toriel-dev: C02B7CWDD0E 54 | welcome-committee: GLFAEL1SL 55 | onboard: C056AMWSFKJ 56 | bakery: C01EZ2MNPTQ 57 | the-wishing-wall: C07443MC9UP 58 | hack-hour: C06SBHMQU8G 59 | days-of-service: C05T8E9GY64 60 | build-programming-languages: C06T22ZFQGP 61 | electronics: C056AMWSFKJ 62 | library: C078Q8PBD4G 63 | happenings: C05B6DBN802 64 | 65 | block: 66 | text: 67 | type: section 68 | text: 69 | type: 'mrkdwn' 70 | text: ${this.text} 71 | context: 72 | type: context 73 | elements: 74 | - type: 'mrkdwn' 75 | text: ${this.text} 76 | image: 77 | type: image 78 | image_url: ${this.url} 79 | alt_text: ${this.altText} 80 | single-button: 81 | type: actions 82 | elements: 83 | - type: button 84 | text: 85 | type: plain_text 86 | text: ${this.text} 87 | value: ${this.value} 88 | double-button: 89 | type: actions 90 | elements: 91 | - type: button 92 | text: 93 | type: plain_text 94 | text: ${this[0].text} 95 | value: ${this[0].value} 96 | - type: button 97 | text: 98 | type: plain_text 99 | text: ${this[1].text} 100 | value: ${this[1].value} 101 | 102 | announcements-to-cave: hello wanderer! head to <#${this.t('channels.cave')}> to continue your journey. 103 | 104 | cave-intro: | 105 | while wandering through the forest, you stumble down the entrance of a cave. it looks too high to climb back out. 106 | 107 | cave-ping: | 108 | _you hear a faint :ping: from the darkness._ 109 | 110 | cave-join: | 111 | oh hello... i don't think i recognize you; you must be new in town. 112 | you may call me <@${this.t('selfUserID')}>! i have tea and a fresh ${this.t('type-of-pie')} pie cooling off... please come over. 113 | 114 | i just sent you a dm! you can tell by the :ping: to the left. 115 | 116 | house: 117 | coc: | 118 | friend, please enjoy the tea and pie while you read this. 119 | 120 | _they point to a page in a book in front of you._ “The Tenants of the Hack Club Slack (abridged 14th ed.), Orpheus et al. 121 | 122 | 123 | 124 | Handwritten in the margins, it says 125 | Be kind, thoughtful and no advertisements. The Fire Department moderates the Hack Club Slack and you read see their actions guide . 126 | 127 | _Toriel leans in._ "Do you vow to uphold the hacker values?" 128 | 129 | You can see I care very much about the wellbeing of everyone here. If you ever have problems or concerns, just email our confidential moderation team (made up primarily of teens!) : conduct@hackclub.com 130 | club-leader: | 131 | are you joining the slack because you're either currently running or considering running a hack club at your school? 132 | 133 | (it's ok if not — this is so i can direct you to the right channels) 134 | added-to-channels: | 135 | And with that, you’re now a full fledged member of the Hack Club online community! 136 | 137 | _You hear the echoes of so many voices and you get a sense of a vast system of cave tunnels and channels and rooms that permeate the darkness. You feel your heart rate increase, just a little. Toriel smiles and refills your tea._ 138 | 139 | Perhaps I can point out a few of my favorite places? 140 | 141 | <#${this.t('channels.announcements')}> to hear big news. 142 | 143 | <#${this.t('channels.library')}> to see all the cool channels & actvity around the slack. 144 | 145 | <#${this.t('channels.ship')}> to be amazed by people just like you 146 | 147 | *Most importantly, you must introduce yourself in <#${this.t('channels.welcome')}>. There are so many intrepid travelers like yourself and they're so excited to meet you. You should tell them about yourself - who you are, what you enjoy doing and anything else you think is interesting.* 148 | added-to-channels-epoch: | 149 | we're so excited to have you here for epoch; *to confirm your registration for epoch, head to <#${this.t('channels.epoch')}> and say hi!* 150 | 151 | i’ve also added you to some other channels you might like. feel free to poke around and get your fellow hackers! 152 | 153 | club-leader: 154 | text: 'a new club leader has joined: <@${this.user}>' 155 | notifiee: U03M1H014CX # Holly 156 | 157 | welcome-committee: | 158 | <@${this.user}> (${this.hs ? 'a high schooler' : 'an adult'} in ${this.continent.toLowerCase()}) just became a full user in the Slack ${this.event ? `from the event *${this.event}*! ` : "!"} \n Here's why they joined: 159 | ${this.message.split('\n').map(line => '> '+line)} 160 | Use \`/slacker gimme\` to take ownership on reaching out. 161 | 162 | notify-stage: | 163 | [ALERT]: <@${this.user}> has been in onboarding for more than 6 hours! 164 | 165 | command: 166 | reason: 167 | wrong-channel: i'm sorry, my child. i can only do that in the welcome committee 168 | no-reason: i'm sorry, my child. i don't know why they joined 169 | no-user: i'm sorry, my child. i don't know who that is 170 | success: i believe they joined because _'${this.reason}'_ 171 | not-found: i'm not sure how to do that, my child. (slash command not found) 172 | cell: 173 | - | 174 | 📞 This is <@${this.t('selfUserID')}>. 175 | You only wanted to say hello...? Well then. 176 | 'Hello!' 177 | I hope that suffices. Hee hee. 178 | - | 179 | 📞 This is <@${this.t('selfUserID')}>. 180 | You wanted to say hello again? 181 | 'Salutations!' 182 | Is that enough? 183 | - | 184 | 📞 This is <@${this.t('selfUserID')}>. 185 | Are you bored? 186 | I should have given you some . 187 | My apologies. 188 | Why not user your imagination to divert yourself? 189 | - | 190 | 📞 This is <@${this.t('selfUserID')}>. 191 | Are you bored? 192 | I should have given to you. 193 | My apologies. 194 | Why not user your imagination to divert yourself? 195 | - | 196 | 📞 This is <@${this.t('selfUserID')}>. 197 | Hello, my child. 198 | Sorry, I do not have much to say. 199 | It was nice to hear your voice, though. 200 | - | 201 | 📞 This is <@${this.t('selfUserID')}>. 202 | You want to know more about me? 203 | Well, I am afraid there is not much to say. 204 | I am just a silly little lady who wants to help everyone! 205 | - | 206 | 📞 This is <@${this.t('selfUserID')}>. 207 | Are you bored? 208 | We could paint the walls at my house ${this.t('themes.array')} 209 | Or, if you prefer another color, you can find one for yourself in <#${this.t('channels.slack-themes')}> 210 | - | 211 | 📞 This is <@${this.t('selfUserID')}>. 212 | Are you bored? 213 | We could catch snails in the garden later. 214 | They make a wonderful pie. 215 | 216 | # Avatars not working? make sure your bot's default avatar is set 217 | # https://github.com/slackapi/hubot-slack/issues/187#issuecomment-158983331 218 | avatar: 219 | happy: https://cloud-ml320gimw-hack-club-bot.vercel.app/0happy.png 220 | default: https://cloud-ml320gimw-hack-club-bot.vercel.app/1default.png 221 | grumpy: https://cloud-ml320gimw-hack-club-bot.vercel.app/2grumpy.png 222 | sad: https://cloud-80hmm9kkv-hack-club-bot.vercel.app/0sad.png 223 | sans: https://cloud-8cdibgmg9-hack-club-bot.vercel.app/0sans.png 224 | log: https://cloud-fy6rskqqp-hack-club-bot.vercel.app/0delta.png 225 | 226 | startup: 227 | avatar: https://cloud-fy6rskqqp-hack-club-bot.vercel.app/0delta.png 228 | message: _${this.t('startup.lines')}... it fills you with determination._ 229 | lines: 230 | - seeing the bots posting logs into <#${this.t('channels.bot-spam')}> 231 | - seeing all the messages posted in <#${this.t('channels.lounge')}> 232 | - knowing that one day the mole in <#${this.t('channels.whack-a-mole')}> might come out of his hole for good 233 | - seeing all the haikus in <#${this.t('channels.haikus')}> 234 | - knowing that there are fish in the <#${this.t('channels.commons')}> 235 | - knowing that despite everything, it's still you 236 | - seeing the fish go "wahoo" 237 | - seeing the squid go "hooray" 238 | - knowing that one day hcb might be open sourced 239 | - knowing that one day you might 240 | - knowing that one day <#${this.t('channels.counttoamillion')}> might reach a million 241 | - knowing more girls will have more opportunities in tech 242 | - smelling the ${this.t('type-of-pie')} pie coming out of the oven 243 | - smelling the fresh baked bread in <#${this.t('channels.bakery')}> 244 | - seeing the boards designed in <#${this.t('channels.onboard')}> 245 | - seeing people play games and trying to get a high score in <#${this.t('channels.sprig')}> 246 | - seeing people show what they want to do this summer in <#${this.t('channels.the-wishing-wall')}> 247 | - seeing people focus on their projects in <#${this.t('channels.hack-hour')}> 248 | - seeing people build their own programming languages in <#${this.t('channels.build-programming-languages')}> 249 | 250 | 251 | themes: 252 | array: 253 | - '#2E3440,#3B4252,#88C0D0,#2E3440,#3B4252,#D8DEE9,#A3BE8C,#81A1C1' # nord 254 | - '#002635,#00384D,#F08E48,#E6E6DC,#00384D,#B7CFF9,#00FFFF,#FF5A67' # https://rigel.netlify.app/#terminal 255 | - '#1F2036,#7986CB,#FE528C,#FFFFFF,#606BA2,#FFFFFF,#2DEBAE,#FE528C,#7986CB,#FFFFFF' 256 | - '#B22130,#E42D40,#E82C3F,#FFFFFF,#CE2334,#FFFFFF,#94E864,#B22130,#B22130,#FFFFFF' # red 257 | - '#161618,#000000,#FFCD00,#161618,#000010,#FFCD00,#FFDA60,#FFB500,#000010,#FFBC00' # gold 258 | - '#130F40,#0E0B01,#37ABD2,#130F40,#342E73,#37ABD2,#FFFFFF,#FF5252,#37ABD2,#0E0B01' # blue 259 | - '#282828,#3C3836,#98971A,#FBF1C7,#3E313C,#EBDBB2,#B8BB26,#FB4934' # gruvbox 260 | 261 | # Slack is very picky about audio files– i've only found uploads and previews to work consistently with m4a files 262 | files: 263 | # home-audio: https://cloud-qx6qgax5b-hack-club-bot.vercel.app/00play_me-home_audio.mp3 264 | home-audio: https://cloud-hz3xqrwce-hack-club-bot.vercel.app/0output.m4a 265 | cave-image: https://cloud-45c5n1f77-hack-club.vercel.app/0ezgif.com-gif-maker.gif 266 | cave-audio: https://cloud-6qf4crf13-hack-club.vercel.app/3undertale_ost_-_001_-_once_upon_a_time-s7rrgf5ve_e.m4a 267 | 268 | rummage: 269 | avatar: 270 | raccoon: https://cloud-1lm9ktddf-hack-club-bot.vercel.app/0rac_yap__1_.gif 271 | trashcan: https://cloud-6cd2dz2n2-hack-club-bot.vercel.app/0screenshot_2024-04-13_at_17.22.02.png 272 | emoji: 273 | pcb: ":pcb:" 274 | yap: ":rac_yap:" 275 | announcements: 276 | init: 🗑️ 277 | raccoon-init: "${this.t('rummage.emoji.yap')} Woah, what's in there?" 278 | raccoon-rummage: "The raccoon starts to RUMMAGE through the trashcan..." 279 | raccoon-join: "Will you join it and RUMMAGE too?" 280 | progress: " _(You've collectively RUMMAGED through ${this.count} items)_" 281 | encouragement: 282 | - "(you think it'd be nice if others start to RUMMAGE with you)" 283 | - "(will others start to RUMMAGE with you?)" 284 | - "" # intentionally left blank to give a slight chance of no encouragement 285 | relationships: 286 | - mom 287 | - teacher 288 | - dad 289 | - sister 290 | - brother 291 | - elementary school bully 292 | - best friend 293 | - friend 294 | - math teacher 295 | - english teacher 296 | groups: 297 | - council of capacitors 298 | - riot of resistors 299 | - squad of soldering irons 300 | - band of breadboards 301 | - loaf of breadboards 302 | - gaggle of guages 303 | - pile of potentiometers 304 | - stack of switches 305 | - bundle of batteries 306 | - cluster of capacitors 307 | individuals: 308 | - capacitor 309 | - resistor 310 | - LED 311 | - screw 312 | - screwdriver 313 | - breadboard 314 | - Raspberry Pi 315 | - motion sensor 316 | - servo motor 317 | - temperature sensor 318 | - humidity sensor (aka 'dank detector') 319 | - battery 320 | low-value: 321 | - banana peel 322 | - banana 323 | - boot 324 | - old sock 325 | - empty can 326 | - empty bottle 327 | - empty box 328 | medium-value: 329 | - a reel of LEDs 330 | - the other sock! now you have a pair! 331 | - a small bag of screws 332 | - a single ${this.t('rummage.individuals')}, all scared and alone 333 | - a ${this.t('rummage.individuals')} 334 | - a rusty ${this.t('rummage.individuals')} 335 | - a few ${this.t('rummage.individuals')}s, bent out of shape 336 | high-value: 337 | - breadboard 338 | - Raspberry Pi 339 | - ${this.t('rummage.groups')} 340 | - shiney new ${this.t('rummage.individuals')} 341 | - Golly Jee! it's a ${this.t('rummage.groups')} 342 | - Gee wizz! it's a ${this.t('rummage.groups')} 343 | first-ten: 344 | - ${this.t('rummage.first-hundred')} ${this.t('rummage.encouragement')} 345 | first-hundred: 346 | - "You find a piece of paper with the following written on it: don't throw away" 347 | - "You hear a faint 'yap' from the trash. Is there something living in there?" 348 | - "You find a ${this.t('rummage.low-value')}! It's not much, but it's something" 349 | - "You peer into the trash. There's a slight shimmer inside it." 350 | - "You find a pretty penny, but... is lincoln winking at you?" 351 | - "You find a two-dollar coin, labeled 'CANADA'. Sounds fake." 352 | - "You root around at the top of the pile and dig under a few things. You find a ${this.t('rummage.low-value')}!" 353 | - "seems like someone threw out a ${this.t('rummage.low-value')}?" 354 | second-hundred: 355 | - "You find a ${this.t('rummage.medium-value')}!" 356 | - "You find a ${this.t('rummage.medium-value')}. Keep this up and you'll have a collection" 357 | - "You feel a vague sense of shame as you reach into the trash without asking the bin's permission" 358 | - "You hear a faint 'yip' from the trash. Is there something living in there?" 359 | - "You gaze down into the trash... and it gazes back up at you" 360 | - "As you reach your hands in you feel a slight tingle. Is there something magical in there? Or just poisonous?" 361 | - "You reach in and grab a warning label that says 'do not touch'" 362 | - "You read a label that says 'prop 65: this product contains chemicals known to the state of california to cause cancer'" 363 | - "You take out a piece of paper that says 'the fitness gram pacer test is a multi-stage aerobic capacity test that progressively gets more difficult as it continues'" 364 | - "you see fingers poking out of the trash... keep digging!" 365 | - "as you dig only one thought pops up in your mind– GET MORE PEOPLEEEEEEEE" 366 | - "you find the shreded parts of a piñata" 367 | - "You keep scrounging & suddenly in your hands is a ${this.t('rummage.medium-value')}" 368 | - "You greedily dig away, searching for something valuable! You find a ${this.t('rummage.medium-value')}!" 369 | - "As you claw your way down, you can only think about finding MORE PEOPEL!!!!1!" 370 | - "You find a gold coin :hc_gold:! Eh, it's probably fake." 371 | - "oh great, you found a ${this.t('rummage.low-value')}! /s" 372 | - "You find a spare tetanus shot. Better keep that for later" 373 | - "You find a pretty penny, but... is lincoln missing his hat?" 374 | - "you find a get out of jail free card & stash it in your pocket" 375 | - "oh great, you found a ${this.t('rummage.low-value')}! Give it to your ${this.t('rummage.relationships')}, maybe they'll be impressed" 376 | third-hundred: 377 | - "who throws out a ${this.t('rummage.high-value')}?" 378 | - "you see a hand poking out of the trash... holding a soldering iron?!" 379 | - "you feel like you're spelunking OwO" 380 | - "you're getting close to the bottom of the trashcan & have a strong urge to GET MORE PEOPLEEE" 381 | - "You start to reach the bottom of the pile and find a ${this.t('rummage.high-value')}!" 382 | - "you hear chittering. Is there something living down there??2?" 383 | - "the hole you dig widens... someone can be seen inside... owo" 384 | - "You find a pretty penny, but... is lincoln wearing a bandit mask?" 385 | - "You find a pretty penny, but... is lincoln OwOing?" 386 | - "you grab something by the tail but it scurries away!" 387 | - "as you dig only one thought pops up in your mind– GET MORE PEOPLEEEEEEEE" 388 | - "you hear the faint sound of power tools down there. what's going on?" 389 | - "you find a ${this.t('rummage.low-value')}! oh well..." 390 | - "you find a ${this.t('rummage.low-value')}! you figure you can trade it for something nicer and throw it back in the trash where it belongs" 391 | - "your fingers brush against a ${this.t('rummage.high-value')}!" 392 | end: 393 | # - "You've reached the bottom of the trashcan..." 394 | - "${this.t('rummage.first-hundred')}" 395 | - "${this.t('rummage.second-hundred')}" 396 | - "${this.t('rummage.third-hundred')}" 397 | raccoon-types: 398 | - raucous 399 | - rocket 400 | - rock'n 401 | - rascal 402 | - rapid 403 | - rambunctious 404 | - rambling 405 | - rabid 406 | - rowdy 407 | - ravenous 408 | -------------------------------------------------------------------------------- /util/upgrade-user.js: -------------------------------------------------------------------------------- 1 | const { client } = require('../app') 2 | const { metrics } = require('./metrics') 3 | const { sendUrgent } = require('./alert') 4 | async function upgradeUser(user) { 5 | const userProfile = await client.users.info({ user }) 6 | const { team_id } = userProfile.user 7 | 8 | if ( 9 | !userProfile.user.is_restricted && 10 | !userProfile.user.is_ultra_restricted 11 | ) { 12 | console.log(`User ${user} is already a full user– skipping`) 13 | return null 14 | } 15 | const startPerf = Date.now() 16 | console.log(`Attempting to upgrade user ${user}`) 17 | 18 | // @msw: This endpoint is undocumented. It's usage and token were taken from 19 | // inspecting the network traffic while upgrading a user. It's the result of 20 | // trial and error replicating the browser calls Slack's admin dashboard 21 | // makes, so duplicate fields (ie. putting user in the URL and JSON body) were 22 | // found necessary get a 200 OK from Slack. 23 | 24 | // The SLACK_COOKIE is a xoxd-* token found in browser cookies under the key 'd' 25 | // The SLACK_BROWSER_TOKEN is a xoxc-* token found in browser local storage using this script: https://gist.github.com/maxwofford/5779ea072a5485ae3b324f03bc5738e1 26 | 27 | const cookieValue = `d=${process.env.SLACK_COOKIE}` 28 | 29 | // Create a new Headers object 30 | const headers = new Headers() 31 | 32 | // Add the cookie to the headers 33 | headers.append('Cookie', cookieValue) 34 | headers.append('Content-Type', 'application/json') 35 | headers.append('Authorization', `Bearer ${process.env.SLACK_BROWSER_TOKEN}`) 36 | 37 | const form = JSON.stringify({ 38 | user, 39 | team_id, 40 | }) 41 | return await fetch( 42 | `https://slack.com/api/users.admin.setRegular?slack_route=${team_id}&user=${user}`, 43 | { 44 | headers, 45 | method: 'POST', 46 | body: form, 47 | } 48 | ) 49 | .then((r) => { 50 | r.json() 51 | metrics.increment('events.flow.user_upgrade', 1) 52 | }) 53 | .catch((e) => { 54 | sendUrgent({ 55 | summary: `A user upgrade failed!`, 56 | detailed: `Upgrading <@${user}> from a multi channel user to a regular user has failed.`, 57 | }) 58 | metrics.increment('events.flow.error.userUpgradeFailure', 1) 59 | console.error() 60 | }) 61 | .finally((e) => { 62 | // send this value to client pls 63 | /* 64 | const time = Date.now() - startPerf 65 | client.timing('events.flow.user_upgrade.time', time)*/ 66 | }) 67 | } 68 | 69 | module.exports = { upgradeUser } 70 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@prisma/client@^5.14.0": 6 | version "5.14.0" 7 | resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.14.0.tgz#dadca5bb1137ddcebb454bbdaf89423823d3363f" 8 | integrity sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg== 9 | 10 | "@prisma/debug@5.14.0": 11 | version "5.14.0" 12 | resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.14.0.tgz#1227c705893c38284f7c63d72441480ebaa12605" 13 | integrity sha512-iq56qBZuFfX3fCxoxT8gBX33lQzomBU0qIUaEj1RebsKVz1ob/BVH1XSBwwwvRVtZEV1b7Fxx2eVu34Ge/mg3w== 14 | 15 | "@prisma/engines-version@5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48": 16 | version "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48" 17 | resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48.tgz#019c3c75a5c3276e580685fe48cdbfd181176858" 18 | integrity sha512-ip6pNkRo1UxWv+6toxNcYvItNYaqQjXdFNGJ+Nuk2eYtRoEdoF13wxo7/jsClJFFenMPVNVqXQDV0oveXnR1cA== 19 | 20 | "@prisma/engines@5.14.0": 21 | version "5.14.0" 22 | resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.14.0.tgz#2ee91dd2220a726c27c906fbea788bbb3efdac6e" 23 | integrity sha512-lgxkKZ6IEygVcw6IZZUlPIfLQ9hjSYAtHjZ5r64sCLDgVzsPFCi2XBBJgzPMkOQ5RHzUD4E/dVdpn9+ez8tk1A== 24 | dependencies: 25 | "@prisma/debug" "5.14.0" 26 | "@prisma/engines-version" "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48" 27 | "@prisma/fetch-engine" "5.14.0" 28 | "@prisma/get-platform" "5.14.0" 29 | 30 | "@prisma/fetch-engine@5.14.0": 31 | version "5.14.0" 32 | resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.14.0.tgz#45297c118d4ec3fea55129886edd5a429da1f6da" 33 | integrity sha512-VrheA9y9DMURK5vu8OJoOgQpxOhas3qF0IBHJ8G/0X44k82kc8E0w98HCn2nhnbOOMwbWsJWXfLC2/F8n5u0gQ== 34 | dependencies: 35 | "@prisma/debug" "5.14.0" 36 | "@prisma/engines-version" "5.14.0-25.e9771e62de70f79a5e1c604a2d7c8e2a0a874b48" 37 | "@prisma/get-platform" "5.14.0" 38 | 39 | "@prisma/get-platform@5.14.0": 40 | version "5.14.0" 41 | resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.14.0.tgz#69112d3dde61905f59a65ed818f153e153ca40f0" 42 | integrity sha512-/yAyBvcEjRv41ynZrhdrPtHgk47xLRRq/o5eWGcUpBJ1YrUZTYB8EoPiopnP7iQrMATK8stXQdPOoVlrzuTQZw== 43 | dependencies: 44 | "@prisma/debug" "5.14.0" 45 | 46 | "@slack/bolt@^3.18.0": 47 | version "3.18.0" 48 | resolved "https://registry.yarnpkg.com/@slack/bolt/-/bolt-3.18.0.tgz#e19dcee8cc5917569bf0806db8dc0c54c98680bd" 49 | integrity sha512-A7bDi5kY50fS6/nsmURkQdO3iMxD8aX/rA+m1UXEM2ue2z4KijeQtx2sOZ4YkJQ/h7BsgTQM0CYh3qqmo+m5sQ== 50 | dependencies: 51 | "@slack/logger" "^4.0.0" 52 | "@slack/oauth" "^2.6.2" 53 | "@slack/socket-mode" "^1.3.3" 54 | "@slack/types" "^2.11.0" 55 | "@slack/web-api" "^6.11.2" 56 | "@types/express" "^4.16.1" 57 | "@types/promise.allsettled" "^1.0.3" 58 | "@types/tsscmp" "^1.0.0" 59 | axios "^1.6.0" 60 | express "^4.16.4" 61 | path-to-regexp "^6.2.1" 62 | please-upgrade-node "^3.2.0" 63 | promise.allsettled "^1.0.2" 64 | raw-body "^2.3.3" 65 | tsscmp "^1.0.6" 66 | 67 | "@slack/logger@^3.0.0": 68 | version "3.0.0" 69 | resolved "https://registry.yarnpkg.com/@slack/logger/-/logger-3.0.0.tgz#b736d4e1c112c22a10ffab0c2d364620aedcb714" 70 | integrity sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA== 71 | dependencies: 72 | "@types/node" ">=12.0.0" 73 | 74 | "@slack/logger@^4.0.0": 75 | version "4.0.0" 76 | resolved "https://registry.yarnpkg.com/@slack/logger/-/logger-4.0.0.tgz#788303ff1840be91bdad7711ef66ca0cbc7073d2" 77 | integrity sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA== 78 | dependencies: 79 | "@types/node" ">=18.0.0" 80 | 81 | "@slack/oauth@^2.6.2": 82 | version "2.6.2" 83 | resolved "https://registry.yarnpkg.com/@slack/oauth/-/oauth-2.6.2.tgz#02fc569ecd0be459c92ac17d4629b7fdc89ff3a9" 84 | integrity sha512-2R3MyB/R63hTRXzk5J6wcui59TBxXzhk+Uh2/Xu3Wp3O4pXg/BNucQhP/DQbL/ScVhLvFtMXirLrKi0Yo5gIVw== 85 | dependencies: 86 | "@slack/logger" "^3.0.0" 87 | "@slack/web-api" "^6.11.2" 88 | "@types/jsonwebtoken" "^8.3.7" 89 | "@types/node" ">=12" 90 | jsonwebtoken "^9.0.0" 91 | lodash.isstring "^4.0.1" 92 | 93 | "@slack/socket-mode@^1.3.3": 94 | version "1.3.5" 95 | resolved "https://registry.yarnpkg.com/@slack/socket-mode/-/socket-mode-1.3.5.tgz#d2596281404c46ec23d612d0940135c2bad25f3f" 96 | integrity sha512-m2J2hVCIxEvBinkNINNmizDZWBa6D9taFD930aknEDi+2DtzFSMhmxci/VeN7DJVUe+ovLl3lPlvLK9p4hd5CQ== 97 | dependencies: 98 | "@slack/logger" "^3.0.0" 99 | "@slack/web-api" "^6.11.2" 100 | "@types/node" ">=12.0.0" 101 | "@types/ws" "^7.4.7" 102 | eventemitter3 "^5" 103 | finity "^0.5.4" 104 | ws "^7.5.3" 105 | 106 | "@slack/types@^2.11.0": 107 | version "2.11.0" 108 | resolved "https://registry.yarnpkg.com/@slack/types/-/types-2.11.0.tgz#948c556081c3db977dfa8433490cc2ff41f47203" 109 | integrity sha512-UlIrDWvuLaDly3QZhCPnwUSI/KYmV1N9LyhuH6EDKCRS1HWZhyTG3Ja46T3D0rYfqdltKYFXbJSSRPwZpwO0cQ== 110 | 111 | "@slack/web-api@^6.11.2": 112 | version "6.12.0" 113 | resolved "https://registry.yarnpkg.com/@slack/web-api/-/web-api-6.12.0.tgz#d0487d90e3db2f7bfabe3430fa5da0cc03d2d9cb" 114 | integrity sha512-RPw6F8rWfGveGkZEJ4+4jUin5iazxRK2q3FpQDz/FvdgzC3nZmPyLx8WRzc6nh0w3MBjEbphNnp2VZksfhpBIQ== 115 | dependencies: 116 | "@slack/logger" "^3.0.0" 117 | "@slack/types" "^2.11.0" 118 | "@types/is-stream" "^1.1.0" 119 | "@types/node" ">=12.0.0" 120 | axios "^1.6.5" 121 | eventemitter3 "^3.1.0" 122 | form-data "^2.5.0" 123 | is-electron "2.2.2" 124 | is-stream "^1.1.0" 125 | p-queue "^6.6.1" 126 | p-retry "^4.0.0" 127 | 128 | "@types/body-parser@*": 129 | version "1.19.5" 130 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" 131 | integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== 132 | dependencies: 133 | "@types/connect" "*" 134 | "@types/node" "*" 135 | 136 | "@types/connect@*": 137 | version "3.4.38" 138 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" 139 | integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== 140 | dependencies: 141 | "@types/node" "*" 142 | 143 | "@types/express-serve-static-core@^4.17.33": 144 | version "4.19.0" 145 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz#3ae8ab3767d98d0b682cda063c3339e1e86ccfaa" 146 | integrity sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ== 147 | dependencies: 148 | "@types/node" "*" 149 | "@types/qs" "*" 150 | "@types/range-parser" "*" 151 | "@types/send" "*" 152 | 153 | "@types/express@^4.16.1": 154 | version "4.17.21" 155 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" 156 | integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== 157 | dependencies: 158 | "@types/body-parser" "*" 159 | "@types/express-serve-static-core" "^4.17.33" 160 | "@types/qs" "*" 161 | "@types/serve-static" "*" 162 | 163 | "@types/http-errors@*": 164 | version "2.0.4" 165 | resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" 166 | integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== 167 | 168 | "@types/is-stream@^1.1.0": 169 | version "1.1.0" 170 | resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1" 171 | integrity sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg== 172 | dependencies: 173 | "@types/node" "*" 174 | 175 | "@types/jsonwebtoken@^8.3.7": 176 | version "8.5.9" 177 | resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz#2c064ecb0b3128d837d2764aa0b117b0ff6e4586" 178 | integrity sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg== 179 | dependencies: 180 | "@types/node" "*" 181 | 182 | "@types/mime@^1": 183 | version "1.3.5" 184 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" 185 | integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== 186 | 187 | "@types/node@*", "@types/node@>=12", "@types/node@>=12.0.0", "@types/node@>=18.0.0": 188 | version "20.12.12" 189 | resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.12.tgz#7cbecdf902085cec634fdb362172dfe12b8f2050" 190 | integrity sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw== 191 | dependencies: 192 | undici-types "~5.26.4" 193 | 194 | "@types/promise.allsettled@^1.0.3": 195 | version "1.0.6" 196 | resolved "https://registry.yarnpkg.com/@types/promise.allsettled/-/promise.allsettled-1.0.6.tgz#3f92657f807f14b165b90ee06cad3123529bd336" 197 | integrity sha512-wA0UT0HeT2fGHzIFV9kWpYz5mdoyLxKrTgMdZQM++5h6pYAFH73HXcQhefg24nD1yivUFEn5KU+EF4b+CXJ4Wg== 198 | 199 | "@types/qs@*": 200 | version "6.9.15" 201 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" 202 | integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== 203 | 204 | "@types/range-parser@*": 205 | version "1.2.7" 206 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" 207 | integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== 208 | 209 | "@types/retry@0.12.0": 210 | version "0.12.0" 211 | resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" 212 | integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== 213 | 214 | "@types/send@*": 215 | version "0.17.4" 216 | resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" 217 | integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== 218 | dependencies: 219 | "@types/mime" "^1" 220 | "@types/node" "*" 221 | 222 | "@types/serve-static@*": 223 | version "1.15.7" 224 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" 225 | integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== 226 | dependencies: 227 | "@types/http-errors" "*" 228 | "@types/node" "*" 229 | "@types/send" "*" 230 | 231 | "@types/tsscmp@^1.0.0": 232 | version "1.0.2" 233 | resolved "https://registry.yarnpkg.com/@types/tsscmp/-/tsscmp-1.0.2.tgz#5f4679b5681bcf916a383ca9245371ce9748db5a" 234 | integrity sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g== 235 | 236 | "@types/ws@^7.4.7": 237 | version "7.4.7" 238 | resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" 239 | integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== 240 | dependencies: 241 | "@types/node" "*" 242 | 243 | accepts@~1.3.8: 244 | version "1.3.8" 245 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 246 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 247 | dependencies: 248 | mime-types "~2.1.34" 249 | negotiator "0.6.3" 250 | 251 | argparse@^2.0.1: 252 | version "2.0.1" 253 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 254 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 255 | 256 | array-buffer-byte-length@^1.0.1: 257 | version "1.0.1" 258 | resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" 259 | integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== 260 | dependencies: 261 | call-bind "^1.0.5" 262 | is-array-buffer "^3.0.4" 263 | 264 | array-flatten@1.1.1: 265 | version "1.1.1" 266 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 267 | integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== 268 | 269 | array.prototype.map@^1.0.5: 270 | version "1.0.7" 271 | resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.7.tgz#82fa4d6027272d1fca28a63bbda424d0185d78a7" 272 | integrity sha512-XpcFfLoBEAhezrrNw1V+yLXkE7M6uR7xJEsxbG6c/V9v043qurwVJB9r9UTnoSioFDoz1i1VOydpWGmJpfVZbg== 273 | dependencies: 274 | call-bind "^1.0.7" 275 | define-properties "^1.2.1" 276 | es-abstract "^1.23.2" 277 | es-array-method-boxes-properly "^1.0.0" 278 | es-object-atoms "^1.0.0" 279 | is-string "^1.0.7" 280 | 281 | arraybuffer.prototype.slice@^1.0.3: 282 | version "1.0.3" 283 | resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" 284 | integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== 285 | dependencies: 286 | array-buffer-byte-length "^1.0.1" 287 | call-bind "^1.0.5" 288 | define-properties "^1.2.1" 289 | es-abstract "^1.22.3" 290 | es-errors "^1.2.1" 291 | get-intrinsic "^1.2.3" 292 | is-array-buffer "^3.0.4" 293 | is-shared-array-buffer "^1.0.2" 294 | 295 | asynckit@^0.4.0: 296 | version "0.4.0" 297 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 298 | integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== 299 | 300 | available-typed-arrays@^1.0.7: 301 | version "1.0.7" 302 | resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" 303 | integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== 304 | dependencies: 305 | possible-typed-array-names "^1.0.0" 306 | 307 | axios@^1.6.0, axios@^1.6.5: 308 | version "1.7.1" 309 | resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.1.tgz#522145622a09dfaf49359837db9649ff245a35b9" 310 | integrity sha512-+LV37nQcd1EpFalkXksWNBiA17NZ5m5/WspmHGmZmdx1qBOg/VNq/c4eRJiA9VQQHBOs+N0ZhhdU10h2TyNK7Q== 311 | dependencies: 312 | follow-redirects "^1.15.6" 313 | form-data "^4.0.0" 314 | proxy-from-env "^1.1.0" 315 | 316 | body-parser@1.20.2: 317 | version "1.20.2" 318 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" 319 | integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== 320 | dependencies: 321 | bytes "3.1.2" 322 | content-type "~1.0.5" 323 | debug "2.6.9" 324 | depd "2.0.0" 325 | destroy "1.2.0" 326 | http-errors "2.0.0" 327 | iconv-lite "0.4.24" 328 | on-finished "2.4.1" 329 | qs "6.11.0" 330 | raw-body "2.5.2" 331 | type-is "~1.6.18" 332 | unpipe "1.0.0" 333 | 334 | buffer-equal-constant-time@1.0.1: 335 | version "1.0.1" 336 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 337 | integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== 338 | 339 | bytes@3.1.2: 340 | version "3.1.2" 341 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 342 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 343 | 344 | call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: 345 | version "1.0.7" 346 | resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" 347 | integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== 348 | dependencies: 349 | es-define-property "^1.0.0" 350 | es-errors "^1.3.0" 351 | function-bind "^1.1.2" 352 | get-intrinsic "^1.2.4" 353 | set-function-length "^1.2.1" 354 | 355 | combined-stream@^1.0.6, combined-stream@^1.0.8: 356 | version "1.0.8" 357 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 358 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 359 | dependencies: 360 | delayed-stream "~1.0.0" 361 | 362 | content-disposition@0.5.4: 363 | version "0.5.4" 364 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 365 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 366 | dependencies: 367 | safe-buffer "5.2.1" 368 | 369 | content-type@~1.0.4, content-type@~1.0.5: 370 | version "1.0.5" 371 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" 372 | integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== 373 | 374 | cookie-signature@1.0.6: 375 | version "1.0.6" 376 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 377 | integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== 378 | 379 | cookie@0.6.0: 380 | version "0.6.0" 381 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" 382 | integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== 383 | 384 | data-view-buffer@^1.0.1: 385 | version "1.0.1" 386 | resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" 387 | integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== 388 | dependencies: 389 | call-bind "^1.0.6" 390 | es-errors "^1.3.0" 391 | is-data-view "^1.0.1" 392 | 393 | data-view-byte-length@^1.0.1: 394 | version "1.0.1" 395 | resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" 396 | integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== 397 | dependencies: 398 | call-bind "^1.0.7" 399 | es-errors "^1.3.0" 400 | is-data-view "^1.0.1" 401 | 402 | data-view-byte-offset@^1.0.0: 403 | version "1.0.0" 404 | resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" 405 | integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== 406 | dependencies: 407 | call-bind "^1.0.6" 408 | es-errors "^1.3.0" 409 | is-data-view "^1.0.1" 410 | 411 | debug@2.6.9: 412 | version "2.6.9" 413 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 414 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 415 | dependencies: 416 | ms "2.0.0" 417 | 418 | define-data-property@^1.0.1, define-data-property@^1.1.4: 419 | version "1.1.4" 420 | resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" 421 | integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== 422 | dependencies: 423 | es-define-property "^1.0.0" 424 | es-errors "^1.3.0" 425 | gopd "^1.0.1" 426 | 427 | define-properties@^1.2.0, define-properties@^1.2.1: 428 | version "1.2.1" 429 | resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" 430 | integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== 431 | dependencies: 432 | define-data-property "^1.0.1" 433 | has-property-descriptors "^1.0.0" 434 | object-keys "^1.1.1" 435 | 436 | delayed-stream@~1.0.0: 437 | version "1.0.0" 438 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 439 | integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== 440 | 441 | depd@2.0.0: 442 | version "2.0.0" 443 | resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 444 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 445 | 446 | destroy@1.2.0: 447 | version "1.2.0" 448 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" 449 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 450 | 451 | dotenv@^16.3.1: 452 | version "16.4.5" 453 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" 454 | integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== 455 | 456 | ecdsa-sig-formatter@1.0.11: 457 | version "1.0.11" 458 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" 459 | integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== 460 | dependencies: 461 | safe-buffer "^5.0.1" 462 | 463 | ee-first@1.1.1: 464 | version "1.1.1" 465 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 466 | integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== 467 | 468 | encodeurl@~1.0.2: 469 | version "1.0.2" 470 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 471 | integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 472 | 473 | es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: 474 | version "1.23.3" 475 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" 476 | integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== 477 | dependencies: 478 | array-buffer-byte-length "^1.0.1" 479 | arraybuffer.prototype.slice "^1.0.3" 480 | available-typed-arrays "^1.0.7" 481 | call-bind "^1.0.7" 482 | data-view-buffer "^1.0.1" 483 | data-view-byte-length "^1.0.1" 484 | data-view-byte-offset "^1.0.0" 485 | es-define-property "^1.0.0" 486 | es-errors "^1.3.0" 487 | es-object-atoms "^1.0.0" 488 | es-set-tostringtag "^2.0.3" 489 | es-to-primitive "^1.2.1" 490 | function.prototype.name "^1.1.6" 491 | get-intrinsic "^1.2.4" 492 | get-symbol-description "^1.0.2" 493 | globalthis "^1.0.3" 494 | gopd "^1.0.1" 495 | has-property-descriptors "^1.0.2" 496 | has-proto "^1.0.3" 497 | has-symbols "^1.0.3" 498 | hasown "^2.0.2" 499 | internal-slot "^1.0.7" 500 | is-array-buffer "^3.0.4" 501 | is-callable "^1.2.7" 502 | is-data-view "^1.0.1" 503 | is-negative-zero "^2.0.3" 504 | is-regex "^1.1.4" 505 | is-shared-array-buffer "^1.0.3" 506 | is-string "^1.0.7" 507 | is-typed-array "^1.1.13" 508 | is-weakref "^1.0.2" 509 | object-inspect "^1.13.1" 510 | object-keys "^1.1.1" 511 | object.assign "^4.1.5" 512 | regexp.prototype.flags "^1.5.2" 513 | safe-array-concat "^1.1.2" 514 | safe-regex-test "^1.0.3" 515 | string.prototype.trim "^1.2.9" 516 | string.prototype.trimend "^1.0.8" 517 | string.prototype.trimstart "^1.0.8" 518 | typed-array-buffer "^1.0.2" 519 | typed-array-byte-length "^1.0.1" 520 | typed-array-byte-offset "^1.0.2" 521 | typed-array-length "^1.0.6" 522 | unbox-primitive "^1.0.2" 523 | which-typed-array "^1.1.15" 524 | 525 | es-array-method-boxes-properly@^1.0.0: 526 | version "1.0.0" 527 | resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" 528 | integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== 529 | 530 | es-define-property@^1.0.0: 531 | version "1.0.0" 532 | resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" 533 | integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== 534 | dependencies: 535 | get-intrinsic "^1.2.4" 536 | 537 | es-errors@^1.2.1, es-errors@^1.3.0: 538 | version "1.3.0" 539 | resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" 540 | integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== 541 | 542 | es-get-iterator@^1.0.2: 543 | version "1.1.3" 544 | resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" 545 | integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== 546 | dependencies: 547 | call-bind "^1.0.2" 548 | get-intrinsic "^1.1.3" 549 | has-symbols "^1.0.3" 550 | is-arguments "^1.1.1" 551 | is-map "^2.0.2" 552 | is-set "^2.0.2" 553 | is-string "^1.0.7" 554 | isarray "^2.0.5" 555 | stop-iteration-iterator "^1.0.0" 556 | 557 | es-object-atoms@^1.0.0: 558 | version "1.0.0" 559 | resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" 560 | integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== 561 | dependencies: 562 | es-errors "^1.3.0" 563 | 564 | es-set-tostringtag@^2.0.3: 565 | version "2.0.3" 566 | resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" 567 | integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== 568 | dependencies: 569 | get-intrinsic "^1.2.4" 570 | has-tostringtag "^1.0.2" 571 | hasown "^2.0.1" 572 | 573 | es-to-primitive@^1.2.1: 574 | version "1.2.1" 575 | resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" 576 | integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== 577 | dependencies: 578 | is-callable "^1.1.4" 579 | is-date-object "^1.0.1" 580 | is-symbol "^1.0.2" 581 | 582 | escape-html@~1.0.3: 583 | version "1.0.3" 584 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 585 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== 586 | 587 | etag@~1.8.1: 588 | version "1.8.1" 589 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 590 | integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== 591 | 592 | eventemitter3@^3.1.0: 593 | version "3.1.2" 594 | resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" 595 | integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== 596 | 597 | eventemitter3@^4.0.4: 598 | version "4.0.7" 599 | resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" 600 | integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== 601 | 602 | eventemitter3@^5: 603 | version "5.0.1" 604 | resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" 605 | integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== 606 | 607 | express@^4.16.4, express@^4.18.2: 608 | version "4.19.2" 609 | resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" 610 | integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== 611 | dependencies: 612 | accepts "~1.3.8" 613 | array-flatten "1.1.1" 614 | body-parser "1.20.2" 615 | content-disposition "0.5.4" 616 | content-type "~1.0.4" 617 | cookie "0.6.0" 618 | cookie-signature "1.0.6" 619 | debug "2.6.9" 620 | depd "2.0.0" 621 | encodeurl "~1.0.2" 622 | escape-html "~1.0.3" 623 | etag "~1.8.1" 624 | finalhandler "1.2.0" 625 | fresh "0.5.2" 626 | http-errors "2.0.0" 627 | merge-descriptors "1.0.1" 628 | methods "~1.1.2" 629 | on-finished "2.4.1" 630 | parseurl "~1.3.3" 631 | path-to-regexp "0.1.7" 632 | proxy-addr "~2.0.7" 633 | qs "6.11.0" 634 | range-parser "~1.2.1" 635 | safe-buffer "5.2.1" 636 | send "0.18.0" 637 | serve-static "1.15.0" 638 | setprototypeof "1.2.0" 639 | statuses "2.0.1" 640 | type-is "~1.6.18" 641 | utils-merge "1.0.1" 642 | vary "~1.1.2" 643 | 644 | finalhandler@1.2.0: 645 | version "1.2.0" 646 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" 647 | integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== 648 | dependencies: 649 | debug "2.6.9" 650 | encodeurl "~1.0.2" 651 | escape-html "~1.0.3" 652 | on-finished "2.4.1" 653 | parseurl "~1.3.3" 654 | statuses "2.0.1" 655 | unpipe "~1.0.0" 656 | 657 | finity@^0.5.4: 658 | version "0.5.4" 659 | resolved "https://registry.yarnpkg.com/finity/-/finity-0.5.4.tgz#f2a8a9198e8286467328ec32c8bfcc19a2229c11" 660 | integrity sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA== 661 | 662 | follow-redirects@^1.15.6: 663 | version "1.15.6" 664 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" 665 | integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== 666 | 667 | for-each@^0.3.3: 668 | version "0.3.3" 669 | resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" 670 | integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== 671 | dependencies: 672 | is-callable "^1.1.3" 673 | 674 | form-data@^2.5.0: 675 | version "2.5.1" 676 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" 677 | integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== 678 | dependencies: 679 | asynckit "^0.4.0" 680 | combined-stream "^1.0.6" 681 | mime-types "^2.1.12" 682 | 683 | form-data@^4.0.0: 684 | version "4.0.0" 685 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" 686 | integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== 687 | dependencies: 688 | asynckit "^0.4.0" 689 | combined-stream "^1.0.8" 690 | mime-types "^2.1.12" 691 | 692 | forwarded@0.2.0: 693 | version "0.2.0" 694 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" 695 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 696 | 697 | fresh@0.5.2: 698 | version "0.5.2" 699 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 700 | integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== 701 | 702 | function-bind@^1.1.2: 703 | version "1.1.2" 704 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" 705 | integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== 706 | 707 | function.prototype.name@^1.1.6: 708 | version "1.1.6" 709 | resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" 710 | integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== 711 | dependencies: 712 | call-bind "^1.0.2" 713 | define-properties "^1.2.0" 714 | es-abstract "^1.22.1" 715 | functions-have-names "^1.2.3" 716 | 717 | functions-have-names@^1.2.3: 718 | version "1.2.3" 719 | resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" 720 | integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== 721 | 722 | get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: 723 | version "1.2.4" 724 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" 725 | integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== 726 | dependencies: 727 | es-errors "^1.3.0" 728 | function-bind "^1.1.2" 729 | has-proto "^1.0.1" 730 | has-symbols "^1.0.3" 731 | hasown "^2.0.0" 732 | 733 | get-symbol-description@^1.0.2: 734 | version "1.0.2" 735 | resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" 736 | integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== 737 | dependencies: 738 | call-bind "^1.0.5" 739 | es-errors "^1.3.0" 740 | get-intrinsic "^1.2.4" 741 | 742 | globalthis@^1.0.3: 743 | version "1.0.4" 744 | resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" 745 | integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== 746 | dependencies: 747 | define-properties "^1.2.1" 748 | gopd "^1.0.1" 749 | 750 | gopd@^1.0.1: 751 | version "1.0.1" 752 | resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" 753 | integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== 754 | dependencies: 755 | get-intrinsic "^1.1.3" 756 | 757 | has-bigints@^1.0.1, has-bigints@^1.0.2: 758 | version "1.0.2" 759 | resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" 760 | integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== 761 | 762 | has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: 763 | version "1.0.2" 764 | resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" 765 | integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== 766 | dependencies: 767 | es-define-property "^1.0.0" 768 | 769 | has-proto@^1.0.1, has-proto@^1.0.3: 770 | version "1.0.3" 771 | resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" 772 | integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== 773 | 774 | has-symbols@^1.0.2, has-symbols@^1.0.3: 775 | version "1.0.3" 776 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 777 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 778 | 779 | has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: 780 | version "1.0.2" 781 | resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" 782 | integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== 783 | dependencies: 784 | has-symbols "^1.0.3" 785 | 786 | hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: 787 | version "2.0.2" 788 | resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" 789 | integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== 790 | dependencies: 791 | function-bind "^1.1.2" 792 | 793 | http-errors@2.0.0: 794 | version "2.0.0" 795 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" 796 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 797 | dependencies: 798 | depd "2.0.0" 799 | inherits "2.0.4" 800 | setprototypeof "1.2.0" 801 | statuses "2.0.1" 802 | toidentifier "1.0.1" 803 | 804 | iconv-lite@0.4.24: 805 | version "0.4.24" 806 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 807 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 808 | dependencies: 809 | safer-buffer ">= 2.1.2 < 3" 810 | 811 | inherits@2.0.4: 812 | version "2.0.4" 813 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 814 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 815 | 816 | internal-slot@^1.0.4, internal-slot@^1.0.7: 817 | version "1.0.7" 818 | resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" 819 | integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== 820 | dependencies: 821 | es-errors "^1.3.0" 822 | hasown "^2.0.0" 823 | side-channel "^1.0.4" 824 | 825 | ipaddr.js@1.9.1: 826 | version "1.9.1" 827 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 828 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 829 | 830 | is-arguments@^1.1.1: 831 | version "1.1.1" 832 | resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" 833 | integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== 834 | dependencies: 835 | call-bind "^1.0.2" 836 | has-tostringtag "^1.0.0" 837 | 838 | is-array-buffer@^3.0.4: 839 | version "3.0.4" 840 | resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" 841 | integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== 842 | dependencies: 843 | call-bind "^1.0.2" 844 | get-intrinsic "^1.2.1" 845 | 846 | is-bigint@^1.0.1: 847 | version "1.0.4" 848 | resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" 849 | integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== 850 | dependencies: 851 | has-bigints "^1.0.1" 852 | 853 | is-boolean-object@^1.1.0: 854 | version "1.1.2" 855 | resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" 856 | integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== 857 | dependencies: 858 | call-bind "^1.0.2" 859 | has-tostringtag "^1.0.0" 860 | 861 | is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: 862 | version "1.2.7" 863 | resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" 864 | integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== 865 | 866 | is-data-view@^1.0.1: 867 | version "1.0.1" 868 | resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" 869 | integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== 870 | dependencies: 871 | is-typed-array "^1.1.13" 872 | 873 | is-date-object@^1.0.1: 874 | version "1.0.5" 875 | resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" 876 | integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== 877 | dependencies: 878 | has-tostringtag "^1.0.0" 879 | 880 | is-electron@2.2.2: 881 | version "2.2.2" 882 | resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.2.tgz#3778902a2044d76de98036f5dc58089ac4d80bb9" 883 | integrity sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg== 884 | 885 | is-map@^2.0.2: 886 | version "2.0.3" 887 | resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" 888 | integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== 889 | 890 | is-negative-zero@^2.0.3: 891 | version "2.0.3" 892 | resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" 893 | integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== 894 | 895 | is-number-object@^1.0.4: 896 | version "1.0.7" 897 | resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" 898 | integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== 899 | dependencies: 900 | has-tostringtag "^1.0.0" 901 | 902 | is-regex@^1.1.4: 903 | version "1.1.4" 904 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" 905 | integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== 906 | dependencies: 907 | call-bind "^1.0.2" 908 | has-tostringtag "^1.0.0" 909 | 910 | is-set@^2.0.2: 911 | version "2.0.3" 912 | resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" 913 | integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== 914 | 915 | is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: 916 | version "1.0.3" 917 | resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" 918 | integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== 919 | dependencies: 920 | call-bind "^1.0.7" 921 | 922 | is-stream@^1.1.0: 923 | version "1.1.0" 924 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 925 | integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== 926 | 927 | is-string@^1.0.5, is-string@^1.0.7: 928 | version "1.0.7" 929 | resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" 930 | integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== 931 | dependencies: 932 | has-tostringtag "^1.0.0" 933 | 934 | is-symbol@^1.0.2, is-symbol@^1.0.3: 935 | version "1.0.4" 936 | resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" 937 | integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== 938 | dependencies: 939 | has-symbols "^1.0.2" 940 | 941 | is-typed-array@^1.1.13: 942 | version "1.1.13" 943 | resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" 944 | integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== 945 | dependencies: 946 | which-typed-array "^1.1.14" 947 | 948 | is-weakref@^1.0.2: 949 | version "1.0.2" 950 | resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" 951 | integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== 952 | dependencies: 953 | call-bind "^1.0.2" 954 | 955 | isarray@^2.0.5: 956 | version "2.0.5" 957 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" 958 | integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== 959 | 960 | iterate-iterator@^1.0.1: 961 | version "1.0.2" 962 | resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.2.tgz#551b804c9eaa15b847ea6a7cdc2f5bf1ec150f91" 963 | integrity sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw== 964 | 965 | iterate-value@^1.0.2: 966 | version "1.0.2" 967 | resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" 968 | integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== 969 | dependencies: 970 | es-get-iterator "^1.0.2" 971 | iterate-iterator "^1.0.1" 972 | 973 | js-yaml@^4.1.0: 974 | version "4.1.0" 975 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" 976 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 977 | dependencies: 978 | argparse "^2.0.1" 979 | 980 | jsonwebtoken@^9.0.0: 981 | version "9.0.2" 982 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" 983 | integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== 984 | dependencies: 985 | jws "^3.2.2" 986 | lodash.includes "^4.3.0" 987 | lodash.isboolean "^3.0.3" 988 | lodash.isinteger "^4.0.4" 989 | lodash.isnumber "^3.0.3" 990 | lodash.isplainobject "^4.0.6" 991 | lodash.isstring "^4.0.1" 992 | lodash.once "^4.0.0" 993 | ms "^2.1.1" 994 | semver "^7.5.4" 995 | 996 | jwa@^1.4.1: 997 | version "1.4.1" 998 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" 999 | integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== 1000 | dependencies: 1001 | buffer-equal-constant-time "1.0.1" 1002 | ecdsa-sig-formatter "1.0.11" 1003 | safe-buffer "^5.0.1" 1004 | 1005 | jws@^3.2.2: 1006 | version "3.2.2" 1007 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" 1008 | integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== 1009 | dependencies: 1010 | jwa "^1.4.1" 1011 | safe-buffer "^5.0.1" 1012 | 1013 | lodash.includes@^4.3.0: 1014 | version "4.3.0" 1015 | resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" 1016 | integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== 1017 | 1018 | lodash.isboolean@^3.0.3: 1019 | version "3.0.3" 1020 | resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" 1021 | integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== 1022 | 1023 | lodash.isinteger@^4.0.4: 1024 | version "4.0.4" 1025 | resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" 1026 | integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== 1027 | 1028 | lodash.isnumber@^3.0.3: 1029 | version "3.0.3" 1030 | resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" 1031 | integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== 1032 | 1033 | lodash.isplainobject@^4.0.6: 1034 | version "4.0.6" 1035 | resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" 1036 | integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== 1037 | 1038 | lodash.isstring@^4.0.1: 1039 | version "4.0.1" 1040 | resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" 1041 | integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== 1042 | 1043 | lodash.once@^4.0.0: 1044 | version "4.1.1" 1045 | resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 1046 | integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== 1047 | 1048 | media-typer@0.3.0: 1049 | version "0.3.0" 1050 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 1051 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== 1052 | 1053 | merge-descriptors@1.0.1: 1054 | version "1.0.1" 1055 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 1056 | integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== 1057 | 1058 | methods@~1.1.2: 1059 | version "1.1.2" 1060 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 1061 | integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== 1062 | 1063 | mime-db@1.52.0: 1064 | version "1.52.0" 1065 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 1066 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 1067 | 1068 | mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34: 1069 | version "2.1.35" 1070 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 1071 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 1072 | dependencies: 1073 | mime-db "1.52.0" 1074 | 1075 | mime@1.6.0: 1076 | version "1.6.0" 1077 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 1078 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 1079 | 1080 | ms@2.0.0: 1081 | version "2.0.0" 1082 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 1083 | integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== 1084 | 1085 | ms@2.1.3, ms@^2.1.1: 1086 | version "2.1.3" 1087 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 1088 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 1089 | 1090 | negotiator@0.6.3: 1091 | version "0.6.3" 1092 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 1093 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 1094 | 1095 | node-statsd@^0.1.1: 1096 | version "0.1.1" 1097 | resolved "https://registry.yarnpkg.com/node-statsd/-/node-statsd-0.1.1.tgz#27a59348763d0af7a037ac2a031fef3f051013d3" 1098 | integrity sha512-QDf6R8VXF56QVe1boek8an/Rb3rSNaxoFWb7Elpsv2m1+Noua1yy0F1FpKpK5VluF8oymWM4w764A4KsYL4pDg== 1099 | 1100 | object-inspect@^1.13.1: 1101 | version "1.13.1" 1102 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" 1103 | integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== 1104 | 1105 | object-keys@^1.1.1: 1106 | version "1.1.1" 1107 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" 1108 | integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== 1109 | 1110 | object.assign@^4.1.5: 1111 | version "4.1.5" 1112 | resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" 1113 | integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== 1114 | dependencies: 1115 | call-bind "^1.0.5" 1116 | define-properties "^1.2.1" 1117 | has-symbols "^1.0.3" 1118 | object-keys "^1.1.1" 1119 | 1120 | on-finished@2.4.1: 1121 | version "2.4.1" 1122 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" 1123 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 1124 | dependencies: 1125 | ee-first "1.1.1" 1126 | 1127 | p-finally@^1.0.0: 1128 | version "1.0.0" 1129 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" 1130 | integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== 1131 | 1132 | p-queue@^6.6.1: 1133 | version "6.6.2" 1134 | resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" 1135 | integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== 1136 | dependencies: 1137 | eventemitter3 "^4.0.4" 1138 | p-timeout "^3.2.0" 1139 | 1140 | p-retry@^4.0.0: 1141 | version "4.6.2" 1142 | resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" 1143 | integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== 1144 | dependencies: 1145 | "@types/retry" "0.12.0" 1146 | retry "^0.13.1" 1147 | 1148 | p-timeout@^3.2.0: 1149 | version "3.2.0" 1150 | resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" 1151 | integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== 1152 | dependencies: 1153 | p-finally "^1.0.0" 1154 | 1155 | parseurl@~1.3.3: 1156 | version "1.3.3" 1157 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 1158 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 1159 | 1160 | path-to-regexp@0.1.7: 1161 | version "0.1.7" 1162 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 1163 | integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== 1164 | 1165 | path-to-regexp@^6.2.1: 1166 | version "6.2.2" 1167 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.2.tgz#324377a83e5049cbecadc5554d6a63a9a4866b36" 1168 | integrity sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw== 1169 | 1170 | please-upgrade-node@^3.2.0: 1171 | version "3.2.0" 1172 | resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" 1173 | integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== 1174 | dependencies: 1175 | semver-compare "^1.0.0" 1176 | 1177 | possible-typed-array-names@^1.0.0: 1178 | version "1.0.0" 1179 | resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" 1180 | integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== 1181 | 1182 | prisma@^5.14.0: 1183 | version "5.14.0" 1184 | resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.14.0.tgz#ffc4696a43b044b636c3303b7aa98c13c2ade4dd" 1185 | integrity sha512-gCNZco7y5XtjrnQYeDJTiVZmT/ncqCr5RY1/Cf8X2wgLRmyh9ayPAGBNziI4qEE4S6SxCH5omQLVo9lmURaJ/Q== 1186 | dependencies: 1187 | "@prisma/engines" "5.14.0" 1188 | 1189 | promise.allsettled@^1.0.2: 1190 | version "1.0.7" 1191 | resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.7.tgz#b9dd51e9cffe496243f5271515652c468865f2d8" 1192 | integrity sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA== 1193 | dependencies: 1194 | array.prototype.map "^1.0.5" 1195 | call-bind "^1.0.2" 1196 | define-properties "^1.2.0" 1197 | es-abstract "^1.22.1" 1198 | get-intrinsic "^1.2.1" 1199 | iterate-value "^1.0.2" 1200 | 1201 | proxy-addr@~2.0.7: 1202 | version "2.0.7" 1203 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" 1204 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 1205 | dependencies: 1206 | forwarded "0.2.0" 1207 | ipaddr.js "1.9.1" 1208 | 1209 | proxy-from-env@^1.1.0: 1210 | version "1.1.0" 1211 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" 1212 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 1213 | 1214 | qs@6.11.0: 1215 | version "6.11.0" 1216 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" 1217 | integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== 1218 | dependencies: 1219 | side-channel "^1.0.4" 1220 | 1221 | range-parser@~1.2.1: 1222 | version "1.2.1" 1223 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 1224 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 1225 | 1226 | raw-body@2.5.2, raw-body@^2.3.3: 1227 | version "2.5.2" 1228 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" 1229 | integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== 1230 | dependencies: 1231 | bytes "3.1.2" 1232 | http-errors "2.0.0" 1233 | iconv-lite "0.4.24" 1234 | unpipe "1.0.0" 1235 | 1236 | regexp.prototype.flags@^1.5.2: 1237 | version "1.5.2" 1238 | resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" 1239 | integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== 1240 | dependencies: 1241 | call-bind "^1.0.6" 1242 | define-properties "^1.2.1" 1243 | es-errors "^1.3.0" 1244 | set-function-name "^2.0.1" 1245 | 1246 | retry@^0.13.1: 1247 | version "0.13.1" 1248 | resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" 1249 | integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== 1250 | 1251 | safe-array-concat@^1.1.2: 1252 | version "1.1.2" 1253 | resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" 1254 | integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== 1255 | dependencies: 1256 | call-bind "^1.0.7" 1257 | get-intrinsic "^1.2.4" 1258 | has-symbols "^1.0.3" 1259 | isarray "^2.0.5" 1260 | 1261 | safe-buffer@5.2.1, safe-buffer@^5.0.1: 1262 | version "5.2.1" 1263 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 1264 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 1265 | 1266 | safe-regex-test@^1.0.3: 1267 | version "1.0.3" 1268 | resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" 1269 | integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== 1270 | dependencies: 1271 | call-bind "^1.0.6" 1272 | es-errors "^1.3.0" 1273 | is-regex "^1.1.4" 1274 | 1275 | "safer-buffer@>= 2.1.2 < 3": 1276 | version "2.1.2" 1277 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 1278 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 1279 | 1280 | semver-compare@^1.0.0: 1281 | version "1.0.0" 1282 | resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" 1283 | integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== 1284 | 1285 | semver@^7.5.4: 1286 | version "7.6.2" 1287 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" 1288 | integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== 1289 | 1290 | send@0.18.0: 1291 | version "0.18.0" 1292 | resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" 1293 | integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== 1294 | dependencies: 1295 | debug "2.6.9" 1296 | depd "2.0.0" 1297 | destroy "1.2.0" 1298 | encodeurl "~1.0.2" 1299 | escape-html "~1.0.3" 1300 | etag "~1.8.1" 1301 | fresh "0.5.2" 1302 | http-errors "2.0.0" 1303 | mime "1.6.0" 1304 | ms "2.1.3" 1305 | on-finished "2.4.1" 1306 | range-parser "~1.2.1" 1307 | statuses "2.0.1" 1308 | 1309 | serve-static@1.15.0: 1310 | version "1.15.0" 1311 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" 1312 | integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== 1313 | dependencies: 1314 | encodeurl "~1.0.2" 1315 | escape-html "~1.0.3" 1316 | parseurl "~1.3.3" 1317 | send "0.18.0" 1318 | 1319 | set-function-length@^1.2.1: 1320 | version "1.2.2" 1321 | resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" 1322 | integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== 1323 | dependencies: 1324 | define-data-property "^1.1.4" 1325 | es-errors "^1.3.0" 1326 | function-bind "^1.1.2" 1327 | get-intrinsic "^1.2.4" 1328 | gopd "^1.0.1" 1329 | has-property-descriptors "^1.0.2" 1330 | 1331 | set-function-name@^2.0.1: 1332 | version "2.0.2" 1333 | resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" 1334 | integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== 1335 | dependencies: 1336 | define-data-property "^1.1.4" 1337 | es-errors "^1.3.0" 1338 | functions-have-names "^1.2.3" 1339 | has-property-descriptors "^1.0.2" 1340 | 1341 | setprototypeof@1.2.0: 1342 | version "1.2.0" 1343 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 1344 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 1345 | 1346 | side-channel@^1.0.4: 1347 | version "1.0.6" 1348 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" 1349 | integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== 1350 | dependencies: 1351 | call-bind "^1.0.7" 1352 | es-errors "^1.3.0" 1353 | get-intrinsic "^1.2.4" 1354 | object-inspect "^1.13.1" 1355 | 1356 | statuses@2.0.1: 1357 | version "2.0.1" 1358 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 1359 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 1360 | 1361 | stop-iteration-iterator@^1.0.0: 1362 | version "1.0.0" 1363 | resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" 1364 | integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== 1365 | dependencies: 1366 | internal-slot "^1.0.4" 1367 | 1368 | string.prototype.trim@^1.2.9: 1369 | version "1.2.9" 1370 | resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" 1371 | integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== 1372 | dependencies: 1373 | call-bind "^1.0.7" 1374 | define-properties "^1.2.1" 1375 | es-abstract "^1.23.0" 1376 | es-object-atoms "^1.0.0" 1377 | 1378 | string.prototype.trimend@^1.0.8: 1379 | version "1.0.8" 1380 | resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" 1381 | integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== 1382 | dependencies: 1383 | call-bind "^1.0.7" 1384 | define-properties "^1.2.1" 1385 | es-object-atoms "^1.0.0" 1386 | 1387 | string.prototype.trimstart@^1.0.8: 1388 | version "1.0.8" 1389 | resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" 1390 | integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== 1391 | dependencies: 1392 | call-bind "^1.0.7" 1393 | define-properties "^1.2.1" 1394 | es-object-atoms "^1.0.0" 1395 | 1396 | toidentifier@1.0.1: 1397 | version "1.0.1" 1398 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 1399 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 1400 | 1401 | tsscmp@^1.0.6: 1402 | version "1.0.6" 1403 | resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" 1404 | integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== 1405 | 1406 | type-is@~1.6.18: 1407 | version "1.6.18" 1408 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 1409 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 1410 | dependencies: 1411 | media-typer "0.3.0" 1412 | mime-types "~2.1.24" 1413 | 1414 | typed-array-buffer@^1.0.2: 1415 | version "1.0.2" 1416 | resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" 1417 | integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== 1418 | dependencies: 1419 | call-bind "^1.0.7" 1420 | es-errors "^1.3.0" 1421 | is-typed-array "^1.1.13" 1422 | 1423 | typed-array-byte-length@^1.0.1: 1424 | version "1.0.1" 1425 | resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" 1426 | integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== 1427 | dependencies: 1428 | call-bind "^1.0.7" 1429 | for-each "^0.3.3" 1430 | gopd "^1.0.1" 1431 | has-proto "^1.0.3" 1432 | is-typed-array "^1.1.13" 1433 | 1434 | typed-array-byte-offset@^1.0.2: 1435 | version "1.0.2" 1436 | resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" 1437 | integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== 1438 | dependencies: 1439 | available-typed-arrays "^1.0.7" 1440 | call-bind "^1.0.7" 1441 | for-each "^0.3.3" 1442 | gopd "^1.0.1" 1443 | has-proto "^1.0.3" 1444 | is-typed-array "^1.1.13" 1445 | 1446 | typed-array-length@^1.0.6: 1447 | version "1.0.6" 1448 | resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" 1449 | integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== 1450 | dependencies: 1451 | call-bind "^1.0.7" 1452 | for-each "^0.3.3" 1453 | gopd "^1.0.1" 1454 | has-proto "^1.0.3" 1455 | is-typed-array "^1.1.13" 1456 | possible-typed-array-names "^1.0.0" 1457 | 1458 | unbox-primitive@^1.0.2: 1459 | version "1.0.2" 1460 | resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" 1461 | integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== 1462 | dependencies: 1463 | call-bind "^1.0.2" 1464 | has-bigints "^1.0.2" 1465 | has-symbols "^1.0.3" 1466 | which-boxed-primitive "^1.0.2" 1467 | 1468 | undici-types@~5.26.4: 1469 | version "5.26.5" 1470 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" 1471 | integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== 1472 | 1473 | unpipe@1.0.0, unpipe@~1.0.0: 1474 | version "1.0.0" 1475 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1476 | integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== 1477 | 1478 | utils-merge@1.0.1: 1479 | version "1.0.1" 1480 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1481 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== 1482 | 1483 | vary@~1.1.2: 1484 | version "1.1.2" 1485 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1486 | integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== 1487 | 1488 | which-boxed-primitive@^1.0.2: 1489 | version "1.0.2" 1490 | resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" 1491 | integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== 1492 | dependencies: 1493 | is-bigint "^1.0.1" 1494 | is-boolean-object "^1.1.0" 1495 | is-number-object "^1.0.4" 1496 | is-string "^1.0.5" 1497 | is-symbol "^1.0.3" 1498 | 1499 | which-typed-array@^1.1.14, which-typed-array@^1.1.15: 1500 | version "1.1.15" 1501 | resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" 1502 | integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== 1503 | dependencies: 1504 | available-typed-arrays "^1.0.7" 1505 | call-bind "^1.0.7" 1506 | for-each "^0.3.3" 1507 | gopd "^1.0.1" 1508 | has-tostringtag "^1.0.2" 1509 | 1510 | ws@^7.5.3: 1511 | version "7.5.9" 1512 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" 1513 | integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== 1514 | --------------------------------------------------------------------------------