├── .eslintrc.json
├── public
├── favicon.ico
├── img
│ ├── pizzaBg.jpg
│ └── shareOrder.png
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── site.webmanifest
└── svg
│ ├── eth.svg
│ ├── github-mark-white.svg
│ ├── nest.svg
│ ├── usdt.svg
│ ├── invite_icon.svg
│ ├── btc.svg
│ ├── NEST_LOGO.svg
│ ├── long.svg
│ └── short.svg
├── README.md
├── discord
├── events
│ ├── error.js
│ └── ready.js
├── commands
│ ├── snatch.js
│ └── link.js
├── deploy-commands.js
└── index.js
├── styles
├── global.css
└── github.css
├── next.config.js
├── pages
├── index.tsx
├── _app.tsx
├── api
│ └── auth.ts
├── rank
│ └── [[...code]].tsx
└── pizza
│ └── index.tsx
├── deploy.sh
├── localazy.json
├── .gitignore
├── tsconfig.json
├── Dockerfile
├── package.json
├── locales
└── en.json
├── bot
└── index.js
└── utils
└── dom-to-image.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NEST-Protocol/NEST-Prize-WebApp/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/img/pizzaBg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NEST-Protocol/NEST-Prize-WebApp/HEAD/public/img/pizzaBg.jpg
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NEST-Protocol/NEST-Prize-WebApp/HEAD/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NEST-Protocol/NEST-Prize-WebApp/HEAD/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/img/shareOrder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NEST-Protocol/NEST-Prize-WebApp/HEAD/public/img/shareOrder.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NEST-Protocol/NEST-Prize-WebApp/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NEST-Prize-WebApp
2 |
3 | NEST-Prize-WebApp for Telegram. [@NESTRedEnvelopesBot](https://t.me/NESTRedEnvelopesBot)
4 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NEST-Protocol/NEST-Prize-WebApp/HEAD/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NEST-Protocol/NEST-Prize-WebApp/HEAD/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/discord/events/error.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'error',
3 | once: true,
4 | execute() {
5 | console.log(`Error! Logged out`)
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/discord/events/ready.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: 'ready',
3 | once: true,
4 | execute(client) {
5 | client.user.setStatus('online');
6 | console.log(`Ready! Logged in as ${client.user.tag}`)
7 | },
8 | };
--------------------------------------------------------------------------------
/styles/global.css:
--------------------------------------------------------------------------------
1 | /*disable show scrollbar*/
2 | ::-webkit-scrollbar {
3 | display: none;
4 | }
5 |
6 | * {
7 | -webkit-tap-highlight-color: transparent;
8 | }
9 |
10 | a, h1, h2, h3, h4, h5, p, td {
11 | font-family: 'Montserrat', sans-serif;
12 | }
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | env: {
5 | NEST_API_TOKEN: process.env.NEST_API_TOKEN,
6 | BOT_TOKEN: process.env.BOT_TOKEN
7 | }
8 | }
9 |
10 | module.exports = nextConfig
11 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import {Stack, Text} from "@chakra-ui/react";
2 | import Link from "next/link";
3 |
4 | export default function Home() {
5 | return (
6 |
7 | Welcome to NEST Prize!
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | # zip bot/index.js to deploy.zip
2 | cp -r locales bot # copy locales to bot
3 | # shellcheck disable=SC2164
4 | cd bot
5 | zip -r deploy.zip index.js locales
6 | # deploy to lambda
7 | aws lambda update-function-code --function-name nest-prize-bot --zip-file fileb://deploy.zip --region ap-northeast-1
8 | # remove deploy.zip
9 | rm deploy.zip
10 |
--------------------------------------------------------------------------------
/localazy.json:
--------------------------------------------------------------------------------
1 | {
2 | "writeKey": "a7910454205759244361-f73a42b4ec8bbd45da75921ebd0c2a617bcf371c99a18adad8cb32111dc91903",
3 | "readKey": "a7910454205759244361-386fb02cbd4b7f663a5066e510e588ea630caf9cb5f6dfc447019150c71135ad",
4 | "upload": {
5 | "type": "json",
6 | "features": ["content_as_object", "plural_object"],
7 | "files": "locales/en.json"
8 | },
9 | "download": {
10 | "files": "locales/${lang}.json",
11 | "includeSourceLang": true
12 | }
13 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 | /.idea/
38 | /bot/locales/
39 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true
17 | },
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/public/svg/eth.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/svg/github-mark-white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/svg/nest.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/discord/commands/snatch.js:
--------------------------------------------------------------------------------
1 | const {ActionRowBuilder, ButtonBuilder, ButtonStyle, SlashCommandBuilder} = require('discord.js');
2 |
3 | module.exports = {
4 | data: new SlashCommandBuilder()
5 | .setName('snatch')
6 | .setDescription('snatch NEST prize')
7 | .addStringOption(option =>
8 | option.setName('id')
9 | .setDescription('NEST Prize code')
10 | .setRequired(true)),
11 | async execute(interaction) {
12 | const code = interaction.options.getString('id');
13 | if (interaction.commandName === 'snatch') {
14 | const row = new ActionRowBuilder()
15 | .addComponents(
16 | new ButtonBuilder()
17 | .setLabel('Snatch')
18 | .setURL(`https://nest-prize-web-app-delta.vercel.app/prize?code=${code}&dcId=${interaction.user.id}`)
19 | .setStyle(ButtonStyle.Link)
20 | );
21 | await interaction.reply({content: `Click Snatch Button to Open It!`, ephemeral: true, components: [row]});
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/public/svg/usdt.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/discord/deploy-commands.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs');
2 | const path = require('node:path');
3 | const { REST } = require('@discordjs/rest');
4 | const { Routes } = require('discord-api-types/v9');
5 | const dotenv = require('dotenv');
6 | dotenv.config();
7 |
8 | const clientId = process.env.CLIENT_ID;
9 | const guildId = process.env.GUILD_ID;
10 | const token = process.env.DISCORD_TOKEN;
11 |
12 | const commands = [];
13 | const commandsPath = path.join(__dirname, 'commands');
14 | const commandFiles = fs
15 | .readdirSync(commandsPath)
16 | .filter((file) => file.endsWith('.js'));
17 |
18 | for (const file of commandFiles) {
19 | const filePath = path.join(commandsPath, file);
20 | const command = require(filePath);
21 | commands.push(command.data.toJSON());
22 | }
23 |
24 | const rest = new REST().setToken(token);
25 |
26 | // rest
27 | // .put(Routes.applicationGuildCommands(clientId, guildId), {
28 | // body: commands,
29 | // })
30 | // .then(() =>
31 | // console.log('Successfully registered application guild commands.')
32 | // )
33 | // .catch(console.log);
34 |
35 | rest
36 | .put(Routes.applicationCommands(clientId), { body: commands })
37 | .then(() => console.log('Successfully registered application commands.'))
38 | .catch(console.log);
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Install dependencies only when needed
2 | FROM node:alpine AS deps
3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
4 | RUN apk add --no-cache libc6-compat
5 | WORKDIR /app
6 | COPY package.json yarn.lock ./
7 | RUN yarn install --frozen-lockfile
8 |
9 | # Rebuild the source code only when needed
10 | FROM node:alpine AS builder
11 | WORKDIR /app
12 | COPY . .
13 | COPY --from=deps /app/node_modules ./node_modules
14 | RUN yarn build && yarn install --production --ignore-scripts --prefer-offline
15 |
16 | # Production image, copy all the files and run next
17 | FROM node:alpine AS runner
18 | WORKDIR /app
19 |
20 | ENV NODE_ENV production
21 |
22 | RUN addgroup -g 1001 -S nodejs
23 | RUN adduser -S nextjs -u 1001
24 |
25 | # You only need to copy next.config.js if you are NOT using the default configuration
26 | # COPY --from=builder /app/next.config.js ./
27 | COPY --from=builder /app/public ./public
28 | COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
29 | COPY --from=builder /app/node_modules ./node_modules
30 | COPY --from=builder /app/package.json ./package.json
31 |
32 | USER nextjs
33 |
34 | EXPOSE 3000
35 |
36 | ENV PORT 3000
37 |
38 | # Next.js collects completely anonymous telemetry data about general usage.
39 | # Learn more here: https://nextjs.org/telemetry
40 | # Uncomment the following line in case you want to disable telemetry.
41 | # ENV NEXT_TELEMETRY_DISABLED 1
42 |
43 | CMD ["node_modules/.bin/next", "start"]
--------------------------------------------------------------------------------
/public/svg/invite_icon.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nest-prize-webapp",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "export": "next export",
10 | "lint": "next lint",
11 | "upload": "localazy upload",
12 | "download": "localazy download"
13 | },
14 | "dependencies": {
15 | "@chakra-ui/react": "^2.4.1",
16 | "@discordjs/builders": "^1.4.0",
17 | "@discordjs/rest": "^1.5.0",
18 | "@emotion/react": "^11.10.5",
19 | "@emotion/styled": "^11.10.5",
20 | "@types/dom-to-image": "^2.6.4",
21 | "@types/node": "18.11.9",
22 | "@types/qrcode": "^1.5.0",
23 | "@types/react": "18.0.25",
24 | "@types/react-dom": "18.0.9",
25 | "@types/sharp": "^0.31.0",
26 | "axios": "^1.2.0",
27 | "b64-to-blob": "^1.2.19",
28 | "discord-api-types": "^0.37.34",
29 | "discord.js": "^14.7.1",
30 | "dom-to-image": "^2.6.0",
31 | "dotenv": "^16.0.3",
32 | "eslint": "8.28.0",
33 | "eslint-config-next": "13.0.4",
34 | "framer-motion": "^7.6.9",
35 | "i18n": "^0.15.1",
36 | "next": "13.0.4",
37 | "node-fetch": "^3.3.0",
38 | "qrcode": "^1.5.1",
39 | "qs": "^6.11.0",
40 | "react": "18.2.0",
41 | "react-dom": "18.2.0",
42 | "react-icons": "^4.7.1",
43 | "react-markdown": "^8.0.4",
44 | "redis": "^4.5.1",
45 | "remark-gfm": "^3.0.1",
46 | "sharp": "^0.31.2",
47 | "typescript": "4.9.3"
48 | },
49 | "devDependencies": {
50 | "@types/qs": "^6.9.7",
51 | "ethers": "^5.7.2",
52 | "limiter": "^2.1.0",
53 | "telegraf": "^4.11.2"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/public/svg/btc.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/discord/commands/link.js:
--------------------------------------------------------------------------------
1 | const {SlashCommandBuilder} = require('@discordjs/builders');
2 | const axios = require("axios");
3 | const {isAddress} = require("ethers/lib/utils");
4 |
5 | module.exports = {
6 | data: new SlashCommandBuilder()
7 | .setName('link')
8 | .setDescription('link your address')
9 | .addStringOption(option =>
10 | option.setName('wallet')
11 | .setDescription('BSC address')
12 | .setRequired(true)),
13 | async execute(interaction) {
14 | try {
15 | const wallet = interaction.options.getString('wallet');
16 | if (!isAddress(wallet)) {
17 | await interaction.reply({
18 | content: `Invalid wallet address!`,
19 | ephemeral: true
20 | });
21 | return;
22 | }
23 | axios({
24 | method: 'post',
25 | url: 'https://cms.nestfi.net/bot-api/red-bot/user/dc',
26 | headers: {
27 | 'Content-Type': 'application/json',
28 | 'Authorization': `Bearer ${process.env.NEST_API_TOKEN}`
29 | },
30 | data: {
31 | dcGroupId: interaction.guild.id,
32 | dcGroupName: interaction.guild.name,
33 | dcId: interaction.user.id,
34 | dcName: interaction.user.username,
35 | wallet: wallet
36 | }
37 | }).catch((e) => {
38 | console.log(e);
39 | })
40 | await interaction.reply({
41 | content: `Update ${interaction.user.username} wallet success!`,
42 | ephemeral: true
43 | });
44 | } catch (e) {
45 | console.log(e);
46 | await interaction.reply({
47 | content: `There was an error while executing this command!`,
48 | ephemeral: true
49 | });
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/discord/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs');
2 | const path = require('node:path');
3 | const { Client, Collection, GatewayIntentBits } = require('discord.js');
4 | const dotenv = require('dotenv');
5 | dotenv.config();
6 |
7 | const token = process.env.DISCORD_TOKEN;
8 |
9 | const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.DirectMessages] });
10 |
11 | client.commands = new Collection();
12 | const commandsPath = path.join(__dirname, 'commands');
13 | const commandFiles = fs
14 | .readdirSync(commandsPath)
15 | .filter((file) => file.endsWith('.js'));
16 |
17 | for (const file of commandFiles) {
18 | const filePath = path.join(commandsPath, file);
19 | const command = require(filePath);
20 | client.commands.set(command.data.name, command);
21 | }
22 |
23 | const eventsPath = path.join(__dirname, 'events');
24 | const eventFiles = fs
25 | .readdirSync(eventsPath)
26 | .filter((file) => file.endsWith('.js'));
27 | for (const file of eventFiles) {
28 | const filePath = path.join(eventsPath, file);
29 | const event = require(filePath);
30 | if (event.once) {
31 | client.once(event.name, (...args) => event.execute(...args));
32 | }
33 | else {
34 | client.on(event.name, (...args) => event.execute(...args));
35 | }
36 | }
37 |
38 | client.on('interactionCreate', async (interaction) => {
39 | if (!interaction.isCommand()) return;
40 |
41 | const command = client.commands.get(interaction.commandName);
42 |
43 | if (!command) return;
44 |
45 | try {
46 | await command.execute(interaction);
47 | } catch (error) {
48 | console.log(error);
49 | await interaction.reply({
50 | content: 'There was an error while executing this command!',
51 | ephemeral: true,
52 | });
53 | }
54 | });
55 |
56 | client.login(token);
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import type {AppProps} from 'next/app'
2 | import {ChakraProvider, Stack} from "@chakra-ui/react";
3 | import Script from "next/script";
4 | import Head from "next/head";
5 | import '../styles/github.css';
6 | import '../styles/global.css';
7 |
8 | export default function App({Component, pageProps}: AppProps) {
9 | return (
10 |
11 |
12 | NEST Prize WebApp
13 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
36 |
37 |
38 |
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/pages/api/auth.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 |
3 | const { subtle } = require("crypto").webcrypto;
4 |
5 | type TransformInitData = {
6 | [k: string]: string;
7 | };
8 |
9 | function transformInitData(initData: string): TransformInitData {
10 | return Object.fromEntries(new URLSearchParams(initData));
11 | }
12 |
13 | async function validate(data: TransformInitData, botToken: string) {
14 | const encoder = new TextEncoder();
15 |
16 | const checkString = Object.keys(data)
17 | .filter((key) => key !== "hash")
18 | .map((key) => `${key}=${data[key]}`)
19 | .sort()
20 | .join("\n");
21 |
22 | const secretKey = await subtle.importKey(
23 | "raw",
24 | encoder.encode("WebAppData"),
25 | { name: "HMAC", hash: "SHA-256" },
26 | true,
27 | ["sign"]
28 | );
29 | const secret = await subtle.sign("HMAC", secretKey, encoder.encode(botToken));
30 | const signatureKey = await subtle.importKey(
31 | "raw",
32 | secret,
33 | { name: "HMAC", hash: "SHA-256" },
34 | true,
35 | ["sign"]
36 | );
37 | const signature = await subtle.sign(
38 | "HMAC",
39 | signatureKey,
40 | encoder.encode(checkString)
41 | );
42 |
43 | // @ts-ignore
44 | const hex = [...new Uint8Array(signature)]
45 | .map((b) => b.toString(16).padStart(2, "0"))
46 | .join("");
47 |
48 | return data.hash === hex;
49 | }
50 |
51 | export default async function handler(
52 | req: NextApiRequest,
53 | res: NextApiResponse
54 | ) {
55 | const initData = req.body._auth;
56 |
57 | if (!initData) {
58 | res.status(400);
59 | return;
60 | }
61 |
62 | const data = transformInitData(initData);
63 | const isOk = await validate(
64 | data,
65 | process.env.BOT_TOKEN!
66 | );
67 |
68 | if (isOk) {
69 | res.status(200).send({
70 | ok: isOk,
71 | });
72 | } else {
73 | res.status(403).send({
74 | error: "Invalid hash",
75 | });
76 | }
77 | }
--------------------------------------------------------------------------------
/public/svg/NEST_LOGO.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/public/svg/long.svg:
--------------------------------------------------------------------------------
1 |
35 |
--------------------------------------------------------------------------------
/public/svg/short.svg:
--------------------------------------------------------------------------------
1 |
40 |
--------------------------------------------------------------------------------
/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "10x draw": "10x draw",
3 | "20x draw": "20x draw",
4 | "5x draw": "5x draw",
5 | "Commission = 0.1% of the trading volume (only calculate the leverage of opening quantity X).": "Commission = 0.1% of the trading volume (only calculate the leverage of opening quantity X).",
6 | "Event Introduction\n\n🍔 Hamburger (New user First Order Bonus)\nBonus: 200NEST\n\n🍕 Pizza (Invitation Bonus)\nOngoing Bonus:0.1% of the trading volume (only calculate the leverage of opening quantity X).\n\n🐣 Butter chicken (Volume Bonus)\nBonus:\n5x leverage bonus 5–50 NEST.\n10x leverage bonus 10–100 NEST.\n20x leverage bonus 20–200 NEST.\n\n🍦 Ice cream\nReward: 0.05% of total trading volume as bonus pool, 50% of trading volume ranking, 50% of profit ranking\nDetails:https://nest-protocol.medium.com/s5-nestfi-food-festival-63120836d5ba": "Event Introduction\n\n🍔 Hamburger (New user First Order Bonus)\nBonus: 200NEST\n\n🍕 Pizza (Invitation Bonus)\nOngoing Bonus:0.1% of the trading volume (only calculate the leverage of opening quantity X).\n\n🐣 Butter chicken (Volume Bonus)\nBonus:\n5x leverage bonus 5–50 NEST.\n10x leverage bonus 10–100 NEST.\n20x leverage bonus 20–200 NEST.\n\n🍦 Ice cream\nReward: 0.05% of total trading volume as bonus pool, 50% of trading volume ranking, 50% of profit ranking\nDetails:https://nest-protocol.medium.com/s5-nestfi-food-festival-63120836d5ba",
7 | "I have no idea what you are talking about.": "I have no idea what you are talking about.",
8 | "NEST Roundtable 24": "NEST Roundtable 24",
9 | "NESTFi S5 Food Festival": "NESTFi S5 Food Festival",
10 | "NESTRoundtableContent": "NESTRoundtable 25: #Ethereum Shanghai upgrade note \nRewards:\n50 $NEST for 200 winners\n\nTasks:\n1. RT the Tweet & @ 3 friends\nLink: https://twitter.com/NEST_Protocol/status/1626070216605319168\n2. Join the Space\nLink: https://twitter.com/i/spaces/1ypKddLQkALKW?s=20\n\nWe will detect whether you complete the task or not, and the reward will be issued through @NESTRedEnvelopesBot.",
11 | "New Issue": "New Issue",
12 | "Once a day": "Once a day",
13 | "Please input a valid twitter account start with @.": "Please input a valid twitter account start with @.",
14 | "Please input a valid wallet address.": "Please input a valid wallet address.",
15 | "Please input your twitter name with @": "Please input your twitter name with @",
16 | "Please send your wallet address:": "Please send your wallet address:",
17 | "Please set your wallet first": "Please set your wallet first",
18 | "Rank": "Rank",
19 | "Set Twitter": "Set Twitter",
20 | "Set Wallet": "Set Wallet",
21 | "Share my positions": "Share my positions",
22 | "Share your futures orders:": "Share your futures orders:",
23 | "Snatch!": "Snatch!",
24 | "Some error occurred.": "Some error occurred.",
25 | "Sorry, this is not a valid NEST prize or a valid reference link.": "Sorry, this is not a valid NEST prize or a valid reference link.",
26 | "Sorry, you are blocked.": "Sorry, you are blocked.",
27 | "Star or Issue": "Star or Issue",
28 | "Welcome": "Welcome to NEST FI\n\nWallet and Twitter must be added to join NEST FI campaign\n\nYour wallet: {{wallet}}\nYour twitter: {{twitter}}\nYour ref link: https://finance.nestprotocol.org/?a={{ref}}",
29 | "You found a NEST Prize!": "You found a NEST Prize!",
30 | "conditions\n\nButter chicken (Trading bonus)\n\nRequirements:\n1.random bonus for every 800 futures NEST volume accumulated\n2. Order length must be greater than 5 minutes, with leverage\noptions of 5x, 10x, 20x(Unlimited times)\n\nBonus:\n5x leverage bonus 5–50 NEST.\n10x leverage bonus 10–100 NEST.\n20x leverage bonus 20–200 NEST.\n\n5x remaining butter chicken: {{ticket5Count}}\n{{ticket5History}}\n\n10x remaining butter chicken: {{ticket10Count}}\n{{ticket10History}}\n\n20x remaining butter chicken: {{ticket20Count}}\n{{ticket20History}}": "conditions\n\nButter chicken (Trading bonus)\n\nRequirements:\n1.random bonus for every 800 futures NEST volume accumulated\n2. Order length must be greater than 5 minutes, with leverage\noptions of 5x, 10x, 20x(Unlimited times)\n\nBonus:\n5x leverage bonus 5–50 NEST.\n10x leverage bonus 10–100 NEST.\n20x leverage bonus 20–200 NEST.\n\n5x remaining butter chicken: {{ticket5Count}}\n{{ticket5History}}\n\n10x remaining butter chicken: {{ticket10Count}}\n{{ticket10History}}\n\n20x remaining butter chicken: {{ticket20Count}}\n{{ticket20History}}",
31 | "go to futures": "go to futures",
32 | "invite": "invite",
33 | "pizza info": "pizza info",
34 | "« Back": "« Back",
35 | "🍔 Hamburger": "🍔 Hamburger",
36 | "🍕 Pizza": "🍕 Pizza",
37 | "🍦 Ice cream\nReward: 0.05% of total trading volume as bonus pool, 50% of trading volume ranking, 50% of profit ranking\n1. Trading Volume Ranking\nConditions: Trading volume must be greater than 100,000 (calculated leverage) to be eligible to participate.\nReward: The top 80 rewards will be awarded every three days according to the trading volume ranking.\n2. Profit Ranking\nConditions: The principal amount of a single trade must be greater than 1000nest (not counting leverage) to be eligible to participate.\nReward: The top 80 rewards will be awarded every three days according to the profit ranking": "🍦 Ice cream\nReward: 0.05% of total trading volume as bonus pool, 50% of trading volume ranking, 50% of profit ranking\n\n1. Trading Volume Ranking\nConditions: Trading volume must be greater than 100,000 (calculated leverage) to be eligible to participate.\nReward: The top 80 rewards will be awarded every three days according to the trading volume ranking.\n\n2. Profit Ranking\nConditions: The principal amount of a single trade must be greater than 1000nest (not counting leverage) to be eligible to participate.\nReward: The top 80 rewards will be awarded every three days according to the profit ranking",
38 | "🍨 Ice cream": "🍨 Ice cream",
39 | "🐣 Butter chicken": "🐣 Butter chicken"
40 | }
--------------------------------------------------------------------------------
/pages/rank/[[...code]].tsx:
--------------------------------------------------------------------------------
1 | import {Button, Divider, HStack, Link, Stack, Text, Avatar, Badge, Spinner} from "@chakra-ui/react";
2 | import {useCallback, useEffect, useMemo, useState} from "react";
3 | import {useRouter} from "next/router";
4 | import {FaTelegramPlane} from "react-icons/fa";
5 |
6 | type TelegramData = {
7 | hash: string,
8 | id: number,
9 | photo_url: string,
10 | first_name?: string,
11 | last_name?: string,
12 | username: string,
13 | auth_date: number,
14 | }
15 |
16 | type RankType = {
17 | txTotalAmount: number,
18 | rewardsTotal: number,
19 | txTotalUsers: number,
20 | kol: {
21 | code: string,
22 | address: string,
23 | tgName: string,
24 | chatId: string,
25 | rate: number,
26 | endTime: string,
27 | startTime: string,
28 | },
29 | rankings: {
30 | chatId: string,
31 | txAmount: number,
32 | wallet: string,
33 | rewards: number,
34 | tgName: string,
35 | }[],
36 | }
37 |
38 | const Rank = () => {
39 | const router = useRouter()
40 | const [userData, setUserData] = useState(undefined)
41 | const [rank, setRank] = useState(undefined)
42 | const [invalid, setInvalid] = useState(false)
43 | const [myCode, setMyCode] = useState(undefined)
44 |
45 | const code = useMemo(() => {
46 | return router.query.code?.[0].toLowerCase()
47 | }, [router])
48 |
49 | useEffect(() => {
50 | if (router.query.chatId) {
51 | setUserData({
52 | hash: '',
53 | id: Number(router.query.chatId),
54 | photo_url: '',
55 | username: '',
56 | auth_date: 0,
57 | })
58 | }
59 | }, [router])
60 |
61 | const loginTelegram = () => {
62 | // @ts-ignore
63 | window?.Telegram.Login.auth({
64 | bot_id: process.env.BOT_TOKEN || '',
65 | request_access: 'write',
66 | embed: 1
67 | }, async (data: TelegramData) => {
68 | if (!data) {
69 | return
70 | }
71 | setUserData(data)
72 | });
73 | };
74 |
75 | const fetchRank = useCallback(async () => {
76 | if (!code) {
77 | return
78 | }
79 | try {
80 | const res = await fetch(`https://cms.nestfi.net/bot-api/kol/ranking/info?code=${code}`, {
81 | method: 'POST',
82 | headers: {
83 | 'Content-Type': 'application/json',
84 | 'Authorization': `Bearer ${process.env.NEST_API_TOKEN}`,
85 | }
86 | })
87 | const data = await res.json()
88 | if (data.value) {
89 | setRank(data.value)
90 | } else {
91 | setInvalid(true)
92 | }
93 | } catch (e) {
94 | setInvalid(true)
95 | }
96 | }, [code])
97 |
98 | const fetchMyCode = useCallback(async () => {
99 | if (!userData) {
100 | return
101 | }
102 | const res = await fetch(`https://cms.nestfi.net/bot-api/kol/code/by/chatId?chatId=${userData.id}`, {
103 | method: 'GET',
104 | headers: {
105 | 'Content-Type': 'application/json',
106 | 'Authorization': `Bearer ${process.env.NEST_API_TOKEN}`,
107 | }
108 | })
109 | const data = await res.json()
110 | if (data.value) {
111 | setMyCode(data.value.toLowerCase())
112 | }
113 | }, [userData])
114 |
115 | useEffect(() => {
116 | fetchMyCode()
117 | }, [fetchMyCode])
118 |
119 | useEffect(() => {
120 | fetchRank()
121 | }, [fetchRank])
122 |
123 | const myInfo = useMemo(() => {
124 | if (!rank || !userData) {
125 | return undefined
126 | }
127 | return rank.rankings?.sort((a, b) => b.txAmount - a.txAmount).find(item => String(item.chatId) === String(userData.id))
128 | }, [rank, userData])
129 |
130 | const myRanking = useMemo(() => {
131 | if (!rank || !userData) {
132 | return undefined
133 | }
134 | return rank.rankings?.sort((a, b) => b.txAmount - a.txAmount).findIndex(item => String(item.chatId) === String(userData.id)) + 1
135 | }, [rank, userData])
136 |
137 | if (invalid) {
138 | return (
139 |
143 | Your kol is not open for activities, you can not participate
144 |
145 | )
146 | }
147 |
148 | if (!rank) {
149 | return (
150 |
154 |
155 | Loading...
156 |
157 | )
158 | }
159 |
160 | return (
161 |
163 |
164 | KOL Transaction Ranking
165 | {rank.kol.startTime.slice(0, 10)} ~ {rank.kol.endTime.slice(0, 10)}
166 |
167 |
169 |
170 | {rank?.txTotalAmount?.toLocaleString('en-US', {
171 | maximumFractionDigits: 2,
172 | }) || '-'}
173 | Transaction amount
174 |
175 |
176 | {rank?.rewardsTotal?.toLocaleString('en-US', {
177 | maximumFractionDigits: 2,
178 | }) || '-'}
179 | Bonus pool
180 |
181 |
182 |
183 | Your Ranking
184 | {
185 | userData ? (
186 | myInfo ? (
187 |
188 |
189 |
191 | NO.{myRanking}
192 |
193 |
194 | {myInfo.tgName}
195 | {myInfo.wallet}
196 |
197 |
198 | Tx amount
199 | Bonus
200 |
201 |
202 | {myInfo.txAmount.toLocaleString('en-US', {
203 | maximumFractionDigits: 2,
204 | })} NEST
205 | {myInfo.rewards.toLocaleString('en-US', {
206 | maximumFractionDigits: 2,
207 | })} NEST
208 |
209 |
210 |
211 | ) : (
212 |
213 | You are not yet eligible to participate in the event
214 | {
215 | myCode === undefined && (
216 |
217 | {`https://finance.nestprotocol.org/?a=${code}`}
219 |
229 |
230 | )
231 | }
232 |
233 | )
234 | ) : (
235 |
236 |
241 |
242 | )
243 | }
244 |
245 |
246 | Ranking
247 | {
248 | rank && rank.rankings?.sort((a, b) => b.txAmount - a.txAmount).map((item, index) => (
249 |
251 | NO.{index + 1}
252 |
253 | @{item.tgName}
254 | {item.wallet}
255 |
256 |
257 | Tx amount
258 | Bonus
259 |
260 |
261 | {item.txAmount.toLocaleString('en-US', {
262 | maximumFractionDigits: 2,
263 | })} NEST
264 | {item.rewards.toLocaleString('en-US', {
265 | maximumFractionDigits: 2,
266 | })} NEST
267 |
268 |
269 |
270 | ))
271 | }
272 |
273 |
274 | )
275 | }
276 |
277 | export default Rank
--------------------------------------------------------------------------------
/bot/index.js:
--------------------------------------------------------------------------------
1 | const {Telegraf, Markup} = require('telegraf')
2 | const {isAddress} = require("ethers/lib/utils");
3 | const axios = require('axios')
4 | const {RateLimiter} = require("limiter");
5 | const i18n = require('i18n');
6 |
7 | // Command
8 | // start - show the menu
9 |
10 | // limit of send message to different chat
11 | const lmt = new RateLimiter({
12 | tokensPerInterval: 30,
13 | interval: 'second',
14 | })
15 |
16 | i18n.configure({
17 | locales: ['en', 'ja', 'bn', 'id', 'tr', 'vi', 'ko', 'ru'],
18 | directory: "./locales",
19 | register: global
20 | })
21 |
22 | const measurement_id = `G-BE17GNN7CH`;
23 | const api_secret = process.env.GA_API_SECRET;
24 |
25 | const t = (p, l, ph) => {
26 | return i18n.__({phrase: p, locale: l}, ph)
27 | }
28 |
29 | const token = process.env.BOT_TOKEN
30 | const nest_token = process.env.NEST_API_TOKEN
31 | if (token === undefined) {
32 | throw new Error('BOT_TOKEN must be provided!')
33 | }
34 |
35 | const bot = new Telegraf(token)
36 |
37 | bot.start(async (ctx) => {
38 | const chatId = ctx.chat.id
39 | const isBot = ctx.from.is_bot
40 | let lang = ctx.from.language_code
41 | if (!['en', 'ja', 'bn', 'id', 'tr', 'vi', 'ko', 'ru'].includes(lang)) {
42 | lang = 'en'
43 | }
44 |
45 | if (chatId < 0 || isBot) {
46 | return
47 | }
48 | axios({
49 | method: 'post',
50 | url: `https://www.google-analytics.com/mp/collect?measurement_id=${measurement_id}&api_secret=${api_secret}`,
51 | data: {
52 | client_id: `${chatId}`,
53 | user_id: `${chatId}`,
54 | events: [{
55 | name: 'click',
56 | params: {
57 | value: 'start',
58 | language_code: lang,
59 | startPayload: ctx.startPayload,
60 | },
61 | }]
62 | }
63 | }).catch((e) => console.log(e))
64 | try {
65 | const res = await Promise.all([
66 | axios({
67 | method: 'POST',
68 | url: `https://cms.nestfi.net/bot-api/red-bot/user`,
69 | data: {
70 | chatId: ctx.from.id,
71 | tgName: ctx.from.username,
72 | },
73 | headers: {
74 | 'Authorization': `Bearer ${nest_token}`
75 | }
76 | }),
77 | axios({
78 | method: 'GET',
79 | url: `https://cms.nestfi.net/bot-api/red-bot/user/${ctx.from.id}`,
80 | headers: {
81 | 'Authorization': `Bearer ${nest_token}`,
82 | }
83 | }),
84 | axios({
85 | method: 'GET',
86 | url: `https://cms.nestfi.net/bot-api/kol/code/by/chatId?chatId=${ctx.from.id}`,
87 | headers: {
88 | 'Authorization': `Bearer ${nest_token}`,
89 | }
90 | })
91 | ])
92 | const user = res[1].data
93 | const myKol = res[2].data
94 | await lmt.removeTokens(1)
95 | ctx.reply(t('Welcome', lang, {
96 | wallet: user?.value?.wallet,
97 | twitter: user?.value?.twitterName,
98 | ref: user?.value?.wallet ? `${user?.value?.wallet?.slice(-8)?.toLowerCase()}` : 'Bind wallet first!'
99 | }), {
100 | disable_web_page_preview: true,
101 | ...Markup.inlineKeyboard([
102 | [Markup.button.url('Invitation Info', `https://nest-prize-web-app-delta.vercel.app/pizza?chatId=${ctx.from.id}`)],
103 | [Markup.button.callback(t('Set Twitter', lang), 'inputUserTwitter', user?.value?.twitterName), Markup.button.callback(t('Set Wallet', lang), 'setUserWallet', user?.value?.wallet)],
104 | [Markup.button.url(t('KOL Ranking', lang), `https://nest-prize-web-app-delta.vercel.app/rank/${myKol?.value}?chatId=${ctx.from.id}`, !myKol.value)],
105 | [Markup.button.url(t('go to futures', lang), 'https://finance.nestprotocol.org/#/futures')],
106 | ])
107 | })
108 |
109 | } catch (e) {
110 | console.log(e)
111 | }
112 | })
113 |
114 | bot.action('inputUserTwitter', async (ctx) => {
115 | let lang = ctx.update.callback_query.from.language_code
116 | if (!['en', 'ja', 'bn', 'id', 'tr', 'vi', 'ko', 'ru'].includes(lang)) {
117 | lang = 'en'
118 | }
119 | const isBot = ctx.update.callback_query.from.is_bot
120 | if (isBot) {
121 | return
122 | }
123 | axios({
124 | method: 'post',
125 | url: `https://www.google-analytics.com/mp/collect?measurement_id=${measurement_id}&api_secret=${api_secret}`,
126 | data: {
127 | client_id: `${ctx.update.callback_query.from.id}`,
128 | user_id: `${ctx.update.callback_query.from.id}`,
129 | events: [{
130 | name: 'click',
131 | params: {
132 | value: 'inputUserTwitter',
133 | language_code: lang,
134 | },
135 | }]
136 | }
137 | }).catch((e) => console.log(e))
138 | try {
139 | await axios({
140 | method: 'POST',
141 | url: `https://cms.nestfi.net/bot-api/red-bot/user`,
142 | data: {
143 | chatId: ctx.update.callback_query.from.id,
144 | intent: 'setUserTwitter',
145 | },
146 | headers: {
147 | 'Authorization': `Bearer ${nest_token}`
148 | }
149 | })
150 | await lmt.removeTokens(1)
151 | await ctx.reply(t('Please input your twitter name with @', lang))
152 | } catch (e) {
153 | console.log(e)
154 | }
155 | })
156 |
157 | bot.action('menu', async (ctx) => {
158 | let lang = ctx.update.callback_query.from.language_code
159 | if (!['en', 'ja', 'bn', 'id', 'tr', 'vi', 'ko', 'ru'].includes(lang)) {
160 | lang = 'en'
161 | }
162 | const isBot = ctx.update.callback_query.from.is_bot
163 | if (isBot) {
164 | return
165 | }
166 | axios({
167 | method: 'post',
168 | url: `https://www.google-analytics.com/mp/collect?measurement_id=${measurement_id}&api_secret=${api_secret}`,
169 | data: {
170 | client_id: `${ctx.update.callback_query.from.id}`,
171 | user_id: `${ctx.update.callback_query.from.id}`,
172 | events: [{
173 | name: 'click',
174 | params: {
175 | value: 'menu',
176 | language_code: lang,
177 | },
178 | }]
179 | }
180 | }).catch((e) => console.log(e))
181 | try {
182 | const res = await Promise.all([
183 | axios({
184 | method: 'GET',
185 | url: `https://cms.nestfi.net/bot-api/red-bot/user/${ctx.from.id}`,
186 | headers: {
187 | 'Authorization': `Bearer ${nest_token}`,
188 | }
189 | }),
190 | axios({
191 | method: 'GET',
192 | url: `https://cms.nestfi.net/bot-api/kol/code/by/chatId?chatId=${ctx.from.id}`,
193 | headers: {
194 | 'Authorization': `Bearer ${nest_token}`,
195 | }
196 | })
197 | ])
198 | const user = res[0].data
199 | const myKol = res[1].data
200 | await lmt.removeTokens(1)
201 | await ctx.answerCbQuery()
202 | .catch((e) => console.log(e))
203 | await ctx.editMessageText(t("Welcome", lang, {
204 | wallet: user?.data?.value?.wallet,
205 | twitter: user?.data?.value?.twitterName,
206 | ref: user?.data?.value?.wallet ? `${user?.data?.value?.wallet?.slice(-8)?.toLowerCase()}` : 'Bind wallet first!'
207 | }), {
208 | disable_web_page_preview: true,
209 | ...Markup.inlineKeyboard([
210 | [Markup.button.url(t('invite', lang), `https://nest-prize-web-app-delta.vercel.app/api/share2?from=${ctx.update.callback_query.from.id}`)],
211 | [Markup.button.url('Invitation Info', `https://nest-prize-web-app-delta.vercel.app/pizza?chatId=${ctx.update.callback_query.from.id}`)],
212 | [Markup.button.callback(t('Set Twitter', lang), 'inputUserTwitter', user?.data?.value?.twitterName), Markup.button.callback(t('Set Wallet', lang), 'setUserWallet', user?.data?.value?.wallet)],
213 | [Markup.button.url(t('KOL Ranking', lang), `https://nest-prize-web-app-delta.vercel.app/rank/${myKol?.value}?chatId=${ctx.from.id}`, !myKol.value)],
214 | [Markup.button.url(t('go to futures', lang), 'https://finance.nestprotocol.org/#/futures')],
215 | ])
216 | })
217 | } catch (e) {
218 | console.log(e)
219 | await lmt.removeTokens(1)
220 | }
221 | })
222 |
223 | bot.action('setUserWallet', async (ctx) => {
224 | let lang = ctx.update.callback_query.from.language_code
225 | if (!['en', 'ja', 'bn', 'id', 'tr', 'vi', 'ko', 'ru'].includes(lang)) {
226 | lang = 'en'
227 | }
228 | const isBot = ctx.update.callback_query.from.is_bot
229 | if (isBot) {
230 | return
231 | }
232 | axios({
233 | method: 'post',
234 | url: `https://www.google-analytics.com/mp/collect?measurement_id=${measurement_id}&api_secret=${api_secret}`,
235 | data: {
236 | client_id: `${ctx.update.callback_query.from.id}`,
237 | user_id: `${ctx.update.callback_query.from.id}`,
238 | events: [{
239 | name: 'click',
240 | params: {
241 | value: 'setUserWallet',
242 | language_code: lang,
243 | },
244 | }]
245 | }
246 | }).catch((e) => console.log(e))
247 | try {
248 | await axios({
249 | method: 'POST',
250 | url: `https://cms.nestfi.net/bot-api/red-bot/user`,
251 | data: {
252 | chatId: ctx.update.callback_query.from.id,
253 | intent: 'setUserWallet',
254 | },
255 | headers: {
256 | 'Authorization': `Bearer ${nest_token}`
257 | }
258 | })
259 | await lmt.removeTokens(1)
260 | await ctx.answerCbQuery()
261 | await ctx.editMessageText(t('Please send your wallet address:', lang))
262 | } catch (e) {
263 | console.log(e)
264 | }
265 | })
266 |
267 | bot.on('message', async (ctx) => {
268 | let lang = ctx.message.from.language_code
269 | if (!['en', 'ja', 'bn', 'id', 'tr', 'vi', 'ko', 'ru'].includes(lang)) {
270 | lang = 'en'
271 | }
272 | const input = ctx.message.text
273 | const chat_id = ctx.message.chat.id
274 | const isBot = ctx.message.from.is_bot
275 | if (chat_id < 0 || isBot) {
276 | return
277 | }
278 | axios({
279 | method: 'post',
280 | url: `https://www.google-analytics.com/mp/collect?measurement_id=${measurement_id}&api_secret=${api_secret}`,
281 | data: {
282 | client_id: `${chat_id}`,
283 | user_id: `${chat_id}`,
284 | events: [{
285 | name: 'message',
286 | params: {
287 | language_code: lang,
288 | },
289 | }]
290 | }
291 | }).catch((e) => console.log(e))
292 | try {
293 | const res = await axios({
294 | method: 'GET',
295 | url: `https://cms.nestfi.net/bot-api/red-bot/user/${ctx.message.from.id}`,
296 | headers: {
297 | 'Authorization': `Bearer ${nest_token}`,
298 | }
299 | })
300 |
301 | const intent = res?.data?.value?.intent
302 | if (res?.data?.value?.disable === 'Y') {
303 | await lmt.removeTokens(1)
304 | await ctx.reply(t("Sorry, you are blocked.", lang))
305 | return
306 | }
307 |
308 | if (intent === null) {
309 | await lmt.removeTokens(1)
310 | ctx.reply(t('I have no idea what you are talking about.', lang))
311 | } else if (intent === 'setUserWallet') {
312 | if (isAddress(input)) {
313 | try {
314 | await axios({
315 | method: 'POST',
316 | url: `https://cms.nestfi.net/bot-api/red-bot/user`,
317 | data: {
318 | chatId: ctx.from.id,
319 | wallet: input,
320 | },
321 | headers: {
322 | 'Authorization': `Bearer ${nest_token}`
323 | }
324 | }).catch((e) => console.log(e))
325 |
326 | await axios({
327 | method: 'POST',
328 | url: `https://cms.nestfi.net/workbench-api/activity/user/update`,
329 | data: JSON.stringify({
330 | user_id: ctx.from.id,
331 | username: res?.data?.value?.tgName || '',
332 | wallet: input
333 | }),
334 | headers: {
335 | 'Content-Type': 'application/json',
336 | 'Authorization': `Bearer ${nest_token}`
337 | }
338 | })
339 | await lmt.removeTokens(1)
340 | ctx.reply(t(`Your wallet address has updated: {{input}}`, lang, {
341 | input: input
342 | }), Markup.inlineKeyboard([
343 | [Markup.button.callback(t('« Back', lang), 'menu')],
344 | ]))
345 | } catch (e) {
346 | await lmt.removeTokens(1)
347 | ctx.reply(t('Some error occurred.', lang), {
348 | reply_to_message_id: ctx.message.message_id,
349 | ...Markup.inlineKeyboard([
350 | [Markup.button.callback(t('« Back', lang), 'menu')],
351 | [Markup.button.url(t('New Issue', lang), 'https://github.com/NEST-Protocol/NEST-Prize-WebApp/issues/new')]
352 | ])
353 | })
354 | }
355 | } else {
356 | await lmt.removeTokens(1)
357 | ctx.reply(t('Please input a valid wallet address.', lang), {
358 | reply_to_message_id: ctx.message.message_id,
359 | })
360 | }
361 | } else if (intent === 'setUserTwitter') {
362 | if (input.startsWith('@')) {
363 | try {
364 | await axios({
365 | method: 'POST',
366 | url: `https://cms.nestfi.net/bot-api/red-bot/user`,
367 | data: {
368 | chatId: ctx.from.id,
369 | twitterName: input.slice(1),
370 | },
371 | headers: {
372 | 'Authorization': `Bearer ${nest_token}`
373 | }
374 | })
375 | await lmt.removeTokens(1)
376 | ctx.reply(t(`Your twitter has updated: {{input}}`, lang, {
377 | input: input.slice(1)
378 | }), Markup.inlineKeyboard([
379 | [Markup.button.callback(t('« Back', lang), 'menu')],
380 | ]))
381 | } catch (e) {
382 | await lmt.removeTokens(1)
383 | ctx.reply(t('Some error occurred.', lang), {
384 | reply_to_message_id: ctx.message.message_id,
385 | ...Markup.inlineKeyboard([
386 | [Markup.button.callback(t('« Back', lang), 'menu')],
387 | [Markup.button.url(t('New Issue', lang), 'https://github.com/NEST-Protocol/NEST-Prize-WebApp/issues/new')]
388 | ])
389 | })
390 | }
391 | } else {
392 | ctx.reply(t('Please input a valid twitter account start with @.', lang))
393 | }
394 | }
395 | } catch (e) {
396 | await lmt.removeTokens(1)
397 | ctx.reply(t('Some error occurred.', lang))
398 | }
399 | })
400 |
401 | exports.handler = async (event, context, callback) => {
402 | const tmp = JSON.parse(event.body);
403 | await bot.handleUpdate(tmp);
404 | return callback(null, {
405 | statusCode: 200,
406 | body: '',
407 | });
408 | };
409 |
--------------------------------------------------------------------------------
/pages/pizza/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Divider,
4 | chakra,
5 | HStack,
6 | Select,
7 | Spacer,
8 | Stack,
9 | Text,
10 | useClipboard,
11 | Input,
12 | Wrap,
13 | WrapItem
14 | } from "@chakra-ui/react"
15 | import {useRouter} from "next/router";
16 | import {useCallback, useEffect, useMemo, useState} from "react";
17 | import axios from "axios";
18 |
19 | type UserInfo = {
20 | notSettled: number | null,
21 | recentRewards: number | null,
22 | tgName: string,
23 | totalInvitees: number | null,
24 | totalCount: number,
25 | totalRewards: number,
26 | totalTrading: number,
27 | wallet: string,
28 | }
29 |
30 | const Pizza = () => {
31 | const router = useRouter()
32 | const chatId = router.query.chatId
33 | const [data, setData] = useState<{
34 | user: UserInfo | null,
35 | details: UserInfo[]
36 | }>({
37 | user: {
38 | notSettled: 0,
39 | recentRewards: 0,
40 | tgName: '-',
41 | totalInvitees: 0,
42 | totalCount: 0,
43 | totalRewards: 0,
44 | totalTrading: 0,
45 | wallet: ''
46 | },
47 | details: [],
48 | })
49 | const {onCopy, setValue, hasCopied, value} = useClipboard('')
50 | const [filter, setFilter] = useState('all')
51 | const [sort, setSort] = useState('totalTrading')
52 | const [settledDay, setSettledDay] = useState('')
53 | const [today, setToday] = useState(new Date().toISOString().slice(0, 10))
54 | const [search, setSearch] = useState('')
55 | const [index, setIndex] = useState(1)
56 |
57 | const fetchFrom = useCallback(async () => {
58 | // https://cms.nestfi.net/bot-api/red-bot/s4/invite/settle-date
59 | const res = await axios({
60 | method: 'GET',
61 | url: `https://cms.nestfi.net/bot-api/red-bot/s4/invite/settle-date`,
62 | headers: {
63 | 'Authorization': `Bearer ${process.env.NEST_API_TOKEN}`
64 | }
65 | })
66 | if (res?.data?.value) {
67 | setSettledDay(res.data.value.slice(0, 10))
68 | }
69 | }, [])
70 |
71 | useEffect(() => {
72 | fetchFrom()
73 | }, [fetchFrom])
74 |
75 | const getCode = useCallback(async () => {
76 | if (chatId) {
77 | const userReq = await axios(`https://cms.nestfi.net/bot-api/red-bot/user/${chatId}`, {
78 | method: 'GET',
79 | headers: {
80 | 'Authorization': `Bearer ${process.env.NEST_API_TOKEN}`
81 | }
82 | })
83 | const code = userReq.data?.value?.wallet?.slice(-8)?.toLowerCase()
84 | if (code) {
85 | setValue(`https://finance.nestprotocol.org/?a=${code}`)
86 | }
87 | }
88 | }, [chatId])
89 |
90 | useEffect(() => {
91 | getCode()
92 | }, [getCode])
93 |
94 | const fetchData = useCallback(async () => {
95 | if (!chatId) return
96 | if (settledDay && today) {
97 | try {
98 | const res = await axios({
99 | method: 'GET',
100 | url: `https://cms.nestfi.net/bot-api/red-bot/s4/invite/info?chatId=${chatId}&from=${index === 0 ? '2023-01-01' : settledDay}&to=${index === 0 ? settledDay : today}`,
101 | headers: {
102 | 'Authorization': `Bearer ${process.env.NEST_API_TOKEN}`
103 | }
104 | })
105 | if (res?.data?.value) {
106 | setData(res.data.value)
107 | }
108 | } catch (e) {
109 | console.log(e)
110 | }
111 | } else {
112 | try {
113 | const res = await axios({
114 | method: 'GET',
115 | url: `https://cms.nestfi.net/bot-api/red-bot/s4/invite/info?chatId=${chatId}`,
116 | headers: {
117 | 'Authorization': `Bearer ${process.env.NEST_API_TOKEN}`
118 | }
119 | })
120 | if (res?.data?.value) {
121 | setData(res.data.value)
122 | }
123 | } catch (e) {
124 | console.log(e)
125 | }
126 | }
127 | }, [chatId, settledDay, today, index])
128 |
129 | const showData = useMemo(() => {
130 | return data?.details.filter((item) => {
131 | if (filter === 'all') return true
132 | if (filter === 'recentRewards' && item.recentRewards) return item.recentRewards > 0
133 | if (filter === 'notSettled' && item.notSettled) return item.notSettled > 0
134 | if (filter === 'neverTraded') return item.totalTrading === 0
135 | return false
136 | }).filter((item) => {
137 | if (search) {
138 | return item.tgName?.toLowerCase().includes(search.toLowerCase()) ||
139 | item.wallet?.toLowerCase().includes(search.toLowerCase())
140 | } else {
141 | return true
142 | }
143 | }).sort((a, b) => {
144 | if (sort === 'totalTrading') return b.totalTrading - a.totalTrading
145 | if (sort === 'totalRewards') return b.totalRewards - a.totalRewards
146 | if (a.recentRewards && b.recentRewards) {
147 | if (sort === 'recentRewards') return b.recentRewards - a.recentRewards
148 | }
149 | if (a.notSettled && b.notSettled) {
150 | if (sort === 'notSettled') return b.notSettled - a.notSettled
151 | }
152 | return 0
153 | })
154 | }, [filter, search, sort, data])
155 |
156 | useEffect(() => {
157 | fetchData()
158 | }, [fetchData])
159 |
160 | return (
161 |
163 |
164 |
165 | @{data.user?.tgName || '-'}
166 | {data.user?.wallet?.slice(0, 8)}...{data.user?.wallet?.slice(-6)}
168 |
169 |
170 |
174 |
175 |
176 |
185 |
194 |
195 |
196 |
197 | {
198 | [
199 | {
200 | key: 'Total trading', value: data.user?.totalTrading.toLocaleString('en-US', {
201 | maximumFractionDigits: 2
202 | }) + ' NEST', hidden: !data.user?.totalTrading
203 | },
204 | {
205 | key: 'Total invitees', value: data.user?.totalInvitees?.toLocaleString('en-US', {
206 | maximumFractionDigits: 2
207 | }), hidden: !data.user?.totalInvitees
208 | },
209 | {key: 'Total number', value: data.user?.totalCount},
210 | {
211 | key: 'Total rewards', value: data.user?.totalRewards.toLocaleString('en-US', {
212 | maximumFractionDigits: 2
213 | }) + ' NEST', hidden: !data.user?.totalRewards
214 | },
215 | {
216 | key: 'Recent rewards', value: data.user?.recentRewards?.toLocaleString('en-US', {
217 | maximumFractionDigits: 2
218 | }) + ' NEST', hidden: !data.user?.recentRewards
219 | },
220 | {
221 | key: 'Not settled', value: data.user?.notSettled?.toLocaleString('en-US', {
222 | maximumFractionDigits: 2
223 | }) + ' NEST', hidden: !data.user?.notSettled
224 | },
225 | ].map((item, index) => (
226 |
227 |
228 | {item.value || '-'}
229 | {item.key}
230 |
231 |
232 | ))
233 | }
234 |
235 |
236 | {
237 | data?.details.length > 0 ? (
238 | <>
239 |
240 |
253 |
266 |
267 |
268 | {
270 | setSearch(e.target.value)
271 | }}/>
272 |
273 | {
274 | showData.map((item: any, index: number) => (
275 |
276 | @{item.tgName || '-'}
277 | {item.wallet}
278 | {
279 | item.totalTrading > 0 && (
280 | <>
281 |
282 |
283 |
284 |
285 | Total trading
286 | {item.totalTrading?.toLocaleString('en-US', {
287 | maximumFractionDigits: 2
288 | })} NEST
289 |
290 |
291 |
292 |
293 | Total rewards
294 | {item.totalRewards?.toLocaleString('en-US', {
295 | maximumFractionDigits: 2
296 | })} NEST
297 |
298 |
299 |
300 |
301 | Recent rewards
302 | {item.recentRewards?.toLocaleString('en-US', {
303 | maximumFractionDigits: 2
304 | })} NEST
305 |
306 |
307 |
308 |
309 | Not settled
310 | {item.notSettled?.toLocaleString('en-US', {
311 | maximumFractionDigits: 2
312 | })} NEST
313 |
314 |
315 |
316 | >
317 | )
318 | }
319 |
320 | ))
321 | }
322 | >
323 | ) : (
324 |
325 |
326 | {/* eslint-disable-next-line react/no-unescaped-entities */}
327 | You haven't invited yet
328 |
329 | )
330 | }
331 |
332 | )
333 | }
334 |
335 | export default Pizza
--------------------------------------------------------------------------------
/utils/dom-to-image.js:
--------------------------------------------------------------------------------
1 | (function (global) {
2 | 'use strict';
3 |
4 | var util = newUtil();
5 | var inliner = newInliner();
6 | var fontFaces = newFontFaces();
7 | var images = newImages();
8 |
9 | // Default impl options
10 | var defaultOptions = {
11 | // Default is to fail on error, no placeholder
12 | imagePlaceholder: undefined,
13 | // Default cache bust is false, it will use the cache
14 | cacheBust: false
15 | };
16 |
17 | var domtoimage = {
18 | toSvg: toSvg,
19 | toPng: toPng,
20 | toJpeg: toJpeg,
21 | toBlob: toBlob,
22 | toPixelData: toPixelData,
23 | impl: {
24 | fontFaces: fontFaces,
25 | images: images,
26 | util: util,
27 | inliner: inliner,
28 | options: {}
29 | }
30 | };
31 |
32 | if (typeof module !== 'undefined')
33 | module.exports = domtoimage;
34 | else
35 | global.domtoimage = domtoimage;
36 |
37 |
38 | /**
39 | * @param {Node} node - The DOM Node object to render
40 | * @param {Object} options - Rendering options
41 | * @param {Function} options.filter - Should return true if passed node should be included in the output
42 | * (excluding node means excluding it's children as well). Not called on the root node.
43 | * @param {String} options.bgcolor - color for the background, any valid CSS color value.
44 | * @param {Number} options.width - width to be applied to node before rendering.
45 | * @param {Number} options.height - height to be applied to node before rendering.
46 | * @param {Object} options.style - an object whose properties to be copied to node's style before rendering.
47 | * @param {Number} options.quality - a Number between 0 and 1 indicating image quality (applicable to JPEG only),
48 | defaults to 1.0.
49 | * @param {String} options.imagePlaceholder - dataURL to use as a placeholder for failed images, default behaviour is to fail fast on images we can't fetch
50 | * @param {Boolean} options.cacheBust - set to true to cache bust by appending the time to the request url
51 | * @return {Promise} - A promise that is fulfilled with a SVG image data URL
52 | * */
53 | function toSvg(node, options) {
54 | options = options || {};
55 | copyOptions(options);
56 | return Promise.resolve(node)
57 | .then(function (node) {
58 | return cloneNode(node, options.filter, true);
59 | })
60 | .then(embedFonts)
61 | .then(inlineImages)
62 | .then(applyOptions)
63 | .then(function (clone) {
64 | return makeSvgDataUri(clone,
65 | options.width || util.width(node),
66 | options.height || util.height(node)
67 | );
68 | });
69 |
70 | function applyOptions(clone) {
71 | if (options.bgcolor) clone.style.backgroundColor = options.bgcolor;
72 |
73 | if (options.width) clone.style.width = options.width + 'px';
74 | if (options.height) clone.style.height = options.height + 'px';
75 |
76 | if (options.style)
77 | Object.keys(options.style).forEach(function (property) {
78 | clone.style[property] = options.style[property];
79 | });
80 |
81 | return clone;
82 | }
83 | }
84 |
85 | /**
86 | * @param {Node} node - The DOM Node object to render
87 | * @param {Object} options - Rendering options, @see {@link toSvg}
88 | * @return {Promise} - A promise that is fulfilled with a Uint8Array containing RGBA pixel data.
89 | * */
90 | function toPixelData(node, options) {
91 | return draw(node, options || {})
92 | .then(function (canvas) {
93 | return canvas.getContext('2d').getImageData(
94 | 0,
95 | 0,
96 | util.width(node),
97 | util.height(node)
98 | ).data;
99 | });
100 | }
101 |
102 | /**
103 | * @param {Node} node - The DOM Node object to render
104 | * @param {Object} options - Rendering options, @see {@link toSvg}
105 | * @return {Promise} - A promise that is fulfilled with a PNG image data URL
106 | * */
107 | function toPng(node, options) {
108 | return draw(node, options || {})
109 | .then(function (canvas) {
110 | return canvas.toDataURL();
111 | });
112 | }
113 |
114 | /**
115 | * @param {Node} node - The DOM Node object to render
116 | * @param {Object} options - Rendering options, @see {@link toSvg}
117 | * @return {Promise} - A promise that is fulfilled with a JPEG image data URL
118 | * */
119 | function toJpeg(node, options) {
120 | options = options || {};
121 | return draw(node, options)
122 | .then(function (canvas) {
123 | return canvas.toDataURL('image/jpeg', options.quality || 1.0);
124 | });
125 | }
126 |
127 | /**
128 | * @param {Node} node - The DOM Node object to render
129 | * @param {Object} options - Rendering options, @see {@link toSvg}
130 | * @return {Promise} - A promise that is fulfilled with a PNG image blob
131 | * */
132 | function toBlob(node, options) {
133 | return draw(node, options || {})
134 | .then(util.canvasToBlob);
135 | }
136 |
137 | function copyOptions(options) {
138 | // Copy options to impl options for use in impl
139 | if(typeof(options.imagePlaceholder) === 'undefined') {
140 | domtoimage.impl.options.imagePlaceholder = defaultOptions.imagePlaceholder;
141 | } else {
142 | domtoimage.impl.options.imagePlaceholder = options.imagePlaceholder;
143 | }
144 |
145 | if(typeof(options.cacheBust) === 'undefined') {
146 | domtoimage.impl.options.cacheBust = defaultOptions.cacheBust;
147 | } else {
148 | domtoimage.impl.options.cacheBust = options.cacheBust;
149 | }
150 | }
151 |
152 | function draw(domNode, options) {
153 | return toSvg(domNode, options)
154 | .then(util.makeImage)
155 | .then(util.delay(100))
156 | .then(function (image) {
157 | var canvas = newCanvas(domNode);
158 | canvas.getContext('2d').drawImage(image, 0, 0);
159 | return canvas;
160 | });
161 |
162 | function newCanvas(domNode) {
163 | var canvas = document.createElement('canvas');
164 | var ctx = canvas.getContext('2d');
165 | ctx.mozImageSmoothingEnabled = false;
166 | ctx.webkitImageSmoothingEnabled = false;
167 | ctx.msImageSmoothingEnabled = false;
168 | ctx.imageSmoothingEnabled = false;
169 | var scale = options.scale || 1;
170 | canvas.width = (options.width * scale) || util.width(domNode);
171 | canvas.height = (options.height * scale) || util.height(domNode);
172 | ctx.scale(scale, scale);
173 | if (options.bgcolor) {
174 | ctx.fillStyle = options.bgcolor;
175 | ctx.fillRect(0, 0, canvas.width, canvas.height);
176 | }
177 | return canvas;
178 | }
179 | }
180 |
181 | function cloneNode(node, filter, root) {
182 | if (!root && filter && !filter(node)) return Promise.resolve();
183 |
184 | return Promise.resolve(node)
185 | .then(makeNodeCopy)
186 | .then(function (clone) {
187 | return cloneChildren(node, clone, filter);
188 | })
189 | .then(function (clone) {
190 | return processClone(node, clone);
191 | });
192 |
193 | function makeNodeCopy(node) {
194 | if (node instanceof HTMLCanvasElement) return util.makeImage(node.toDataURL());
195 | return node.cloneNode(false);
196 | }
197 |
198 | function cloneChildren(original, clone, filter) {
199 | var children = original.childNodes;
200 | if (children.length === 0) return Promise.resolve(clone);
201 |
202 | return cloneChildrenInOrder(clone, util.asArray(children), filter)
203 | .then(function () {
204 | return clone;
205 | });
206 |
207 | function cloneChildrenInOrder(parent, children, filter) {
208 | var done = Promise.resolve();
209 | children.forEach(function (child) {
210 | done = done
211 | .then(function () {
212 | return cloneNode(child, filter);
213 | })
214 | .then(function (childClone) {
215 | if (childClone) parent.appendChild(childClone);
216 | });
217 | });
218 | return done;
219 | }
220 | }
221 |
222 | function processClone(original, clone) {
223 | if (!(clone instanceof Element)) return clone;
224 |
225 | return Promise.resolve()
226 | .then(cloneStyle)
227 | .then(clonePseudoElements)
228 | .then(copyUserInput)
229 | .then(fixSvg)
230 | .then(function () {
231 | return clone;
232 | });
233 |
234 | function cloneStyle() {
235 | copyStyle(window.getComputedStyle(original), clone.style);
236 |
237 | function copyStyle(source, target) {
238 | if (source.cssText) target.cssText = source.cssText;
239 | else copyProperties(source, target);
240 |
241 | function copyProperties(source, target) {
242 | util.asArray(source).forEach(function (name) {
243 | target.setProperty(
244 | name,
245 | source.getPropertyValue(name),
246 | source.getPropertyPriority(name)
247 | );
248 | });
249 | }
250 | }
251 | }
252 |
253 | function clonePseudoElements() {
254 | [':before', ':after'].forEach(function (element) {
255 | clonePseudoElement(element);
256 | });
257 |
258 | function clonePseudoElement(element) {
259 | var style = window.getComputedStyle(original, element);
260 | var content = style.getPropertyValue('content');
261 |
262 | if (content === '' || content === 'none') return;
263 |
264 | var className = util.uid();
265 | clone.className = clone.className + ' ' + className;
266 | var styleElement = document.createElement('style');
267 | styleElement.appendChild(formatPseudoElementStyle(className, element, style));
268 | clone.appendChild(styleElement);
269 |
270 | function formatPseudoElementStyle(className, element, style) {
271 | var selector = '.' + className + ':' + element;
272 | var cssText = style.cssText ? formatCssText(style) : formatCssProperties(style);
273 | return document.createTextNode(selector + '{' + cssText + '}');
274 |
275 | function formatCssText(style) {
276 | var content = style.getPropertyValue('content');
277 | return style.cssText + ' content: ' + content + ';';
278 | }
279 |
280 | function formatCssProperties(style) {
281 |
282 | return util.asArray(style)
283 | .map(formatProperty)
284 | .join('; ') + ';';
285 |
286 | function formatProperty(name) {
287 | return name + ': ' +
288 | style.getPropertyValue(name) +
289 | (style.getPropertyPriority(name) ? ' !important' : '');
290 | }
291 | }
292 | }
293 | }
294 | }
295 |
296 | function copyUserInput() {
297 | if (original instanceof HTMLTextAreaElement) clone.innerHTML = original.value;
298 | if (original instanceof HTMLInputElement) clone.setAttribute("value", original.value);
299 | }
300 |
301 | function fixSvg() {
302 | if (!(clone instanceof SVGElement)) return;
303 | clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
304 |
305 | if (!(clone instanceof SVGRectElement)) return;
306 | ['width', 'height'].forEach(function (attribute) {
307 | var value = clone.getAttribute(attribute);
308 | if (!value) return;
309 |
310 | clone.style.setProperty(attribute, value);
311 | });
312 | }
313 | }
314 | }
315 |
316 | function embedFonts(node) {
317 | return fontFaces.resolveAll()
318 | .then(function (cssText) {
319 | var styleNode = document.createElement('style');
320 | node.appendChild(styleNode);
321 | styleNode.appendChild(document.createTextNode(cssText));
322 | return node;
323 | });
324 | }
325 |
326 | function inlineImages(node) {
327 | return images.inlineAll(node)
328 | .then(function () {
329 | return node;
330 | });
331 | }
332 |
333 | function makeSvgDataUri(node, width, height) {
334 | return Promise.resolve(node)
335 | .then(function (node) {
336 | node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
337 | return new XMLSerializer().serializeToString(node);
338 | })
339 | .then(util.escapeXhtml)
340 | .then(function (xhtml) {
341 | return '' + xhtml + '';
342 | })
343 | .then(function (foreignObject) {
344 | return '';
346 | })
347 | .then(function (svg) {
348 | return 'data:image/svg+xml;charset=utf-8,' + svg;
349 | });
350 | }
351 |
352 | function newUtil() {
353 | return {
354 | escape: escape,
355 | parseExtension: parseExtension,
356 | mimeType: mimeType,
357 | dataAsUrl: dataAsUrl,
358 | isDataUrl: isDataUrl,
359 | canvasToBlob: canvasToBlob,
360 | resolveUrl: resolveUrl,
361 | getAndEncode: getAndEncode,
362 | uid: uid(),
363 | delay: delay,
364 | asArray: asArray,
365 | escapeXhtml: escapeXhtml,
366 | makeImage: makeImage,
367 | width: width,
368 | height: height
369 | };
370 |
371 | function mimes() {
372 | /*
373 | * Only WOFF and EOT mime types for fonts are 'real'
374 | * see http://www.iana.org/assignments/media-types/media-types.xhtml
375 | */
376 | var WOFF = 'application/font-woff';
377 | var JPEG = 'image/jpeg';
378 |
379 | return {
380 | 'woff': WOFF,
381 | 'woff2': WOFF,
382 | 'ttf': 'application/font-truetype',
383 | 'eot': 'application/vnd.ms-fontobject',
384 | 'png': 'image/png',
385 | 'jpg': JPEG,
386 | 'jpeg': JPEG,
387 | 'gif': 'image/gif',
388 | 'tiff': 'image/tiff',
389 | 'svg': 'image/svg+xml'
390 | };
391 | }
392 |
393 | function parseExtension(url) {
394 | var match = /\.([^\.\/]*?)$/g.exec(url);
395 | if (match) return match[1];
396 | else return '';
397 | }
398 |
399 | function mimeType(url) {
400 | var extension = parseExtension(url).toLowerCase();
401 | return mimes()[extension] || '';
402 | }
403 |
404 | function isDataUrl(url) {
405 | return url.search(/^(data:)/) !== -1;
406 | }
407 |
408 | function toBlob(canvas) {
409 | return new Promise(function (resolve) {
410 | var binaryString = window.atob(canvas.toDataURL().split(',')[1]);
411 | var length = binaryString.length;
412 | var binaryArray = new Uint8Array(length);
413 |
414 | for (var i = 0; i < length; i++)
415 | binaryArray[i] = binaryString.charCodeAt(i);
416 |
417 | resolve(new Blob([binaryArray], {
418 | type: 'image/png'
419 | }));
420 | });
421 | }
422 |
423 | function canvasToBlob(canvas) {
424 | if (canvas.toBlob)
425 | return new Promise(function (resolve) {
426 | canvas.toBlob(resolve);
427 | });
428 |
429 | return toBlob(canvas);
430 | }
431 |
432 | function resolveUrl(url, baseUrl) {
433 | var doc = document.implementation.createHTMLDocument();
434 | var base = doc.createElement('base');
435 | doc.head.appendChild(base);
436 | var a = doc.createElement('a');
437 | doc.body.appendChild(a);
438 | base.href = baseUrl;
439 | a.href = url;
440 | return a.href;
441 | }
442 |
443 | function uid() {
444 | var index = 0;
445 |
446 | return function () {
447 | return 'u' + fourRandomChars() + index++;
448 |
449 | function fourRandomChars() {
450 | /* see http://stackoverflow.com/a/6248722/2519373 */
451 | return ('0000' + (Math.random() * Math.pow(36, 4) << 0).toString(36)).slice(-4);
452 | }
453 | };
454 | }
455 |
456 | function makeImage(uri) {
457 | return new Promise(function (resolve, reject) {
458 | var image = new Image();
459 | image.onload = function () {
460 | resolve(image);
461 | };
462 | image.onerror = reject;
463 | image.src = uri;
464 | });
465 | }
466 |
467 | function getAndEncode(url) {
468 | var TIMEOUT = 30000;
469 | if(domtoimage.impl.options.cacheBust) {
470 | // Cache bypass so we dont have CORS issues with cached images
471 | // Source: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache
472 | url += ((/\?/).test(url) ? "&" : "?") + (new Date()).getTime();
473 | }
474 |
475 | return new Promise(function (resolve) {
476 | var request = new XMLHttpRequest();
477 |
478 | request.onreadystatechange = done;
479 | request.ontimeout = timeout;
480 | request.responseType = 'blob';
481 | request.timeout = TIMEOUT;
482 | request.open('GET', url, true);
483 | request.send();
484 |
485 | var placeholder;
486 | if(domtoimage.impl.options.imagePlaceholder) {
487 | var split = domtoimage.impl.options.imagePlaceholder.split(/,/);
488 | if(split && split[1]) {
489 | placeholder = split[1];
490 | }
491 | }
492 |
493 | function done() {
494 | if (request.readyState !== 4) return;
495 |
496 | if (request.status !== 200) {
497 | if(placeholder) {
498 | resolve(placeholder);
499 | } else {
500 | fail('cannot fetch resource: ' + url + ', status: ' + request.status);
501 | }
502 |
503 | return;
504 | }
505 |
506 | var encoder = new FileReader();
507 | encoder.onloadend = function () {
508 | var content = encoder.result.split(/,/)[1];
509 | resolve(content);
510 | };
511 | encoder.readAsDataURL(request.response);
512 | }
513 |
514 | function timeout() {
515 | if(placeholder) {
516 | resolve(placeholder);
517 | } else {
518 | fail('timeout of ' + TIMEOUT + 'ms occured while fetching resource: ' + url);
519 | }
520 | }
521 |
522 | function fail(message) {
523 | console.error(message);
524 | resolve('');
525 | }
526 | });
527 | }
528 |
529 | function dataAsUrl(content, type) {
530 | return 'data:' + type + ';base64,' + content;
531 | }
532 |
533 | function escape(string) {
534 | return string.replace(/([.*+?^${}()|\[\]\/\\])/g, '\\$1');
535 | }
536 |
537 | function delay(ms) {
538 | return function (arg) {
539 | return new Promise(function (resolve) {
540 | setTimeout(function () {
541 | resolve(arg);
542 | }, ms);
543 | });
544 | };
545 | }
546 |
547 | function asArray(arrayLike) {
548 | var array = [];
549 | var length = arrayLike.length;
550 | for (var i = 0; i < length; i++) array.push(arrayLike[i]);
551 | return array;
552 | }
553 |
554 | function escapeXhtml(string) {
555 | return string.replace(/#/g, '%23').replace(/\n/g, '%0A');
556 | }
557 |
558 | function width(node) {
559 | var leftBorder = px(node, 'border-left-width');
560 | var rightBorder = px(node, 'border-right-width');
561 | return node.scrollWidth + leftBorder + rightBorder;
562 | }
563 |
564 | function height(node) {
565 | var topBorder = px(node, 'border-top-width');
566 | var bottomBorder = px(node, 'border-bottom-width');
567 | return node.scrollHeight + topBorder + bottomBorder;
568 | }
569 |
570 | function px(node, styleProperty) {
571 | var value = window.getComputedStyle(node).getPropertyValue(styleProperty);
572 | return parseFloat(value.replace('px', ''));
573 | }
574 | }
575 |
576 | function newInliner() {
577 | var URL_REGEX = /url\(['"]?([^'"]+?)['"]?\)/g;
578 |
579 | return {
580 | inlineAll: inlineAll,
581 | shouldProcess: shouldProcess,
582 | impl: {
583 | readUrls: readUrls,
584 | inline: inline
585 | }
586 | };
587 |
588 | function shouldProcess(string) {
589 | return string.search(URL_REGEX) !== -1;
590 | }
591 |
592 | function readUrls(string) {
593 | var result = [];
594 | var match;
595 | while ((match = URL_REGEX.exec(string)) !== null) {
596 | result.push(match[1]);
597 | }
598 | return result.filter(function (url) {
599 | return !util.isDataUrl(url);
600 | });
601 | }
602 |
603 | function inline(string, url, baseUrl, get) {
604 | return Promise.resolve(url)
605 | .then(function (url) {
606 | return baseUrl ? util.resolveUrl(url, baseUrl) : url;
607 | })
608 | .then(get || util.getAndEncode)
609 | .then(function (data) {
610 | return util.dataAsUrl(data, util.mimeType(url));
611 | })
612 | .then(function (dataUrl) {
613 | return string.replace(urlAsRegex(url), '$1' + dataUrl + '$3');
614 | });
615 |
616 | function urlAsRegex(url) {
617 | return new RegExp('(url\\([\'"]?)(' + util.escape(url) + ')([\'"]?\\))', 'g');
618 | }
619 | }
620 |
621 | function inlineAll(string, baseUrl, get) {
622 | if (nothingToInline()) return Promise.resolve(string);
623 |
624 | return Promise.resolve(string)
625 | .then(readUrls)
626 | .then(function (urls) {
627 | var done = Promise.resolve(string);
628 | urls.forEach(function (url) {
629 | done = done.then(function (string) {
630 | return inline(string, url, baseUrl, get);
631 | });
632 | });
633 | return done;
634 | });
635 |
636 | function nothingToInline() {
637 | return !shouldProcess(string);
638 | }
639 | }
640 | }
641 |
642 | function newFontFaces() {
643 | return {
644 | resolveAll: resolveAll,
645 | impl: {
646 | readAll: readAll
647 | }
648 | };
649 |
650 | function resolveAll() {
651 | return readAll(document)
652 | .then(function (webFonts) {
653 | return Promise.all(
654 | webFonts.map(function (webFont) {
655 | return webFont.resolve();
656 | })
657 | );
658 | })
659 | .then(function (cssStrings) {
660 | return cssStrings.join('\n');
661 | });
662 | }
663 |
664 | function readAll() {
665 | return Promise.resolve(util.asArray(document.styleSheets))
666 | .then(getCssRules)
667 | .then(selectWebFontRules)
668 | .then(function (rules) {
669 | return rules.map(newWebFont);
670 | });
671 |
672 | function selectWebFontRules(cssRules) {
673 | return cssRules
674 | .filter(function (rule) {
675 | return rule.type === CSSRule.FONT_FACE_RULE;
676 | })
677 | .filter(function (rule) {
678 | return inliner.shouldProcess(rule.style.getPropertyValue('src'));
679 | });
680 | }
681 |
682 | function getCssRules(styleSheets) {
683 | var cssRules = [];
684 | styleSheets.forEach(function (sheet) {
685 | try {
686 | util.asArray(sheet.cssRules || []).forEach(cssRules.push.bind(cssRules));
687 | } catch (e) {
688 | console.log('Error while reading CSS rules from ' + sheet.href, e.toString());
689 | }
690 | });
691 | return cssRules;
692 | }
693 |
694 | function newWebFont(webFontRule) {
695 | return {
696 | resolve: function resolve() {
697 | var baseUrl = (webFontRule.parentStyleSheet || {}).href;
698 | return inliner.inlineAll(webFontRule.cssText, baseUrl);
699 | },
700 | src: function () {
701 | return webFontRule.style.getPropertyValue('src');
702 | }
703 | };
704 | }
705 | }
706 | }
707 |
708 | function newImages() {
709 | return {
710 | inlineAll: inlineAll,
711 | impl: {
712 | newImage: newImage
713 | }
714 | };
715 |
716 | function newImage(element) {
717 | return {
718 | inline: inline
719 | };
720 |
721 | function inline(get) {
722 | if (util.isDataUrl(element.src)) return Promise.resolve();
723 |
724 | return Promise.resolve(element.src)
725 | .then(get || util.getAndEncode)
726 | .then(function (data) {
727 | return util.dataAsUrl(data, util.mimeType(element.src));
728 | })
729 | .then(function (dataUrl) {
730 | return new Promise(function (resolve, reject) {
731 | element.onload = resolve;
732 | element.onerror = reject;
733 | element.src = dataUrl;
734 | });
735 | });
736 | }
737 | }
738 |
739 | function inlineAll(node) {
740 | if (!(node instanceof Element)) return Promise.resolve(node);
741 |
742 | return inlineBackground(node)
743 | .then(function () {
744 | if (node instanceof HTMLImageElement)
745 | return newImage(node).inline();
746 | else
747 | return Promise.all(
748 | util.asArray(node.childNodes).map(function (child) {
749 | return inlineAll(child);
750 | })
751 | );
752 | });
753 |
754 | function inlineBackground(node) {
755 | var background = node.style.getPropertyValue('background');
756 |
757 | if (!background) return Promise.resolve(node);
758 |
759 | return inliner.inlineAll(background)
760 | .then(function (inlined) {
761 | node.style.setProperty(
762 | 'background',
763 | inlined,
764 | node.style.getPropertyPriority('background')
765 | );
766 | })
767 | .then(function () {
768 | return node;
769 | });
770 | }
771 | }
772 | }
773 | })(this);
774 |
--------------------------------------------------------------------------------
/styles/github.css:
--------------------------------------------------------------------------------
1 | /*@media (prefers-color-scheme: dark) {*/
2 | /* .markdown-body {*/
3 | /* color-scheme: dark;*/
4 | /* --color-prettylights-syntax-comment: #8b949e;*/
5 | /* --color-prettylights-syntax-constant: #79c0ff;*/
6 | /* --color-prettylights-syntax-entity: #d2a8ff;*/
7 | /* --color-prettylights-syntax-storage-modifier-import: #c9d1d9;*/
8 | /* --color-prettylights-syntax-entity-tag: #7ee787;*/
9 | /* --color-prettylights-syntax-keyword: #ff7b72;*/
10 | /* --color-prettylights-syntax-string: #a5d6ff;*/
11 | /* --color-prettylights-syntax-variable: #ffa657;*/
12 | /* --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;*/
13 | /* --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;*/
14 | /* --color-prettylights-syntax-invalid-illegal-bg: #8e1519;*/
15 | /* --color-prettylights-syntax-carriage-return-text: #f0f6fc;*/
16 | /* --color-prettylights-syntax-carriage-return-bg: #b62324;*/
17 | /* --color-prettylights-syntax-string-regexp: #7ee787;*/
18 | /* --color-prettylights-syntax-markup-list: #f2cc60;*/
19 | /* --color-prettylights-syntax-markup-heading: #1f6feb;*/
20 | /* --color-prettylights-syntax-markup-italic: #c9d1d9;*/
21 | /* --color-prettylights-syntax-markup-bold: #c9d1d9;*/
22 | /* --color-prettylights-syntax-markup-deleted-text: #ffdcd7;*/
23 | /* --color-prettylights-syntax-markup-deleted-bg: #67060c;*/
24 | /* --color-prettylights-syntax-markup-inserted-text: #aff5b4;*/
25 | /* --color-prettylights-syntax-markup-inserted-bg: #033a16;*/
26 | /* --color-prettylights-syntax-markup-changed-text: #ffdfb6;*/
27 | /* --color-prettylights-syntax-markup-changed-bg: #5a1e02;*/
28 | /* --color-prettylights-syntax-markup-ignored-text: #c9d1d9;*/
29 | /* --color-prettylights-syntax-markup-ignored-bg: #1158c7;*/
30 | /* --color-prettylights-syntax-meta-diff-range: #d2a8ff;*/
31 | /* --color-prettylights-syntax-brackethighlighter-angle: #8b949e;*/
32 | /* --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;*/
33 | /* --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;*/
34 | /* --color-fg-default: #c9d1d9;*/
35 | /* --color-fg-muted: #8b949e;*/
36 | /* --color-fg-subtle: #484f58;*/
37 | /* --color-canvas-default: #0d1117;*/
38 | /* --color-canvas-subtle: #161b22;*/
39 | /* --color-border-default: #30363d;*/
40 | /* --color-border-muted: #21262d;*/
41 | /* --color-neutral-muted: rgba(110,118,129,0.4);*/
42 | /* --color-accent-fg: #58a6ff;*/
43 | /* --color-accent-emphasis: #1f6feb;*/
44 | /* --color-attention-subtle: rgba(187,128,9,0.15);*/
45 | /* --color-danger-fg: #f85149;*/
46 | /* }*/
47 | /*}*/
48 |
49 | @media (prefers-color-scheme: light) {
50 | .markdown-body {
51 | color-scheme: light;
52 | --color-prettylights-syntax-comment: #6e7781;
53 | --color-prettylights-syntax-constant: #0550ae;
54 | --color-prettylights-syntax-entity: #8250df;
55 | --color-prettylights-syntax-storage-modifier-import: #24292f;
56 | --color-prettylights-syntax-entity-tag: #116329;
57 | --color-prettylights-syntax-keyword: #cf222e;
58 | --color-prettylights-syntax-string: #0a3069;
59 | --color-prettylights-syntax-variable: #953800;
60 | --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
61 | --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
62 | --color-prettylights-syntax-invalid-illegal-bg: #82071e;
63 | --color-prettylights-syntax-carriage-return-text: #f6f8fa;
64 | --color-prettylights-syntax-carriage-return-bg: #cf222e;
65 | --color-prettylights-syntax-string-regexp: #116329;
66 | --color-prettylights-syntax-markup-list: #3b2300;
67 | --color-prettylights-syntax-markup-heading: #0550ae;
68 | --color-prettylights-syntax-markup-italic: #24292f;
69 | --color-prettylights-syntax-markup-bold: #24292f;
70 | --color-prettylights-syntax-markup-deleted-text: #82071e;
71 | --color-prettylights-syntax-markup-deleted-bg: #FFEBE9;
72 | --color-prettylights-syntax-markup-inserted-text: #116329;
73 | --color-prettylights-syntax-markup-inserted-bg: #dafbe1;
74 | --color-prettylights-syntax-markup-changed-text: #953800;
75 | --color-prettylights-syntax-markup-changed-bg: #ffd8b5;
76 | --color-prettylights-syntax-markup-ignored-text: #eaeef2;
77 | --color-prettylights-syntax-markup-ignored-bg: #0550ae;
78 | --color-prettylights-syntax-meta-diff-range: #8250df;
79 | --color-prettylights-syntax-brackethighlighter-angle: #57606a;
80 | --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
81 | --color-prettylights-syntax-constant-other-reference-link: #0a3069;
82 | --color-fg-default: #24292f;
83 | --color-fg-muted: #57606a;
84 | --color-fg-subtle: #6e7781;
85 | --color-canvas-default: #ffffff;
86 | --color-canvas-subtle: #f6f8fa;
87 | --color-border-default: #d0d7de;
88 | --color-border-muted: hsla(210,18%,87%,1);
89 | --color-neutral-muted: rgba(175,184,193,0.2);
90 | --color-accent-fg: #0969da;
91 | --color-accent-emphasis: #0969da;
92 | --color-attention-subtle: #fff8c5;
93 | --color-danger-fg: #cf222e;
94 | }
95 | }
96 |
97 | .markdown-body {
98 | -ms-text-size-adjust: 100%;
99 | -webkit-text-size-adjust: 100%;
100 | margin: 0;
101 | color: var(--color-fg-default);
102 | background-color: var(--color-canvas-default);
103 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
104 | font-size: 14px;
105 | line-height: 1.5;
106 | word-wrap: break-word;
107 | }
108 |
109 | .markdown-body .octicon {
110 | display: inline-block;
111 | fill: currentColor;
112 | vertical-align: text-bottom;
113 | }
114 |
115 | .markdown-body h1:hover .anchor .octicon-link:before,
116 | .markdown-body h2:hover .anchor .octicon-link:before,
117 | .markdown-body h3:hover .anchor .octicon-link:before,
118 | .markdown-body h4:hover .anchor .octicon-link:before,
119 | .markdown-body h5:hover .anchor .octicon-link:before,
120 | .markdown-body h6:hover .anchor .octicon-link:before {
121 | width: 16px;
122 | height: 16px;
123 | content: ' ';
124 | display: inline-block;
125 | background-color: currentColor;
126 | -webkit-mask-image: url("data:image/svg+xml,");
127 | mask-image: url("data:image/svg+xml,");
128 | }
129 |
130 | .markdown-body details,
131 | .markdown-body figcaption,
132 | .markdown-body figure {
133 | display: block;
134 | }
135 |
136 | .markdown-body summary {
137 | display: list-item;
138 | }
139 |
140 | .markdown-body [hidden] {
141 | display: none !important;
142 | }
143 |
144 | .markdown-body a {
145 | background-color: transparent;
146 | color: var(--color-accent-fg);
147 | text-decoration: none;
148 | }
149 |
150 | .markdown-body a:active,
151 | .markdown-body a:hover {
152 | outline-width: 0;
153 | }
154 |
155 | .markdown-body abbr[title] {
156 | border-bottom: none;
157 | text-decoration: underline dotted;
158 | }
159 |
160 | .markdown-body b,
161 | .markdown-body strong {
162 | font-weight: 600;
163 | }
164 |
165 | .markdown-body dfn {
166 | font-style: italic;
167 | }
168 |
169 | .markdown-body h1 {
170 | margin: .67em 0;
171 | font-weight: 600;
172 | padding-bottom: .3em;
173 | font-size: 2em;
174 | border-bottom: 1px solid var(--color-border-muted);
175 | }
176 |
177 | .markdown-body mark {
178 | background-color: var(--color-attention-subtle);
179 | color: var(--color-text-primary);
180 | }
181 |
182 | .markdown-body small {
183 | font-size: 90%;
184 | }
185 |
186 | .markdown-body sub,
187 | .markdown-body sup {
188 | font-size: 75%;
189 | line-height: 0;
190 | position: relative;
191 | vertical-align: baseline;
192 | }
193 |
194 | .markdown-body sub {
195 | bottom: -0.25em;
196 | }
197 |
198 | .markdown-body sup {
199 | top: -0.5em;
200 | }
201 |
202 | .markdown-body img {
203 | border-style: none;
204 | max-width: 100%;
205 | box-sizing: content-box;
206 | background-color: var(--color-canvas-default);
207 | }
208 |
209 | .markdown-body code,
210 | .markdown-body kbd,
211 | .markdown-body pre,
212 | .markdown-body samp {
213 | font-family: monospace,monospace;
214 | font-size: 1em;
215 | }
216 |
217 | .markdown-body figure {
218 | margin: 1em 40px;
219 | }
220 |
221 | .markdown-body hr {
222 | box-sizing: content-box;
223 | overflow: hidden;
224 | background: transparent;
225 | border-bottom: 1px solid var(--color-border-muted);
226 | height: .25em;
227 | padding: 0;
228 | margin: 24px 0;
229 | background-color: var(--color-border-default);
230 | border: 0;
231 | }
232 |
233 | .markdown-body input {
234 | font: inherit;
235 | margin: 0;
236 | overflow: visible;
237 | font-family: inherit;
238 | font-size: inherit;
239 | line-height: inherit;
240 | }
241 |
242 | .markdown-body [type=button],
243 | .markdown-body [type=reset],
244 | .markdown-body [type=submit] {
245 | -webkit-appearance: button;
246 | }
247 |
248 | .markdown-body [type=button]::-moz-focus-inner,
249 | .markdown-body [type=reset]::-moz-focus-inner,
250 | .markdown-body [type=submit]::-moz-focus-inner {
251 | border-style: none;
252 | padding: 0;
253 | }
254 |
255 | .markdown-body [type=button]:-moz-focusring,
256 | .markdown-body [type=reset]:-moz-focusring,
257 | .markdown-body [type=submit]:-moz-focusring {
258 | outline: 1px dotted ButtonText;
259 | }
260 |
261 | .markdown-body [type=checkbox],
262 | .markdown-body [type=radio] {
263 | box-sizing: border-box;
264 | padding: 0;
265 | }
266 |
267 | .markdown-body [type=number]::-webkit-inner-spin-button,
268 | .markdown-body [type=number]::-webkit-outer-spin-button {
269 | height: auto;
270 | }
271 |
272 | .markdown-body [type=search] {
273 | -webkit-appearance: textfield;
274 | outline-offset: -2px;
275 | }
276 |
277 | .markdown-body [type=search]::-webkit-search-cancel-button,
278 | .markdown-body [type=search]::-webkit-search-decoration {
279 | -webkit-appearance: none;
280 | }
281 |
282 | .markdown-body ::-webkit-input-placeholder {
283 | color: inherit;
284 | opacity: .54;
285 | }
286 |
287 | .markdown-body ::-webkit-file-upload-button {
288 | -webkit-appearance: button;
289 | font: inherit;
290 | }
291 |
292 | .markdown-body a:hover {
293 | text-decoration: underline;
294 | }
295 |
296 | .markdown-body hr::before {
297 | display: table;
298 | content: "";
299 | }
300 |
301 | .markdown-body hr::after {
302 | display: table;
303 | clear: both;
304 | content: "";
305 | }
306 |
307 | .markdown-body table {
308 | border-spacing: 0;
309 | border-collapse: collapse;
310 | display: block;
311 | width: max-content;
312 | max-width: 100%;
313 | overflow: auto;
314 | }
315 |
316 | .markdown-body td,
317 | .markdown-body th {
318 | padding: 0;
319 | }
320 |
321 | .markdown-body details summary {
322 | cursor: pointer;
323 | }
324 |
325 | .markdown-body details:not([open])>*:not(summary) {
326 | display: none !important;
327 | }
328 |
329 | .markdown-body kbd {
330 | display: inline-block;
331 | padding: 3px 5px;
332 | font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
333 | line-height: 10px;
334 | color: var(--color-fg-default);
335 | vertical-align: middle;
336 | background-color: var(--color-canvas-subtle);
337 | border: solid 1px var(--color-neutral-muted);
338 | border-bottom-color: var(--color-neutral-muted);
339 | border-radius: 6px;
340 | box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
341 | }
342 |
343 | .markdown-body h1,
344 | .markdown-body h2,
345 | .markdown-body h3,
346 | .markdown-body h4,
347 | .markdown-body h5,
348 | .markdown-body h6 {
349 | margin-top: 24px;
350 | margin-bottom: 16px;
351 | font-weight: 600;
352 | line-height: 1.25;
353 | }
354 |
355 | .markdown-body h2 {
356 | font-weight: 600;
357 | padding-bottom: .3em;
358 | font-size: 1.5em;
359 | border-bottom: 1px solid var(--color-border-muted);
360 | }
361 |
362 | .markdown-body h3 {
363 | font-weight: 600;
364 | font-size: 1.25em;
365 | }
366 |
367 | .markdown-body h4 {
368 | font-weight: 600;
369 | font-size: 1em;
370 | }
371 |
372 | .markdown-body h5 {
373 | font-weight: 600;
374 | font-size: .875em;
375 | }
376 |
377 | .markdown-body h6 {
378 | font-weight: 600;
379 | font-size: .85em;
380 | color: var(--color-fg-muted);
381 | }
382 |
383 | .markdown-body p {
384 | margin-top: 0;
385 | margin-bottom: 10px;
386 | }
387 |
388 | .markdown-body blockquote {
389 | margin: 0;
390 | padding: 0 1em;
391 | color: var(--color-fg-muted);
392 | border-left: .25em solid var(--color-border-default);
393 | }
394 |
395 | .markdown-body ul,
396 | .markdown-body ol {
397 | margin-top: 0;
398 | margin-bottom: 0;
399 | padding-left: 2em;
400 | }
401 |
402 | .markdown-body ol ol,
403 | .markdown-body ul ol {
404 | list-style-type: lower-roman;
405 | }
406 |
407 | .markdown-body ul ul ol,
408 | .markdown-body ul ol ol,
409 | .markdown-body ol ul ol,
410 | .markdown-body ol ol ol {
411 | list-style-type: lower-alpha;
412 | }
413 |
414 | .markdown-body dd {
415 | margin-left: 0;
416 | }
417 |
418 | .markdown-body tt,
419 | .markdown-body code {
420 | font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
421 | font-size: 12px;
422 | }
423 |
424 | .markdown-body pre {
425 | margin-top: 0;
426 | margin-bottom: 0;
427 | font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
428 | font-size: 12px;
429 | word-wrap: normal;
430 | }
431 |
432 | .markdown-body .octicon {
433 | display: inline-block;
434 | overflow: visible !important;
435 | vertical-align: text-bottom;
436 | fill: currentColor;
437 | }
438 |
439 | .markdown-body ::placeholder {
440 | color: var(--color-fg-subtle);
441 | opacity: 1;
442 | }
443 |
444 | .markdown-body input::-webkit-outer-spin-button,
445 | .markdown-body input::-webkit-inner-spin-button {
446 | margin: 0;
447 | -webkit-appearance: none;
448 | appearance: none;
449 | }
450 |
451 | .markdown-body .pl-c {
452 | color: var(--color-prettylights-syntax-comment);
453 | }
454 |
455 | .markdown-body .pl-c1,
456 | .markdown-body .pl-s .pl-v {
457 | color: var(--color-prettylights-syntax-constant);
458 | }
459 |
460 | .markdown-body .pl-e,
461 | .markdown-body .pl-en {
462 | color: var(--color-prettylights-syntax-entity);
463 | }
464 |
465 | .markdown-body .pl-smi,
466 | .markdown-body .pl-s .pl-s1 {
467 | color: var(--color-prettylights-syntax-storage-modifier-import);
468 | }
469 |
470 | .markdown-body .pl-ent {
471 | color: var(--color-prettylights-syntax-entity-tag);
472 | }
473 |
474 | .markdown-body .pl-k {
475 | color: var(--color-prettylights-syntax-keyword);
476 | }
477 |
478 | .markdown-body .pl-s,
479 | .markdown-body .pl-pds,
480 | .markdown-body .pl-s .pl-pse .pl-s1,
481 | .markdown-body .pl-sr,
482 | .markdown-body .pl-sr .pl-cce,
483 | .markdown-body .pl-sr .pl-sre,
484 | .markdown-body .pl-sr .pl-sra {
485 | color: var(--color-prettylights-syntax-string);
486 | }
487 |
488 | .markdown-body .pl-v,
489 | .markdown-body .pl-smw {
490 | color: var(--color-prettylights-syntax-variable);
491 | }
492 |
493 | .markdown-body .pl-bu {
494 | color: var(--color-prettylights-syntax-brackethighlighter-unmatched);
495 | }
496 |
497 | .markdown-body .pl-ii {
498 | color: var(--color-prettylights-syntax-invalid-illegal-text);
499 | background-color: var(--color-prettylights-syntax-invalid-illegal-bg);
500 | }
501 |
502 | .markdown-body .pl-c2 {
503 | color: var(--color-prettylights-syntax-carriage-return-text);
504 | background-color: var(--color-prettylights-syntax-carriage-return-bg);
505 | }
506 |
507 | .markdown-body .pl-sr .pl-cce {
508 | font-weight: bold;
509 | color: var(--color-prettylights-syntax-string-regexp);
510 | }
511 |
512 | .markdown-body .pl-ml {
513 | color: var(--color-prettylights-syntax-markup-list);
514 | }
515 |
516 | .markdown-body .pl-mh,
517 | .markdown-body .pl-mh .pl-en,
518 | .markdown-body .pl-ms {
519 | font-weight: bold;
520 | color: var(--color-prettylights-syntax-markup-heading);
521 | }
522 |
523 | .markdown-body .pl-mi {
524 | font-style: italic;
525 | color: var(--color-prettylights-syntax-markup-italic);
526 | }
527 |
528 | .markdown-body .pl-mb {
529 | font-weight: bold;
530 | color: var(--color-prettylights-syntax-markup-bold);
531 | }
532 |
533 | .markdown-body .pl-md {
534 | color: var(--color-prettylights-syntax-markup-deleted-text);
535 | background-color: var(--color-prettylights-syntax-markup-deleted-bg);
536 | }
537 |
538 | .markdown-body .pl-mi1 {
539 | color: var(--color-prettylights-syntax-markup-inserted-text);
540 | background-color: var(--color-prettylights-syntax-markup-inserted-bg);
541 | }
542 |
543 | .markdown-body .pl-mc {
544 | color: var(--color-prettylights-syntax-markup-changed-text);
545 | background-color: var(--color-prettylights-syntax-markup-changed-bg);
546 | }
547 |
548 | .markdown-body .pl-mi2 {
549 | color: var(--color-prettylights-syntax-markup-ignored-text);
550 | background-color: var(--color-prettylights-syntax-markup-ignored-bg);
551 | }
552 |
553 | .markdown-body .pl-mdr {
554 | font-weight: bold;
555 | color: var(--color-prettylights-syntax-meta-diff-range);
556 | }
557 |
558 | .markdown-body .pl-ba {
559 | color: var(--color-prettylights-syntax-brackethighlighter-angle);
560 | }
561 |
562 | .markdown-body .pl-sg {
563 | color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);
564 | }
565 |
566 | .markdown-body .pl-corl {
567 | text-decoration: underline;
568 | color: var(--color-prettylights-syntax-constant-other-reference-link);
569 | }
570 |
571 | .markdown-body [data-catalyst] {
572 | display: block;
573 | }
574 |
575 | .markdown-body g-emoji {
576 | font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
577 | font-size: 1em;
578 | font-style: normal !important;
579 | font-weight: 400;
580 | line-height: 1;
581 | vertical-align: -0.075em;
582 | }
583 |
584 | .markdown-body g-emoji img {
585 | width: 1em;
586 | height: 1em;
587 | }
588 |
589 | .markdown-body::before {
590 | display: table;
591 | content: "";
592 | }
593 |
594 | .markdown-body::after {
595 | display: table;
596 | clear: both;
597 | content: "";
598 | }
599 |
600 | .markdown-body>*:first-child {
601 | margin-top: 0 !important;
602 | }
603 |
604 | .markdown-body>*:last-child {
605 | margin-bottom: 0 !important;
606 | }
607 |
608 | .markdown-body a:not([href]) {
609 | color: inherit;
610 | text-decoration: none;
611 | }
612 |
613 | .markdown-body .absent {
614 | color: var(--color-danger-fg);
615 | }
616 |
617 | .markdown-body .anchor {
618 | float: left;
619 | padding-right: 4px;
620 | margin-left: -20px;
621 | line-height: 1;
622 | }
623 |
624 | .markdown-body .anchor:focus {
625 | outline: none;
626 | }
627 |
628 | .markdown-body p,
629 | .markdown-body blockquote,
630 | .markdown-body ul,
631 | .markdown-body ol,
632 | .markdown-body dl,
633 | .markdown-body table,
634 | .markdown-body pre,
635 | .markdown-body details {
636 | margin-top: 0;
637 | margin-bottom: 16px;
638 | }
639 |
640 | .markdown-body blockquote>:first-child {
641 | margin-top: 0;
642 | }
643 |
644 | .markdown-body blockquote>:last-child {
645 | margin-bottom: 0;
646 | }
647 |
648 | .markdown-body sup>a::before {
649 | content: "[";
650 | }
651 |
652 | .markdown-body sup>a::after {
653 | content: "]";
654 | }
655 |
656 | .markdown-body h1 .octicon-link,
657 | .markdown-body h2 .octicon-link,
658 | .markdown-body h3 .octicon-link,
659 | .markdown-body h4 .octicon-link,
660 | .markdown-body h5 .octicon-link,
661 | .markdown-body h6 .octicon-link {
662 | color: var(--color-fg-default);
663 | vertical-align: middle;
664 | visibility: hidden;
665 | }
666 |
667 | .markdown-body h1:hover .anchor,
668 | .markdown-body h2:hover .anchor,
669 | .markdown-body h3:hover .anchor,
670 | .markdown-body h4:hover .anchor,
671 | .markdown-body h5:hover .anchor,
672 | .markdown-body h6:hover .anchor {
673 | text-decoration: none;
674 | }
675 |
676 | .markdown-body h1:hover .anchor .octicon-link,
677 | .markdown-body h2:hover .anchor .octicon-link,
678 | .markdown-body h3:hover .anchor .octicon-link,
679 | .markdown-body h4:hover .anchor .octicon-link,
680 | .markdown-body h5:hover .anchor .octicon-link,
681 | .markdown-body h6:hover .anchor .octicon-link {
682 | visibility: visible;
683 | }
684 |
685 | .markdown-body h1 tt,
686 | .markdown-body h1 code,
687 | .markdown-body h2 tt,
688 | .markdown-body h2 code,
689 | .markdown-body h3 tt,
690 | .markdown-body h3 code,
691 | .markdown-body h4 tt,
692 | .markdown-body h4 code,
693 | .markdown-body h5 tt,
694 | .markdown-body h5 code,
695 | .markdown-body h6 tt,
696 | .markdown-body h6 code {
697 | padding: 0 .2em;
698 | font-size: inherit;
699 | }
700 |
701 | .markdown-body ul.no-list,
702 | .markdown-body ol.no-list {
703 | padding: 0;
704 | list-style-type: none;
705 | }
706 |
707 | .markdown-body ol[type="1"] {
708 | list-style-type: decimal;
709 | }
710 |
711 | .markdown-body ol[type=a] {
712 | list-style-type: lower-alpha;
713 | }
714 |
715 | .markdown-body ol[type=i] {
716 | list-style-type: lower-roman;
717 | }
718 |
719 | .markdown-body div>ol:not([type]) {
720 | list-style-type: decimal;
721 | }
722 |
723 | .markdown-body ul ul,
724 | .markdown-body ul ol,
725 | .markdown-body ol ol,
726 | .markdown-body ol ul {
727 | margin-top: 0;
728 | margin-bottom: 0;
729 | }
730 |
731 | .markdown-body li>p {
732 | margin-top: 16px;
733 | }
734 |
735 | .markdown-body li+li {
736 | margin-top: .25em;
737 | }
738 |
739 | .markdown-body dl {
740 | padding: 0;
741 | }
742 |
743 | .markdown-body dl dt {
744 | padding: 0;
745 | margin-top: 16px;
746 | font-size: 1em;
747 | font-style: italic;
748 | font-weight: 600;
749 | }
750 |
751 | .markdown-body dl dd {
752 | padding: 0 16px;
753 | margin-bottom: 16px;
754 | }
755 |
756 | .markdown-body table th {
757 | font-weight: 600;
758 | }
759 |
760 | .markdown-body table th,
761 | .markdown-body table td {
762 | padding: 6px 13px;
763 | border: 1px solid var(--color-border-default);
764 | }
765 |
766 | .markdown-body table tr {
767 | background-color: var(--color-canvas-default);
768 | border-top: 1px solid var(--color-border-muted);
769 | }
770 |
771 | .markdown-body table tr:nth-child(2n) {
772 | background-color: var(--color-canvas-subtle);
773 | }
774 |
775 | .markdown-body table img {
776 | background-color: transparent;
777 | }
778 |
779 | .markdown-body img[align=right] {
780 | padding-left: 20px;
781 | }
782 |
783 | .markdown-body img[align=left] {
784 | padding-right: 20px;
785 | }
786 |
787 | .markdown-body .emoji {
788 | max-width: none;
789 | vertical-align: text-top;
790 | background-color: transparent;
791 | }
792 |
793 | .markdown-body span.frame {
794 | display: block;
795 | overflow: hidden;
796 | }
797 |
798 | .markdown-body span.frame>span {
799 | display: block;
800 | float: left;
801 | width: auto;
802 | padding: 7px;
803 | margin: 13px 0 0;
804 | overflow: hidden;
805 | border: 1px solid var(--color-border-default);
806 | }
807 |
808 | .markdown-body span.frame span img {
809 | display: block;
810 | float: left;
811 | }
812 |
813 | .markdown-body span.frame span span {
814 | display: block;
815 | padding: 5px 0 0;
816 | clear: both;
817 | color: var(--color-fg-default);
818 | }
819 |
820 | .markdown-body span.align-center {
821 | display: block;
822 | overflow: hidden;
823 | clear: both;
824 | }
825 |
826 | .markdown-body span.align-center>span {
827 | display: block;
828 | margin: 13px auto 0;
829 | overflow: hidden;
830 | text-align: center;
831 | }
832 |
833 | .markdown-body span.align-center span img {
834 | margin: 0 auto;
835 | text-align: center;
836 | }
837 |
838 | .markdown-body span.align-right {
839 | display: block;
840 | overflow: hidden;
841 | clear: both;
842 | }
843 |
844 | .markdown-body span.align-right>span {
845 | display: block;
846 | margin: 13px 0 0;
847 | overflow: hidden;
848 | text-align: right;
849 | }
850 |
851 | .markdown-body span.align-right span img {
852 | margin: 0;
853 | text-align: right;
854 | }
855 |
856 | .markdown-body span.float-left {
857 | display: block;
858 | float: left;
859 | margin-right: 13px;
860 | overflow: hidden;
861 | }
862 |
863 | .markdown-body span.float-left span {
864 | margin: 13px 0 0;
865 | }
866 |
867 | .markdown-body span.float-right {
868 | display: block;
869 | float: right;
870 | margin-left: 13px;
871 | overflow: hidden;
872 | }
873 |
874 | .markdown-body span.float-right>span {
875 | display: block;
876 | margin: 13px auto 0;
877 | overflow: hidden;
878 | text-align: right;
879 | }
880 |
881 | .markdown-body code,
882 | .markdown-body tt {
883 | padding: .2em .4em;
884 | margin: 0;
885 | font-size: 85%;
886 | background-color: var(--color-neutral-muted);
887 | border-radius: 6px;
888 | }
889 |
890 | .markdown-body code br,
891 | .markdown-body tt br {
892 | display: none;
893 | }
894 |
895 | .markdown-body del code {
896 | text-decoration: inherit;
897 | }
898 |
899 | .markdown-body pre code {
900 | font-size: 100%;
901 | }
902 |
903 | .markdown-body pre>code {
904 | padding: 0;
905 | margin: 0;
906 | word-break: normal;
907 | white-space: pre;
908 | background: transparent;
909 | border: 0;
910 | }
911 |
912 | .markdown-body .highlight {
913 | margin-bottom: 16px;
914 | }
915 |
916 | .markdown-body .highlight pre {
917 | margin-bottom: 0;
918 | word-break: normal;
919 | }
920 |
921 | .markdown-body .highlight pre,
922 | .markdown-body pre {
923 | padding: 16px;
924 | overflow: auto;
925 | font-size: 85%;
926 | line-height: 1.45;
927 | background-color: var(--color-canvas-subtle);
928 | border-radius: 6px;
929 | }
930 |
931 | .markdown-body pre code,
932 | .markdown-body pre tt {
933 | display: inline;
934 | max-width: auto;
935 | padding: 0;
936 | margin: 0;
937 | overflow: visible;
938 | line-height: inherit;
939 | word-wrap: normal;
940 | background-color: transparent;
941 | border: 0;
942 | }
943 |
944 | .markdown-body .csv-data td,
945 | .markdown-body .csv-data th {
946 | padding: 5px;
947 | overflow: hidden;
948 | font-size: 12px;
949 | line-height: 1;
950 | text-align: left;
951 | white-space: nowrap;
952 | }
953 |
954 | .markdown-body .csv-data .blob-num {
955 | padding: 10px 8px 9px;
956 | text-align: right;
957 | background: var(--color-canvas-default);
958 | border: 0;
959 | }
960 |
961 | .markdown-body .csv-data tr {
962 | border-top: 0;
963 | }
964 |
965 | .markdown-body .csv-data th {
966 | font-weight: 600;
967 | background: var(--color-canvas-subtle);
968 | border-top: 0;
969 | }
970 |
971 | .markdown-body .footnotes {
972 | font-size: 12px;
973 | color: var(--color-fg-muted);
974 | border-top: 1px solid var(--color-border-default);
975 | }
976 |
977 | .markdown-body .footnotes ol {
978 | padding-left: 16px;
979 | }
980 |
981 | .markdown-body .footnotes li {
982 | position: relative;
983 | }
984 |
985 | .markdown-body .footnotes li:target::before {
986 | position: absolute;
987 | top: -8px;
988 | right: -8px;
989 | bottom: -8px;
990 | left: -24px;
991 | pointer-events: none;
992 | content: "";
993 | border: 2px solid var(--color-accent-emphasis);
994 | border-radius: 6px;
995 | }
996 |
997 | .markdown-body .footnotes li:target {
998 | color: var(--color-fg-default);
999 | }
1000 |
1001 | .markdown-body .footnotes .data-footnote-backref g-emoji {
1002 | font-family: monospace;
1003 | }
1004 |
1005 | .markdown-body .task-list-item {
1006 | list-style-type: none;
1007 | }
1008 |
1009 | .markdown-body .task-list-item label {
1010 | font-weight: 400;
1011 | }
1012 |
1013 | .markdown-body .task-list-item.enabled label {
1014 | cursor: pointer;
1015 | }
1016 |
1017 | .markdown-body .task-list-item+.task-list-item {
1018 | margin-top: 3px;
1019 | }
1020 |
1021 | .markdown-body .task-list-item .handle {
1022 | display: none;
1023 | }
1024 |
1025 | .markdown-body .task-list-item-checkbox {
1026 | margin: 0 .2em .25em -1.6em;
1027 | vertical-align: middle;
1028 | }
1029 |
1030 | .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
1031 | margin: 0 -1.6em .25em .2em;
1032 | }
1033 |
1034 | .markdown-body ::-webkit-calendar-picker-indicator {
1035 | filter: invert(50%);
1036 | }
--------------------------------------------------------------------------------