├── .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 | 2 | 3 | 4 | 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 | 2 | 3 | 4 | 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 | 2 | 3 | 9 | 10 | 11 | 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 | 2 | 3 | 4 | 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/svg/long.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /public/svg/short.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 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 | 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 | 291 | 299 | 307 | 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 '' + 345 | foreignObject + ''; 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 | } --------------------------------------------------------------------------------