├── bun.lockb ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── src ├── rank.ts ├── swagger.ts ├── types.ts └── index.ts ├── tsconfig.json ├── biome.json ├── package.json └── README.md /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kerolloz/aktive/HEAD/bun.lockb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .deta/ 3 | 4 | .env 5 | .space 6 | package-lock.json -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["streetsidesoftware.code-spell-checker", "biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.organizeImports.biome": "explicit", 4 | "quickfix.biome": "explicit" 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "biomejs.biome" 8 | }, 9 | "[json]": { 10 | "editor.defaultFormatter": "biomejs.biome" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/rank.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import ordinal from 'ordinal'; 3 | import YAML from 'yamljs'; 4 | 5 | export async function getOrdinalRank(username: string, country: string) { 6 | const githubTopRepo = 'ashkulz/committers.top'; 7 | const countryPath = `gh-pages/_data/locations/${country}.yml`; 8 | 9 | const { data } = await axios({ 10 | url: `https://raw.githubusercontent.com/${githubTopRepo}/${countryPath}`, 11 | }); 12 | 13 | const userData = data.split('\n\n').find((u) => u.includes(username)); 14 | if (!userData) return null; 15 | const json = YAML.parse(userData)[0]; 16 | return ordinal(json.rank); 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | 9 | //source 10 | "outDir": "dist", 11 | "rootDir": "src", 12 | 13 | // Bundler mode 14 | "moduleResolution": "bundler", 15 | "allowImportingTsExtensions": true, 16 | "verbatimModuleSyntax": true, 17 | "noEmit": true, 18 | 19 | // Best practices 20 | "strict": true, 21 | "skipLibCheck": true, 22 | "noFallthroughCasesInSwitch": true, 23 | 24 | // Some stricter flags 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "formatWithErrors": false, 6 | "indentStyle": "space", 7 | "indentWidth": 2, 8 | "lineEnding": "lf", 9 | "lineWidth": 80, 10 | "attributePosition": "auto" 11 | }, 12 | "organizeImports": { "enabled": true }, 13 | "linter": { "enabled": true, "rules": { "recommended": true } }, 14 | "javascript": { 15 | "formatter": { 16 | "jsxQuoteStyle": "double", 17 | "quoteProperties": "asNeeded", 18 | "trailingComma": "all", 19 | "semicolons": "always", 20 | "arrowParentheses": "always", 21 | "bracketSpacing": true, 22 | "bracketSameLine": false, 23 | "quoteStyle": "single", 24 | "attributePosition": "auto" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/swagger.ts: -------------------------------------------------------------------------------- 1 | import fastifySwagger from '@fastify/swagger'; 2 | import fastifySwaggerUI from '@fastify/swagger-ui'; 3 | import type { FastifyInstance } from 'fastify'; 4 | import { jsonSchemaTransform } from 'fastify-type-provider-zod'; 5 | 6 | export const registerSwagger = (app: FastifyInstance) => { 7 | app.register(fastifySwagger, { 8 | openapi: { 9 | info: { 10 | title: 'Aktive', 11 | description: 12 | 'Aktive is a simple web service. It returns a badge (or JSON) that shows your rank among other GitHub users from your country according to your GitHub contributions.', 13 | contact: { 14 | name: 'Kerollos Magdy', 15 | url: 'https://aktive.kerolloz.dev', 16 | email: 'kerolloz@yahoo.com', 17 | }, 18 | version: '1.0.0', 19 | }, 20 | servers: [], 21 | }, 22 | transform: jsonSchemaTransform, 23 | }); 24 | 25 | app.register(fastifySwaggerUI, { routePrefix: '/swagger' }); 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aktive", 3 | "version": "1.0.0", 4 | "main": "src/index.ts", 5 | "scripts": { 6 | "start": "bun .", 7 | "dev": "bun --watch .", 8 | "lint": "biome ci ." 9 | }, 10 | "keywords": [], 11 | "author": "Kerollos Magdy ", 12 | "license": "ISC", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/kerolloz/aktive.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/kerolloz/aktive/issues" 19 | }, 20 | "homepage": "https://github.com/kerolloz/aktive#readme", 21 | "description": "", 22 | "dependencies": { 23 | "@fastify/swagger": "^8.14.0", 24 | "@fastify/swagger-ui": "^3.0.0", 25 | "axios": "^1.7.2", 26 | "badge-maker": "^3.3.1", 27 | "fastify": "^4.27.0", 28 | "fastify-type-provider-zod": "^1.2.0", 29 | "ordinal": "^1.0.3", 30 | "yamljs": "^0.3.0", 31 | "zod": "^3.23.8" 32 | }, 33 | "devDependencies": { 34 | "@biomejs/biome": "^1.7.3", 35 | "@types/bun": "latest", 36 | "@types/yamljs": "^0.2.34", 37 | "typescript": "^5.4.5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const rankResponse = z.object({ rank: z.string().nullish() }); 4 | export const aktiveRequestSchema = { 5 | params: z.object({ 6 | country: z.string().describe('The country name.'), 7 | username: z.string().describe('The username.'), 8 | }), 9 | querystring: z.object({ 10 | style: z 11 | .enum(['flat', 'plastic', 'flat-square', 'for-the-badge', 'social']) 12 | .default('flat') 13 | .describe('Set the style of the badge.'), 14 | label: z 15 | .string() 16 | .default('Most Active GitHub User Rank') 17 | .describe('Set the left-hand-side text.'), 18 | labelColor: z 19 | .string() 20 | .default('') 21 | .describe('Set background color of the left part.'), 22 | color: z 23 | .string() 24 | .default('') 25 | .describe('Set background color of the right part.'), 26 | rnkPrefix: z 27 | .string() 28 | .default('') 29 | .describe('The prefix to display before the rank value.'), 30 | rnkSuffix: z 31 | .string() 32 | .default('') 33 | .describe('The suffix to display after the rank value.'), 34 | }), 35 | }; 36 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { makeBadge } from 'badge-maker'; 2 | import fastify from 'fastify'; 3 | import { 4 | type ZodTypeProvider, 5 | serializerCompiler, 6 | validatorCompiler, 7 | } from 'fastify-type-provider-zod'; 8 | import { ZodError } from 'zod'; 9 | import { getOrdinalRank } from './rank'; 10 | import { registerSwagger } from './swagger'; 11 | import { aktiveRequestSchema, rankResponse } from './types'; 12 | 13 | const app = fastify(); 14 | 15 | app.get('/', async (_, reply) => 16 | reply.redirect('https://github.com/kerolloz/aktive'), 17 | ); 18 | 19 | app.setValidatorCompiler(validatorCompiler); 20 | app.setSerializerCompiler(serializerCompiler); 21 | 22 | registerSwagger(app); 23 | 24 | app.setErrorHandler((error, _, reply) => 25 | error instanceof ZodError 26 | ? reply.status(400).send({ message: 'Bad Request', error: error.issues }) 27 | : reply.send(error), 28 | ); 29 | 30 | app.after(() => { 31 | app.withTypeProvider().route({ 32 | method: 'GET', 33 | url: '/rank/:country/:username', 34 | schema: { 35 | params: aktiveRequestSchema.params, 36 | response: { 200: rankResponse, 400: rankResponse }, 37 | summary: 'Get the rank of a user in a country (JSON format).', 38 | tags: ['json'], 39 | }, 40 | handler: async ({ params }, reply) => { 41 | const rank = await getOrdinalRank(params.username, params.country); 42 | return rank ? { rank } : reply.status(400).send({ rank }); 43 | }, 44 | }); 45 | app.withTypeProvider().route({ 46 | method: 'GET', 47 | url: '/:country/:username', 48 | schema: { 49 | ...aktiveRequestSchema, 50 | summary: 'Get the rank of a user in a country (badge format).', 51 | tags: ['badge'], 52 | }, 53 | handler: async (request, reply) => { 54 | const { username, country } = request.params; 55 | const { style, label, labelColor, color, rnkPrefix, rnkSuffix } = 56 | request.query; 57 | 58 | const userRank = await getOrdinalRank(username, country); 59 | if (!userRank) { 60 | return reply.status(400).send({ 61 | statusCode: 400, 62 | error: 'Bad Request', 63 | message: 64 | 'Username not found. Please check your username and country here https://commits.top', 65 | }); 66 | } 67 | 68 | const badge = makeBadge({ 69 | style, 70 | label, 71 | labelColor, 72 | color, 73 | message: rnkPrefix + userRank + rnkSuffix, 74 | }); 75 | 76 | reply 77 | .headers({ 78 | 'Content-Type': 'image/svg+xml', 79 | 'Cache-Control': 'max-age=86400', // 1 day 80 | }) 81 | .send(badge); 82 | }, 83 | }); 84 | }); 85 | 86 | app.listen( 87 | { port: +(process.env.PORT ?? 3000), host: '::' }, 88 | (err, address) => { 89 | if (err) { 90 | console.error(err); 91 | process.exit(1); 92 | } 93 | console.log(`Server listening at ${address}`); 94 | }, 95 | ); 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Aktive 🟩 2 | fastify 3 | Railway 4 |

5 | 6 | Aktive is a simple web service. It returns a badge (or JSON) that shows your rank among other GitHub users from your country according to your GitHub contributions. 7 | 8 | > **Note** 9 | > 10 | > Aktive depends on the data provided by [ashkulz/committers.top](//github.com/ashkulz/committers.top). 11 | > So please make sure that your name appears on your country list here [committers.top](https://committers.top). 12 | 13 | ## Docs 14 | 15 | > [!NOTE] 16 | > You can also check the Swagger [API documentation](https://aktive.kerolloz.dev/swagger) for more details. 17 | 18 | ### Endpoints 19 | 20 | > Base URL: 21 | 22 | #### GET `/` 23 | 24 | Redirects to this repository. 25 | 26 | --- 27 | 28 | #### GET `/:country/:username` 29 | 30 | - Returns a [shields.io](https://shields.io) badge (SVG image) with your rank. 31 | 32 | ##### Parameters 33 | 34 | - `country` - The country you live in (make sure it's visible on your profile). 35 | - `username` - Your GitHub username. 36 | 37 | ##### Query Parameters 38 | 39 | - `style` - Set the style of the badge. Can be one of `flat`, `flat-square`, `for-the-badge`, or `plastic`. Defaults to `flat`. 40 | - `label` - Set the left-hand-side text. Defaults to `Most Active GitHub User Rank`. 41 | - `color` - Set the background of the right part (hex, rgb, rgba, hsl, hsla and css named colors supported). Defaults to `brightgreen`. 42 | - `labelColor` - Set the background of the left part (hex, rgb, rgba, hsl, hsla and css named colors supported). Defaults to `grey`. 43 | - `rnkPrefix` - Set prefix to display before the rank value. Defaults to `""` empty string. 44 | - `rnkSuffix` - Set suffix to display after the rank value. Defaults to `""` empty string. 45 | 46 | ##### Examples 47 | 48 | > `![badge](https://aktive.kerolloz.dev/egypt/kerolloz)` 49 | > ![badge](https://aktive.kerolloz.dev/egypt/kerolloz) 50 | 51 | > `![badge](https://aktive.kerolloz.dev/egypt/kerolloz?style=flat-square&color=blue)` 52 | > ![badge](https://aktive.kerolloz.dev/egypt/kerolloz?style=flat-square&color=blue) 53 | 54 | > `![badge](https://aktive.kerolloz.dev/egypt/kerolloz?label=Most%20Active%20GitHub%20User%20In%20Egypt&labelColor=white&rnkPrefix=Rank%20)` 55 | > ![badge](https://aktive.kerolloz.dev/egypt/kerolloz?label=Most%20Active%20GitHub%20User%20In%20Egypt&labelColor=white&rnkPrefix=Rank%20) 56 | 57 | > `![badge](https://aktive.kerolloz.dev/egypt/kerolloz?label=&color=cyan&style=for-the-badge&rnkPrefix=Ranked%20&rnkSuffix=%20In%20Egypt)` 58 | > ![badge](https://aktive.kerolloz.dev/egypt/kerolloz?label=&color=cyan&style=for-the-badge&rnkPrefix=Ranked%20&rnkSuffix=%20In%20Egypt) 59 | 60 | --- 61 | 62 | #### GET `/rank/:country/:username` 63 | 64 | 65 | Try it 66 | 67 | 68 | - Returns a JSON object with your rank. 69 | 70 | **Same parameters** as [GET `/:country/:username`](#get-countryusername) 71 | 72 | ##### Example 73 | 74 | ```bash 75 | $ curl https://aktive.kerolloz.dev/rank/egypt/kerolloz 76 | 77 | { 78 | "rank": "109th" 79 | } 80 | ``` 81 | --------------------------------------------------------------------------------