Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto

Merge pull request #27 from teal-fm/lexicons

Add lexicons!

authored by natalie and committed by GitHub 73d54df3 59b762f4

Changed files
+843 -14
apps
aqua
packages
+1 -1
apps/aqua/src/auth/client.ts
··· 15 15 ? `${url}/client-metadata.json` 16 16 : `http://localhost?redirect_uri=${enc(`${url}/oauth/callback`)}&scope=${enc("atproto transition:generic")}`, 17 17 client_uri: url, 18 - redirect_uris: [`${url}/oauth/callback`], 18 + redirect_uris: [`${url}/oauth/callback`, `${url}/oauth/callback/app`], 19 19 scope: "atproto transition:generic", 20 20 grant_types: ["authorization_code", "refresh_token"], 21 21 response_types: ["code"],
+39 -3
apps/aqua/src/auth/router.ts
··· 8 8 import { setCookie } from "hono/cookie"; 9 9 import { env } from "@/lib/env"; 10 10 11 - export async function callback(c: TealContext) { 11 + const publicUrl = env.PUBLIC_URL; 12 + const redirectBase = publicUrl || `http://127.0.0.1:${env.PORT}`; 13 + 14 + export function generateState(prefix?: string) { 15 + const state = crypto.randomUUID(); 16 + return `${prefix}${prefix ? ":" : ""}${state}`; 17 + } 18 + 19 + const SPA_PREFIX = "a37d"; 20 + 21 + // /oauth/login?handle=teal.fm 22 + export async function login(c: TealContext) { 23 + const { handle, spa } = c.req.query(); 24 + if (!handle) { 25 + return Response.json({ error: "Missing handle" }); 26 + } 27 + const url = await atclient.authorize(handle, { 28 + scope: "atproto transition:generic", 29 + // state.appState in callback 30 + state: generateState(spa ? SPA_PREFIX : undefined), 31 + }); 32 + return c.json({ url }); 33 + } 34 + 35 + // Redirect to the app's callback URL. 36 + async function callbackToApp(c: TealContext) { 37 + const queries = c.req.query(); 38 + const params = new URLSearchParams(queries); 39 + return c.redirect(`${env.APP_URI}/oauth/callback?${params.toString()}`); 40 + } 41 + 42 + export async function callback(c: TealContext, isSpa: boolean = false) { 12 43 try { 13 44 const honoParams = c.req.query(); 14 45 console.log("params", honoParams); 15 46 const params = new URLSearchParams(honoParams); 16 - const { session } = await atclient.callback(params); 47 + 48 + const { session, state } = await atclient.callback(params); 49 + 50 + console.log("state", state); 17 51 18 52 const did = session.did; 19 53 ··· 42 76 maxAge: 60 * 60 * 24 * 365, 43 77 }); 44 78 45 - if (params.get("spa")) { 79 + if (isSpa) { 46 80 return c.json({ 47 81 provider: "atproto", 48 82 jwt: did, ··· 122 156 123 157 const app = new Hono<EnvWithCtx>(); 124 158 159 + app.get("/login", async (c) => login(c)); 125 160 app.get("/callback", async (c) => callback(c)); 161 + app.get("/callback/app", async (c) => callback(c, true)); 126 162 app.get("/refresh", async (c) => refresh(c)); 127 163 128 164 export const getAuthRouter = () => {
-1
apps/aqua/src/auth/storage.ts
··· 17 17 .where(eq(authState.key, key)) 18 18 .limit(1) 19 19 .execute(); 20 - console.log("getting state", key, result); 21 20 if (!result[0]) return; 22 21 return JSON.parse(result[0].state) as NodeSavedState; 23 22 }
+1
apps/aqua/src/lib/env.ts
··· 14 14 HOST: host({ devDefault: testOnly("0.0.0.0") }), 15 15 PORT: port({ devDefault: testOnly(3000) }), 16 16 PUBLIC_URL: str({}), 17 + APP_URI: str({ devDefault: "fm.teal.amethyst://" }), 17 18 DB_PATH: str({ devDefault: "file:./db.sqlite" }), 18 19 COOKIE_SECRET: str({ devDefault: "secret_cookie! very secret!" }), 19 20 });
bun.lockb

This is a binary file and will not be displayed.

+1 -1
package.json
··· 16 16 "db:seed": "drizzle-kit seed" 17 17 }, 18 18 "devDependencies": { 19 - "turbo": "^2.3.0" 19 + "turbo": "^2.3.1" 20 20 }, 21 21 "engines": { 22 22 "node": ">=21.0.0"
+24 -8
packages/jetstring/src/index.ts
··· 3 3 import { status } from "@teal/db/schema"; 4 4 import { CommitCreateEvent, Jetstream } from "@skyware/jetstream"; 5 5 6 + import { 7 + Record as XyzStatusphereStatus, 8 + isRecord as isStatusphereStatus, 9 + } from "@teal/lexicons/generated/server/types/xyz/statusphere/status"; 10 + 6 11 class Handler { 7 12 private static instance: Handler; 8 13 private constructor() {} ··· 13 18 return Handler.instance; 14 19 } 15 20 16 - handle(msg_type: string, msg: any) { 21 + handle(msg_type: string, record: CommitCreateEvent<string & {}>) { 17 22 // Handle message logic here 23 + const msg = record.commit.record; 18 24 console.log("Handling" + msg_type + "message:", msg); 19 - if (msg_type === "xyz.statusphere.status") { 20 - // serialize message as xyz.statusphere.status 21 - const st = db.insert(status).values({ 22 - status: msg.status, 23 - uri: msg.uri, 24 - authorDid: msg.authorDid, 25 - }); 25 + if (isStatusphereStatus(msg) && msg.$type === "xyz.statusphere.status") { 26 + if (record.commit.operation === "create") { 27 + // serialize message as xyz.statusphere.status 28 + db.insert(status).values({ 29 + createdAt: new Date().getSeconds().toString(), 30 + indexedAt: new Date(record.time_us).getSeconds().toString(), 31 + status: msg.status, 32 + // the AT path 33 + uri: record.commit.rkey, 34 + authorDid: record.did, 35 + }); 36 + } else { 37 + console.log("unsupported operation:", record.commit.operation); 38 + } 39 + } else { 40 + console.log("Unknown message type:", msg_type); 41 + console.log("Message:", record); 26 42 } 27 43 } 28 44 }
+50
packages/lexicons/generated/server/index.ts
··· 17 17 export class Server { 18 18 xrpc: XrpcServer 19 19 app: AppNS 20 + fm: FmNS 20 21 xyz: XyzNS 21 22 22 23 constructor(options?: XrpcOptions) { 23 24 this.xrpc = createXrpcServer(schemas, options) 24 25 this.app = new AppNS(this) 26 + this.fm = new FmNS(this) 25 27 this.xyz = new XyzNS(this) 26 28 } 27 29 } ··· 39 41 export class AppBskyNS { 40 42 _server: Server 41 43 actor: AppBskyActorNS 44 + richtext: AppBskyRichtextNS 42 45 43 46 constructor(server: Server) { 44 47 this._server = server 45 48 this.actor = new AppBskyActorNS(server) 49 + this.richtext = new AppBskyRichtextNS(server) 46 50 } 47 51 } 48 52 49 53 export class AppBskyActorNS { 54 + _server: Server 55 + 56 + constructor(server: Server) { 57 + this._server = server 58 + } 59 + } 60 + 61 + export class AppBskyRichtextNS { 62 + _server: Server 63 + 64 + constructor(server: Server) { 65 + this._server = server 66 + } 67 + } 68 + 69 + export class FmNS { 70 + _server: Server 71 + teal: FmTealNS 72 + 73 + constructor(server: Server) { 74 + this._server = server 75 + this.teal = new FmTealNS(server) 76 + } 77 + } 78 + 79 + export class FmTealNS { 80 + _server: Server 81 + alpha: FmTealAlphaNS 82 + 83 + constructor(server: Server) { 84 + this._server = server 85 + this.alpha = new FmTealAlphaNS(server) 86 + } 87 + } 88 + 89 + export class FmTealAlphaNS { 90 + _server: Server 91 + actor: FmTealAlphaActorNS 92 + 93 + constructor(server: Server) { 94 + this._server = server 95 + this.actor = new FmTealAlphaActorNS(server) 96 + } 97 + } 98 + 99 + export class FmTealAlphaActorNS { 50 100 _server: Server 51 101 52 102 constructor(server: Server) {
+274
packages/lexicons/generated/server/lexicons.ts
··· 59 59 }, 60 60 }, 61 61 }, 62 + AppBskyRichtextFacet: { 63 + lexicon: 1, 64 + id: 'app.bsky.richtext.facet', 65 + defs: { 66 + main: { 67 + type: 'object', 68 + description: 'Annotation of a sub-string within rich text.', 69 + required: ['index', 'features'], 70 + properties: { 71 + index: { 72 + type: 'ref', 73 + ref: 'lex:app.bsky.richtext.facet#byteSlice', 74 + }, 75 + features: { 76 + type: 'array', 77 + items: { 78 + type: 'union', 79 + refs: [ 80 + 'lex:app.bsky.richtext.facet#mention', 81 + 'lex:app.bsky.richtext.facet#link', 82 + 'lex:app.bsky.richtext.facet#tag', 83 + ], 84 + }, 85 + }, 86 + }, 87 + }, 88 + mention: { 89 + type: 'object', 90 + description: 91 + "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.", 92 + required: ['did'], 93 + properties: { 94 + did: { 95 + type: 'string', 96 + format: 'did', 97 + }, 98 + }, 99 + }, 100 + link: { 101 + type: 'object', 102 + description: 103 + 'Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.', 104 + required: ['uri'], 105 + properties: { 106 + uri: { 107 + type: 'string', 108 + format: 'uri', 109 + }, 110 + }, 111 + }, 112 + tag: { 113 + type: 'object', 114 + description: 115 + "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').", 116 + required: ['tag'], 117 + properties: { 118 + tag: { 119 + type: 'string', 120 + maxLength: 640, 121 + maxGraphemes: 64, 122 + }, 123 + }, 124 + }, 125 + byteSlice: { 126 + type: 'object', 127 + description: 128 + 'Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.', 129 + required: ['byteStart', 'byteEnd'], 130 + properties: { 131 + byteStart: { 132 + type: 'integer', 133 + minimum: 0, 134 + }, 135 + byteEnd: { 136 + type: 'integer', 137 + minimum: 0, 138 + }, 139 + }, 140 + }, 141 + }, 142 + }, 143 + FmTealAlphaActorProfile: { 144 + lexicon: 1, 145 + id: 'fm.teal.alpha.actor.profile', 146 + defs: { 147 + main: { 148 + type: 'record', 149 + description: 150 + 'This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm account profile.', 151 + key: 'literal:self', 152 + record: { 153 + type: 'object', 154 + properties: { 155 + displayName: { 156 + type: 'string', 157 + maxGraphemes: 64, 158 + maxLength: 640, 159 + }, 160 + description: { 161 + type: 'string', 162 + description: 'Free-form profile description text.', 163 + maxGraphemes: 256, 164 + maxLength: 2560, 165 + }, 166 + descriptionFacets: { 167 + type: 'array', 168 + description: 169 + 'Annotations of text in the profile description (mentions, URLs, hashtags, etc).', 170 + items: { 171 + type: 'ref', 172 + ref: 'lex:app.bsky.richtext.facet', 173 + }, 174 + }, 175 + featuredItem: { 176 + type: 'ref', 177 + description: 178 + "The user's most recent item featured on their profile.", 179 + ref: 'lex:fm.teal.alpha.actor.profile#featuredItem', 180 + }, 181 + avatar: { 182 + type: 'blob', 183 + description: 184 + "Small image to be displayed next to posts from account. AKA, 'profile picture'", 185 + accept: ['image/png', 'image/jpeg'], 186 + maxSize: 1000000, 187 + }, 188 + banner: { 189 + type: 'blob', 190 + description: 191 + 'Larger horizontal image to display behind profile view.', 192 + accept: ['image/png', 'image/jpeg'], 193 + maxSize: 1000000, 194 + }, 195 + createdAt: { 196 + type: 'string', 197 + format: 'datetime', 198 + }, 199 + }, 200 + }, 201 + }, 202 + featuredItem: { 203 + type: 'object', 204 + required: ['mbid', 'type'], 205 + properties: { 206 + mbid: { 207 + type: 'string', 208 + description: 'The Musicbrainz ID of the item', 209 + }, 210 + type: { 211 + type: 'string', 212 + description: 213 + 'The type of the item. Must be a valid Musicbrainz type, e.g. album, track, recording, etc.', 214 + }, 215 + }, 216 + }, 217 + }, 218 + }, 219 + FmTealAlphaActorStatus: { 220 + lexicon: 1, 221 + id: 'fm.teal.alpha.actor.status', 222 + defs: { 223 + main: { 224 + type: 'record', 225 + description: 226 + 'This lexicon is in a not officially released state. It is subject to change. | A declaration of the status of the actor. Only one can be shown at a time. If there are multiple, the latest record should be picked and earlier records should be deleted or tombstoned.', 227 + key: 'literal:self', 228 + record: { 229 + type: 'object', 230 + required: ['time', 'item'], 231 + properties: { 232 + time: { 233 + type: 'string', 234 + format: 'datetime', 235 + description: 'The unix timestamp of when the item was recorded', 236 + }, 237 + item: { 238 + type: 'union', 239 + refs: ['lex:fm.teal.alpha.play#record'], 240 + }, 241 + }, 242 + }, 243 + }, 244 + }, 245 + }, 246 + FmTealAlphaPlay: { 247 + lexicon: 1, 248 + id: 'fm.teal.alpha.play', 249 + description: 250 + "This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm play. Plays are submitted as a result of a user listening to a track. Plays should be marked as tracked when a user has listened to the entire track if it's under 2 minutes long, or half of the track's duration up to 4 minutes, whichever is longest.", 251 + defs: { 252 + main: { 253 + type: 'record', 254 + key: 'tid', 255 + record: { 256 + type: 'object', 257 + required: ['trackName', 'artistName'], 258 + properties: { 259 + trackName: { 260 + type: 'string', 261 + minLength: 1, 262 + maxLength: 256, 263 + maxGraphemes: 2560, 264 + description: 'The name of the track', 265 + }, 266 + trackMbId: { 267 + type: 'string', 268 + description: 'The Musicbrainz ID of the track', 269 + }, 270 + recordingMbId: { 271 + type: 'string', 272 + description: 'The Musicbrainz recording ID of the track', 273 + }, 274 + duration: { 275 + type: 'integer', 276 + description: 'The length of the track in seconds', 277 + }, 278 + artistName: { 279 + type: 'string', 280 + minLength: 1, 281 + maxLength: 256, 282 + maxGraphemes: 2560, 283 + description: 'The name of the artist', 284 + }, 285 + artistMbIds: { 286 + type: 'array', 287 + items: { 288 + type: 'string', 289 + }, 290 + description: 'Array of Musicbrainz artist IDs', 291 + }, 292 + releaseName: { 293 + type: 'string', 294 + maxLength: 256, 295 + maxGraphemes: 2560, 296 + description: 'The name of the release/album', 297 + }, 298 + releaseMbId: { 299 + type: 'string', 300 + description: 'The Musicbrainz release ID', 301 + }, 302 + isrc: { 303 + type: 'string', 304 + description: 'The ISRC code associated with the recording', 305 + }, 306 + originUrl: { 307 + type: 'string', 308 + description: 'The URL associated with this track', 309 + }, 310 + musicServiceBaseDomain: { 311 + type: 'string', 312 + description: 313 + 'The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com.', 314 + }, 315 + submissionClientAgent: { 316 + type: 'string', 317 + maxLength: 256, 318 + maxGraphemes: 2560, 319 + description: 320 + 'A user-agent style string specifying the user agent. e.g. tealtracker/0.0.1b', 321 + }, 322 + playedTime: { 323 + type: 'string', 324 + format: 'datetime', 325 + description: 'The unix timestamp of when the track was played', 326 + }, 327 + }, 328 + }, 329 + }, 330 + }, 331 + }, 62 332 XyzStatusphereStatus: { 63 333 lexicon: 1, 64 334 id: 'xyz.statusphere.status', ··· 90 360 export const lexicons: Lexicons = new Lexicons(schemas) 91 361 export const ids = { 92 362 AppBskyActorProfile: 'app.bsky.actor.profile', 363 + AppBskyRichtextFacet: 'app.bsky.richtext.facet', 364 + FmTealAlphaActorProfile: 'fm.teal.alpha.actor.profile', 365 + FmTealAlphaActorStatus: 'fm.teal.alpha.actor.status', 366 + FmTealAlphaPlay: 'fm.teal.alpha.play', 93 367 XyzStatusphereStatus: 'xyz.statusphere.status', 94 368 }
+98
packages/lexicons/generated/server/types/app/bsky/richtext/facet.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { lexicons } from '../../../../lexicons' 6 + import { isObj, hasProp } from '../../../../util' 7 + import { CID } from 'multiformats/cid' 8 + 9 + /** Annotation of a sub-string within rich text. */ 10 + export interface Main { 11 + index: ByteSlice 12 + features: (Mention | Link | Tag | { $type: string; [k: string]: unknown })[] 13 + [k: string]: unknown 14 + } 15 + 16 + export function isMain(v: unknown): v is Main { 17 + return ( 18 + isObj(v) && 19 + hasProp(v, '$type') && 20 + (v.$type === 'app.bsky.richtext.facet#main' || 21 + v.$type === 'app.bsky.richtext.facet') 22 + ) 23 + } 24 + 25 + export function validateMain(v: unknown): ValidationResult { 26 + return lexicons.validate('app.bsky.richtext.facet#main', v) 27 + } 28 + 29 + /** Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. */ 30 + export interface Mention { 31 + did: string 32 + [k: string]: unknown 33 + } 34 + 35 + export function isMention(v: unknown): v is Mention { 36 + return ( 37 + isObj(v) && 38 + hasProp(v, '$type') && 39 + v.$type === 'app.bsky.richtext.facet#mention' 40 + ) 41 + } 42 + 43 + export function validateMention(v: unknown): ValidationResult { 44 + return lexicons.validate('app.bsky.richtext.facet#mention', v) 45 + } 46 + 47 + /** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. */ 48 + export interface Link { 49 + uri: string 50 + [k: string]: unknown 51 + } 52 + 53 + export function isLink(v: unknown): v is Link { 54 + return ( 55 + isObj(v) && 56 + hasProp(v, '$type') && 57 + v.$type === 'app.bsky.richtext.facet#link' 58 + ) 59 + } 60 + 61 + export function validateLink(v: unknown): ValidationResult { 62 + return lexicons.validate('app.bsky.richtext.facet#link', v) 63 + } 64 + 65 + /** Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags'). */ 66 + export interface Tag { 67 + tag: string 68 + [k: string]: unknown 69 + } 70 + 71 + export function isTag(v: unknown): v is Tag { 72 + return ( 73 + isObj(v) && hasProp(v, '$type') && v.$type === 'app.bsky.richtext.facet#tag' 74 + ) 75 + } 76 + 77 + export function validateTag(v: unknown): ValidationResult { 78 + return lexicons.validate('app.bsky.richtext.facet#tag', v) 79 + } 80 + 81 + /** Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. */ 82 + export interface ByteSlice { 83 + byteStart: number 84 + byteEnd: number 85 + [k: string]: unknown 86 + } 87 + 88 + export function isByteSlice(v: unknown): v is ByteSlice { 89 + return ( 90 + isObj(v) && 91 + hasProp(v, '$type') && 92 + v.$type === 'app.bsky.richtext.facet#byteSlice' 93 + ) 94 + } 95 + 96 + export function validateByteSlice(v: unknown): ValidationResult { 97 + return lexicons.validate('app.bsky.richtext.facet#byteSlice', v) 98 + }
+56
packages/lexicons/generated/server/types/fm/teal/alpha/actor/profile.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { lexicons } from '../../../../../lexicons' 6 + import { isObj, hasProp } from '../../../../../util' 7 + import { CID } from 'multiformats/cid' 8 + import * as AppBskyRichtextFacet from '../../../../app/bsky/richtext/facet' 9 + 10 + export interface Record { 11 + displayName?: string 12 + /** Free-form profile description text. */ 13 + description?: string 14 + /** Annotations of text in the profile description (mentions, URLs, hashtags, etc). */ 15 + descriptionFacets?: AppBskyRichtextFacet.Main[] 16 + featuredItem?: FeaturedItem 17 + /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ 18 + avatar?: BlobRef 19 + /** Larger horizontal image to display behind profile view. */ 20 + banner?: BlobRef 21 + createdAt?: string 22 + [k: string]: unknown 23 + } 24 + 25 + export function isRecord(v: unknown): v is Record { 26 + return ( 27 + isObj(v) && 28 + hasProp(v, '$type') && 29 + (v.$type === 'fm.teal.alpha.actor.profile#main' || 30 + v.$type === 'fm.teal.alpha.actor.profile') 31 + ) 32 + } 33 + 34 + export function validateRecord(v: unknown): ValidationResult { 35 + return lexicons.validate('fm.teal.alpha.actor.profile#main', v) 36 + } 37 + 38 + export interface FeaturedItem { 39 + /** The Musicbrainz ID of the item */ 40 + mbid: string 41 + /** The type of the item. Must be a valid Musicbrainz type, e.g. album, track, recording, etc. */ 42 + type: string 43 + [k: string]: unknown 44 + } 45 + 46 + export function isFeaturedItem(v: unknown): v is FeaturedItem { 47 + return ( 48 + isObj(v) && 49 + hasProp(v, '$type') && 50 + v.$type === 'fm.teal.alpha.actor.profile#featuredItem' 51 + ) 52 + } 53 + 54 + export function validateFeaturedItem(v: unknown): ValidationResult { 55 + return lexicons.validate('fm.teal.alpha.actor.profile#featuredItem', v) 56 + }
+28
packages/lexicons/generated/server/types/fm/teal/alpha/actor/status.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { lexicons } from '../../../../../lexicons' 6 + import { isObj, hasProp } from '../../../../../util' 7 + import { CID } from 'multiformats/cid' 8 + import * as FmTealAlphaPlay from '../play' 9 + 10 + export interface Record { 11 + /** The unix timestamp of when the item was recorded */ 12 + time: string 13 + item: FmTealAlphaPlay.Record | { $type: string; [k: string]: unknown } 14 + [k: string]: unknown 15 + } 16 + 17 + export function isRecord(v: unknown): v is Record { 18 + return ( 19 + isObj(v) && 20 + hasProp(v, '$type') && 21 + (v.$type === 'fm.teal.alpha.actor.status#main' || 22 + v.$type === 'fm.teal.alpha.actor.status') 23 + ) 24 + } 25 + 26 + export function validateRecord(v: unknown): ValidationResult { 27 + return lexicons.validate('fm.teal.alpha.actor.status#main', v) 28 + }
+49
packages/lexicons/generated/server/types/fm/teal/alpha/play.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { lexicons } from '../../../../lexicons' 6 + import { isObj, hasProp } from '../../../../util' 7 + import { CID } from 'multiformats/cid' 8 + 9 + export interface Record { 10 + /** The name of the track */ 11 + trackName: string 12 + /** The Musicbrainz ID of the track */ 13 + trackMbId?: string 14 + /** The Musicbrainz recording ID of the track */ 15 + recordingMbId?: string 16 + /** The length of the track in seconds */ 17 + duration?: number 18 + /** The name of the artist */ 19 + artistName: string 20 + /** Array of Musicbrainz artist IDs */ 21 + artistMbIds?: string[] 22 + /** The name of the release/album */ 23 + releaseName?: string 24 + /** The Musicbrainz release ID */ 25 + releaseMbId?: string 26 + /** The ISRC code associated with the recording */ 27 + isrc?: string 28 + /** The URL associated with this track */ 29 + originUrl?: string 30 + /** The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com. */ 31 + musicServiceBaseDomain?: string 32 + /** A user-agent style string specifying the user agent. e.g. tealtracker/0.0.1b */ 33 + submissionClientAgent?: string 34 + /** The unix timestamp of when the track was played */ 35 + playedTime?: string 36 + [k: string]: unknown 37 + } 38 + 39 + export function isRecord(v: unknown): v is Record { 40 + return ( 41 + isObj(v) && 42 + hasProp(v, '$type') && 43 + (v.$type === 'fm.teal.alpha.play#main' || v.$type === 'fm.teal.alpha.play') 44 + ) 45 + } 46 + 47 + export function validateRecord(v: unknown): ValidationResult { 48 + return lexicons.validate('fm.teal.alpha.play#main', v) 49 + }
+51
packages/lexicons/src/app.bsky.richtext.facet.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.richtext.facet", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "description": "Annotation of a sub-string within rich text.", 8 + "required": ["index", "features"], 9 + "properties": { 10 + "index": { "type": "ref", "ref": "#byteSlice" }, 11 + "features": { 12 + "type": "array", 13 + "items": { "type": "union", "refs": ["#mention", "#link", "#tag"] } 14 + } 15 + } 16 + }, 17 + "mention": { 18 + "type": "object", 19 + "description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.", 20 + "required": ["did"], 21 + "properties": { 22 + "did": { "type": "string", "format": "did" } 23 + } 24 + }, 25 + "link": { 26 + "type": "object", 27 + "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.", 28 + "required": ["uri"], 29 + "properties": { 30 + "uri": { "type": "string", "format": "uri" } 31 + } 32 + }, 33 + "tag": { 34 + "type": "object", 35 + "description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').", 36 + "required": ["tag"], 37 + "properties": { 38 + "tag": { "type": "string", "maxLength": 640, "maxGraphemes": 64 } 39 + } 40 + }, 41 + "byteSlice": { 42 + "type": "object", 43 + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.", 44 + "required": ["byteStart", "byteEnd"], 45 + "properties": { 46 + "byteStart": { "type": "integer", "minimum": 0 }, 47 + "byteEnd": { "type": "integer", "minimum": 0 } 48 + } 49 + } 50 + } 51 + }
+64
packages/lexicons/src/fm.teal.alpha.actor.profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm account profile.", 8 + "key": "literal:self", 9 + "record": { 10 + "type": "object", 11 + "properties": { 12 + "displayName": { 13 + "type": "string", 14 + "maxGraphemes": 64, 15 + "maxLength": 640 16 + }, 17 + "description": { 18 + "type": "string", 19 + "description": "Free-form profile description text.", 20 + "maxGraphemes": 256, 21 + "maxLength": 2560 22 + }, 23 + "descriptionFacets": { 24 + "type": "array", 25 + "description": "Annotations of text in the profile description (mentions, URLs, hashtags, etc).", 26 + "items": { "type": "ref", "ref": "app.bsky.richtext.facet" } 27 + }, 28 + "featuredItem": { 29 + "type": "ref", 30 + "description": "The user's most recent item featured on their profile.", 31 + "ref": "#featuredItem" 32 + }, 33 + "avatar": { 34 + "type": "blob", 35 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'", 36 + "accept": ["image/png", "image/jpeg"], 37 + "maxSize": 1000000 38 + }, 39 + "banner": { 40 + "type": "blob", 41 + "description": "Larger horizontal image to display behind profile view.", 42 + "accept": ["image/png", "image/jpeg"], 43 + "maxSize": 1000000 44 + }, 45 + "createdAt": { "type": "string", "format": "datetime" } 46 + } 47 + } 48 + }, 49 + "featuredItem": { 50 + "type": "object", 51 + "required": ["mbid", "type"], 52 + "properties": { 53 + "mbid": { 54 + "type": "string", 55 + "description": "The Musicbrainz ID of the item" 56 + }, 57 + "type": { 58 + "type": "string", 59 + "description": "The type of the item. Must be a valid Musicbrainz type, e.g. album, track, recording, etc." 60 + } 61 + } 62 + } 63 + } 64 + }
+23
packages/lexicons/src/fm.teal.alpha.actor.status.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.actor.status", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "This lexicon is in a not officially released state. It is subject to change. | A declaration of the status of the actor. Only one can be shown at a time. If there are multiple, the latest record should be picked and earlier records should be deleted or tombstoned.", 8 + "key": "literal:self", 9 + "record": { 10 + "type": "object", 11 + "required": ["time", "item"], 12 + "properties": { 13 + "time": { 14 + "type": "string", 15 + "format": "datetime", 16 + "description": "The unix timestamp of when the item was recorded" 17 + }, 18 + "item": { "type": "union", "refs": ["fm.teal.alpha.play#record"] } 19 + } 20 + } 21 + } 22 + } 23 + }
+84
packages/lexicons/src/fm.teal.alpha.play.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "fm.teal.alpha.play", 4 + "description": "This lexicon is in a not officially released state. It is subject to change. | A declaration of a teal.fm play. Plays are submitted as a result of a user listening to a track. Plays should be marked as tracked when a user has listened to the entire track if it's under 2 minutes long, or half of the track's duration up to 4 minutes, whichever is longest.", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["trackName", "artistName"], 12 + "properties": { 13 + "trackName": { 14 + "type": "string", 15 + "minLength": 1, 16 + "maxLength": 256, 17 + "maxGraphemes": 2560, 18 + "description": "The name of the track" 19 + }, 20 + "trackMbId": { 21 + "type": "string", 22 + 23 + "description": "The Musicbrainz ID of the track" 24 + }, 25 + "recordingMbId": { 26 + "type": "string", 27 + "description": "The Musicbrainz recording ID of the track" 28 + }, 29 + "duration": { 30 + "type": "integer", 31 + "description": "The length of the track in seconds" 32 + }, 33 + "artistName": { 34 + "type": "string", 35 + "minLength": 1, 36 + "maxLength": 256, 37 + "maxGraphemes": 2560, 38 + "description": "The name of the artist" 39 + }, 40 + "artistMbIds": { 41 + "type": "array", 42 + "items": { 43 + "type": "string" 44 + }, 45 + "description": "Array of Musicbrainz artist IDs" 46 + }, 47 + "releaseName": { 48 + "type": "string", 49 + "maxLength": 256, 50 + "maxGraphemes": 2560, 51 + "description": "The name of the release/album" 52 + }, 53 + "releaseMbId": { 54 + "type": "string", 55 + "description": "The Musicbrainz release ID" 56 + }, 57 + "isrc": { 58 + "type": "string", 59 + "description": "The ISRC code associated with the recording" 60 + }, 61 + "originUrl": { 62 + "type": "string", 63 + "description": "The URL associated with this track" 64 + }, 65 + "musicServiceBaseDomain": { 66 + "type": "string", 67 + "description": "The base domain of the music service. e.g. music.apple.com, tidal.com, spotify.com." 68 + }, 69 + "submissionClientAgent": { 70 + "type": "string", 71 + "maxLength": 256, 72 + "maxGraphemes": 2560, 73 + "description": "A user-agent style string specifying the user agent. e.g. tealtracker/0.0.1b" 74 + }, 75 + "playedTime": { 76 + "type": "string", 77 + "format": "datetime", 78 + "description": "The unix timestamp of when the track was played" 79 + } 80 + } 81 + } 82 + } 83 + } 84 + }