diff --git a/apps/atproto-starter/.claude/settings.local.json b/apps/atproto-starter/.claude/settings.local.json new file mode 100644 index 0000000..32f6420 --- /dev/null +++ b/apps/atproto-starter/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(npx tsc:*)", + "mcp__plugin_svelte_svelte__svelte-autofixer", + "mcp__plugin_svelte_svelte__get-documentation", + "Bash(pnpm check:*)", + "Bash(pnpm env:generate-secret:*)", + "Bash(pnpm build:agent-setup:*)" + ] + } +} diff --git a/apps/atproto-starter/.env.example b/apps/atproto-starter/.env.example new file mode 100644 index 0000000..ede6fba --- /dev/null +++ b/apps/atproto-starter/.env.example @@ -0,0 +1,6 @@ +# Generate both with: pnpm env:setup-dev +CLIENT_ASSERTION_KEY= +COOKIE_SECRET= + +# Set to your tunnel URL to use a confidential client in dev +# OAUTH_PUBLIC_URL=https://your-tunnel.trycloudflare.com diff --git a/apps/atproto-starter/.gitignore b/apps/atproto-starter/.gitignore new file mode 100644 index 0000000..1c43954 --- /dev/null +++ b/apps/atproto-starter/.gitignore @@ -0,0 +1,26 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# Generated types (regenerated via pnpm generate:pull) +src/lexicon-types/ diff --git a/apps/atproto-starter/.npmrc b/apps/atproto-starter/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/apps/atproto-starter/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/apps/atproto-starter/.prettierignore b/apps/atproto-starter/.prettierignore new file mode 100644 index 0000000..ab78a95 --- /dev/null +++ b/apps/atproto-starter/.prettierignore @@ -0,0 +1,4 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/apps/atproto-starter/.prettierrc b/apps/atproto-starter/.prettierrc new file mode 100644 index 0000000..7ebb855 --- /dev/null +++ b/apps/atproto-starter/.prettierrc @@ -0,0 +1,15 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/apps/atproto-starter/LICENSE b/apps/atproto-starter/LICENSE new file mode 100644 index 0000000..696e4f6 --- /dev/null +++ b/apps/atproto-starter/LICENSE @@ -0,0 +1,21 @@ +MIT License Copyright (c) 2025 flo-bit + +Permission is hereby granted, free of +charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice +(including the next paragraph) shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/apps/atproto-starter/README.md b/apps/atproto-starter/README.md new file mode 100644 index 0000000..50917ff --- /dev/null +++ b/apps/atproto-starter/README.md @@ -0,0 +1,77 @@ +# sveltekit + contrail + atproto OAuth + +A sveltekit example app on cloudflare workers with self-hosted atproto record indexing via [contrail](https://github.com/flo-bit/contrail), fully typed queries, and OAuth authentication. + +## Setup + +```sh +pnpm install +pnpm dev +``` + +Dev mode uses a loopback OAuth client — no keys or Cloudflare setup needed. + +## Config + +Define which AT Protocol collections to index in `src/lib/contrail.config.ts`: + +```ts +import type { ContrailConfig } from '@atmo-dev/contrail'; + +export const config: ContrailConfig = { + namespace: 'statusphere.app', + collections: { + status: { // short name → URL path segment + collection: 'xyz.statusphere.status', // full NSID of the record type + queryable: { + status: {}, // equality filter (?status=...) + createdAt: { type: 'range' } // range filter (?createdAtMin=...&createdAtMax=...) + } + } + } +}; +``` + +After changing the config, run `pnpm generate:pull` to regenerate lexicons and types. + +Run `pnpm sync` to backfill existing records from the network. + +Wrangler bindings (`wrangler.jsonc`): + +- **D1** (`DB`) — contrail's database +- **KV** (`OAUTH_SESSIONS`, `OAUTH_STATES`) — OAuth session storage +- **Vars** — `CRON_SECRET`, `OAUTH_PUBLIC_URL`, `CLIENT_ASSERTION_KEY`, `COOKIE_SECRET` + +## Deploy + +```sh +npx wrangler d1 create statusphere +# Add database_id to wrangler.jsonc + +pnpm build +npx wrangler deploy +``` + +## How it works + +**Contrail** indexes AT Protocol records into D1 via Jetstream (cron, every minute). When a user posts, `contrail.notify()` indexes it immediately. + +**Typed queries** use `@atcute/client` with an in-process handler — full type safety, zero HTTP overhead: + +```ts +const client = getClient(platform!.env.DB); +const res = await client.get('statusphere.app.status.listRecords', { + params: { limit: 50, profiles: true } // typed params +}); +res.data.records // typed response +``` + +Types are generated from contrail's config via `pnpm generate:pull`, which produces lexicon JSON and TypeScript types that register with `@atcute/client`. + +**Scheduled ingestion** works around SvelteKit's lack of `scheduled` export support ([sveltejs/kit#4841](https://github.com/sveltejs/kit/issues/4841)) by appending a handler post-build that self-calls `/api/cron`. + +**OAuth** uses `@atcute/oauth-node-client` with KV-backed sessions and HMAC-signed cookies. + +## License + +MIT diff --git a/apps/atproto-starter/eslint.config.js b/apps/atproto-starter/eslint.config.js new file mode 100644 index 0000000..aa5987f --- /dev/null +++ b/apps/atproto-starter/eslint.config.js @@ -0,0 +1,34 @@ +import prettier from 'eslint-config-prettier'; +import js from '@eslint/js'; +import { includeIgnoreFile } from '@eslint/compat'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript-eslint'; +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default ts.config( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs['flat/recommended'], + prettier, + ...svelte.configs['flat/prettier'], + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + } + } + }, + { + files: ['**/*.svelte'], + + languageOptions: { + parserOptions: { + parser: ts.parser + } + } + } +); diff --git a/apps/atproto-starter/lexicons/generated/index.ts b/apps/atproto-starter/lexicons/generated/index.ts new file mode 100644 index 0000000..e1faba6 --- /dev/null +++ b/apps/atproto-starter/lexicons/generated/index.ts @@ -0,0 +1,15 @@ +// Auto-generated by @atmo-dev/contrail-lexicons. Do not edit. +// Pass `lexicons` to `createWorker(config, { lexicons })` to expose them +// at `/xrpc/.lexicons` for consumer apps to typegen against. + +import _0 from "../pulled/app/bsky/actor/profile.json"; +import _1 from "../pulled/xyz/statusphere/status.json"; +import _2 from "./statusphere/app/getCursor.json"; +import _3 from "./statusphere/app/getOverview.json"; +import _4 from "./statusphere/app/getProfile.json"; +import _5 from "./statusphere/app/notifyOfUpdate.json"; +import _6 from "./statusphere/app/permissionSet.json"; +import _7 from "./statusphere/app/status/getRecord.json"; +import _8 from "./statusphere/app/status/listRecords.json"; + +export const lexicons: object[] = [_0, _1, _2, _3, _4, _5, _6, _7, _8]; diff --git a/apps/atproto-starter/lexicons/generated/statusphere/app/getCursor.json b/apps/atproto-starter/lexicons/generated/statusphere/app/getCursor.json new file mode 100644 index 0000000..f85bdbf --- /dev/null +++ b/apps/atproto-starter/lexicons/generated/statusphere/app/getCursor.json @@ -0,0 +1,27 @@ +{ + "lexicon": 1, + "id": "statusphere.app.getCursor", + "defs": { + "main": { + "type": "query", + "description": "Get the current cursor position", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "time_us": { + "type": "integer" + }, + "date": { + "type": "string" + }, + "seconds_ago": { + "type": "integer" + } + } + } + } + } + } +} diff --git a/apps/atproto-starter/lexicons/generated/statusphere/app/getOverview.json b/apps/atproto-starter/lexicons/generated/statusphere/app/getOverview.json new file mode 100644 index 0000000..67dd2f5 --- /dev/null +++ b/apps/atproto-starter/lexicons/generated/statusphere/app/getOverview.json @@ -0,0 +1,51 @@ +{ + "lexicon": 1, + "id": "statusphere.app.getOverview", + "defs": { + "main": { + "type": "query", + "description": "Get an overview of all indexed collections", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "total_records", + "collections" + ], + "properties": { + "total_records": { + "type": "integer" + }, + "collections": { + "type": "array", + "items": { + "type": "ref", + "ref": "#collectionStats" + } + } + } + } + } + }, + "collectionStats": { + "type": "object", + "required": [ + "collection", + "records", + "unique_users" + ], + "properties": { + "collection": { + "type": "string" + }, + "records": { + "type": "integer" + }, + "unique_users": { + "type": "integer" + } + } + } + } +} diff --git a/apps/atproto-starter/lexicons/generated/statusphere/app/getProfile.json b/apps/atproto-starter/lexicons/generated/statusphere/app/getProfile.json new file mode 100644 index 0000000..82732a4 --- /dev/null +++ b/apps/atproto-starter/lexicons/generated/statusphere/app/getProfile.json @@ -0,0 +1,138 @@ +{ + "lexicon": 1, + "id": "statusphere.app.getProfile", + "defs": { + "main": { + "type": "query", + "description": "Get a user's profiles by DID or handle", + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier", + "description": "DID or handle of the user" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "profiles" + ], + "properties": { + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } + } + } + }, + "profileEntry": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "handle": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "#appBskyActorProfile" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + } + } + }, + "appBskyActorProfile": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + } + } +} diff --git a/apps/atproto-starter/lexicons/generated/statusphere/app/notifyOfUpdate.json b/apps/atproto-starter/lexicons/generated/statusphere/app/notifyOfUpdate.json new file mode 100644 index 0000000..f34ded3 --- /dev/null +++ b/apps/atproto-starter/lexicons/generated/statusphere/app/notifyOfUpdate.json @@ -0,0 +1,59 @@ +{ + "lexicon": 1, + "id": "statusphere.app.notifyOfUpdate", + "defs": { + "main": { + "type": "procedure", + "description": "Notify of a record change for immediate indexing. Fetches the record from the user's PDS and indexes (or deletes) it.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "Single AT URI to fetch and index" + }, + "uris": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + }, + "maxLength": 25, + "description": "Batch of AT URIs to fetch and index (max 25)" + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "indexed", + "deleted" + ], + "properties": { + "indexed": { + "type": "integer", + "description": "Number of records created or updated" + }, + "deleted": { + "type": "integer", + "description": "Number of records deleted (not found on PDS)" + }, + "errors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Errors for individual URIs that could not be processed" + } + } + } + } + } + } +} diff --git a/apps/atproto-starter/lexicons/generated/statusphere/app/permissionSet.json b/apps/atproto-starter/lexicons/generated/statusphere/app/permissionSet.json new file mode 100644 index 0000000..c40af0f --- /dev/null +++ b/apps/atproto-starter/lexicons/generated/statusphere/app/permissionSet.json @@ -0,0 +1,26 @@ +{ + "lexicon": 1, + "id": "statusphere.app.permissionSet", + "defs": { + "main": { + "type": "permission-set", + "title": "statusphere.app", + "description": "All XRPC methods exposed by the statusphere.app service.", + "permissions": [ + { + "type": "permission", + "resource": "rpc", + "aud": "*", + "lxm": [ + "statusphere.app.getCursor", + "statusphere.app.getOverview", + "statusphere.app.getProfile", + "statusphere.app.notifyOfUpdate", + "statusphere.app.status.getRecord", + "statusphere.app.status.listRecords" + ] + } + ] + } + } +} diff --git a/apps/atproto-starter/lexicons/generated/statusphere/app/status/getRecord.json b/apps/atproto-starter/lexicons/generated/statusphere/app/status/getRecord.json new file mode 100644 index 0000000..c866e5e --- /dev/null +++ b/apps/atproto-starter/lexicons/generated/statusphere/app/status/getRecord.json @@ -0,0 +1,173 @@ +{ + "lexicon": 1, + "id": "statusphere.app.status.getRecord", + "defs": { + "main": { + "type": "query", + "description": "Get a single xyz.statusphere.status record by AT URI", + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "AT URI of the record" + }, + "profiles": { + "type": "boolean", + "description": "Include profile + identity info keyed by DID" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uri", + "value", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "xyz.statusphere.status#main" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "time_us": { + "type": "integer" + }, + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } + } + } + }, + "profileEntry": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "handle": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "#appBskyActorProfile" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + } + } + }, + "appBskyActorProfile": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + } + } +} diff --git a/apps/atproto-starter/lexicons/generated/statusphere/app/status/listRecords.json b/apps/atproto-starter/lexicons/generated/statusphere/app/status/listRecords.json new file mode 100644 index 0000000..762f83f --- /dev/null +++ b/apps/atproto-starter/lexicons/generated/statusphere/app/status/listRecords.json @@ -0,0 +1,226 @@ +{ + "lexicon": 1, + "id": "statusphere.app.status.listRecords", + "defs": { + "main": { + "type": "query", + "description": "Query xyz.statusphere.status records with filters", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Filter by DID or handle (triggers on-demand backfill)" + }, + "profiles": { + "type": "boolean", + "description": "Include profile + identity info keyed by DID" + }, + "status": { + "type": "string", + "description": "Filter by status" + }, + "createdAtMin": { + "type": "string", + "description": "Minimum value for createdAt" + }, + "createdAtMax": { + "type": "string", + "description": "Maximum value for createdAt" + }, + "sort": { + "type": "string", + "knownValues": [ + "status", + "createdAt" + ], + "description": "Field to sort by (default: time_us)" + }, + "order": { + "type": "string", + "knownValues": [ + "asc", + "desc" + ], + "description": "Sort direction (default: desc for dates/numbers/counts, asc for strings)" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "records": { + "type": "array", + "items": { + "type": "ref", + "ref": "#record" + } + }, + "cursor": { + "type": "string" + }, + "profiles": { + "type": "array", + "items": { + "type": "ref", + "ref": "#profileEntry" + } + } + } + } + } + }, + "record": { + "type": "object", + "required": [ + "uri", + "cid", + "value", + "did", + "collection", + "rkey", + "time_us" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "xyz.statusphere.status#main" + }, + "did": { + "type": "string", + "format": "did" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + }, + "time_us": { + "type": "integer" + } + } + }, + "profileEntry": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "handle": { + "type": "string" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "value": { + "type": "ref", + "ref": "#appBskyActorProfile" + }, + "collection": { + "type": "string", + "format": "nsid" + }, + "rkey": { + "type": "string" + } + } + }, + "appBskyActorProfile": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + } + } +} diff --git a/apps/atproto-starter/lexicons/pulled/README.md b/apps/atproto-starter/lexicons/pulled/README.md new file mode 100644 index 0000000..812df07 --- /dev/null +++ b/apps/atproto-starter/lexicons/pulled/README.md @@ -0,0 +1,5 @@ +# lexicon sources + +this directory contains lexicon documents pulled from the following sources: + +- atproto (nsids: app.bsky.actor.profile, xyz.statusphere.status) diff --git a/apps/atproto-starter/lexicons/pulled/app/bsky/actor/profile.json b/apps/atproto-starter/lexicons/pulled/app/bsky/actor/profile.json new file mode 100644 index 0000000..1d22cc4 --- /dev/null +++ b/apps/atproto-starter/lexicons/pulled/app/bsky/actor/profile.json @@ -0,0 +1,67 @@ +{ + "id": "app.bsky.actor.profile", + "defs": { + "main": { + "key": "literal:self", + "type": "record", + "record": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": ["image/png", "image/jpeg"], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": ["image/png", "image/jpeg"], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": ["com.atproto.label.defs#selfLabels"], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + }, + "description": "A declaration of a Bluesky account profile." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/apps/atproto-starter/lexicons/pulled/xyz/statusphere/status.json b/apps/atproto-starter/lexicons/pulled/xyz/statusphere/status.json new file mode 100644 index 0000000..c5a314e --- /dev/null +++ b/apps/atproto-starter/lexicons/pulled/xyz/statusphere/status.json @@ -0,0 +1,27 @@ +{ + "id": "xyz.statusphere.status", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": ["status", "createdAt"], + "properties": { + "status": { + "type": "string", + "maxLength": 32, + "minLength": 1, + "maxGraphemes": 1 + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/apps/atproto-starter/package.json b/apps/atproto-starter/package.json new file mode 100644 index 0000000..150c246 --- /dev/null +++ b/apps/atproto-starter/package.json @@ -0,0 +1,71 @@ +{ + "name": "svelte-cloudflare-statusphere", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "contrail-lex generate && contrail-lex types && vite build && contrail append-scheduled", + "generate": "contrail-lex generate", + "generate:pull": "contrail-lex all", + "backfill": "contrail backfill", + "backfill:remote": "contrail backfill --remote", + "refresh": "contrail refresh", + "refresh:remote": "contrail refresh --remote", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint .", + "env:generate-key": "npx tsx src/lib/atproto/scripts/generate-key.ts", + "env:generate-secret": "npx tsx src/lib/atproto/scripts/generate-secret.ts", + "env:setup-dev": "npx tsx src/lib/atproto/scripts/setup-dev.ts", + "tunnel": "npx tsx src/lib/atproto/scripts/tunnel.ts" + }, + "devDependencies": { + "@atcute/atproto": "^3.1.10", + "@atcute/bluesky": "^3.3.0", + "@atcute/client": "^4.2.1", + "@atcute/identity-resolver": "^1.2.2", + "@atcute/lex-cli": "^2.5.3", + "@atcute/lexicon-doc": "^2.1.2", + "@atcute/lexicons": "^1.2.9", + "@atcute/oauth-node-client": "^1.1.0", + "@atcute/tid": "^1.1.2", + "@cloudflare/workers-types": "^4.20260317.1", + "@eslint/compat": "^2.0.3", + "@eslint/js": "^10.0.1", + "@sveltejs/adapter-cloudflare": "^7.2.8", + "@sveltejs/kit": "^2.55.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@tailwindcss/forms": "^0.5.11", + "@tailwindcss/vite": "^4.2.2", + "bits-ui": "^2.16.4", + "eslint": "^10.1.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.16.0", + "globals": "^17.4.0", + "prettier": "^3.8.1", + "prettier-plugin-svelte": "^3.5.1", + "prettier-plugin-tailwindcss": "^0.7.2", + "svelte": "^5.55.0", + "svelte-check": "^4.4.5", + "tailwindcss": "^4.2.2", + "tsx": "^4.21.0", + "typescript": "^6.0.2", + "typescript-eslint": "^8.57.2", + "vite": "^8.0.3", + "wrangler": "^4.78.0" + }, + "license": "MIT", + "dependencies": { + "@atcute/jetstream": "^1.1.2", + "@atmo-dev/contrail": "workspace:*", + "@atmo-dev/contrail-lexicons": "workspace:*", + "@foxui/core": "^0.9.1", + "@foxui/social": "^0.8.10", + "@foxui/time": "^0.8.5", + "valibot": "^1.3.1" + } +} diff --git a/apps/atproto-starter/src/app.css b/apps/atproto-starter/src/app.css new file mode 100644 index 0000000..6289b7f --- /dev/null +++ b/apps/atproto-starter/src/app.css @@ -0,0 +1,9 @@ +@import 'tailwindcss'; + +@plugin '@tailwindcss/forms'; + +@custom-variant dark (&:where(.dark, .dark *)); + +@import '@foxui/core/theme.css'; + +@source "../node_modules/@foxui"; diff --git a/apps/atproto-starter/src/app.d.ts b/apps/atproto-starter/src/app.d.ts new file mode 100644 index 0000000..831d778 --- /dev/null +++ b/apps/atproto-starter/src/app.d.ts @@ -0,0 +1,34 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +import type { OAuthSession } from '@atcute/oauth-node-client'; +import type { Client } from '@atcute/client'; +import type { Did } from '@atcute/lexicons'; + +declare global { + namespace App { + // interface Error {} + interface Locals { + session: OAuthSession | null; + client: Client | null; + did: Did | null; + } + // interface PageData {} + // interface PageState {} + interface Platform { + env: { + OAUTH_SESSIONS: KVNamespace; + OAUTH_STATES: KVNamespace; + CLIENT_ASSERTION_KEY: string; + COOKIE_SECRET: string; + OAUTH_PUBLIC_URL: string; + PROFILE_CACHE?: KVNamespace; + DB: D1Database; + CRON_SECRET: string; + }; + } + } +} +import type {} from '@atcute/atproto'; +import type {} from '@atcute/bluesky'; + +export {}; diff --git a/apps/atproto-starter/src/app.html b/apps/atproto-starter/src/app.html new file mode 100644 index 0000000..03c33ff --- /dev/null +++ b/apps/atproto-starter/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/atproto-starter/src/hooks.server.ts b/apps/atproto-starter/src/hooks.server.ts new file mode 100644 index 0000000..98b4fae --- /dev/null +++ b/apps/atproto-starter/src/hooks.server.ts @@ -0,0 +1,15 @@ +import type { Handle } from '@sveltejs/kit'; +import { restoreSession } from '$lib/atproto/server/session'; + +export const handle: Handle = async ({ event, resolve }) => { + const { session, client, did } = await restoreSession( + event.cookies, + event.platform?.env + ); + + event.locals.session = session; + event.locals.client = client; + event.locals.did = did; + + return resolve(event); +}; diff --git a/apps/atproto-starter/src/lib/atproto/auth.svelte.ts b/apps/atproto-starter/src/lib/atproto/auth.svelte.ts new file mode 100644 index 0000000..2859236 --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/auth.svelte.ts @@ -0,0 +1,75 @@ +import { AppBskyActorDefs } from '@atcute/bluesky'; +import type { ActorIdentifier, Did } from '@atcute/lexicons'; +import { page } from '$app/state'; +import { ALLOW_SIGNUP, REDIRECT_TO_LAST_PAGE_ON_LOGIN } from './settings'; + +export const user = { + get profile() { + return (page.data?.profile as AppBskyActorDefs.ProfileViewDetailed | null) ?? null; + }, + get isLoggedIn() { + return !!page.data?.did; + }, + get did() { + return (page.data?.did as Did | null) ?? null; + } +}; + +function saveReturnTo() { + if (REDIRECT_TO_LAST_PAGE_ON_LOGIN) { + document.cookie = `oauth_return_to=${encodeURIComponent(window.location.pathname + window.location.search)};path=/;max-age=600;samesite=lax`; + } +} + +export async function login(handle: string) { + if (handle.startsWith('did:')) { + if (handle.length < 6) throw new Error('DID must be at least 6 characters'); + } else if (handle.includes('.') && handle.length > 3) { + handle = (handle.startsWith('@') ? handle.slice(1) : handle) as ActorIdentifier; + if (handle.length < 4) throw new Error('Handle must be at least 4 characters'); + } else if (handle.length > 3) { + handle = ((handle.startsWith('@') ? handle.slice(1) : handle) + + '.bsky.social') as ActorIdentifier; + } else { + throw new Error('Please provide a valid handle or DID.'); + } + + const { oauthLogin } = await import('./server/oauth.remote'); + const { url } = await oauthLogin({ handle }); + saveReturnTo(); + window.location.assign(url); + + // Wait for navigation (prevents UI flash) + await new Promise((_resolve, reject) => { + window.addEventListener('pageshow', () => reject(new Error('user aborted the login request')), { + once: true + }); + }); +} + +export async function signup() { + if (!ALLOW_SIGNUP) throw new Error('Signup is not enabled'); + + const { oauthLogin } = await import('./server/oauth.remote'); + const { url } = await oauthLogin({ signup: true }); + saveReturnTo(); + window.location.assign(url); + + await new Promise((_resolve, reject) => { + window.addEventListener('pageshow', () => reject(new Error('user aborted the signup request')), { + once: true + }); + }); +} + +export async function logout() { + try { + const { oauthLogout } = await import('./server/oauth.remote'); + await oauthLogout(); + } catch (e) { + console.error('Error logging out:', e); + } + + // Full reload to clear server session state + window.location.href = '/'; +} diff --git a/apps/atproto-starter/src/lib/atproto/image-helper.ts b/apps/atproto-starter/src/lib/atproto/image-helper.ts new file mode 100644 index 0000000..08ddf93 --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/image-helper.ts @@ -0,0 +1,152 @@ +import { getCDNImageBlobUrl, uploadBlob } from './methods'; + +export function compressImage( + file: File | Blob, + maxSize: number = 900 * 1024, + maxDimension: number = 2048 +): Promise<{ + blob: Blob; + aspectRatio: { + width: number; + height: number; + }; +}> { + return new Promise((resolve, reject) => { + const img = new Image(); + const reader = new FileReader(); + + reader.onload = (e) => { + if (!e.target?.result) { + return reject(new Error('Failed to read file.')); + } + img.src = e.target.result as string; + }; + + reader.onerror = (err) => reject(err); + reader.readAsDataURL(file); + + img.onload = () => { + let width = img.width; + let height = img.height; + + // If image is already small enough, return original + if (file.size <= maxSize) { + console.log('skipping compression+resizing, already small enough'); + return resolve({ + blob: file, + aspectRatio: { + width, + height + } + }); + } + + if (width > maxDimension || height > maxDimension) { + if (width > height) { + height = Math.round((maxDimension / width) * height); + width = maxDimension; + } else { + width = Math.round((maxDimension / height) * width); + height = maxDimension; + } + } + + // Create a canvas to draw the image + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (!ctx) return reject(new Error('Failed to get canvas context.')); + ctx.drawImage(img, 0, 0, width, height); + + // Use WebP for both compression and transparency support + let quality = 0.9; + + function attemptCompression() { + canvas.toBlob( + (blob) => { + if (!blob) { + return reject(new Error('Compression failed.')); + } + if (blob.size <= maxSize || quality < 0.3) { + resolve({ + blob, + aspectRatio: { + width, + height + } + }); + } else { + quality -= 0.1; + attemptCompression(); + } + }, + 'image/webp', + quality + ); + } + + attemptCompression(); + }; + + img.onerror = (err) => reject(err); + }); +} + +export async function checkAndUploadImage( + recordWithImage: Record, + key: string = 'image', + // e.g. /api/image-proxy?url= + imageProxy?: string +) { + if (!recordWithImage[key]) return; + + // Already uploaded as blob + if (typeof recordWithImage[key] === 'object' && recordWithImage[key].$type === 'blob') { + return; + } + + if (typeof recordWithImage[key] === 'string' && imageProxy) { + const proxyUrl = imageProxy + encodeURIComponent(recordWithImage[key]); + const response = await fetch(proxyUrl); + if (!response.ok) { + throw Error('failed to get image from image proxy'); + } + + const blob = await response.blob(); + const { blob: compressed, aspectRatio } = await compressImage(blob); + + recordWithImage[key] = await uploadBlob({ blob: compressed, aspectRatio }); + + return; + } + + if (recordWithImage[key]?.blob) { + if (recordWithImage[key].objectUrl) { + URL.revokeObjectURL(recordWithImage[key].objectUrl); + } + const { blob: compressed, aspectRatio } = await compressImage(recordWithImage[key].blob); + recordWithImage[key] = await uploadBlob({ blob: compressed, aspectRatio }); + } +} + +export function getImageFromRecord( + recordWithImage: Record | undefined, + did: string, + key: string = 'image' +): string | undefined { + if (!recordWithImage?.[key]) return; + + if (typeof recordWithImage[key] === 'object' && recordWithImage[key].$type === 'blob') { + return getCDNImageBlobUrl({ did, blob: recordWithImage[key] }); + } + + if (recordWithImage[key].objectUrl) return recordWithImage[key].objectUrl; + + if (recordWithImage[key].blob) { + recordWithImage[key].objectUrl = URL.createObjectURL(recordWithImage[key].blob); + return recordWithImage[key].objectUrl; + } + + return recordWithImage[key]; +} diff --git a/apps/atproto-starter/src/lib/atproto/index.ts b/apps/atproto-starter/src/lib/atproto/index.ts new file mode 100644 index 0000000..8d3232a --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/index.ts @@ -0,0 +1,20 @@ +export { user, login, signup, logout } from './auth.svelte'; + +export { + parseUri, + resolveHandle, + actorToDid, + getPDS, + getDetailedProfile, + getClient, + listRecords, + getRecord, + putRecord, + deleteRecord, + uploadBlob, + describeRepo, + getBlobURL, + getCDNImageBlobUrl, + searchActorsTypeahead, + createTID +} from './methods'; diff --git a/apps/atproto-starter/src/lib/atproto/methods.ts b/apps/atproto-starter/src/lib/atproto/methods.ts new file mode 100644 index 0000000..4a84fd5 --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/methods.ts @@ -0,0 +1,373 @@ +import { parseResourceUri, type Did, type Handle } from '@atcute/lexicons'; +import { isDid } from '@atcute/lexicons/syntax'; +import { user } from './auth.svelte'; +import { DOH_RESOLVER, type AllowedCollection } from './settings'; +import { + CompositeDidDocumentResolver, + CompositeHandleResolver, + DohJsonHandleResolver, + PlcDidDocumentResolver, + WebDidDocumentResolver, + WellKnownHandleResolver +} from '@atcute/identity-resolver'; +import { Client, simpleFetchHandler } from '@atcute/client'; +import { type AppBskyActorDefs } from '@atcute/bluesky'; + +export type Collection = `${string}.${string}.${string}`; +import * as TID from '@atcute/tid'; + +/** + * Parses an AT Protocol URI into its components. + */ +export function parseUri(uri: string) { + const parts = parseResourceUri(uri); + if (!parts.ok) return; + return parts.value; +} + +/** + * Resolves a handle to a DID using DNS and HTTP methods. + */ +export async function resolveHandle({ handle }: { handle: Handle }) { + const handleResolver = new CompositeHandleResolver({ + methods: { + dns: new DohJsonHandleResolver({ dohUrl: DOH_RESOLVER }), + http: new WellKnownHandleResolver() + } + }); + + const data = await handleResolver.resolve(handle); + return data; +} + +/** + * Returns a DID given a handle or DID string. + */ +export async function actorToDid(actor: string): Promise { + if (isDid(actor)) return actor; + return await resolveHandle({ handle: actor as Handle }); +} + +const didResolver = new CompositeDidDocumentResolver({ + methods: { + plc: new PlcDidDocumentResolver(), + web: new WebDidDocumentResolver() + } +}); + +/** + * Gets the PDS (Personal Data Server) URL for a given DID. + */ +export async function getPDS(did: Did) { + const doc = await didResolver.resolve(did as Did<'plc'> | Did<'web'>); + if (!doc.service) throw new Error('No PDS found'); + for (const service of doc.service) { + if (service.id === '#atproto_pds') { + return service.serviceEndpoint.toString(); + } + } +} + +/** + * Fetches a detailed Bluesky profile for a user. + */ +export async function getDetailedProfile(data?: { did?: Did; client?: Client }) { + data ??= {}; + data.did ??= user.did ?? undefined; + + if (!data.did) throw new Error('Error getting detailed profile: no did'); + + data.client ??= new Client({ + handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) + }); + + const response = await data.client.get('app.bsky.actor.getProfile', { + params: { actor: data.did } + }); + + if (!response.ok) return; + + return response.data; +} + +/** + * Creates an AT Protocol client for a user's PDS. + */ +export async function getClient({ did }: { did: Did }) { + const pds = await getPDS(did); + if (!pds) throw new Error('PDS not found'); + + const client = new Client({ + handler: simpleFetchHandler({ service: pds }) + }); + + return client; +} + +/** + * Lists records from a repository collection with pagination support. + */ +export async function listRecords({ + did, + collection, + cursor, + limit = 100, + client +}: { + did?: Did; + collection: `${string}.${string}.${string}`; + cursor?: string; + limit?: number; + client?: Client; +}) { + did ??= user.did ?? undefined; + if (!collection) { + throw new Error('Missing parameters for listRecords'); + } + if (!did) { + throw new Error('Missing did for listRecords'); + } + + client ??= await getClient({ did }); + + const allRecords = []; + + let currentCursor = cursor; + do { + const response = await client.get('com.atproto.repo.listRecords', { + params: { + repo: did, + collection, + limit: !limit || limit > 100 ? 100 : limit, + cursor: currentCursor + } + }); + + if (!response.ok) { + return allRecords; + } + + allRecords.push(...response.data.records); + currentCursor = response.data.cursor; + } while (currentCursor && (!limit || allRecords.length < limit)); + + return allRecords; +} + +/** + * Fetches a single record from a repository. + */ +export async function getRecord({ + did, + collection, + rkey = 'self', + client +}: { + did?: Did; + collection: Collection; + rkey?: string; + client?: Client; +}) { + did ??= user.did ?? undefined; + + if (!collection) { + throw new Error('Missing parameters for getRecord'); + } + if (!did) { + throw new Error('Missing did for getRecord'); + } + + client ??= await getClient({ did }); + + const record = await client.get('com.atproto.repo.getRecord', { + params: { + repo: did, + collection, + rkey + } + }); + + return JSON.parse(JSON.stringify(record.data)); +} + +/** + * Creates or updates a record via remote function. + */ +export async function putRecord({ + collection, + rkey = 'self', + record +}: { + collection: AllowedCollection; + rkey?: string; + record: Record; +}) { + if (!user.did) throw new Error('Not logged in'); + + const { putRecord: putRecordRemote } = await import('./server/repo.remote'); + const data = await putRecordRemote({ collection, rkey, record }); + return { ok: true, data }; +} + +/** + * Deletes a record via remote function. + */ +export async function deleteRecord({ + collection, + rkey = 'self' +}: { + collection: AllowedCollection; + rkey: string; +}) { + if (!user.did) throw new Error('Not logged in'); + + const { deleteRecord: deleteRecordRemote } = await import('./server/repo.remote'); + const data = await deleteRecordRemote({ collection, rkey }); + return data.ok; +} + +/** + * Gets the dimensions of an image blob. + */ +function getImageDimensions(blob: Blob): Promise<{ width: number; height: number }> { + return new Promise((resolve, reject) => { + const img = new Image(); + const url = URL.createObjectURL(blob); + img.onload = () => { + URL.revokeObjectURL(url); + resolve({ width: img.naturalWidth, height: img.naturalHeight }); + }; + img.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error('Failed to load image for dimensions')); + }; + img.src = url; + }); +} + +/** + * Uploads a blob via remote function. + * Converts the Blob to a byte array for serialization across the remote boundary. + * For image blobs, automatically includes aspectRatio with width and height. + */ +export async function uploadBlob({ + blob, + aspectRatio +}: { + blob: Blob; + aspectRatio?: { width: number; height: number }; +}) { + if (!user.did) throw new Error("Can't upload blob: Not logged in"); + + // Auto-detect dimensions for image blobs if not provided + if (!aspectRatio && blob.type.startsWith('image/')) { + try { + aspectRatio = await getImageDimensions(blob); + } catch { + // Non-critical — proceed without aspectRatio + } + } + + const arrayBuffer = await blob.arrayBuffer(); + const bytes = Array.from(new Uint8Array(arrayBuffer)); + + const { uploadBlob: uploadBlobRemote } = await import('./server/repo.remote'); + const result = await uploadBlobRemote({ bytes, mimeType: blob.type || 'application/octet-stream' }); + + if (aspectRatio) { + return { ...result, aspectRatio }; + } + return result; +} + +/** + * Gets metadata about a repository. + */ +export async function describeRepo({ client, did }: { client?: Client; did?: Did }) { + did ??= user.did ?? undefined; + if (!did) { + throw new Error('Error describeRepo: No did'); + } + client ??= await getClient({ did }); + + const repo = await client.get('com.atproto.repo.describeRepo', { + params: { + repo: did + } + }); + if (!repo.ok) return; + + return repo.data; +} + +/** + * Constructs a URL to fetch a blob directly from a user's PDS. + */ +export async function getBlobURL({ + did, + blob +}: { + did: Did; + blob: { + $type: 'blob'; + ref: { + $link: string; + }; + }; +}) { + const pds = await getPDS(did); + return `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${blob.ref.$link}`; +} + +/** + * Constructs a Bluesky CDN URL for an image blob. + */ +export function getCDNImageBlobUrl({ + did, + blob +}: { + did?: string; + blob: { + $type: 'blob'; + ref: { + $link: string; + }; + }; +}) { + did ??= user.did ?? undefined; + + return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${blob.ref.$link}@webp`; +} + +/** + * Searches for actors with typeahead/autocomplete functionality. + */ +export async function searchActorsTypeahead( + q: string, + limit: number = 10, + host?: string +): Promise<{ actors: AppBskyActorDefs.ProfileViewBasic[]; q: string }> { + host ??= 'https://public.api.bsky.app'; + + const client = new Client({ + handler: simpleFetchHandler({ service: host }) + }); + + const response = await client.get('app.bsky.actor.searchActorsTypeahead', { + params: { + q, + limit + } + }); + + if (!response.ok) return { actors: [], q }; + + return { actors: response.data.actors, q }; +} + +/** + * Return a TID based on current time + */ +export function createTID() { + return TID.now(); +} diff --git a/apps/atproto-starter/src/lib/atproto/port.ts b/apps/atproto-starter/src/lib/atproto/port.ts new file mode 100644 index 0000000..4d6686b --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/port.ts @@ -0,0 +1,3 @@ +// Dev server port — generated by setup-dev, shared by vite, oauth, and tunnel. +// Each project gets a unique random port (5200–7200) so multiple projects can run simultaneously. +export const DEV_PORT = 5291; diff --git a/apps/atproto-starter/src/lib/atproto/scripts/generate-key.ts b/apps/atproto-starter/src/lib/atproto/scripts/generate-key.ts new file mode 100644 index 0000000..3d1cc8a --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/scripts/generate-key.ts @@ -0,0 +1,4 @@ +import { generateClientAssertionKey } from '@atcute/oauth-node-client'; + +const key = await generateClientAssertionKey('main-key'); +console.log(JSON.stringify(key)); diff --git a/apps/atproto-starter/src/lib/atproto/scripts/generate-secret.ts b/apps/atproto-starter/src/lib/atproto/scripts/generate-secret.ts new file mode 100644 index 0000000..9092066 --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/scripts/generate-secret.ts @@ -0,0 +1,3 @@ +import { randomBytes } from 'node:crypto'; + +console.log(randomBytes(32).toString('base64url')); diff --git a/apps/atproto-starter/src/lib/atproto/scripts/setup-dev.ts b/apps/atproto-starter/src/lib/atproto/scripts/setup-dev.ts new file mode 100644 index 0000000..bdc8ccf --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/scripts/setup-dev.ts @@ -0,0 +1,60 @@ +import { existsSync } from 'node:fs'; +import { copyFile, readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { randomBytes, randomInt } from 'node:crypto'; + +import { generateClientAssertionKey } from '@atcute/oauth-node-client'; + +const cwd = process.cwd(); +const examplePath = resolve(cwd, '.env.example'); +const envPath = resolve(cwd, '.env'); + +if (!existsSync(envPath)) { + if (!existsSync(examplePath)) { + throw new Error(`missing .env.example (expected at ${examplePath})`); + } + await copyFile(examplePath, envPath); + console.log(`created ${envPath}`); +} + +const upsertVar = (input: string, key: string, value: string): string => { + const line = `${key}=${value}`; + const re = new RegExp(`^${key}=.*$`, 'm'); + + if (re.test(input)) { + const match = input.match(re); + const current = match ? match[0].slice(key.length + 1).trim() : ''; + // Only overwrite if empty/placeholder + if (current === '' || current === "''" || current === '""' || current.includes('...')) { + return input.replace(re, line); + } + return input; + } + + const suffix = input.endsWith('\n') || input.length === 0 ? '' : '\n'; + return `${input}${suffix}${line}\n`; +}; + +let vars = await readFile(envPath, 'utf8'); + +const secret = randomBytes(32).toString('base64url'); +vars = upsertVar(vars, 'COOKIE_SECRET', secret); + +const jwk = await generateClientAssertionKey('main-key'); +vars = upsertVar(vars, 'CLIENT_ASSERTION_KEY', JSON.stringify(jwk)); + +await writeFile(envPath, vars); +console.log(`updated ${envPath}`); + +// Generate a random dev port (5200–7200) so multiple projects can run simultaneously +const portPath = resolve(cwd, 'src/lib/atproto/port.ts'); +const portFile = await readFile(portPath, 'utf8'); +const currentPort = portFile.match(/DEV_PORT\s*=\s*(\d+)/); +if (currentPort && parseInt(currentPort[1]) === 5183) { + const port = randomInt(5200, 7201); + const updated = portFile.replace(/DEV_PORT\s*=\s*\d+/, `DEV_PORT = ${port}`); + await writeFile(portPath, updated); + console.log(`set DEV_PORT to ${port} in port.ts`); +} else { + console.log(`DEV_PORT already customized (${currentPort?.[1]}), skipping`); +} diff --git a/apps/atproto-starter/src/lib/atproto/scripts/tunnel.ts b/apps/atproto-starter/src/lib/atproto/scripts/tunnel.ts new file mode 100644 index 0000000..9c37d5f --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/scripts/tunnel.ts @@ -0,0 +1,195 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { spawn } from 'node:child_process'; +import { DEV_PORT } from '../port'; + +const cwd = process.cwd(); +const envPath = resolve(cwd, '.env'); +const vitePath = resolve(cwd, 'vite.config.ts'); + +let tunnelUrl: string | null = null; +let statusBarActive = false; + +// ── ANSI status bar ────────────────────────────────────────────── +// Reserves the bottom row of the terminal for a persistent status line. +// Logs scroll in the region above it. + +function getColumns(): number { + return process.stdout.columns || 80; +} + +function getRows(): number { + return process.stdout.rows || 24; +} + +function setupScrollRegion(): void { + if (!process.stdout.isTTY) return; + statusBarActive = true; + const rows = getRows(); + // Set scroll region to all rows except the last + process.stdout.write(`\x1b[1;${rows - 1}r`); + // Move cursor into scroll region + process.stdout.write(`\x1b[${rows - 1};1H`); +} + +function drawStatusBar(text: string): void { + if (!process.stdout.isTTY) { + process.stdout.write(text + '\n'); + return; + } + const rows = getRows(); + const cols = getColumns(); + // Save cursor, move to bottom row, clear it, write status, restore cursor + process.stdout.write('\x1b7'); + process.stdout.write(`\x1b[${rows};1H`); + process.stdout.write('\x1b[2K'); + // Inverse video for the bar + process.stdout.write(`\x1b[7m ${text.padEnd(cols - 1)}\x1b[0m`); + process.stdout.write('\x1b8'); +} + +function clearStatusBar(): void { + if (!process.stdout.isTTY || !statusBarActive) return; + const rows = getRows(); + // Reset scroll region to full terminal + process.stdout.write(`\x1b[1;${rows}r`); + // Clear the bottom row + process.stdout.write(`\x1b[${rows};1H\x1b[2K`); + // Move cursor up + process.stdout.write(`\x1b[${rows - 1};1H`); + statusBarActive = false; +} + +function writeLog(text: string): void { + if (statusBarActive && process.stdout.isTTY) { + // Write inside the scroll region, which auto-scrolls above the bar + process.stdout.write(text); + } else { + process.stdout.write(text); + } +} + +// Redraw on terminal resize +process.stdout.on('resize', () => { + if (!statusBarActive || !tunnelUrl) return; + setupScrollRegion(); + drawStatusBar(`Tunnel: ${tunnelUrl} | Ctrl+C to stop`); +}); + +// ── .env helpers ───────────────────────────────────────────────── + +function readEnv(): string { + return readFileSync(envPath, 'utf8'); +} + +function writeEnv(content: string): void { + writeFileSync(envPath, content); +} + +function setEnvVar(key: string, value: string): void { + let env = readEnv(); + const re = new RegExp(`^(#\\s*)?${key}=.*$`, 'm'); + const line = `${key}=${value}`; + + if (re.test(env)) { + env = env.replace(re, line); + } else { + env = env.trimEnd() + '\n' + line + '\n'; + } + writeEnv(env); +} + +function clearEnvVar(key: string): void { + let env = readEnv(); + const re = new RegExp(`^${key}=.*$`, 'm'); + + if (re.test(env)) { + env = env.replace(re, `# ${key}=`); + writeEnv(env); + } +} + +// ── vite config helpers ────────────────────────────────────────── + +function setViteAllowedHosts(hostname: string): void { + let vite = readFileSync(vitePath, 'utf8'); + + if (/allowedHosts\s*:/.test(vite)) { + vite = vite.replace(/allowedHosts\s*:\s*\[.*?\]/s, `allowedHosts: ['${hostname}']`); + } else if (/server\s*:\s*\{/.test(vite)) { + vite = vite.replace(/server\s*:\s*\{/, `server: {\n\t\tallowedHosts: ['${hostname}'],`); + } + + writeFileSync(vitePath, vite); +} + +function clearViteAllowedHosts(): void { + let vite = readFileSync(vitePath, 'utf8'); + + if (/allowedHosts\s*:/.test(vite)) { + vite = vite.replace(/allowedHosts\s*:\s*\[.*?\]/s, 'allowedHosts: []'); + } + + writeFileSync(vitePath, vite); +} + +// ── cleanup ────────────────────────────────────────────────────── + +function cleanup(): void { + clearStatusBar(); + console.log('\nCleaning up...'); + if (tunnelUrl) { + clearEnvVar('OAUTH_PUBLIC_URL'); + console.log(' Cleared OAUTH_PUBLIC_URL from .env'); + clearViteAllowedHosts(); + console.log(' Cleared allowedHosts from vite.config.ts'); + } +} + +// ── main ───────────────────────────────────────────────────────── + +const child = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${DEV_PORT}`], { + stdio: ['ignore', 'pipe', 'pipe'] +}); + +child.stderr.on('data', (data: Buffer) => { + const output = data.toString(); + + if (!tunnelUrl) { + const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/); + if (match) { + tunnelUrl = match[0]; + const hostname = new URL(tunnelUrl).hostname; + + setEnvVar('OAUTH_PUBLIC_URL', tunnelUrl); + setViteAllowedHosts(hostname); + + writeLog(`\n Set OAUTH_PUBLIC_URL=${tunnelUrl}\n`); + writeLog(` Set vite allowedHosts to [${hostname}]\n`); + writeLog(` Tunnel is ready! Restart your dev server to pick up the new URL.\n\n`); + + setupScrollRegion(); + drawStatusBar(`Tunnel: ${tunnelUrl} | Ctrl+C to stop`); + return; + } + } + + writeLog(output); +}); + +child.stdout.on('data', (data: Buffer) => { + writeLog(data.toString()); +}); + +child.on('close', (code) => { + cleanup(); + process.exit(code ?? 0); +}); + +process.on('SIGINT', () => { + child.kill('SIGINT'); +}); + +process.on('SIGTERM', () => { + child.kill('SIGTERM'); +}); diff --git a/apps/atproto-starter/src/lib/atproto/server/kv-store.ts b/apps/atproto-starter/src/lib/atproto/server/kv-store.ts new file mode 100644 index 0000000..35c3dcb --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/server/kv-store.ts @@ -0,0 +1,38 @@ +import type { Store } from '@atcute/oauth-node-client'; + +export class KVStore implements Store { + private kv: KVNamespace; + private expirationTtl?: number; + + constructor(kv: KVNamespace, options?: { expirationTtl?: number }) { + this.kv = kv; + this.expirationTtl = options?.expirationTtl; + } + + async get(key: K): Promise { + const value = await this.kv.get(key, 'text'); + if (value === null) return undefined; + return JSON.parse(value) as V; + } + + async set(key: K, value: V): Promise { + await this.kv.put(key, JSON.stringify(value), { + expirationTtl: this.expirationTtl + }); + } + + async delete(key: K): Promise { + await this.kv.delete(key); + } + + async clear(): Promise { + let cursor: string | undefined; + do { + const result = await this.kv.list({ cursor }); + for (const key of result.keys) { + await this.kv.delete(key.name); + } + cursor = result.list_complete ? undefined : result.cursor; + } while (cursor); + } +} diff --git a/apps/atproto-starter/src/lib/atproto/server/oauth.remote.ts b/apps/atproto-starter/src/lib/atproto/server/oauth.remote.ts new file mode 100644 index 0000000..f087d53 --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/server/oauth.remote.ts @@ -0,0 +1,56 @@ +import * as v from 'valibot'; +import { error } from '@sveltejs/kit'; +import { command, getRequestEvent } from '$app/server'; +import { createOAuthClient } from './oauth'; +import { getSignedCookie } from './signed-cookie'; +import { scopes, signUpPDS } from '../settings'; +import type { ActorIdentifier, Did } from '@atcute/lexicons'; + +export const oauthLogin = command( + v.object({ + handle: v.optional(v.pipe(v.string(), v.minLength(3))), + signup: v.optional(v.boolean()) + }), + async (input) => { + const { platform } = getRequestEvent(); + + try { + const oauth = createOAuthClient(platform?.env); + + const target = input.signup + ? ({ type: 'pds', serviceUrl: signUpPDS } as const) + : ({ type: 'account', identifier: input.handle as ActorIdentifier } as const); + + const { url } = await oauth.authorize({ + target, + scope: scopes.join(' '), + prompt: input.signup ? 'create' : undefined + }); + + return { url: url.toString() }; + } catch (e) { + if (e && typeof e === 'object' && 'status' in e) throw e; // re-throw SvelteKit errors + const message = e instanceof Error ? e.message : 'Login failed'; + error(400, message); + } + } +); + +export const oauthLogout = command(async () => { + const { cookies, platform } = getRequestEvent(); + const did = getSignedCookie(cookies, 'did') as Did | null; + + if (did) { + try { + const oauth = createOAuthClient(platform?.env); + await oauth.revoke(did); + } catch (e) { + console.error('Error revoking session:', e); + } + } + + cookies.delete('did', { path: '/' }); + cookies.delete('scope', { path: '/' }); + + return { ok: true }; +}); diff --git a/apps/atproto-starter/src/lib/atproto/server/oauth.ts b/apps/atproto-starter/src/lib/atproto/server/oauth.ts new file mode 100644 index 0000000..e0d0f95 --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/server/oauth.ts @@ -0,0 +1,97 @@ +import { + OAuthClient, + MemoryStore, + type ClientAssertionPrivateJwk, + type OAuthClientStores, + type OAuthSession, + type StoredSession, + type StoredState +} from '@atcute/oauth-node-client'; +import type { Did } from '@atcute/lexicons'; +import { + CompositeDidDocumentResolver, + CompositeHandleResolver, + DohJsonHandleResolver, + LocalActorResolver, + PlcDidDocumentResolver, + WebDidDocumentResolver, + WellKnownHandleResolver +} from '@atcute/identity-resolver'; +import { KVStore } from './kv-store'; +import { DOH_RESOLVER, REDIRECT_PATH, scopes } from '../settings'; +import { DEV_PORT } from '../port'; +import { dev } from '$app/environment'; + +function createActorResolver() { + return new LocalActorResolver({ + handleResolver: new CompositeHandleResolver({ + methods: { + dns: new DohJsonHandleResolver({ dohUrl: DOH_RESOLVER }), + http: new WellKnownHandleResolver() + } + }), + didDocumentResolver: new CompositeDidDocumentResolver({ + methods: { + plc: new PlcDidDocumentResolver(), + web: new WebDidDocumentResolver() + } + }) + }); +} + +function createStores(env?: App.Platform['env']): OAuthClientStores { + if (env?.OAUTH_SESSIONS && env?.OAUTH_STATES) { + return { + sessions: new KVStore(env.OAUTH_SESSIONS), + states: new KVStore(env.OAUTH_STATES, { expirationTtl: 600 }) + }; + } + // Fallback to in-memory stores (dev without wrangler) + return { + sessions: new MemoryStore(), + states: new MemoryStore({ ttl: 600_000 }) + }; +} + +export function createOAuthClient(env?: App.Platform['env']): OAuthClient { + const actorResolver = createActorResolver(); + const stores = createStores(env); + + if (dev && !env?.OAUTH_PUBLIC_URL) { + // Dev without tunnel: loopback public client (no keyset). + // Omit client_id — the library builds it automatically from redirect_uris + scope. + // redirect_uris must use 127.0.0.1 (not localhost). + return new OAuthClient({ + metadata: { + redirect_uris: [`http://127.0.0.1:${DEV_PORT}${REDIRECT_PATH}`], + scope: scopes + }, + actorResolver, + stores + }); + } + + // Confidential client (production, or dev with tunnel via OAUTH_PUBLIC_URL) + if (!env?.OAUTH_PUBLIC_URL) { + throw new Error('OAUTH_PUBLIC_URL is not set'); + } + if (!env.CLIENT_ASSERTION_KEY) { + throw new Error('CLIENT_ASSERTION_KEY secret is not set. Run: pnpm env:generate-key'); + } + const site = env.OAUTH_PUBLIC_URL; + const key: ClientAssertionPrivateJwk = JSON.parse(env.CLIENT_ASSERTION_KEY); + + return new OAuthClient({ + metadata: { + client_id: site + '/oauth-client-metadata.json', + redirect_uris: [site + REDIRECT_PATH], + scope: scopes, + jwks_uri: site + '/oauth/jwks.json' + }, + keyset: [key], + actorResolver, + stores + }); +} + +export type { OAuthSession }; diff --git a/apps/atproto-starter/src/lib/atproto/server/profile.ts b/apps/atproto-starter/src/lib/atproto/server/profile.ts new file mode 100644 index 0000000..20521fc --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/server/profile.ts @@ -0,0 +1,51 @@ +import type { Did } from '@atcute/lexicons'; +import { getDetailedProfile, describeRepo } from '../methods'; + +const PROFILE_CACHE_TTL = 60 * 60; // 1 hour + +/** + * Loads a user's profile, with optional KV caching. + * Falls back to a fresh fetch if the cache KV doesn't exist or on cache miss. + * Returns undefined if the profile can't be loaded. + */ +export async function loadProfile(did: Did, profileCache?: KVNamespace) { + // Try cache first + if (profileCache) { + try { + const cached = await profileCache.get(did, 'json'); + if (cached) return cached as Record; + } catch { + // Cache read failed, continue to fresh fetch + } + } + + const profile = await fetchProfile(did); + + // Write to cache (fire-and-forget) + if (profileCache && profile) { + profileCache + .put(did, JSON.stringify(profile), { expirationTtl: PROFILE_CACHE_TTL }) + .catch(() => {}); + } + + return profile; +} + +async function fetchProfile(did: Did) { + try { + let profile = await getDetailedProfile({ did }); + + if (!profile || profile.handle === 'handle.invalid') { + const repo = await describeRepo({ did }); + profile = { + did, + handle: repo?.handle || 'handle.invalid' + } as typeof profile; + } + + return profile; + } catch (e) { + console.error('Failed to load profile:', e); + return undefined; + } +} diff --git a/apps/atproto-starter/src/lib/atproto/server/repo.remote.ts b/apps/atproto-starter/src/lib/atproto/server/repo.remote.ts new file mode 100644 index 0000000..fc7f4d8 --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/server/repo.remote.ts @@ -0,0 +1,96 @@ +import { error } from '@sveltejs/kit'; +import { command, getRequestEvent } from '$app/server'; +import * as v from 'valibot'; +import { collections } from '../settings'; +import { contrail, ensureInit } from '$lib/contrail'; + +// Validate collection format and check against allowed list from settings +const collectionSchema = v.pipe( + v.string(), + v.regex(/^[a-zA-Z][a-zA-Z0-9-]*(\.[a-zA-Z][a-zA-Z0-9-]*){2,}$/), + v.check((c) => collections.includes(c as (typeof collections)[number]), 'Collection not in allowed list') +); + +// AT Protocol rkey: TID, 'self', or other valid record keys (alphanumeric, dash, underscore, dot) +const rkeySchema = v.optional(v.pipe(v.string(), v.regex(/^[a-zA-Z0-9._:~-]{1,512}$/))); + +export const putRecord = command( + v.object({ + collection: collectionSchema, + rkey: rkeySchema, + record: v.record(v.string(), v.unknown()) + }), + async (input) => { + const { locals } = getRequestEvent(); + if (!locals.client || !locals.did) error(401, 'Not authenticated'); + + const response = await locals.client.post('com.atproto.repo.putRecord', { + input: { + collection: input.collection as `${string}.${string}.${string}`, + repo: locals.did, + rkey: input.rkey || 'self', + record: input.record + } + }); + + if (!response.ok) error(502, 'putRecord failed'); + + // Immediately index the new/updated record in contrail + const { platform } = getRequestEvent(); + const db = platform?.env?.DB; + if (db) { + await ensureInit(db); + await contrail.notify(response.data.uri, db).catch(() => {}); + } + + return response.data; + } +); + +export const deleteRecord = command( + v.object({ + collection: collectionSchema, + rkey: rkeySchema + }), + async (input) => { + const { locals } = getRequestEvent(); + if (!locals.client || !locals.did) error(401, 'Not authenticated'); + + const response = await locals.client.post('com.atproto.repo.deleteRecord', { + input: { + collection: input.collection as `${string}.${string}.${string}`, + repo: locals.did, + rkey: input.rkey || 'self' + } + }); + + return { ok: response.ok }; + } +); + +export const uploadBlob = command( + v.object({ + bytes: v.array(v.number()), + mimeType: v.string() + }), + async (input) => { + const { locals } = getRequestEvent(); + if (!locals.client || !locals.did) error(401, 'Not authenticated'); + + const blob = new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }); + + const response = await locals.client.post('com.atproto.repo.uploadBlob', { + params: { repo: locals.did }, + input: blob + }); + + if (!response.ok) error(500, 'Upload failed'); + + return response.data.blob as { + $type: 'blob'; + ref: { $link: string }; + mimeType: string; + size: number; + }; + } +); diff --git a/apps/atproto-starter/src/lib/atproto/server/session.ts b/apps/atproto-starter/src/lib/atproto/server/session.ts new file mode 100644 index 0000000..2c1fe8f --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/server/session.ts @@ -0,0 +1,73 @@ +import type { Cookies } from '@sveltejs/kit'; +import { Client } from '@atcute/client'; +import type { Did } from '@atcute/lexicons'; +import { + type OAuthSession, + TokenInvalidError, + TokenRefreshError, + TokenRevokedError, + AuthMethodUnsatisfiableError +} from '@atcute/oauth-node-client'; +import { createOAuthClient } from './oauth'; +import { getSignedCookie } from './signed-cookie'; +import { scopes } from '../settings'; + +export type SessionLocals = { + session: OAuthSession | null; + client: Client | null; + did: Did | null; +}; + +/** + * Restores an OAuth session from the signed `did` cookie. + * Returns session locals to be assigned to `event.locals`. + * Deletes the cookie only if the session is genuinely unrecoverable. + * Transient failures (network, KV) preserve the cookie for retry. + */ +export async function restoreSession( + cookies: Cookies, + env?: App.Platform['env'] +): Promise { + const did = getSignedCookie(cookies, 'did') as Did | null; + + if (!did) { + return { session: null, client: null, did: null }; + } + + // If permissions changed since login, invalidate the session + const savedScope = getSignedCookie(cookies, 'scope'); + if (savedScope !== null && savedScope !== scopes.join(' ')) { + cookies.delete('did', { path: '/' }); + cookies.delete('scope', { path: '/' }); + return { session: null, client: null, did: null }; + } + + try { + const oauth = createOAuthClient(env); + const session = await oauth.restore(did); + + return { + session, + client: new Client({ handler: session }), + did + }; + } catch (e) { + console.error('Failed to restore session:', e); + + // Only delete cookies when the session is genuinely unrecoverable. + // Transient errors (network issues, KV hiccups) should preserve the + // cookie so the next request can retry without forcing a full re-login. + const isSessionGone = + e instanceof TokenInvalidError || + e instanceof TokenRevokedError || + e instanceof TokenRefreshError || + e instanceof AuthMethodUnsatisfiableError; + + if (isSessionGone) { + cookies.delete('did', { path: '/' }); + cookies.delete('scope', { path: '/' }); + } + + return { session: null, client: null, did: null }; + } +} diff --git a/apps/atproto-starter/src/lib/atproto/server/signed-cookie.ts b/apps/atproto-starter/src/lib/atproto/server/signed-cookie.ts new file mode 100644 index 0000000..6cde19c --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/server/signed-cookie.ts @@ -0,0 +1,69 @@ +import { createHmac, timingSafeEqual } from 'node:crypto'; + +import type { Cookies } from '@sveltejs/kit'; + +import { env } from '$env/dynamic/private'; +import { dev } from '$app/environment'; + +const SEPARATOR = '.'; + +function getSecret(): string { + const secret = env.COOKIE_SECRET; + if (secret) return secret; + if (dev) return 'dev-cookie-secret-not-for-production'; + throw new Error('COOKIE_SECRET is not set'); +} + +function toBase64Url(bytes: Uint8Array): string { + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +function fromBase64Url(str: string): Uint8Array { + const padded = str + '='.repeat((4 - (str.length % 4)) % 4); + const base64 = padded.replace(/-/g, '+').replace(/_/g, '/'); + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return bytes; +} + +function hmacSha256(data: string): Uint8Array { + return createHmac('sha256', getSecret()).update(data).digest(); +} + +export function getSignedCookie(cookies: Cookies, name: string): string | null { + const signed = cookies.get(name); + if (!signed) return null; + + const idx = signed.lastIndexOf(SEPARATOR); + if (idx === -1) return null; + + const value = signed.slice(0, idx); + const sig = signed.slice(idx + 1); + + let expected: Uint8Array; + let got: Uint8Array; + try { + expected = hmacSha256(value); + got = fromBase64Url(sig); + } catch { + return null; + } + + if (got.length !== expected.length || !timingSafeEqual(got, expected)) return null; + + return value; +} + +export function setSignedCookie( + cookies: Cookies, + name: string, + value: string, + options: Parameters[2] +): void { + const sig = toBase64Url(hmacSha256(value)); + const signed = `${value}${SEPARATOR}${sig}`; + cookies.set(name, signed, options); +} diff --git a/apps/atproto-starter/src/lib/atproto/settings.ts b/apps/atproto-starter/src/lib/atproto/settings.ts new file mode 100644 index 0000000..b187107 --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/settings.ts @@ -0,0 +1,30 @@ +import { dev } from '$app/environment'; +import { scope } from '@atcute/oauth-node-client'; + +// writable collections +export const collections = ['xyz.statusphere.status', 'social.atmo.test.blob'] as const; + +export type AllowedCollection = (typeof collections)[number]; + +// OAuth scope — add scope.blob(), scope.rpc(), etc. as needed +export const scopes = [ + 'atproto', + scope.repo({ collection: [...collections] }), + scope.blob({ accept: ['image/*'] }) +]; + +// set to false to disable signup +export const ALLOW_SIGNUP = true; + +// which PDS to use for signup (change to your preferred PDS) +const devPDS = 'https://pds.rip/'; +const prodPDS = 'https://selfhosted.social/'; +export const signUpPDS = dev ? devPDS : prodPDS; + +// where to redirect after oauth login/signup +export const REDIRECT_PATH = '/oauth/callback'; + +// redirect the user back to the page they were on before login +export const REDIRECT_TO_LAST_PAGE_ON_LOGIN = true; + +export const DOH_RESOLVER = 'https://mozilla.cloudflare-dns.com/dns-query'; diff --git a/apps/atproto-starter/src/lib/atproto/ui/LoginModal.svelte b/apps/atproto-starter/src/lib/atproto/ui/LoginModal.svelte new file mode 100644 index 0000000..b3bad3a --- /dev/null +++ b/apps/atproto-starter/src/lib/atproto/ui/LoginModal.svelte @@ -0,0 +1,22 @@ + + + + + { + await login(handle); + return true; + }} + signup={async () => { + signup(); + return true; + }} +/> diff --git a/apps/atproto-starter/src/lib/contrail.config.ts b/apps/atproto-starter/src/lib/contrail.config.ts new file mode 100644 index 0000000..b3aa09a --- /dev/null +++ b/apps/atproto-starter/src/lib/contrail.config.ts @@ -0,0 +1,14 @@ +import type { ContrailConfig } from '@atmo-dev/contrail'; + +export const config: ContrailConfig = { + namespace: 'statusphere.app', + collections: { + status: { + collection: 'xyz.statusphere.status', + queryable: { + status: {}, + createdAt: { type: 'range' } + } + } + } +}; diff --git a/apps/atproto-starter/src/lib/contrail/client.ts b/apps/atproto-starter/src/lib/contrail/client.ts new file mode 100644 index 0000000..13f8f51 --- /dev/null +++ b/apps/atproto-starter/src/lib/contrail/client.ts @@ -0,0 +1,32 @@ +import { Client, simpleFetchHandler } from '@atcute/client'; + +export interface Profile { + handle: string; + displayName?: string; + avatar?: string; +} + +/** + * Extract a simple profile from a contrail profile entry. + * Contrail returns { did, handle, value: { displayName, avatar, ... } } + * while components expect { handle, displayName?, avatar? }. + */ +export function extractProfile(entry: { + did: string; + handle?: string; + value?: unknown; +}): Profile { + const value = entry.value as { displayName?: string; avatar?: string } | undefined; + return { + handle: entry.handle ?? entry.did, + displayName: value?.displayName, + avatar: value?.avatar + }; +} + +/** + * Client-side: fully typed @atcute/client that queries the app's own /xrpc/ endpoints. + */ +export function getClient() { + return new Client({ handler: simpleFetchHandler({ service: '' }) }); +} diff --git a/apps/atproto-starter/src/lib/contrail/index.ts b/apps/atproto-starter/src/lib/contrail/index.ts new file mode 100644 index 0000000..29f4474 --- /dev/null +++ b/apps/atproto-starter/src/lib/contrail/index.ts @@ -0,0 +1,29 @@ +import { Contrail } from '@atmo-dev/contrail'; +import { createHandler, createServerClient } from '@atmo-dev/contrail/server'; +import type { Client } from '@atcute/client'; +import { config } from '../contrail.config'; + +export const contrail = new Contrail(config); + +let initialized = false; + +export async function ensureInit(db: D1Database) { + if (!initialized) { + await contrail.init(db); + initialized = true; + } +} + +const handle = createHandler(contrail); + +/** + * Typed `@atcute/client` that calls contrail in-process. Pass `did` to act + * as that user (server-side principal via WeakMap marker — no JWT, no PDS + * roundtrip). Omit for anonymous calls against public endpoints. + */ +export function getServerClient(db: D1Database, did?: string): Client { + return createServerClient(async (req) => { + await ensureInit(db); + return handle(req, db) as Promise; + }, did); +} diff --git a/apps/atproto-starter/src/lib/emojis/icons.json b/apps/atproto-starter/src/lib/emojis/icons.json new file mode 100644 index 0000000..6266e50 --- /dev/null +++ b/apps/atproto-starter/src/lib/emojis/icons.json @@ -0,0 +1,7255 @@ +{ + "host": "", + "asset_url_pattern": "", + "families": ["Animated Emoji"], + "icons": [ + { + "name": "emoji_u1f600", + "version": 1, + "popularity": 720, + "codepoint": "1f600", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smile:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f603", + "version": 1, + "popularity": 719, + "codepoint": "1f603", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smile-with-big-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f604", + "version": 1, + "popularity": 718, + "codepoint": "1f604", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":grin:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f601", + "version": 1, + "popularity": 717, + "codepoint": "1f601", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":grinning:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f606", + "version": 1, + "popularity": 716, + "codepoint": "1f606", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":laughing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f605", + "version": 1, + "popularity": 715, + "codepoint": "1f605", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":grin-sweat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f602", + "version": 1, + "popularity": 714, + "codepoint": "1f602", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":joy:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f923", + "version": 1, + "popularity": 713, + "codepoint": "1f923", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rofl:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62d", + "version": 1, + "popularity": 712, + "codepoint": "1f62d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":loudly-crying:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f609", + "version": 1, + "popularity": 711, + "codepoint": "1f609", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wink:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f617", + "version": 1, + "popularity": 710, + "codepoint": "1f617", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kissing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f619", + "version": 1, + "popularity": 709, + "codepoint": "1f619", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kissing-smiling-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61a", + "version": 1, + "popularity": 708, + "codepoint": "1f61a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kissing-closed-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f618", + "version": 1, + "popularity": 707, + "codepoint": "1f618", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kissing-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f970", + "version": 1, + "popularity": 706, + "codepoint": "1f970", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-face:", ":3-hearts:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60d", + "version": 1, + "popularity": 705, + "codepoint": "1f60d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f929", + "version": 1, + "popularity": 704, + "codepoint": "1f929", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":star-struck:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f973", + "version": 1, + "popularity": 703, + "codepoint": "1f973", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":partying-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae0", + "version": 1, + "popularity": 702, + "codepoint": "1fae0", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":melting:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f643", + "version": 1, + "popularity": 701, + "codepoint": "1f643", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":upside-down-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f642", + "version": 1, + "popularity": 700, + "codepoint": "1f642", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":slightly-happy:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f972", + "version": 1, + "popularity": 699, + "codepoint": "1f972", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":happy-cry:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f979", + "version": 1, + "popularity": 698, + "codepoint": "1f979", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":holding-back-tears:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60a", + "version": 1, + "popularity": 697, + "codepoint": "1f60a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":blush:"], + "sizes_px": [] + }, + { + "name": "emoji_u263a_fe0f", + "version": 1, + "popularity": 696, + "codepoint": "263a_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":warm-smile:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60c", + "version": 1, + "popularity": 695, + "codepoint": "1f60c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":relieved:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f642_200d_2195_fe0f", + "version": 1, + "popularity": 694, + "codepoint": "1f642_200d_2195_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":head-nod:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f642_200d_2194_fe0f", + "version": 1, + "popularity": 693, + "codepoint": "1f642_200d_2194_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":head-shake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60f", + "version": 1, + "popularity": 692, + "codepoint": "1f60f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smirk:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f924", + "version": 1, + "popularity": 691, + "codepoint": "1f924", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":drool:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60b", + "version": 1, + "popularity": 690, + "codepoint": "1f60b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":yum:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61b", + "version": 1, + "popularity": 689, + "codepoint": "1f61b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":stuck-out-tongue:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61d", + "version": 1, + "popularity": 688, + "codepoint": "1f61d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":squinting-tongue:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61c", + "version": 1, + "popularity": 687, + "codepoint": "1f61c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":winky-tongue:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92a", + "version": 1, + "popularity": 686, + "codepoint": "1f92a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":zany-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f974", + "version": 1, + "popularity": 685, + "codepoint": "1f974", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":woozy:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f614", + "version": 1, + "popularity": 684, + "codepoint": "1f614", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pensive:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f97a", + "version": 1, + "popularity": 683, + "codepoint": "1f97a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pleading:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62c", + "version": 1, + "popularity": 682, + "codepoint": "1f62c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":grimacing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f611", + "version": 1, + "popularity": 681, + "codepoint": "1f611", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":expressionless:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f610", + "version": 1, + "popularity": 680, + "codepoint": "1f610", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":neutral-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f636", + "version": 1, + "popularity": 679, + "codepoint": "1f636", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":mouth-none:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f636_200d_1f32b_fe0f", + "version": 1, + "popularity": 678, + "codepoint": "1f636_200d_1f32b_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":face-in-clouds:", ":lost:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae5", + "version": 1, + "popularity": 677, + "codepoint": "1fae5", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":dotted-line-face:", ":invisible:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f910", + "version": 1, + "popularity": 676, + "codepoint": "1f910", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":zipper-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae1", + "version": 1, + "popularity": 675, + "codepoint": "1fae1", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":salute:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f914", + "version": 1, + "popularity": 674, + "codepoint": "1f914", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thinking-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92b", + "version": 1, + "popularity": 673, + "codepoint": "1f92b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":shushing-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae2", + "version": 1, + "popularity": 672, + "codepoint": "1fae2", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-over-mouth:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92d", + "version": 1, + "popularity": 671, + "codepoint": "1f92d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smiling-eyes-with-hand-over-mouth:", ":chuckling:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f971", + "version": 1, + "popularity": 670, + "codepoint": "1f971", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":yawn:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f917", + "version": 1, + "popularity": 669, + "codepoint": "1f917", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hug-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae3", + "version": 1, + "popularity": 668, + "codepoint": "1fae3", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":peeking:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f631", + "version": 1, + "popularity": 667, + "codepoint": "1f631", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":screaming:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f928", + "version": 1, + "popularity": 666, + "codepoint": "1f928", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-eyebrow:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9d0", + "version": 1, + "popularity": 665, + "codepoint": "1f9d0", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":monocle:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f612", + "version": 1, + "popularity": 664, + "codepoint": "1f612", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":unamused:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f644", + "version": 1, + "popularity": 663, + "codepoint": "1f644", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rolling-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62e_200d_1f4a8", + "version": 1, + "popularity": 662, + "codepoint": "1f62e_200d_1f4a8", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":exhale:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f624", + "version": 1, + "popularity": 661, + "codepoint": "1f624", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":triumph:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f620", + "version": 1, + "popularity": 660, + "codepoint": "1f620", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":angry:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f621", + "version": 1, + "popularity": 659, + "codepoint": "1f621", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rage:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92c", + "version": 1, + "popularity": 658, + "codepoint": "1f92c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":cursing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61e", + "version": 1, + "popularity": 657, + "codepoint": "1f61e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sad:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f613", + "version": 1, + "popularity": 656, + "codepoint": "1f613", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sweat:", ":downcast:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f61f", + "version": 1, + "popularity": 655, + "codepoint": "1f61f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":worried:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f625", + "version": 1, + "popularity": 654, + "codepoint": "1f625", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":concerned:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f622", + "version": 1, + "popularity": 653, + "codepoint": "1f622", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":cry:"], + "sizes_px": [] + }, + { + "name": "emoji_u2639_fe0f", + "version": 1, + "popularity": 652, + "codepoint": "2639_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":big-frown:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f641", + "version": 1, + "popularity": 651, + "codepoint": "1f641", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":frown:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae4", + "version": 1, + "popularity": 650, + "codepoint": "1fae4", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":diagonal-mouth:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f615", + "version": 1, + "popularity": 649, + "codepoint": "1f615", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":slightly-frowning:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f630", + "version": 1, + "popularity": 648, + "codepoint": "1f630", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":anxious-with-sweat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f628", + "version": 1, + "popularity": 647, + "codepoint": "1f628", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":scared:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f627", + "version": 1, + "popularity": 646, + "codepoint": "1f627", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":anguished:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f626", + "version": 1, + "popularity": 645, + "codepoint": "1f626", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":gasp:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62e", + "version": 1, + "popularity": 644, + "codepoint": "1f62e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":mouth-open:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62f", + "version": 1, + "popularity": 643, + "codepoint": "1f62f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":surprised:", ":hushed:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f632", + "version": 1, + "popularity": 642, + "codepoint": "1f632", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":astonished:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f633", + "version": 1, + "popularity": 641, + "codepoint": "1f633", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":flushed:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92f", + "version": 1, + "popularity": 640, + "codepoint": "1f92f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":mind-blown:", ":exploding-head:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f616", + "version": 1, + "popularity": 639, + "codepoint": "1f616", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":scrunched-mouth:", ":confounded:", ":zigzag-mouth:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f623", + "version": 1, + "popularity": 638, + "codepoint": "1f623", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":scrunched-eyes:", ":persevering:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f629", + "version": 1, + "popularity": 637, + "codepoint": "1f629", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":weary:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62b", + "version": 1, + "popularity": 636, + "codepoint": "1f62b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":distraught:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f635", + "version": 1, + "popularity": 635, + "codepoint": "1f635", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":x-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f635_200d_1f4ab", + "version": 1, + "popularity": 634, + "codepoint": "1f635_200d_1f4ab", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":dizzy-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae8", + "version": 1, + "popularity": 633, + "codepoint": "1fae8", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":shaking-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f976", + "version": 1, + "popularity": 632, + "codepoint": "1f976", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":cold-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f975", + "version": 1, + "popularity": 631, + "codepoint": "1f975", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hot-face:", ":sweat-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f922", + "version": 1, + "popularity": 630, + "codepoint": "1f922", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sick:", ":nauseated:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f92e", + "version": 1, + "popularity": 629, + "codepoint": "1f92e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vomit:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae9", + "version": 1, + "popularity": 628, + "codepoint": "1fae9", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":tired:", ":bags-under-eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f634", + "version": 1, + "popularity": 627, + "codepoint": "1f634", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sleep:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f62a", + "version": 1, + "popularity": 626, + "codepoint": "1f62a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sleepy:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f927", + "version": 1, + "popularity": 625, + "codepoint": "1f927", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sneeze:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f912", + "version": 1, + "popularity": 624, + "codepoint": "1f912", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thermometer-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f915", + "version": 1, + "popularity": 623, + "codepoint": "1f915", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":bandage-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f637", + "version": 1, + "popularity": 622, + "codepoint": "1f637", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":mask:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f925", + "version": 1, + "popularity": 621, + "codepoint": "1f925", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":liar:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f607", + "version": 1, + "popularity": 620, + "codepoint": "1f607", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":halo:", ":innocent:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f920", + "version": 1, + "popularity": 619, + "codepoint": "1f920", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":cowboy:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f911", + "version": 1, + "popularity": 618, + "codepoint": "1f911", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":money-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f913", + "version": 1, + "popularity": 617, + "codepoint": "1f913", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nerd-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f60e", + "version": 1, + "popularity": 616, + "codepoint": "1f60e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sunglasses-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f978", + "version": 1, + "popularity": 615, + "codepoint": "1f978", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":disguise:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f921", + "version": 1, + "popularity": 614, + "codepoint": "1f921", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clown:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4a9", + "version": 1, + "popularity": 613, + "codepoint": "1f4a9", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":poop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f608", + "version": 1, + "popularity": 612, + "codepoint": "1f608", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":imp-smile:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f47f", + "version": 1, + "popularity": 611, + "codepoint": "1f47f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":imp-frown:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f47b", + "version": 1, + "popularity": 610, + "codepoint": "1f47b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ghost:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f480", + "version": 1, + "popularity": 609, + "codepoint": "1f480", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":skull:"], + "sizes_px": [] + }, + { + "name": "emoji_u2603_fe0f", + "version": 1, + "popularity": 608, + "codepoint": "2603_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":snowman-with-snow:"], + "sizes_px": [] + }, + { + "name": "emoji_u26c4", + "version": 1, + "popularity": 607, + "codepoint": "26c4", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":snowman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f383", + "version": 1, + "popularity": 606, + "codepoint": "1f383", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":jack-o-lantern:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f916", + "version": 1, + "popularity": 605, + "codepoint": "1f916", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":robot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f47d", + "version": 1, + "popularity": 604, + "codepoint": "1f47d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":alien:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f47e", + "version": 1, + "popularity": 603, + "codepoint": "1f47e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":alien-monster:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f31e", + "version": 1, + "popularity": 602, + "codepoint": "1f31e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sun-with-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f31b", + "version": 1, + "popularity": 601, + "codepoint": "1f31b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":moon-face-first-quarter:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f31c", + "version": 1, + "popularity": 600, + "codepoint": "1f31c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":moon-face-last-quarter:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63a", + "version": 1, + "popularity": 599, + "codepoint": "1f63a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smiley-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f638", + "version": 1, + "popularity": 598, + "codepoint": "1f638", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smile-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f639", + "version": 1, + "popularity": 597, + "codepoint": "1f639", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":joy-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63b", + "version": 1, + "popularity": 596, + "codepoint": "1f63b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-eyes-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63c", + "version": 1, + "popularity": 595, + "codepoint": "1f63c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":smirk-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63d", + "version": 1, + "popularity": 594, + "codepoint": "1f63d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kissing-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f640", + "version": 1, + "popularity": 593, + "codepoint": "1f640", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":scream-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63f", + "version": 1, + "popularity": 592, + "codepoint": "1f63f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crying-cat-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f63e", + "version": 1, + "popularity": 591, + "codepoint": "1f63e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pouting-cat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f648", + "version": 1, + "popularity": 590, + "codepoint": "1f648", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":see-no-evil-monkey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f649", + "version": 1, + "popularity": 589, + "codepoint": "1f649", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hear-no-evil-monkey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64a", + "version": 1, + "popularity": 588, + "codepoint": "1f64a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":speak-no-evil-monkey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f31f", + "version": 1, + "popularity": 587, + "codepoint": "1f31f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":glowing-star:"], + "sizes_px": [] + }, + { + "name": "emoji_u2728", + "version": 1, + "popularity": 586, + "codepoint": "2728", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sparkles:"], + "sizes_px": [] + }, + { + "name": "emoji_u26a1", + "version": 1, + "popularity": 585, + "codepoint": "26a1", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":electricity:", ":zap:", ":lightning:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4a5", + "version": 1, + "popularity": 584, + "codepoint": "1f4a5", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":collision:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f525", + "version": 1, + "popularity": 583, + "codepoint": "1f525", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fire:", ":burn:", ":lit:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4af", + "version": 1, + "popularity": 582, + "codepoint": "1f4af", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":100:", ":one-hundred:", ":hundred:", ":points:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f389", + "version": 1, + "popularity": 581, + "codepoint": "1f389", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":party-popper:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f38a", + "version": 1, + "popularity": 580, + "codepoint": "1f38a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":confetti-ball:"], + "sizes_px": [] + }, + { + "name": "emoji_u2764_fe0f", + "version": 1, + "popularity": 579, + "codepoint": "2764_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":red-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9e1", + "version": 1, + "popularity": 578, + "codepoint": "1f9e1", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":orange-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49b", + "version": 1, + "popularity": 577, + "codepoint": "1f49b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":yellow-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49a", + "version": 1, + "popularity": 576, + "codepoint": "1f49a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":green-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa75", + "version": 1, + "popularity": 575, + "codepoint": "1fa75", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":light-blue-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f499", + "version": 1, + "popularity": 574, + "codepoint": "1f499", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":blue-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49c", + "version": 1, + "popularity": 573, + "codepoint": "1f49c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":purple-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90e", + "version": 1, + "popularity": 572, + "codepoint": "1f90e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":brown-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f5a4", + "version": 1, + "popularity": 571, + "codepoint": "1f5a4", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":black-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa76", + "version": 1, + "popularity": 570, + "codepoint": "1fa76", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":grey-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90d", + "version": 1, + "popularity": 569, + "codepoint": "1f90d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":white-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa77", + "version": 1, + "popularity": 568, + "codepoint": "1fa77", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pink-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f498", + "version": 1, + "popularity": 567, + "codepoint": "1f498", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":cupid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49d", + "version": 1, + "popularity": 566, + "codepoint": "1f49d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":gift-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f496", + "version": 1, + "popularity": 565, + "codepoint": "1f496", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":sparkling-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f497", + "version": 1, + "popularity": 564, + "codepoint": "1f497", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-grow:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f493", + "version": 1, + "popularity": 563, + "codepoint": "1f493", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":beating-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49e", + "version": 1, + "popularity": 562, + "codepoint": "1f49e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":revolving-hearts:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f495", + "version": 1, + "popularity": 561, + "codepoint": "1f495", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":two-hearts:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f48c", + "version": 1, + "popularity": 560, + "codepoint": "1f48c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-letter:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f49f", + "version": 1, + "popularity": 559, + "codepoint": "1f49f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-box:"], + "sizes_px": [] + }, + { + "name": "emoji_u2763_fe0f", + "version": 1, + "popularity": 558, + "codepoint": "2763_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-exclamation-point:"], + "sizes_px": [] + }, + { + "name": "emoji_u2764_fe0f_200d_1fa79", + "version": 1, + "popularity": 557, + "codepoint": "2764_fe0f_200d_1fa79", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":bandaged-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f494", + "version": 1, + "popularity": 556, + "codepoint": "1f494", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":broken-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u2764_fe0f_200d_1f525", + "version": 1, + "popularity": 555, + "codepoint": "2764_fe0f_200d_1f525", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fire-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f48b", + "version": 1, + "popularity": 554, + "codepoint": "1f48b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":kiss:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f463", + "version": 1, + "popularity": 553, + "codepoint": "1f463", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":footprints:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fac6", + "version": 1, + "popularity": 552, + "codepoint": "1fac6", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fingerprint:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fac0", + "version": 1, + "popularity": 551, + "codepoint": "1fac0", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":anatomical-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa78", + "version": 1, + "popularity": 550, + "codepoint": "1fa78", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":blood:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9a0", + "version": 1, + "popularity": 549, + "codepoint": "1f9a0", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":microbe:", ":virus:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f440", + "version": 1, + "popularity": 548, + "codepoint": "1f440", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":eyes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f441_fe0f", + "version": 1, + "popularity": 547, + "codepoint": "1f441_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":eye:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae6", + "version": 1, + "popularity": 546, + "codepoint": "1fae6", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":biting-lip:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443", + "version": 1, + "popularity": 545, + "codepoint": "1f443", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443_1f3fb", + "version": 1, + "popularity": 544, + "codepoint": "1f443_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443_1f3fc", + "version": 1, + "popularity": 543, + "codepoint": "1f443_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443_1f3fd", + "version": 1, + "popularity": 542, + "codepoint": "1f443_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443_1f3fe", + "version": 1, + "popularity": 541, + "codepoint": "1f443_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f443_1f3ff", + "version": 1, + "popularity": 540, + "codepoint": "1f443_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442", + "version": 1, + "popularity": 539, + "codepoint": "1f442", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442_1f3fb", + "version": 1, + "popularity": 538, + "codepoint": "1f442_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442_1f3fc", + "version": 1, + "popularity": 537, + "codepoint": "1f442_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442_1f3fd", + "version": 1, + "popularity": 536, + "codepoint": "1f442_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442_1f3fe", + "version": 1, + "popularity": 535, + "codepoint": "1f442_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f442_1f3ff", + "version": 1, + "popularity": 534, + "codepoint": "1f442_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ear:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb", + "version": 1, + "popularity": 533, + "codepoint": "1f9bb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb_1f3fb", + "version": 1, + "popularity": 532, + "codepoint": "1f9bb_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb_1f3fc", + "version": 1, + "popularity": 531, + "codepoint": "1f9bb_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb_1f3fd", + "version": 1, + "popularity": 530, + "codepoint": "1f9bb_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb_1f3fe", + "version": 1, + "popularity": 529, + "codepoint": "1f9bb_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bb_1f3ff", + "version": 1, + "popularity": 528, + "codepoint": "1f9bb_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hearing-aid:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6", + "version": 1, + "popularity": 527, + "codepoint": "1f9b6", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6_1f3fb", + "version": 1, + "popularity": 526, + "codepoint": "1f9b6_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6_1f3fc", + "version": 1, + "popularity": 525, + "codepoint": "1f9b6_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6_1f3fd", + "version": 1, + "popularity": 524, + "codepoint": "1f9b6_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6_1f3fe", + "version": 1, + "popularity": 523, + "codepoint": "1f9b6_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b6_1f3ff", + "version": 1, + "popularity": 522, + "codepoint": "1f9b6_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":foot:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5", + "version": 1, + "popularity": 521, + "codepoint": "1f9b5", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5_1f3fb", + "version": 1, + "popularity": 520, + "codepoint": "1f9b5_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5_1f3fc", + "version": 1, + "popularity": 519, + "codepoint": "1f9b5_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5_1f3fd", + "version": 1, + "popularity": 518, + "codepoint": "1f9b5_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5_1f3fe", + "version": 1, + "popularity": 517, + "codepoint": "1f9b5_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9b5_1f3ff", + "version": 1, + "popularity": 516, + "codepoint": "1f9b5_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9bf", + "version": 1, + "popularity": 515, + "codepoint": "1f9bf", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leg-mechanical:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9be", + "version": 1, + "popularity": 514, + "codepoint": "1f9be", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":arm-mechanical:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa", + "version": 1, + "popularity": 513, + "codepoint": "1f4aa", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa_1f3fb", + "version": 1, + "popularity": 512, + "codepoint": "1f4aa_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa_1f3fc", + "version": 1, + "popularity": 511, + "codepoint": "1f4aa_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa_1f3fd", + "version": 1, + "popularity": 510, + "codepoint": "1f4aa_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa_1f3fe", + "version": 1, + "popularity": 509, + "codepoint": "1f4aa_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4aa_1f3ff", + "version": 1, + "popularity": 508, + "codepoint": "1f4aa_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":muscle:", ":flex:", ":bicep:", ":strong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f", + "version": 1, + "popularity": 507, + "codepoint": "1f44f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f_1f3fb", + "version": 1, + "popularity": 506, + "codepoint": "1f44f_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f_1f3fc", + "version": 1, + "popularity": 505, + "codepoint": "1f44f_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f_1f3fd", + "version": 1, + "popularity": 504, + "codepoint": "1f44f_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f_1f3fe", + "version": 1, + "popularity": 503, + "codepoint": "1f44f_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44f_1f3ff", + "version": 1, + "popularity": 502, + "codepoint": "1f44f_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":clap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d", + "version": 1, + "popularity": 501, + "codepoint": "1f44d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d_1f3fb", + "version": 1, + "popularity": 500, + "codepoint": "1f44d_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d_1f3fc", + "version": 1, + "popularity": 499, + "codepoint": "1f44d_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d_1f3fd", + "version": 1, + "popularity": 498, + "codepoint": "1f44d_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d_1f3fe", + "version": 1, + "popularity": 497, + "codepoint": "1f44d_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44d_1f3ff", + "version": 1, + "popularity": 496, + "codepoint": "1f44d_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-up:", ":+1:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e", + "version": 1, + "popularity": 495, + "codepoint": "1f44e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e_1f3fb", + "version": 1, + "popularity": 494, + "codepoint": "1f44e_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e_1f3fc", + "version": 1, + "popularity": 493, + "codepoint": "1f44e_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e_1f3fd", + "version": 1, + "popularity": 492, + "codepoint": "1f44e_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e_1f3fe", + "version": 1, + "popularity": 491, + "codepoint": "1f44e_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44e_1f3ff", + "version": 1, + "popularity": 490, + "codepoint": "1f44e_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":thumbs-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6", + "version": 1, + "popularity": 489, + "codepoint": "1faf6", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6_1f3fb", + "version": 1, + "popularity": 488, + "codepoint": "1faf6_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6_1f3fc", + "version": 1, + "popularity": 487, + "codepoint": "1faf6_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6_1f3fd", + "version": 1, + "popularity": 486, + "codepoint": "1faf6_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6_1f3fe", + "version": 1, + "popularity": 485, + "codepoint": "1faf6_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf6_1f3ff", + "version": 1, + "popularity": 484, + "codepoint": "1faf6_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":heart-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c", + "version": 1, + "popularity": 483, + "codepoint": "1f64c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c_1f3fb", + "version": 1, + "popularity": 482, + "codepoint": "1f64c_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c_1f3fc", + "version": 1, + "popularity": 481, + "codepoint": "1f64c_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c_1f3fd", + "version": 1, + "popularity": 480, + "codepoint": "1f64c_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c_1f3fe", + "version": 1, + "popularity": 479, + "codepoint": "1f64c_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64c_1f3ff", + "version": 1, + "popularity": 478, + "codepoint": "1f64c_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raising-hands:", ":hooray:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450", + "version": 1, + "popularity": 477, + "codepoint": "1f450", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450_1f3fb", + "version": 1, + "popularity": 476, + "codepoint": "1f450_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450_1f3fc", + "version": 1, + "popularity": 475, + "codepoint": "1f450_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450_1f3fd", + "version": 1, + "popularity": 474, + "codepoint": "1f450_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450_1f3fe", + "version": 1, + "popularity": 473, + "codepoint": "1f450_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f450_1f3ff", + "version": 1, + "popularity": 472, + "codepoint": "1f450_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":open-hands:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932", + "version": 1, + "popularity": 471, + "codepoint": "1f932", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932_1f3fb", + "version": 1, + "popularity": 470, + "codepoint": "1f932_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932_1f3fc", + "version": 1, + "popularity": 469, + "codepoint": "1f932_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932_1f3fd", + "version": 1, + "popularity": 468, + "codepoint": "1f932_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932_1f3fe", + "version": 1, + "popularity": 467, + "codepoint": "1f932_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f932_1f3ff", + "version": 1, + "popularity": 466, + "codepoint": "1f932_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palms-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c", + "version": 1, + "popularity": 465, + "codepoint": "1f91c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c_1f3fb", + "version": 1, + "popularity": 464, + "codepoint": "1f91c_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c_1f3fc", + "version": 1, + "popularity": 463, + "codepoint": "1f91c_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c_1f3fd", + "version": 1, + "popularity": 462, + "codepoint": "1f91c_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c_1f3fe", + "version": 1, + "popularity": 461, + "codepoint": "1f91c_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91c_1f3ff", + "version": 1, + "popularity": 460, + "codepoint": "1f91c_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b", + "version": 1, + "popularity": 459, + "codepoint": "1f91b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b_1f3fb", + "version": 1, + "popularity": 458, + "codepoint": "1f91b_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b_1f3fc", + "version": 1, + "popularity": 457, + "codepoint": "1f91b_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b_1f3fd", + "version": 1, + "popularity": 456, + "codepoint": "1f91b_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b_1f3fe", + "version": 1, + "popularity": 455, + "codepoint": "1f91b_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91b_1f3ff", + "version": 1, + "popularity": 454, + "codepoint": "1f91b_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a", + "version": 1, + "popularity": 453, + "codepoint": "270a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a_1f3fb", + "version": 1, + "popularity": 452, + "codepoint": "270a_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a_1f3fc", + "version": 1, + "popularity": 451, + "codepoint": "270a_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a_1f3fd", + "version": 1, + "popularity": 450, + "codepoint": "270a_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a_1f3fe", + "version": 1, + "popularity": 449, + "codepoint": "270a_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u270a_1f3ff", + "version": 1, + "popularity": 448, + "codepoint": "270a_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-fist:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a", + "version": 1, + "popularity": 447, + "codepoint": "1f44a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a_1f3fb", + "version": 1, + "popularity": 446, + "codepoint": "1f44a_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a_1f3fc", + "version": 1, + "popularity": 445, + "codepoint": "1f44a_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a_1f3fd", + "version": 1, + "popularity": 444, + "codepoint": "1f44a_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a_1f3fe", + "version": 1, + "popularity": 443, + "codepoint": "1f44a_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44a_1f3ff", + "version": 1, + "popularity": 442, + "codepoint": "1f44a_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":fist:", ":bump:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3", + "version": 1, + "popularity": 441, + "codepoint": "1faf3", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3_1f3fb", + "version": 1, + "popularity": 440, + "codepoint": "1faf3_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3_1f3fc", + "version": 1, + "popularity": 439, + "codepoint": "1faf3_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3_1f3fd", + "version": 1, + "popularity": 438, + "codepoint": "1faf3_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3_1f3fe", + "version": 1, + "popularity": 437, + "codepoint": "1faf3_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf3_1f3ff", + "version": 1, + "popularity": 436, + "codepoint": "1faf3_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-down:", ":drop:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4", + "version": 1, + "popularity": 435, + "codepoint": "1faf4", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4_1f3fb", + "version": 1, + "popularity": 434, + "codepoint": "1faf4_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4_1f3fc", + "version": 1, + "popularity": 433, + "codepoint": "1faf4_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4_1f3fd", + "version": 1, + "popularity": 432, + "codepoint": "1faf4_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4_1f3fe", + "version": 1, + "popularity": 431, + "codepoint": "1faf4_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf4_1f3ff", + "version": 1, + "popularity": 430, + "codepoint": "1faf4_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm-up:", ":throw:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1", + "version": 1, + "popularity": 429, + "codepoint": "1faf1", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fb", + "version": 1, + "popularity": 428, + "codepoint": "1faf1_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fc", + "version": 1, + "popularity": 427, + "codepoint": "1faf1_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fd", + "version": 1, + "popularity": 426, + "codepoint": "1faf1_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fe", + "version": 1, + "popularity": 425, + "codepoint": "1faf1_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3ff", + "version": 1, + "popularity": 424, + "codepoint": "1faf1_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":rightwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2", + "version": 1, + "popularity": 423, + "codepoint": "1faf2", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2_1f3fb", + "version": 1, + "popularity": 422, + "codepoint": "1faf2_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2_1f3fc", + "version": 1, + "popularity": 421, + "codepoint": "1faf2_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2_1f3fd", + "version": 1, + "popularity": 420, + "codepoint": "1faf2_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2_1f3fe", + "version": 1, + "popularity": 419, + "codepoint": "1faf2_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf2_1f3ff", + "version": 1, + "popularity": 418, + "codepoint": "1faf2_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":leftwards-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8", + "version": 1, + "popularity": 417, + "codepoint": "1faf8", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8_1f3fb", + "version": 1, + "popularity": 416, + "codepoint": "1faf8_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8_1f3fc", + "version": 1, + "popularity": 415, + "codepoint": "1faf8_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8_1f3fd", + "version": 1, + "popularity": 414, + "codepoint": "1faf8_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8_1f3fe", + "version": 1, + "popularity": 413, + "codepoint": "1faf8_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf8_1f3ff", + "version": 1, + "popularity": 412, + "codepoint": "1faf8_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-rightwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7", + "version": 1, + "popularity": 411, + "codepoint": "1faf7", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7_1f3fb", + "version": 1, + "popularity": 410, + "codepoint": "1faf7_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7_1f3fc", + "version": 1, + "popularity": 409, + "codepoint": "1faf7_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7_1f3fd", + "version": 1, + "popularity": 408, + "codepoint": "1faf7_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7_1f3fe", + "version": 1, + "popularity": 407, + "codepoint": "1faf7_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf7_1f3ff", + "version": 1, + "popularity": 406, + "codepoint": "1faf7_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":push-leftwards:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b", + "version": 1, + "popularity": 405, + "codepoint": "1f44b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b_1f3fb", + "version": 1, + "popularity": 404, + "codepoint": "1f44b_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b_1f3fc", + "version": 1, + "popularity": 403, + "codepoint": "1f44b_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b_1f3fd", + "version": 1, + "popularity": 402, + "codepoint": "1f44b_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b_1f3fe", + "version": 1, + "popularity": 401, + "codepoint": "1f44b_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44b_1f3ff", + "version": 1, + "popularity": 400, + "codepoint": "1f44b_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":wave:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a", + "version": 1, + "popularity": 399, + "codepoint": "1f91a", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a_1f3fb", + "version": 1, + "popularity": 398, + "codepoint": "1f91a_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a_1f3fc", + "version": 1, + "popularity": 397, + "codepoint": "1f91a_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a_1f3fd", + "version": 1, + "popularity": 396, + "codepoint": "1f91a_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a_1f3fe", + "version": 1, + "popularity": 395, + "codepoint": "1f91a_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91a_1f3ff", + "version": 1, + "popularity": 394, + "codepoint": "1f91a_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":back-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_fe0f", + "version": 1, + "popularity": 393, + "codepoint": "1f590_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_1f3fb", + "version": 1, + "popularity": 392, + "codepoint": "1f590_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_1f3fc", + "version": 1, + "popularity": 391, + "codepoint": "1f590_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_1f3fd", + "version": 1, + "popularity": 390, + "codepoint": "1f590_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_1f3fe", + "version": 1, + "popularity": 389, + "codepoint": "1f590_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f590_1f3ff", + "version": 1, + "popularity": 388, + "codepoint": "1f590_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":palm:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b", + "version": 1, + "popularity": 387, + "codepoint": "270b", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b_1f3fb", + "version": 1, + "popularity": 386, + "codepoint": "270b_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b_1f3fc", + "version": 1, + "popularity": 385, + "codepoint": "270b_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b_1f3fd", + "version": 1, + "popularity": 384, + "codepoint": "270b_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b_1f3fe", + "version": 1, + "popularity": 383, + "codepoint": "270b_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270b_1f3ff", + "version": 1, + "popularity": 382, + "codepoint": "270b_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":raised-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596", + "version": 1, + "popularity": 381, + "codepoint": "1f596", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596_1f3fb", + "version": 1, + "popularity": 380, + "codepoint": "1f596_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596_1f3fc", + "version": 1, + "popularity": 379, + "codepoint": "1f596_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596_1f3fd", + "version": 1, + "popularity": 378, + "codepoint": "1f596_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596_1f3fe", + "version": 1, + "popularity": 377, + "codepoint": "1f596_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f596_1f3ff", + "version": 1, + "popularity": 376, + "codepoint": "1f596_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":vulcan:", ":prosper:", ":spock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f", + "version": 1, + "popularity": 375, + "codepoint": "1f91f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f_1f3fb", + "version": 1, + "popularity": 374, + "codepoint": "1f91f_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f_1f3fc", + "version": 1, + "popularity": 373, + "codepoint": "1f91f_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f_1f3fd", + "version": 1, + "popularity": 372, + "codepoint": "1f91f_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f_1f3fe", + "version": 1, + "popularity": 371, + "codepoint": "1f91f_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91f_1f3ff", + "version": 1, + "popularity": 370, + "codepoint": "1f91f_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":love-you-gesture:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918", + "version": 1, + "popularity": 369, + "codepoint": "1f918", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918_1f3fb", + "version": 1, + "popularity": 368, + "codepoint": "1f918_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918_1f3fc", + "version": 1, + "popularity": 367, + "codepoint": "1f918_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918_1f3fd", + "version": 1, + "popularity": 366, + "codepoint": "1f918_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918_1f3fe", + "version": 1, + "popularity": 365, + "codepoint": "1f918_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f918_1f3ff", + "version": 1, + "popularity": 364, + "codepoint": "1f918_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":metal:", ":horns:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_fe0f", + "version": 1, + "popularity": 363, + "codepoint": "270c_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_1f3fb", + "version": 1, + "popularity": 362, + "codepoint": "270c_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_1f3fc", + "version": 1, + "popularity": 361, + "codepoint": "270c_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_1f3fd", + "version": 1, + "popularity": 360, + "codepoint": "270c_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_1f3fe", + "version": 1, + "popularity": 359, + "codepoint": "270c_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270c_1f3ff", + "version": 1, + "popularity": 358, + "codepoint": "270c_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":victory:", ":v:", ":peace-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e", + "version": 1, + "popularity": 357, + "codepoint": "1f91e", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e_1f3fb", + "version": 1, + "popularity": 356, + "codepoint": "1f91e_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e_1f3fc", + "version": 1, + "popularity": 355, + "codepoint": "1f91e_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e_1f3fd", + "version": 1, + "popularity": 354, + "codepoint": "1f91e_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e_1f3fe", + "version": 1, + "popularity": 353, + "codepoint": "1f91e_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91e_1f3ff", + "version": 1, + "popularity": 352, + "codepoint": "1f91e_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":crossed-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0", + "version": 1, + "popularity": 351, + "codepoint": "1faf0", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0_1f3fb", + "version": 1, + "popularity": 350, + "codepoint": "1faf0_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0_1f3fc", + "version": 1, + "popularity": 349, + "codepoint": "1faf0_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0_1f3fd", + "version": 1, + "popularity": 348, + "codepoint": "1faf0_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0_1f3fe", + "version": 1, + "popularity": 347, + "codepoint": "1faf0_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf0_1f3ff", + "version": 1, + "popularity": 346, + "codepoint": "1faf0_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":hand-with-index-finger-and-thumb-crossed:", ":snap:", ":finger-heart:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919", + "version": 1, + "popularity": 345, + "codepoint": "1f919", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919_1f3fb", + "version": 1, + "popularity": 344, + "codepoint": "1f919_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919_1f3fc", + "version": 1, + "popularity": 343, + "codepoint": "1f919_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919_1f3fd", + "version": 1, + "popularity": 342, + "codepoint": "1f919_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919_1f3fe", + "version": 1, + "popularity": 341, + "codepoint": "1f919_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f919_1f3ff", + "version": 1, + "popularity": 340, + "codepoint": "1f919_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":call-me-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c", + "version": 1, + "popularity": 339, + "codepoint": "1f90c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c_1f3fb", + "version": 1, + "popularity": 338, + "codepoint": "1f90c_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c_1f3fc", + "version": 1, + "popularity": 337, + "codepoint": "1f90c_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c_1f3fd", + "version": 1, + "popularity": 336, + "codepoint": "1f90c_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c_1f3fe", + "version": 1, + "popularity": 335, + "codepoint": "1f90c_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90c_1f3ff", + "version": 1, + "popularity": 334, + "codepoint": "1f90c_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinched-fingers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f", + "version": 1, + "popularity": 333, + "codepoint": "1f90f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f_1f3fb", + "version": 1, + "popularity": 332, + "codepoint": "1f90f_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f_1f3fc", + "version": 1, + "popularity": 331, + "codepoint": "1f90f_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f_1f3fd", + "version": 1, + "popularity": 330, + "codepoint": "1f90f_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f_1f3fe", + "version": 1, + "popularity": 329, + "codepoint": "1f90f_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f90f_1f3ff", + "version": 1, + "popularity": 328, + "codepoint": "1f90f_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pinch:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c", + "version": 1, + "popularity": 327, + "codepoint": "1f44c", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c_1f3fb", + "version": 1, + "popularity": 326, + "codepoint": "1f44c_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c_1f3fc", + "version": 1, + "popularity": 325, + "codepoint": "1f44c_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c_1f3fd", + "version": 1, + "popularity": 324, + "codepoint": "1f44c_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c_1f3fe", + "version": 1, + "popularity": 323, + "codepoint": "1f44c_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f44c_1f3ff", + "version": 1, + "popularity": 322, + "codepoint": "1f44c_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":ok:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5", + "version": 1, + "popularity": 321, + "codepoint": "1faf5", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5_1f3fb", + "version": 1, + "popularity": 320, + "codepoint": "1faf5_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5_1f3fc", + "version": 1, + "popularity": 319, + "codepoint": "1faf5_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5_1f3fd", + "version": 1, + "popularity": 318, + "codepoint": "1faf5_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5_1f3fe", + "version": 1, + "popularity": 317, + "codepoint": "1faf5_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf5_1f3ff", + "version": 1, + "popularity": 316, + "codepoint": "1faf5_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":pointing:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449", + "version": 1, + "popularity": 315, + "codepoint": "1f449", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449_1f3fb", + "version": 1, + "popularity": 314, + "codepoint": "1f449_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449_1f3fc", + "version": 1, + "popularity": 313, + "codepoint": "1f449_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449_1f3fd", + "version": 1, + "popularity": 312, + "codepoint": "1f449_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449_1f3fe", + "version": 1, + "popularity": 311, + "codepoint": "1f449_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f449_1f3ff", + "version": 1, + "popularity": 310, + "codepoint": "1f449_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-right:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448", + "version": 1, + "popularity": 309, + "codepoint": "1f448", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448_1f3fb", + "version": 1, + "popularity": 308, + "codepoint": "1f448_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448_1f3fc", + "version": 1, + "popularity": 307, + "codepoint": "1f448_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448_1f3fd", + "version": 1, + "popularity": 306, + "codepoint": "1f448_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448_1f3fe", + "version": 1, + "popularity": 305, + "codepoint": "1f448_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f448_1f3ff", + "version": 1, + "popularity": 304, + "codepoint": "1f448_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-left:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_fe0f", + "version": 1, + "popularity": 303, + "codepoint": "261d_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_1f3fb", + "version": 1, + "popularity": 302, + "codepoint": "261d_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_1f3fc", + "version": 1, + "popularity": 301, + "codepoint": "261d_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_1f3fd", + "version": 1, + "popularity": 300, + "codepoint": "261d_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_1f3fe", + "version": 1, + "popularity": 299, + "codepoint": "261d_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u261d_1f3ff", + "version": 1, + "popularity": 298, + "codepoint": "261d_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":index-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446", + "version": 1, + "popularity": 297, + "codepoint": "1f446", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446_1f3fb", + "version": 1, + "popularity": 296, + "codepoint": "1f446_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446_1f3fc", + "version": 1, + "popularity": 295, + "codepoint": "1f446_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446_1f3fd", + "version": 1, + "popularity": 294, + "codepoint": "1f446_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446_1f3fe", + "version": 1, + "popularity": 293, + "codepoint": "1f446_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f446_1f3ff", + "version": 1, + "popularity": 292, + "codepoint": "1f446_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-up:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447", + "version": 1, + "popularity": 291, + "codepoint": "1f447", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447_1f3fb", + "version": 1, + "popularity": 290, + "codepoint": "1f447_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447_1f3fc", + "version": 1, + "popularity": 289, + "codepoint": "1f447_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447_1f3fd", + "version": 1, + "popularity": 288, + "codepoint": "1f447_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447_1f3fe", + "version": 1, + "popularity": 287, + "codepoint": "1f447_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f447_1f3ff", + "version": 1, + "popularity": 286, + "codepoint": "1f447_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":point-down:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595", + "version": 1, + "popularity": 285, + "codepoint": "1f595", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595_1f3fb", + "version": 1, + "popularity": 284, + "codepoint": "1f595_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595_1f3fc", + "version": 1, + "popularity": 283, + "codepoint": "1f595_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595_1f3fd", + "version": 1, + "popularity": 282, + "codepoint": "1f595_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595_1f3fe", + "version": 1, + "popularity": 281, + "codepoint": "1f595_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f595_1f3ff", + "version": 1, + "popularity": 280, + "codepoint": "1f595_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":middle-finger:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_fe0f", + "version": 1, + "popularity": 279, + "codepoint": "270d_fe0f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_1f3fb", + "version": 1, + "popularity": 278, + "codepoint": "270d_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_1f3fc", + "version": 1, + "popularity": 277, + "codepoint": "270d_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_1f3fd", + "version": 1, + "popularity": 276, + "codepoint": "270d_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_1f3fe", + "version": 1, + "popularity": 275, + "codepoint": "270d_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u270d_1f3ff", + "version": 1, + "popularity": 274, + "codepoint": "270d_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":writing-hand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933", + "version": 1, + "popularity": 273, + "codepoint": "1f933", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933_1f3fb", + "version": 1, + "popularity": 272, + "codepoint": "1f933_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933_1f3fc", + "version": 1, + "popularity": 271, + "codepoint": "1f933_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933_1f3fd", + "version": 1, + "popularity": 270, + "codepoint": "1f933_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933_1f3fe", + "version": 1, + "popularity": 269, + "codepoint": "1f933_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f933_1f3ff", + "version": 1, + "popularity": 268, + "codepoint": "1f933_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":selfie:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f", + "version": 1, + "popularity": 267, + "codepoint": "1f64f", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f_1f3fb", + "version": 1, + "popularity": 266, + "codepoint": "1f64f_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f_1f3fc", + "version": 1, + "popularity": 265, + "codepoint": "1f64f_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f_1f3fd", + "version": 1, + "popularity": 264, + "codepoint": "1f64f_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f_1f3fe", + "version": 1, + "popularity": 263, + "codepoint": "1f64f_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f64f_1f3ff", + "version": 1, + "popularity": 262, + "codepoint": "1f64f_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [ + ":folded-hands:", + ":please:", + ":pray:", + ":hope:", + ":wish:", + ":thank-you:", + ":high-five:" + ], + "sizes_px": [] + }, + { + "name": "emoji_u1f485", + "version": 1, + "popularity": 261, + "codepoint": "1f485", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f485_1f3fb", + "version": 1, + "popularity": 260, + "codepoint": "1f485_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f485_1f3fc", + "version": 1, + "popularity": 259, + "codepoint": "1f485_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f485_1f3fd", + "version": 1, + "popularity": 258, + "codepoint": "1f485_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f485_1f3fe", + "version": 1, + "popularity": 257, + "codepoint": "1f485_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f485_1f3ff", + "version": 1, + "popularity": 256, + "codepoint": "1f485_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":nail-care:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d", + "version": 1, + "popularity": 255, + "codepoint": "1f91d", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d_1f3fb", + "version": 1, + "popularity": 254, + "codepoint": "1f91d_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fb_200d_1faf2_1f3fc", + "version": 1, + "popularity": 253, + "codepoint": "1faf1_1f3fb_200d_1faf2_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fb_200d_1faf2_1f3fd", + "version": 1, + "popularity": 252, + "codepoint": "1faf1_1f3fb_200d_1faf2_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fb_200d_1faf2_1f3fe", + "version": 1, + "popularity": 251, + "codepoint": "1faf1_1f3fb_200d_1faf2_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fb_200d_1faf2_1f3ff", + "version": 1, + "popularity": 250, + "codepoint": "1faf1_1f3fb_200d_1faf2_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fc_200d_1faf2_1f3fb", + "version": 1, + "popularity": 249, + "codepoint": "1faf1_1f3fc_200d_1faf2_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d_1f3fc", + "version": 1, + "popularity": 248, + "codepoint": "1f91d_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fc_200d_1faf2_1f3fd", + "version": 1, + "popularity": 247, + "codepoint": "1faf1_1f3fc_200d_1faf2_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fc_200d_1faf2_1f3fe", + "version": 1, + "popularity": 246, + "codepoint": "1faf1_1f3fc_200d_1faf2_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fc_200d_1faf2_1f3ff", + "version": 1, + "popularity": 245, + "codepoint": "1faf1_1f3fc_200d_1faf2_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fd_200d_1faf2_1f3fb", + "version": 1, + "popularity": 244, + "codepoint": "1faf1_1f3fd_200d_1faf2_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fd_200d_1faf2_1f3fc", + "version": 1, + "popularity": 243, + "codepoint": "1faf1_1f3fd_200d_1faf2_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d_1f3fd", + "version": 1, + "popularity": 242, + "codepoint": "1f91d_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fd_200d_1faf2_1f3fe", + "version": 1, + "popularity": 241, + "codepoint": "1faf1_1f3fd_200d_1faf2_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fd_200d_1faf2_1f3ff", + "version": 1, + "popularity": 240, + "codepoint": "1faf1_1f3fd_200d_1faf2_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fe_200d_1faf2_1f3fb", + "version": 1, + "popularity": 239, + "codepoint": "1faf1_1f3fe_200d_1faf2_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fe_200d_1faf2_1f3fc", + "version": 1, + "popularity": 238, + "codepoint": "1faf1_1f3fe_200d_1faf2_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fe_200d_1faf2_1f3fd", + "version": 1, + "popularity": 237, + "codepoint": "1faf1_1f3fe_200d_1faf2_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d_1f3fe", + "version": 1, + "popularity": 236, + "codepoint": "1f91d_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3fe_200d_1faf2_1f3ff", + "version": 1, + "popularity": 235, + "codepoint": "1faf1_1f3fe_200d_1faf2_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3ff_200d_1faf2_1f3fb", + "version": 1, + "popularity": 234, + "codepoint": "1faf1_1f3ff_200d_1faf2_1f3fb", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3ff_200d_1faf2_1f3fc", + "version": 1, + "popularity": 233, + "codepoint": "1faf1_1f3ff_200d_1faf2_1f3fc", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3ff_200d_1faf2_1f3fd", + "version": 1, + "popularity": 232, + "codepoint": "1faf1_1f3ff_200d_1faf2_1f3fd", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faf1_1f3ff_200d_1faf2_1f3fe", + "version": 1, + "popularity": 231, + "codepoint": "1faf1_1f3ff_200d_1faf2_1f3fe", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f91d_1f3ff", + "version": 1, + "popularity": 230, + "codepoint": "1f91d_1f3ff", + "unsupported_families": [], + "categories": ["Smileys and emotions"], + "tags": [":handshake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483", + "version": 1, + "popularity": 229, + "codepoint": "1f483", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483_1f3fb", + "version": 1, + "popularity": 228, + "codepoint": "1f483_1f3fb", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483_1f3fc", + "version": 1, + "popularity": 227, + "codepoint": "1f483_1f3fc", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483_1f3fd", + "version": 1, + "popularity": 226, + "codepoint": "1f483_1f3fd", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483_1f3fe", + "version": 1, + "popularity": 225, + "codepoint": "1f483_1f3fe", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f483_1f3ff", + "version": 1, + "popularity": 224, + "codepoint": "1f483_1f3ff", + "unsupported_families": [], + "categories": ["People"], + "tags": [":dancer-woman:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f490", + "version": 1, + "popularity": 223, + "codepoint": "1f490", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bouquet:", ":flowers:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f339", + "version": 1, + "popularity": 222, + "codepoint": "1f339", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f940", + "version": 1, + "popularity": 221, + "codepoint": "1f940", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":wilted-flower:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f342", + "version": 1, + "popularity": 220, + "codepoint": "1f342", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":fallen-leaf:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f331", + "version": 1, + "popularity": 219, + "codepoint": "1f331", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":plant:", ":seed:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f343", + "version": 1, + "popularity": 218, + "codepoint": "1f343", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":leaves:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f340", + "version": 1, + "popularity": 217, + "codepoint": "1f340", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":luck:", ":four-leaf-clover:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fabe", + "version": 1, + "popularity": 216, + "codepoint": "1fabe", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":leafless-tree:"], + "sizes_px": [] + }, + { + "name": "emoji_u2744_fe0f", + "version": 1, + "popularity": 215, + "codepoint": "2744_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":snowflake:", ":winter:", ":cold:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f30b", + "version": 1, + "popularity": 214, + "codepoint": "1f30b", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":volcano:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f305", + "version": 1, + "popularity": 213, + "codepoint": "1f305", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":sunrise:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f304", + "version": 1, + "popularity": 212, + "codepoint": "1f304", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":sunrise-over-mountains:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f308", + "version": 1, + "popularity": 211, + "codepoint": "1f308", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rainbow:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fae7", + "version": 1, + "popularity": 210, + "codepoint": "1fae7", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bubbles:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f30a", + "version": 1, + "popularity": 209, + "codepoint": "1f30a", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":ocean:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f32c_fe0f", + "version": 1, + "popularity": 208, + "codepoint": "1f32c_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":wind-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f32a_fe0f", + "version": 1, + "popularity": 207, + "codepoint": "1f32a_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":tornado:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4a7", + "version": 1, + "popularity": 206, + "codepoint": "1f4a7", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":droplet:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f327_fe0f", + "version": 1, + "popularity": 205, + "codepoint": "1f327_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rain-cloud:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f329_fe0f", + "version": 1, + "popularity": 204, + "codepoint": "1f329_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":cloud-with-lightning:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f30d", + "version": 1, + "popularity": 203, + "codepoint": "1f30d", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":globe-showing-Europe-Africa:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f30e", + "version": 1, + "popularity": 202, + "codepoint": "1f30e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":globe-showing-Americas:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f30f", + "version": 1, + "popularity": 201, + "codepoint": "1f30f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":globe-showing-Asia-Australia:"], + "sizes_px": [] + }, + { + "name": "emoji_u2604_fe0f", + "version": 1, + "popularity": 200, + "codepoint": "2604_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":comet:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f42e", + "version": 1, + "popularity": 199, + "codepoint": "1f42e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":cow-face:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f984", + "version": 1, + "popularity": 198, + "codepoint": "1f984", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":unicorn:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f98e", + "version": 1, + "popularity": 197, + "codepoint": "1f98e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":lizard:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f409", + "version": 1, + "popularity": 196, + "codepoint": "1f409", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":dragon:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f996", + "version": 1, + "popularity": 195, + "codepoint": "1f996", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":t-rex:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f995", + "version": 1, + "popularity": 194, + "codepoint": "1f995", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":dinosaur:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f422", + "version": 1, + "popularity": 193, + "codepoint": "1f422", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":turtle:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f40a", + "version": 1, + "popularity": 192, + "codepoint": "1f40a", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":crocodile:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f40d", + "version": 1, + "popularity": 191, + "codepoint": "1f40d", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":snake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f438", + "version": 1, + "popularity": 190, + "codepoint": "1f438", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":frog:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f407", + "version": 1, + "popularity": 189, + "codepoint": "1f407", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rabbit:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f400", + "version": 1, + "popularity": 188, + "codepoint": "1f400", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f429", + "version": 1, + "popularity": 187, + "codepoint": "1f429", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":poodle:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f415", + "version": 1, + "popularity": 186, + "codepoint": "1f415", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":dog:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9ae", + "version": 1, + "popularity": 185, + "codepoint": "1f9ae", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":guide-dog:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f415_200d_1f9ba", + "version": 1, + "popularity": 184, + "codepoint": "1f415_200d_1f9ba", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":service-dog:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f416", + "version": 1, + "popularity": 183, + "codepoint": "1f416", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":pig:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f40e", + "version": 1, + "popularity": 182, + "codepoint": "1f40e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":racehorse:"], + "sizes_px": [] + }, + { + "name": "emoji_u1facf", + "version": 1, + "popularity": 181, + "codepoint": "1facf", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":donkey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f402", + "version": 1, + "popularity": 180, + "codepoint": "1f402", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":ox:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f410", + "version": 1, + "popularity": 179, + "codepoint": "1f410", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":goat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f998", + "version": 1, + "popularity": 178, + "codepoint": "1f998", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":kangaroo:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f405", + "version": 1, + "popularity": 177, + "codepoint": "1f405", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":tiger:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f412", + "version": 1, + "popularity": 176, + "codepoint": "1f412", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":monkey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f98d", + "version": 1, + "popularity": 175, + "codepoint": "1f98d", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":gorilla:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9a7", + "version": 1, + "popularity": 174, + "codepoint": "1f9a7", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":orangutan:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f43f_fe0f", + "version": 1, + "popularity": 173, + "codepoint": "1f43f_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":chipmunk:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9a6", + "version": 1, + "popularity": 172, + "codepoint": "1f9a6", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":otter:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f987", + "version": 1, + "popularity": 171, + "codepoint": "1f987", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f426", + "version": 1, + "popularity": 170, + "codepoint": "1f426", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bird:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f426_200d_2b1b", + "version": 1, + "popularity": 169, + "codepoint": "1f426_200d_2b1b", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":black-bird:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f413", + "version": 1, + "popularity": 168, + "codepoint": "1f413", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":rooster:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f423", + "version": 1, + "popularity": 167, + "codepoint": "1f423", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":hatching-chick:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f424", + "version": 1, + "popularity": 166, + "codepoint": "1f424", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":baby-chick:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f425", + "version": 1, + "popularity": 165, + "codepoint": "1f425", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":hatched-chick:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f985", + "version": 1, + "popularity": 164, + "codepoint": "1f985", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":eagle:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f989", + "version": 1, + "popularity": 163, + "codepoint": "1f989", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":owl:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f54a_fe0f", + "version": 1, + "popularity": 162, + "codepoint": "1f54a_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":peace:", ":dove:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fabf", + "version": 1, + "popularity": 161, + "codepoint": "1fabf", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":goose:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f99a", + "version": 1, + "popularity": 160, + "codepoint": "1f99a", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":peacock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f426_200d_1f525", + "version": 1, + "popularity": 159, + "codepoint": "1f426_200d_1f525", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":phoenix:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f9ad", + "version": 1, + "popularity": 158, + "codepoint": "1f9ad", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":seal:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f988", + "version": 1, + "popularity": 157, + "codepoint": "1f988", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":shark:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f42c", + "version": 1, + "popularity": 156, + "codepoint": "1f42c", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":dolphin:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f433", + "version": 1, + "popularity": 155, + "codepoint": "1f433", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":whale:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f41f", + "version": 1, + "popularity": 154, + "codepoint": "1f41f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":fish:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f421", + "version": 1, + "popularity": 153, + "codepoint": "1f421", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":blowfish:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f99e", + "version": 1, + "popularity": 152, + "codepoint": "1f99e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":lobster:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f980", + "version": 1, + "popularity": 151, + "codepoint": "1f980", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":crab:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f419", + "version": 1, + "popularity": 150, + "codepoint": "1f419", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":octopus:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fabc", + "version": 1, + "popularity": 149, + "codepoint": "1fabc", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":jellyfish:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f982", + "version": 1, + "popularity": 148, + "codepoint": "1f982", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":scorpion:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f577_fe0f", + "version": 1, + "popularity": 147, + "codepoint": "1f577_fe0f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":spider:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f40c", + "version": 1, + "popularity": 146, + "codepoint": "1f40c", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":snail:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f41c", + "version": 1, + "popularity": 145, + "codepoint": "1f41c", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":ant:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f99f", + "version": 1, + "popularity": 144, + "codepoint": "1f99f", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":mosquito:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fab3", + "version": 1, + "popularity": 143, + "codepoint": "1fab3", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":cockroach:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fab0", + "version": 1, + "popularity": 142, + "codepoint": "1fab0", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":fly:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f41d", + "version": 1, + "popularity": 141, + "codepoint": "1f41d", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bee:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f41e", + "version": 1, + "popularity": 140, + "codepoint": "1f41e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":lady-bug:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f98b", + "version": 1, + "popularity": 139, + "codepoint": "1f98b", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":butterfly:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f41b", + "version": 1, + "popularity": 138, + "codepoint": "1f41b", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":bug:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fab1", + "version": 1, + "popularity": 137, + "codepoint": "1fab1", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":worm:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f43e", + "version": 1, + "popularity": 136, + "codepoint": "1f43e", + "unsupported_families": [], + "categories": ["Animals and nature"], + "tags": [":paw prints:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f345", + "version": 1, + "popularity": 135, + "codepoint": "1f345", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":tomato:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fadc", + "version": 1, + "popularity": 134, + "codepoint": "1fadc", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":root-vegetable:", ":beet:", ":turnip:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f373", + "version": 1, + "popularity": 133, + "codepoint": "1f373", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":cooking:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f32f", + "version": 1, + "popularity": 132, + "codepoint": "1f32f", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":burrito:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f35d", + "version": 1, + "popularity": 131, + "codepoint": "1f35d", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":spaghetti:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f35c", + "version": 1, + "popularity": 130, + "codepoint": "1f35c", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":steaming-bowl:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f37f", + "version": 1, + "popularity": 129, + "codepoint": "1f37f", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":popcorn:"], + "sizes_px": [] + }, + { + "name": "emoji_u2615", + "version": 1, + "popularity": 128, + "codepoint": "2615", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":hot-beverage:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f37b", + "version": 1, + "popularity": 127, + "codepoint": "1f37b", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":clinking-beer-mugs:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f942", + "version": 1, + "popularity": 126, + "codepoint": "1f942", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":clinking-glasses:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f37e", + "version": 1, + "popularity": 125, + "codepoint": "1f37e", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":bottle-with-popping-cork:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f377", + "version": 1, + "popularity": 124, + "codepoint": "1f377", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":wine-glass:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fad7", + "version": 1, + "popularity": 123, + "codepoint": "1fad7", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":pour:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f379", + "version": 1, + "popularity": 122, + "codepoint": "1f379", + "unsupported_families": [], + "categories": ["Food and drink"], + "tags": [":tropical-drink:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6a7", + "version": 1, + "popularity": 121, + "codepoint": "1f6a7", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":construction:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6a8", + "version": 1, + "popularity": 120, + "codepoint": "1f6a8", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":police-car-light:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6b2", + "version": 1, + "popularity": 119, + "codepoint": "1f6b2", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":bicycle:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f697", + "version": 1, + "popularity": 118, + "codepoint": "1f697", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":automobile:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3ce_fe0f", + "version": 1, + "popularity": 117, + "codepoint": "1f3ce_fe0f", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":racing-car:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f695", + "version": 1, + "popularity": 116, + "codepoint": "1f695", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":taxi:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f68c", + "version": 1, + "popularity": 115, + "codepoint": "1f68c", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":bus:"], + "sizes_px": [] + }, + { + "name": "emoji_u26f5", + "version": 1, + "popularity": 114, + "codepoint": "26f5", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":sailboat:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6f6", + "version": 1, + "popularity": 113, + "codepoint": "1f6f6", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":canoe:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6f8", + "version": 1, + "popularity": 112, + "codepoint": "1f6f8", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":flying-saucer:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f680", + "version": 1, + "popularity": 111, + "codepoint": "1f680", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":rocket:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6eb", + "version": 1, + "popularity": 110, + "codepoint": "1f6eb", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":airplane-departure:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6ec", + "version": 1, + "popularity": 109, + "codepoint": "1f6ec", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":airplane-arrival:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3a2", + "version": 1, + "popularity": 108, + "codepoint": "1f3a2", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":roller-coaster:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3a1", + "version": 1, + "popularity": 107, + "codepoint": "1f3a1", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":ferris-wheel:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3d5_fe0f", + "version": 1, + "popularity": 106, + "codepoint": "1f3d5_fe0f", + "unsupported_families": [], + "categories": ["Travel and places"], + "tags": [":camping:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f388", + "version": 1, + "popularity": 105, + "codepoint": "1f388", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":balloon:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f382", + "version": 1, + "popularity": 104, + "codepoint": "1f382", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":birthday-cake:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f381", + "version": 1, + "popularity": 103, + "codepoint": "1f381", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":wrapped-gift:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f386", + "version": 1, + "popularity": 102, + "codepoint": "1f386", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":fireworks:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa85", + "version": 1, + "popularity": 101, + "codepoint": "1fa85", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":piñata:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faa9", + "version": 1, + "popularity": 100, + "codepoint": "1faa9", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":mirror-ball:", ":disco-ball:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f947", + "version": 1, + "popularity": 99, + "codepoint": "1f947", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":gold-medal:", ":1st-place-medal:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f948", + "version": 1, + "popularity": 98, + "codepoint": "1f948", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":silver-medal:", ":2nd-place-medal:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f949", + "version": 1, + "popularity": 97, + "codepoint": "1f949", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":bronze-medal:", ":3rd-place-medal:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3c6", + "version": 1, + "popularity": 96, + "codepoint": "1f3c6", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":trophy:"], + "sizes_px": [] + }, + { + "name": "emoji_u26bd", + "version": 1, + "popularity": 95, + "codepoint": "26bd", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":soccer-ball:"], + "sizes_px": [] + }, + { + "name": "emoji_u26be", + "version": 1, + "popularity": 94, + "codepoint": "26be", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":baseball:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f94e", + "version": 1, + "popularity": 93, + "codepoint": "1f94e", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":softball:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3be", + "version": 1, + "popularity": 92, + "codepoint": "1f3be", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":tennis:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3f8", + "version": 1, + "popularity": 91, + "codepoint": "1f3f8", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":badminton:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f94d", + "version": 1, + "popularity": 90, + "codepoint": "1f94d", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":lacrosse:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3cf", + "version": 1, + "popularity": 89, + "codepoint": "1f3cf", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":cricket-game:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3d1", + "version": 1, + "popularity": 88, + "codepoint": "1f3d1", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":field-hockey:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3d2", + "version": 1, + "popularity": 87, + "codepoint": "1f3d2", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":ice-hockey:"], + "sizes_px": [] + }, + { + "name": "emoji_u26f8_fe0f", + "version": 1, + "popularity": 86, + "codepoint": "26f8_fe0f", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":ice-skate:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6fc", + "version": 1, + "popularity": 85, + "codepoint": "1f6fc", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":roller-skates:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa70", + "version": 1, + "popularity": 84, + "codepoint": "1fa70", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":ballet-shoes:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6f9", + "version": 1, + "popularity": 83, + "codepoint": "1f6f9", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":skateboard:"], + "sizes_px": [] + }, + { + "name": "emoji_u26f3", + "version": 1, + "popularity": 82, + "codepoint": "26f3", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":flag-in-hole:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3af", + "version": 1, + "popularity": 81, + "codepoint": "1f3af", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":direct-hit:", ":target:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f94f", + "version": 1, + "popularity": 80, + "codepoint": "1f94f", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":flying-disc:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa83", + "version": 1, + "popularity": 79, + "codepoint": "1fa83", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":boomerang:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa81", + "version": 1, + "popularity": 78, + "codepoint": "1fa81", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":kite:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3a3", + "version": 1, + "popularity": 77, + "codepoint": "1f3a3", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":fishing-pole:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f94b", + "version": 1, + "popularity": 76, + "codepoint": "1f94b", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":martial-arts-uniform:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b1", + "version": 1, + "popularity": 75, + "codepoint": "1f3b1", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":8-ball:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3d3", + "version": 1, + "popularity": 74, + "codepoint": "1f3d3", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":ping-pong:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b3", + "version": 1, + "popularity": 73, + "codepoint": "1f3b3", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":bowling:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b2", + "version": 1, + "popularity": 72, + "codepoint": "1f3b2", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":die:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b0", + "version": 1, + "popularity": 71, + "codepoint": "1f3b0", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":slot-machine:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa84", + "version": 1, + "popularity": 70, + "codepoint": "1fa84", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":wand:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4f8", + "version": 1, + "popularity": 69, + "codepoint": "1f4f8", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":camera-flash:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fadf", + "version": 1, + "popularity": 68, + "codepoint": "1fadf", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":splatter:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b7", + "version": 1, + "popularity": 67, + "codepoint": "1f3b7", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":saxophone:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3ba", + "version": 1, + "popularity": 66, + "codepoint": "1f3ba", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":trumpet:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3bb", + "version": 1, + "popularity": 65, + "codepoint": "1f3bb", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":violin:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa89", + "version": 1, + "popularity": 64, + "codepoint": "1fa89", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":harp:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f941", + "version": 1, + "popularity": 63, + "codepoint": "1f941", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":drum:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa87", + "version": 1, + "popularity": 62, + "codepoint": "1fa87", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":maracas:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3ac", + "version": 1, + "popularity": 61, + "codepoint": "1f3ac", + "unsupported_families": [], + "categories": ["Activities and events"], + "tags": [":clapper:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f50b", + "version": 1, + "popularity": 60, + "codepoint": "1f50b", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":battery-full:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faab", + "version": 1, + "popularity": 59, + "codepoint": "1faab", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":battery-low:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa99", + "version": 1, + "popularity": 58, + "codepoint": "1fa99", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":coin:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4b8", + "version": 1, + "popularity": 57, + "codepoint": "1f4b8", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":money-with-wings:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f48e", + "version": 1, + "popularity": 56, + "codepoint": "1f48e", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":gem-stone:"], + "sizes_px": [] + }, + { + "name": "emoji_u2696_fe0f", + "version": 1, + "popularity": 55, + "codepoint": "2696_fe0f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":balance-scale:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4a1", + "version": 1, + "popularity": 54, + "codepoint": "1f4a1", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":light-bulb:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f393", + "version": 1, + "popularity": 53, + "codepoint": "1f393", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":graduation-cap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f48d", + "version": 1, + "popularity": 52, + "codepoint": "1f48d", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":ring:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faad", + "version": 1, + "popularity": 51, + "codepoint": "1faad", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":fan:"], + "sizes_px": [] + }, + { + "name": "emoji_u2602_fe0f", + "version": 1, + "popularity": 50, + "codepoint": "2602_fe0f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":umbrella:"], + "sizes_px": [] + }, + { + "name": "emoji_u1fa8f", + "version": 1, + "popularity": 49, + "codepoint": "1fa8f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":shovel:", ":dig:"], + "sizes_px": [] + }, + { + "name": "emoji_u2699_fe0f", + "version": 1, + "popularity": 48, + "codepoint": "2699_fe0f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":gear:"], + "sizes_px": [] + }, + { + "name": "emoji_u26d3_fe0f_200d_1f4a5", + "version": 1, + "popularity": 47, + "codepoint": "26d3_fe0f_200d_1f4a5", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":broken-chain:"], + "sizes_px": [] + }, + { + "name": "emoji_u270f_fe0f", + "version": 1, + "popularity": 46, + "codepoint": "270f_fe0f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":pencil:"], + "sizes_px": [] + }, + { + "name": "emoji_u23f0", + "version": 1, + "popularity": 45, + "codepoint": "23f0", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":alarm-clock:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6ce_fe0f", + "version": 1, + "popularity": 44, + "codepoint": "1f6ce_fe0f", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":bellhop-bell:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f514", + "version": 1, + "popularity": 43, + "codepoint": "1f514", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":bell:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f52e", + "version": 1, + "popularity": 42, + "codepoint": "1f52e", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":crystal-ball:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4a3", + "version": 1, + "popularity": 41, + "codepoint": "1f4a3", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":bomb:"], + "sizes_px": [] + }, + { + "name": "emoji_u1faa4", + "version": 1, + "popularity": 40, + "codepoint": "1faa4", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":mouse-trap:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f512", + "version": 1, + "popularity": 39, + "codepoint": "1f512", + "unsupported_families": [], + "categories": ["Objects"], + "tags": [":locked:"], + "sizes_px": [] + }, + { + "name": "emoji_u2648", + "version": 1, + "popularity": 38, + "codepoint": "2648", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Aries:"], + "sizes_px": [] + }, + { + "name": "emoji_u2649", + "version": 1, + "popularity": 37, + "codepoint": "2649", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Taurus:"], + "sizes_px": [] + }, + { + "name": "emoji_u264a", + "version": 1, + "popularity": 36, + "codepoint": "264a", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Gemini:"], + "sizes_px": [] + }, + { + "name": "emoji_u264b", + "version": 1, + "popularity": 35, + "codepoint": "264b", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Cancer:"], + "sizes_px": [] + }, + { + "name": "emoji_u264c", + "version": 1, + "popularity": 34, + "codepoint": "264c", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Leo:"], + "sizes_px": [] + }, + { + "name": "emoji_u264d", + "version": 1, + "popularity": 33, + "codepoint": "264d", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Virgo:"], + "sizes_px": [] + }, + { + "name": "emoji_u264e", + "version": 1, + "popularity": 32, + "codepoint": "264e", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Libra:"], + "sizes_px": [] + }, + { + "name": "emoji_u264f", + "version": 1, + "popularity": 31, + "codepoint": "264f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Scorpio:"], + "sizes_px": [] + }, + { + "name": "emoji_u2650", + "version": 1, + "popularity": 30, + "codepoint": "2650", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Sagittarius:"], + "sizes_px": [] + }, + { + "name": "emoji_u2651", + "version": 1, + "popularity": 29, + "codepoint": "2651", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Capricorn:"], + "sizes_px": [] + }, + { + "name": "emoji_u2652", + "version": 1, + "popularity": 28, + "codepoint": "2652", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Aquarius:"], + "sizes_px": [] + }, + { + "name": "emoji_u2653", + "version": 1, + "popularity": 27, + "codepoint": "2653", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Pisces:"], + "sizes_px": [] + }, + { + "name": "emoji_u26ce", + "version": 1, + "popularity": 26, + "codepoint": "26ce", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":Ophiuchus:"], + "sizes_px": [] + }, + { + "name": "emoji_u2757", + "version": 1, + "popularity": 25, + "codepoint": "2757", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":exclamation:", ":exclamation-mark:"], + "sizes_px": [] + }, + { + "name": "emoji_u2753", + "version": 1, + "popularity": 24, + "codepoint": "2753", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":question:", ":question-mark:", ":?:"], + "sizes_px": [] + }, + { + "name": "emoji_u2049_fe0f", + "version": 1, + "popularity": 23, + "codepoint": "2049_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":exclamation-question-mark:", ":!?:"], + "sizes_px": [] + }, + { + "name": "emoji_u203c_fe0f", + "version": 1, + "popularity": 22, + "codepoint": "203c_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":exclamation-double:", ":!!:"], + "sizes_px": [] + }, + { + "name": "emoji_u274c", + "version": 1, + "popularity": 21, + "codepoint": "274c", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":cross-mark:", ":x:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f198", + "version": 1, + "popularity": 20, + "codepoint": "1f198", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":sos:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f4f4", + "version": 1, + "popularity": 19, + "codepoint": "1f4f4", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":phone-off:"], + "sizes_px": [] + }, + { + "name": "emoji_u2622_fe0f", + "version": 1, + "popularity": 18, + "codepoint": "2622_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":radioactive:"], + "sizes_px": [] + }, + { + "name": "emoji_u2623_fe0f", + "version": 1, + "popularity": 17, + "codepoint": "2623_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":biohazard:"], + "sizes_px": [] + }, + { + "name": "emoji_u26a0_fe0f", + "version": 1, + "popularity": 16, + "codepoint": "26a0_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":warning:"], + "sizes_px": [] + }, + { + "name": "emoji_u2705", + "version": 1, + "popularity": 15, + "codepoint": "2705", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":check-mark:", ":check-mark-green:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f195", + "version": 1, + "popularity": 14, + "codepoint": "1f195", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":new:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f193", + "version": 1, + "popularity": 13, + "codepoint": "1f193", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":free:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f199", + "version": 1, + "popularity": 12, + "codepoint": "1f199", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":up!:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f192", + "version": 1, + "popularity": 11, + "codepoint": "1f192", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":cool:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6ae", + "version": 1, + "popularity": 10, + "codepoint": "1f6ae", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":litter:"], + "sizes_px": [] + }, + { + "name": "emoji_u262e_fe0f", + "version": 1, + "popularity": 9, + "codepoint": "262e_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":peace-symbol:"], + "sizes_px": [] + }, + { + "name": "emoji_u262f_fe0f", + "version": 1, + "popularity": 8, + "codepoint": "262f_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":yin-yang:"], + "sizes_px": [] + }, + { + "name": "emoji_u267e_fe0f", + "version": 1, + "popularity": 7, + "codepoint": "267e_fe0f", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":infinity:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3b6", + "version": 1, + "popularity": 6, + "codepoint": "1f3b6", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":musical-notes:"], + "sizes_px": [] + }, + { + "name": "emoji_u2795", + "version": 1, + "popularity": 5, + "codepoint": "2795", + "unsupported_families": [], + "categories": ["Symbols"], + "tags": [":plus-sign:", ":+:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3c1", + "version": 1, + "popularity": 4, + "codepoint": "1f3c1", + "unsupported_families": [], + "categories": ["Flags"], + "tags": [":chequered-flag:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f6a9", + "version": 1, + "popularity": 3, + "codepoint": "1f6a9", + "unsupported_families": [], + "categories": ["Flags"], + "tags": [":triangular-flag:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3f4", + "version": 1, + "popularity": 2, + "codepoint": "1f3f4", + "unsupported_families": [], + "categories": ["Flags"], + "tags": [":black-flag:"], + "sizes_px": [] + }, + { + "name": "emoji_u1f3f3_fe0f", + "version": 1, + "popularity": 1, + "codepoint": "1f3f3_fe0f", + "unsupported_families": [], + "categories": ["Flags"], + "tags": [":white-flag:"], + "sizes_px": [] + } + ] +} diff --git a/apps/atproto-starter/src/lib/emojis/index.ts b/apps/atproto-starter/src/lib/emojis/index.ts new file mode 100644 index 0000000..c9c843f --- /dev/null +++ b/apps/atproto-starter/src/lib/emojis/index.ts @@ -0,0 +1,23 @@ +// animated emojis from +// https://googlefonts.github.io/noto-emoji-animation/ + +import icons from './icons.json'; + +export function emojiToNotoAnimatedWebp(emoji: string | undefined): string | undefined { + if (!emoji) return; + // Convert emoji to lowercase hex codepoints joined by "_" + const codepoints: string[] = []; + for (const char of emoji) { + codepoints.push(char.codePointAt(0)!.toString(16).toLowerCase()); + } + + let key = codepoints.join('_'); + + if (icons.icons.find((v) => v.codepoint == key)) { + return `https://fonts.gstatic.com/s/e/notoemoji/latest/${key}/512.webp`; + } + + key = codepoints.filter((cp) => cp !== 'fe0f' && cp !== 'fe0e').join('_'); + if (icons.icons.find((v) => v.codepoint == key)) + return `https://fonts.gstatic.com/s/e/notoemoji/latest/${key}/512.webp`; +} diff --git a/apps/atproto-starter/src/lib/index.ts b/apps/atproto-starter/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/apps/atproto-starter/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/apps/atproto-starter/src/routes/(oauth)/oauth-client-metadata.json/+server.ts b/apps/atproto-starter/src/routes/(oauth)/oauth-client-metadata.json/+server.ts new file mode 100644 index 0000000..44794e4 --- /dev/null +++ b/apps/atproto-starter/src/routes/(oauth)/oauth-client-metadata.json/+server.ts @@ -0,0 +1,8 @@ +import { json } from '@sveltejs/kit'; +import { createOAuthClient } from '$lib/atproto/server/oauth'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ platform }) => { + const oauth = createOAuthClient(platform?.env); + return json(oauth.metadata); +}; diff --git a/apps/atproto-starter/src/routes/(oauth)/oauth/callback/+server.ts b/apps/atproto-starter/src/routes/(oauth)/oauth/callback/+server.ts new file mode 100644 index 0000000..f6a71e8 --- /dev/null +++ b/apps/atproto-starter/src/routes/(oauth)/oauth/callback/+server.ts @@ -0,0 +1,41 @@ +import { redirect } from '@sveltejs/kit'; +import { createOAuthClient } from '$lib/atproto/server/oauth'; +import { setSignedCookie } from '$lib/atproto/server/signed-cookie'; +import { scopes } from '$lib/atproto/settings'; +import { dev } from '$app/environment'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ url, platform, cookies }) => { + const oauth = createOAuthClient(platform?.env); + + // oauth.callback() validates the state parameter (CSRF protection) and + // exchanges the authorization code for tokens via the token endpoint. + try { + const { session } = await oauth.callback(url.searchParams); + + const cookieOpts = { + path: '/', + httpOnly: true, + secure: !dev, + sameSite: 'lax' as const, + maxAge: 60 * 60 * 24 * 180 // 180 days + }; + + setSignedCookie(cookies, 'did', session.did, cookieOpts); + setSignedCookie(cookies, 'scope', scopes.join(' '), cookieOpts); + } catch (e) { + console.error('OAuth callback failed:', e); + redirect(303, '/?error=auth_failed'); + } + + const returnTo = cookies.get('oauth_return_to'); + if (returnTo) { + cookies.delete('oauth_return_to', { path: '/' }); + const decoded = decodeURIComponent(returnTo); + if (decoded.startsWith('/') && !decoded.startsWith('//')) { + redirect(303, decoded); + } + } + + redirect(303, '/'); +}; diff --git a/apps/atproto-starter/src/routes/(oauth)/oauth/jwks.json/+server.ts b/apps/atproto-starter/src/routes/(oauth)/oauth/jwks.json/+server.ts new file mode 100644 index 0000000..ae4dd84 --- /dev/null +++ b/apps/atproto-starter/src/routes/(oauth)/oauth/jwks.json/+server.ts @@ -0,0 +1,8 @@ +import { json } from '@sveltejs/kit'; +import { createOAuthClient } from '$lib/atproto/server/oauth'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ platform }) => { + const oauth = createOAuthClient(platform?.env); + return json(oauth.jwks ?? { keys: [] }); +}; diff --git a/apps/atproto-starter/src/routes/+layout.server.ts b/apps/atproto-starter/src/routes/+layout.server.ts new file mode 100644 index 0000000..72b6c58 --- /dev/null +++ b/apps/atproto-starter/src/routes/+layout.server.ts @@ -0,0 +1,26 @@ +import type { LayoutServerLoad } from './$types'; +import { getServerClient } from '$lib/contrail'; +import { extractProfile } from '$lib/contrail/client'; + +export const load: LayoutServerLoad = async ({ locals, platform }) => { + if (!locals.did || !locals.client) { + return { did: null, profile: null }; + } + + try { + const client = getServerClient(platform!.env.DB); + const res = await client.get('statusphere.app.getProfile', { + params: { actor: locals.did } + }); + + if (!res.ok) return { did: locals.did, profile: null }; + const entry = res.data.profiles?.[0]; + + return { + did: locals.did, + profile: entry ? extractProfile(entry) : null + }; + } catch { + return { did: locals.did, profile: null }; + } +}; diff --git a/apps/atproto-starter/src/routes/+layout.svelte b/apps/atproto-starter/src/routes/+layout.svelte new file mode 100644 index 0000000..3b11797 --- /dev/null +++ b/apps/atproto-starter/src/routes/+layout.svelte @@ -0,0 +1,22 @@ + + +{@render children()} + + + + + + diff --git a/apps/atproto-starter/src/routes/+layout.ts b/apps/atproto-starter/src/routes/+layout.ts new file mode 100644 index 0000000..77ab0a0 --- /dev/null +++ b/apps/atproto-starter/src/routes/+layout.ts @@ -0,0 +1 @@ +export const ssr = true; diff --git a/apps/atproto-starter/src/routes/+page.server.ts b/apps/atproto-starter/src/routes/+page.server.ts new file mode 100644 index 0000000..70c2c94 --- /dev/null +++ b/apps/atproto-starter/src/routes/+page.server.ts @@ -0,0 +1,42 @@ +import { getServerClient } from '$lib/contrail'; +import { extractProfile, type Profile } from '$lib/contrail/client'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ platform }) => { + try { + const client = getServerClient(platform!.env.DB); + + const res = await client.get('statusphere.app.status.listRecords', { + params: { limit: 50, profiles: true, sort: 'createdAt', order: 'desc' } + }); + + if (!res.ok) return { statuses: [], profiles: {} }; + + const records = res.data.records; + const statuses = records + .map((r) => { + const value = r.value as { status: string; createdAt: string }; + return { + did: r.did, + rkey: r.rkey, + status: value.status, + createdAt: value.createdAt + }; + }) + .filter((s) => !isNaN(new Date(s.createdAt).getTime())); + + const profiles: Record = {}; + if (res.data.profiles) { + for (const p of res.data.profiles) { + profiles[p.did] = extractProfile(p); + } + } + + return { statuses, profiles }; + } catch { + return { + statuses: [] as { did: string; rkey: string; status: string; createdAt: string }[], + profiles: {} as Record + }; + } +}; diff --git a/apps/atproto-starter/src/routes/+page.svelte b/apps/atproto-starter/src/routes/+page.svelte new file mode 100644 index 0000000..2129ed5 --- /dev/null +++ b/apps/atproto-starter/src/routes/+page.svelte @@ -0,0 +1,190 @@ + + +
+

svelte + cloudflare workers statusphere

+ + + + {#if !user.isLoggedIn} + + {:else} +
+ { + const rkey = createTID(); + const createdAt = new Date().toISOString(); + const key = `${user.did!}-${rkey}`; + seenKeys.add(key); + localStatuses = [ + { did: user.did!, rkey, status: emoji.unicode, createdAt }, + ...localStatuses + ]; + open = false; + await putRecord({ + rkey, + collection: 'xyz.statusphere.status', + record: { + status: emoji.unicode, + createdAt + } + }); + }} + bind:open + > + {#snippet child({ props })} + + {/snippet} + + +
+ {/if} + + {#if allStatuses.length > 0} +
    + {#each allStatuses as status, i (`${status.did}-${status.rkey}`)} + {@const profile = + profiles[status.did] ?? + (status.did === user.did && data.profile + ? { displayName: data.profile.displayName, handle: data.profile.handle } + : null)} + {@const animated = emojiToNotoAnimatedWebp(status.status)} +
  • +
    +
    + {#if animated} + {#if i === 0} + {status.status} + {:else} + {status.status} + {/if} + {:else if i === 0} + {status.status} + {:else} + {status.status} + {/if} +
    + {#if i < allStatuses.length - 1} +
    + {/if} +
    +
    + {#if profile} + {profile.displayName || profile.handle} + {:else} + {status.did.slice(0, 20)}... + {/if} + · + + + +
    +
  • + {/each} +
+ {/if} +
diff --git a/apps/atproto-starter/src/routes/api/cron/+server.ts b/apps/atproto-starter/src/routes/api/cron/+server.ts new file mode 100644 index 0000000..77ff19e --- /dev/null +++ b/apps/atproto-starter/src/routes/api/cron/+server.ts @@ -0,0 +1,15 @@ +import { contrail, ensureInit } from '$lib/contrail'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ request, platform }) => { + const secret = request.headers.get('X-Cron-Secret'); + if (secret !== platform!.env.CRON_SECRET) { + return new Response('Unauthorized', { status: 401 }); + } + + const db = platform!.env.DB; + await ensureInit(db); + await contrail.ingest({}, db); + + return new Response('OK'); +}; diff --git a/apps/atproto-starter/src/routes/xrpc/[...path]/+server.ts b/apps/atproto-starter/src/routes/xrpc/[...path]/+server.ts new file mode 100644 index 0000000..400502c --- /dev/null +++ b/apps/atproto-starter/src/routes/xrpc/[...path]/+server.ts @@ -0,0 +1,14 @@ +import { createHandler } from '@atmo-dev/contrail/server'; +import { contrail, ensureInit } from '$lib/contrail'; +import type { RequestHandler } from './$types'; + +const handle = createHandler(contrail); + +async function handler(request: Request, platform: App.Platform | undefined) { + const db = platform!.env.DB; + await ensureInit(db); + return handle(request, db) as Promise; +} + +export const GET: RequestHandler = async ({ request, platform }) => handler(request, platform); +export const POST: RequestHandler = async ({ request, platform }) => handler(request, platform); diff --git a/apps/atproto-starter/static/favicon.png b/apps/atproto-starter/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmeAS@N?(olHy`uVBq!ia0y~yU}ykg4i*LmhD`I74h#$ojKx9jP7LeL$-HD>V7=?< z;uunK>uv1q+}9QYp`3aLRhP6(c_0+yMGvW%`8w9Z4+AO$r=LiV^}WjuyMS z-`u@3yY$$SKDCAN>H9tj&u}g}XS4E2pY^*PkNZ3lroTLubH?oMf4#J9vzMpzCckct zowYnCeNLl>-tC#oR|mW7^83!__-WF*w@N!}ZnWO)*sV9e?K!tS_p_50%zk=doa_6a z9Ze7`Oq(1d_U2FyZ`xGx-xFfEE={d84w$f9rS697HscGE-dGl;{V=G?`XscL>&++e z>-U#%oq6{6`Q0U4b%OVNS0!jYj1*aM;g7Z{*A0W>9o{wqst=zQyeQ;ij+$byPs!T7 zP$;gE@43N(Z#KN^`%g~qnDC*fQ@9u5T z`010b%(4WBW*Igy)jqkB43?QUzqIP?pR?n;uR)>M{Z!UJzVqjLRQx?D9Xlud=JZMH zM4Dt}4@n0eu2Ixgk=cKk=N7x!uIU#Qr)QK{iRNADxA(7D>^{r+(1Q28%d+>r`1fM! z3!#PnEzgzrWhp(JYGANY`@x@o);^8RydN}I>CasHT;#z0|7n-*i5nc8wD_Qhgrn&1 z_=)HIc7)82SrZn{n5O;yq=&Wj!=(QfoT>{MZZVX#Y(9Ll{di*M8HGwlu2VN3EWtQvP&QN2PVR8N7!YC#E{m*$kY8i3^zQiv$JVzqYQu{#r;icEl z=UhCnkiGRom%yd1b*>!Dbz(=$xA=OCK9sWW?kjHnIi=(8N8<^v!`m)TWlU#3bX%EA zWW|L?Hd)uCpL{-aJW{t>_loq&RRZ@V7u56iZFu;9u7AgewTA2m_dYzmg!LfHN%jio z6)Tz8tu4PtytF=2_HyzK35Her8CehRz9{eceYP>8gyZOsf1e+R9eAyLo&U`AQ!>jL z>i##s-geN-*K&%b<6Sll}2Yvce^~*xG<6XX?1V3gp#~N zkiz|sN2i}Ey4w@5>yt(|hpKnhq#5tcY68zPo5(r8-F4X1_Ok2&?FGW>zrU(?x;Q#c?bx4-5dPgKtZr*@mZ=`3%X zy6%Z`^e5Hr^Y+>AzJm9>p2Z^BF4@_(4b1il6W?8xzhU;SdYz;2?R&rX$y{t=F!Y`E zR$}w<@9ihF9(X_5v`6rxTqRqD=!eToZeQ5@{QQC0#~w>6ukx<+wTnGF|CY_6xJ%zz zHr<`$x>nEcQ1ZHi?IAA|7rHVEt3Nz@U3=2vrr*f|MGoxkLSkxhhdzJg^6Z@Zq2`!6 zN9X(x&t6IHdUbyJoZ{}h2h3KD+a6rvIIkc7=B91g%$DcR#pk?L*qd_b^D~z6tu4>l zcb}QJe%gmWZyk5-zxe)Gtrh1xH}U_oBV*EcOaCc*(s}2g+Fj}Irl)7WovQcub7w{E upWAk8&OVF%_w#jD-*4ISJB6Y@xbv5oeJi^*U66r+fx*+&&t;ucLK6TBI`Z29 literal 0 HcmV?d00001 diff --git a/apps/atproto-starter/static/og.png b/apps/atproto-starter/static/og.png new file mode 100644 index 0000000000000000000000000000000000000000..62cbbc5fad01c8f7605857bca64b1066af12afb8 GIT binary patch literal 61667 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�ci>6UV9@evpHD#sN& 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%0A`;->j7qD~&v#J~BQHokhjx4uXF@H)94@80$Q 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