···5566huge thanks to [Microcosm](https://microcosm.blue/) for making this possible
7788+issue tracker kanban board: [https://github.com/users/rimar1337/projects/1/views/1]https://github.com/users/rimar1337/projects/1/views/1
99+810## running dev and build
911in the `vite.config.ts` file you should change these values
1012```ts
···3032all core data fetching logic is now centralized in `src/utils/useQuery.ts` and exposed as a collection of custom react hooks. theres two basic types of custom hooks, the use-once, and the inifinite query ones (used for paginated requests like feed skeletons and listrecord)
31333234## UniversalPostRenderer
3535+> [!NOTE]
3636+> UPR is undergoing a refactor, so this info might be out of date
3737+3338its a mega component rooted in my Masonry "[TestFront](https://testfront-87q.pages.dev/)" project. its goal is simple: have one component render everything. it has several shims to normalize different post data formats into a single format the component can handle. unlike TestFront, it has no animations, though some weird component splits might linger from the old version.
34393540to adapt TestFront's bsky-api-based `UniversalPostRenderer` to Red Dwarf's model of fetching records directly from each user's PDS and then querying constellation for backlinks, i wrap it in `UniversalPostRendererATURILoader`, which handles raw record and backlink fetching. to bridge the gap between bsky api shapes like `PostView` and the raw record, i use `UniversalPostRendererRawRecordShim`. this way, the core `UniversalPostRenderer` remains the same between TestFront and Red Dwarf (with the only difference being in the red dwarf version the framer motion animations are removed).
···11// please change the branding if you are not it hosting on reddwarf.app
22-export const HOST_TITLE = "Red Dwarf"
22+export const HOST_MAIN_TITLE = "Red Dwarf" // large text in branding
33+export const HOST_SUB_TITLE = " .app" // smaller text in branding
44+export const HOST_TITLE = HOST_MAIN_TITLE + HOST_SUB_TITLE // composite used in paragraphs
35// also replace favicon files and defaultpfp.png and check LogoSvg.tsx
44-// todo generate manifest.json and index.html from this file
55-// todo have the bottom left and right blurbs on the desktop (should move it to settings for mobile) also customizable
66+export const HOST_LOGO_USE_FAVICON = false; // ignores LogoSvg.tsx (recolorable svg) for a static image (the favicon)
77+export const HOST_DEFAULT_HUE = 28; // default is 28 for red. mod 360. 294 is a nice purple
88+export const HOST_HERO = "/sunset.jpg" // path to the "banner" image of the instance
99+export const HOST_ADMIN = "did:plc:tufumi46dykq4fzwtp2ur6kx" // did of the owner/admin, does not give special perms
1010+export const HOST_DESCRIPTION = "The official flagship hosted Red Dwarf instance, hosted on reddwarf.app, running the latest updates and features" // short 1 sentence description
1111+/**
1212+ * --- RED DWARF POLICY.TS — MARKDOWN FLAVOR ---
1313+ *
1414+ * This file uses a deliberately minimal Markdown subset.
1515+ *
1616+ * Supported syntax:
1717+ * - Two consecutive line breaks create a new paragraph
1818+ * - `##` denotes a collapsible section heading
1919+ * - links via [link text](link url)
2020+ * - Self-closing predefined components (e.g. `<PolicyViewer />`)
2121+ *
2222+ * REQUIRED COMPONENTS (strictly one of each):
2323+ * - <PolicyViewer />
2424+ *
2525+ * NOTE:
2626+ * If the app detects that any required custom component is missing,
2727+ * the application will refuse to run. Treat this file as critical.
2828+ */
2929+export const HOST_ABOUT_MARKDOWN = `
3030+## About this instance
3131+3232+reddwarf.app is the flagship hosted instance of Red Dwarf, a Bluesky application built to provide an independent social experience.
3333+This hosted instance mandates Bluesky Moderation to limit moderation scope and keep resources focused on developing Red Dwarf software.
3434+3535+3636+## About Red Dwarf
3737+3838+Red Dwarf is a Bluesky client that does not rely on Bluesky API App Servers.
3939+Instead, it uses Microcosm to fetch records directly from each user’s PDS (via Slingshot)
4040+and connect them using backlinks (via Constellation).
4141+4242+## Hosting Your Own Instance
4343+4444+Red Dwarf is open source. You can host your own instance specifically tailored to your community.
4545+Hosting your own instance gives you full control over policy, branding, and additional features.
4646+4747+Repository: [https://tangled.org/whey.party/red-dwarf](https://tangled.org/whey.party/red-dwarf)
4848+4949+Instructions for setup, configuration, and labeler policies are included in the repository.
5050+5151+## RedDwarf.app Policy
6525353+<PolicyViewer />
5454+`
5555+5656+export const HOST_UNAUTHED_DEFAULT_FEEDS = [
5757+ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"
5858+]
5959+6060+export const HOST_LOGIN_BLURB = "Experience Bluesky under a different light" //todo dont be corny
6161+export const HOST_SIGNUP_PDS = false // false-able string
6262+6363+// very important. if this is empty the app will refuse to load anything
6464+// because this powers all the labels in the app (assuming youre using the newer useAutoLabels and not useModeration)
6565+export const HOST_LABELMERGE = "https://labelmerge.reddwarf.app"
6666+6767+6868+// forced label providers
6969+// applies to everyone
770export const FORCED_LABELER_DIDS = [
871 "did:plc:ar7c4by46qjdydhdevvrndac" // bluesky moderation
972];
10737474+7575+7676+// unauthed forced label policy
7777+// hides some content only when not logged in
7878+// needs labelers to be set in FORCED_LABELER_DIDS
7979+// TODO: add separate unauthed_forced_labeler_dids later
8080+1181export const UNAUTHED_FORCE_WARN_LABELS = new Set([
1212- // i dont know if some of these are even valid labels
1382 "porn",
1483 "sexual",
1584 "graphic-media",
1685 "nudity",
1786 "nsfl",
1818- "corpse",
1987 "gore",
2020- "!no-unauthenticated"
8888+ "!no-unauthenticated",
8989+ "illicit",
9090+ "self-harm",
9191+ "sensitive",
2192]);
22932323-export const UNAUTHED_PREVENT_OPENING_WARNS = true;9494+export const UNAUTHED_PREVENT_OPENING_WARNS = true;
9595+9696+9797+9898+// forced label policy
9999+// hides content labeled with FORCE_HIDE_LABELS
100100+// needs labelers to be set in FORCED_LABELER_DIDS and FORCE_HIDE_LABELS_WHITELISTED_SOURCE
101101+102102+export const FORCE_HIDE_LABELS_WHITELISTED_SOURCE = new Set([
103103+ "did:plc:ar7c4by46qjdydhdevvrndac" // bluesky moderation
104104+])
105105+106106+export const FORCE_HIDE_LABELS = new Set([
107107+ "!takedown",
108108+ "!hide",
109109+]);
110110+111111+112112+113113+// todo generate manifest.json and index.html from this file
public/sunset.jpg
This is a binary file and will not be displayed.
+72
src/api/labelmerge/index.ts
···11+/* eslint-disable unused-imports/no-unused-imports */
22+/* eslint-disable simple-import-sort/imports */
33+/**
44+ * GENERATED CODE - DO NOT MODIFY
55+ */
66+import {
77+ XrpcClient,
88+ type FetchHandler,
99+ type FetchHandlerOptions,
1010+} from '@atproto/xrpc'
1111+import { schemas } from './lexicons.js'
1212+// import { CID } from 'multiformats/cid'
1313+import { type OmitKey, type Un$Typed } from './util.js'
1414+import * as AppReddwarfLabelmergeQueryLabels from './types/app/reddwarf/labelmerge/queryLabels.js'
1515+import * as ComAtprotoLabelDefs from './types/com/atproto/label/defs.js'
1616+1717+export * as AppReddwarfLabelmergeQueryLabels from './types/app/reddwarf/labelmerge/queryLabels.js'
1818+export * as ComAtprotoLabelDefs from './types/com/atproto/label/defs.js'
1919+2020+export class AtpBaseClient extends XrpcClient {
2121+ app: AppNS
2222+2323+ constructor(options: FetchHandler | FetchHandlerOptions) {
2424+ super(options, schemas)
2525+ this.app = new AppNS(this)
2626+ }
2727+2828+ /** @deprecated use `this` instead */
2929+ get xrpc(): XrpcClient {
3030+ return this
3131+ }
3232+}
3333+3434+export class AppNS {
3535+ _client: XrpcClient
3636+ reddwarf: AppReddwarfNS
3737+3838+ constructor(client: XrpcClient) {
3939+ this._client = client
4040+ this.reddwarf = new AppReddwarfNS(client)
4141+ }
4242+}
4343+4444+export class AppReddwarfNS {
4545+ _client: XrpcClient
4646+ labelmerge: AppReddwarfLabelmergeNS
4747+4848+ constructor(client: XrpcClient) {
4949+ this._client = client
5050+ this.labelmerge = new AppReddwarfLabelmergeNS(client)
5151+ }
5252+}
5353+5454+export class AppReddwarfLabelmergeNS {
5555+ _client: XrpcClient
5656+5757+ constructor(client: XrpcClient) {
5858+ this._client = client
5959+ }
6060+6161+ queryLabels(
6262+ params?: AppReddwarfLabelmergeQueryLabels.QueryParams,
6363+ opts?: AppReddwarfLabelmergeQueryLabels.CallOptions,
6464+ ): Promise<AppReddwarfLabelmergeQueryLabels.Response> {
6565+ return this._client.call(
6666+ 'app.reddwarf.labelmerge.queryLabels',
6767+ params,
6868+ undefined,
6969+ opts,
7070+ )
7171+ }
7272+}
+302
src/api/labelmerge/lexicons.ts
···11+/* eslint-disable unused-imports/no-unused-imports */
22+/* eslint-disable simple-import-sort/imports */
33+/**
44+ * GENERATED CODE - DO NOT MODIFY
55+ */
66+import {
77+ type LexiconDoc,
88+ Lexicons,
99+ ValidationError,
1010+ type ValidationResult,
1111+} from '@atproto/lexicon'
1212+import { type $Typed, is$typed, maybe$typed } from './util.js'
1313+1414+export const schemaDict = {
1515+ AppReddwarfLabelmergeQueryLabels: {
1616+ lexicon: 1,
1717+ id: 'app.reddwarf.labelmerge.queryLabels',
1818+ defs: {
1919+ main: {
2020+ type: 'query',
2121+ description:
2222+ 'Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.',
2323+ parameters: {
2424+ type: 'params',
2525+ properties: {
2626+ s: {
2727+ type: 'array',
2828+ items: {
2929+ type: 'string',
3030+ },
3131+ description: 'List of label subjects (strings).',
3232+ },
3333+ l: {
3434+ type: 'array',
3535+ items: {
3636+ type: 'string',
3737+ format: 'did',
3838+ },
3939+ description: 'List of label sources (labeler DIDs) to filter on.',
4040+ },
4141+ strict: {
4242+ type: 'boolean',
4343+ description:
4444+ 'If true then any errors will throw the entire query',
4545+ },
4646+ },
4747+ required: ['s', 'l'],
4848+ },
4949+ output: {
5050+ encoding: 'application/json',
5151+ schema: {
5252+ type: 'object',
5353+ properties: {
5454+ labels: {
5555+ type: 'array',
5656+ items: {
5757+ type: 'ref',
5858+ ref: 'lex:com.atproto.label.defs#label',
5959+ },
6060+ },
6161+ error: {
6262+ type: 'array',
6363+ items: {
6464+ type: 'ref',
6565+ ref: 'lex:app.reddwarf.labelmerge.queryLabels#error',
6666+ },
6767+ },
6868+ },
6969+ required: ['labels'],
7070+ },
7171+ },
7272+ },
7373+ error: {
7474+ type: 'object',
7575+ properties: {
7676+ s: {
7777+ type: 'string',
7878+ format: 'did',
7979+ },
8080+ e: {
8181+ type: 'string',
8282+ },
8383+ },
8484+ required: ['s'],
8585+ },
8686+ },
8787+ },
8888+ ComAtprotoLabelDefs: {
8989+ id: 'com.atproto.label.defs',
9090+ defs: {
9191+ label: {
9292+ type: 'object',
9393+ required: ['src', 'uri', 'val', 'cts'],
9494+ properties: {
9595+ cid: {
9696+ type: 'string',
9797+ format: 'cid',
9898+ description:
9999+ "Optionally, CID specifying the specific version of 'uri' resource this label applies to.",
100100+ },
101101+ cts: {
102102+ type: 'string',
103103+ format: 'datetime',
104104+ description: 'Timestamp when this label was created.',
105105+ },
106106+ exp: {
107107+ type: 'string',
108108+ format: 'datetime',
109109+ description:
110110+ 'Timestamp at which this label expires (no longer applies).',
111111+ },
112112+ neg: {
113113+ type: 'boolean',
114114+ description:
115115+ 'If true, this is a negation label, overwriting a previous label.',
116116+ },
117117+ sig: {
118118+ type: 'bytes',
119119+ description: 'Signature of dag-cbor encoded label.',
120120+ },
121121+ src: {
122122+ type: 'string',
123123+ format: 'did',
124124+ description: 'DID of the actor who created this label.',
125125+ },
126126+ uri: {
127127+ type: 'string',
128128+ format: 'uri',
129129+ description:
130130+ 'AT URI of the record, repository (account), or other resource that this label applies to.',
131131+ },
132132+ val: {
133133+ type: 'string',
134134+ maxLength: 128,
135135+ description:
136136+ 'The short string name of the value or type of this label.',
137137+ },
138138+ ver: {
139139+ type: 'integer',
140140+ description: 'The AT Protocol version of the label object.',
141141+ },
142142+ },
143143+ description:
144144+ 'Metadata tag on an atproto resource (eg, repo or record).',
145145+ },
146146+ selfLabel: {
147147+ type: 'object',
148148+ required: ['val'],
149149+ properties: {
150150+ val: {
151151+ type: 'string',
152152+ maxLength: 128,
153153+ description:
154154+ 'The short string name of the value or type of this label.',
155155+ },
156156+ },
157157+ description:
158158+ 'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.',
159159+ },
160160+ labelValue: {
161161+ type: 'string',
162162+ knownValues: [
163163+ '!hide',
164164+ '!no-promote',
165165+ '!warn',
166166+ '!no-unauthenticated',
167167+ 'dmca-violation',
168168+ 'doxxing',
169169+ 'porn',
170170+ 'sexual',
171171+ 'nudity',
172172+ 'nsfl',
173173+ 'gore',
174174+ ],
175175+ },
176176+ selfLabels: {
177177+ type: 'object',
178178+ required: ['values'],
179179+ properties: {
180180+ values: {
181181+ type: 'array',
182182+ items: {
183183+ ref: 'lex:com.atproto.label.defs#selfLabel',
184184+ type: 'ref',
185185+ },
186186+ maxLength: 10,
187187+ },
188188+ },
189189+ description:
190190+ 'Metadata tags on an atproto record, published by the author within the record.',
191191+ },
192192+ labelValueDefinition: {
193193+ type: 'object',
194194+ required: ['identifier', 'severity', 'blurs', 'locales'],
195195+ properties: {
196196+ blurs: {
197197+ type: 'string',
198198+ description:
199199+ "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
200200+ knownValues: ['content', 'media', 'none'],
201201+ },
202202+ locales: {
203203+ type: 'array',
204204+ items: {
205205+ ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings',
206206+ type: 'ref',
207207+ },
208208+ },
209209+ severity: {
210210+ type: 'string',
211211+ description:
212212+ "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
213213+ knownValues: ['inform', 'alert', 'none'],
214214+ },
215215+ adultOnly: {
216216+ type: 'boolean',
217217+ description:
218218+ 'Does the user need to have adult content enabled in order to configure this label?',
219219+ },
220220+ identifier: {
221221+ type: 'string',
222222+ maxLength: 100,
223223+ description:
224224+ "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
225225+ maxGraphemes: 100,
226226+ },
227227+ defaultSetting: {
228228+ type: 'string',
229229+ default: 'warn',
230230+ description: 'The default setting for this label.',
231231+ knownValues: ['ignore', 'warn', 'hide'],
232232+ },
233233+ },
234234+ description:
235235+ 'Declares a label value and its expected interpretations and behaviors.',
236236+ },
237237+ labelValueDefinitionStrings: {
238238+ type: 'object',
239239+ required: ['lang', 'name', 'description'],
240240+ properties: {
241241+ lang: {
242242+ type: 'string',
243243+ format: 'language',
244244+ description:
245245+ 'The code of the language these strings are written in.',
246246+ },
247247+ name: {
248248+ type: 'string',
249249+ maxLength: 640,
250250+ description: 'A short human-readable name for the label.',
251251+ maxGraphemes: 64,
252252+ },
253253+ description: {
254254+ type: 'string',
255255+ maxLength: 100000,
256256+ description:
257257+ 'A longer description of what the label means and why it might be applied.',
258258+ maxGraphemes: 10000,
259259+ },
260260+ },
261261+ description:
262262+ 'Strings which describe the label in the UI, localized into a specific language.',
263263+ },
264264+ },
265265+ lexicon: 1,
266266+ },
267267+} as const satisfies Record<string, LexiconDoc>
268268+export const schemas = Object.values(schemaDict) satisfies LexiconDoc[]
269269+export const lexicons: Lexicons = new Lexicons(schemas)
270270+271271+export function validate<T extends { $type: string }>(
272272+ v: unknown,
273273+ id: string,
274274+ hash: string,
275275+ requiredType: true,
276276+): ValidationResult<T>
277277+export function validate<T extends { $type?: string }>(
278278+ v: unknown,
279279+ id: string,
280280+ hash: string,
281281+ requiredType?: false,
282282+): ValidationResult<T>
283283+export function validate(
284284+ v: unknown,
285285+ id: string,
286286+ hash: string,
287287+ requiredType?: boolean,
288288+): ValidationResult {
289289+ return (requiredType ? is$typed : maybe$typed)(v, id, hash)
290290+ ? lexicons.validate(`${id}#${hash}`, v)
291291+ : {
292292+ success: false,
293293+ error: new ValidationError(
294294+ `Must be an object with "${hash === 'main' ? id : `${id}#${hash}`}" $type property`,
295295+ ),
296296+ }
297297+}
298298+299299+export const ids = {
300300+ AppReddwarfLabelmergeQueryLabels: 'app.reddwarf.labelmerge.queryLabels',
301301+ ComAtprotoLabelDefs: 'com.atproto.label.defs',
302302+} as const
···11+/* eslint-disable unused-imports/no-unused-imports */
22+/* eslint-disable simple-import-sort/imports */
33+/**
44+ * GENERATED CODE - DO NOT MODIFY
55+ */
66+import { type ValidationResult, BlobRef } from '@atproto/lexicon'
77+// import { CID } from 'multiformats/cid'
88+import { validate as _validate } from '../../../../lexicons'
99+import {
1010+ type $Typed,
1111+ is$typed as _is$typed,
1212+ type OmitKey,
1313+} from '../../../../util'
1414+1515+const is$typed = _is$typed,
1616+ validate = _validate
1717+const id = 'com.atproto.label.defs'
1818+1919+/** Metadata tag on an atproto resource (eg, repo or record). */
2020+export interface Label {
2121+ $type?: 'com.atproto.label.defs#label'
2222+ /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */
2323+ cid?: string
2424+ /** Timestamp when this label was created. */
2525+ cts: string
2626+ /** Timestamp at which this label expires (no longer applies). */
2727+ exp?: string
2828+ /** If true, this is a negation label, overwriting a previous label. */
2929+ neg?: boolean
3030+ /** Signature of dag-cbor encoded label. */
3131+ sig?: Uint8Array
3232+ /** DID of the actor who created this label. */
3333+ src: string
3434+ /** AT URI of the record, repository (account), or other resource that this label applies to. */
3535+ uri: string
3636+ /** The short string name of the value or type of this label. */
3737+ val: string
3838+ /** The AT Protocol version of the label object. */
3939+ ver?: number
4040+}
4141+4242+const hashLabel = 'label'
4343+4444+export function isLabel<V>(v: V) {
4545+ return is$typed(v, id, hashLabel)
4646+}
4747+4848+export function validateLabel<V>(v: V) {
4949+ return validate<Label & V>(v, id, hashLabel)
5050+}
5151+5252+/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */
5353+export interface SelfLabel {
5454+ $type?: 'com.atproto.label.defs#selfLabel'
5555+ /** The short string name of the value or type of this label. */
5656+ val: string
5757+}
5858+5959+const hashSelfLabel = 'selfLabel'
6060+6161+export function isSelfLabel<V>(v: V) {
6262+ return is$typed(v, id, hashSelfLabel)
6363+}
6464+6565+export function validateSelfLabel<V>(v: V) {
6666+ return validate<SelfLabel & V>(v, id, hashSelfLabel)
6767+}
6868+6969+export type LabelValue =
7070+ | '!hide'
7171+ | '!no-promote'
7272+ | '!warn'
7373+ | '!no-unauthenticated'
7474+ | 'dmca-violation'
7575+ | 'doxxing'
7676+ | 'porn'
7777+ | 'sexual'
7878+ | 'nudity'
7979+ | 'nsfl'
8080+ | 'gore'
8181+ | (string & {})
8282+8383+/** Metadata tags on an atproto record, published by the author within the record. */
8484+export interface SelfLabels {
8585+ $type?: 'com.atproto.label.defs#selfLabels'
8686+ values: SelfLabel[]
8787+}
8888+8989+const hashSelfLabels = 'selfLabels'
9090+9191+export function isSelfLabels<V>(v: V) {
9292+ return is$typed(v, id, hashSelfLabels)
9393+}
9494+9595+export function validateSelfLabels<V>(v: V) {
9696+ return validate<SelfLabels & V>(v, id, hashSelfLabels)
9797+}
9898+9999+/** Declares a label value and its expected interpretations and behaviors. */
100100+export interface LabelValueDefinition {
101101+ $type?: 'com.atproto.label.defs#labelValueDefinition'
102102+ /** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */
103103+ blurs: 'content' | 'media' | 'none' | (string & {})
104104+ locales: LabelValueDefinitionStrings[]
105105+ /** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */
106106+ severity: 'inform' | 'alert' | 'none' | (string & {})
107107+ /** Does the user need to have adult content enabled in order to configure this label? */
108108+ adultOnly?: boolean
109109+ /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */
110110+ identifier: string
111111+ /** The default setting for this label. */
112112+ defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {})
113113+}
114114+115115+const hashLabelValueDefinition = 'labelValueDefinition'
116116+117117+export function isLabelValueDefinition<V>(v: V) {
118118+ return is$typed(v, id, hashLabelValueDefinition)
119119+}
120120+121121+export function validateLabelValueDefinition<V>(v: V) {
122122+ return validate<LabelValueDefinition & V>(v, id, hashLabelValueDefinition)
123123+}
124124+125125+/** Strings which describe the label in the UI, localized into a specific language. */
126126+export interface LabelValueDefinitionStrings {
127127+ $type?: 'com.atproto.label.defs#labelValueDefinitionStrings'
128128+ /** The code of the language these strings are written in. */
129129+ lang: string
130130+ /** A short human-readable name for the label. */
131131+ name: string
132132+ /** A longer description of what the label means and why it might be applied. */
133133+ description: string
134134+}
135135+136136+const hashLabelValueDefinitionStrings = 'labelValueDefinitionStrings'
137137+138138+export function isLabelValueDefinitionStrings<V>(v: V) {
139139+ return is$typed(v, id, hashLabelValueDefinitionStrings)
140140+}
141141+142142+export function validateLabelValueDefinitionStrings<V>(v: V) {
143143+ return validate<LabelValueDefinitionStrings & V>(
144144+ v,
145145+ id,
146146+ hashLabelValueDefinitionStrings,
147147+ )
148148+}
+84
src/api/labelmerge/util.ts
···11+/* eslint-disable unused-imports/no-unused-imports */
22+/* eslint-disable simple-import-sort/imports */
33+/**
44+ * GENERATED CODE - DO NOT MODIFY
55+ */
66+77+import { type ValidationResult } from '@atproto/lexicon'
88+99+export type OmitKey<T, K extends keyof T> = {
1010+ [K2 in keyof T as K2 extends K ? never : K2]: T[K2]
1111+}
1212+1313+export type $Typed<V, T extends string = string> = V & { $type: T }
1414+export type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>
1515+1616+export type $Type<Id extends string, Hash extends string> = Hash extends 'main'
1717+ ? Id
1818+ : `${Id}#${Hash}`
1919+2020+function isObject<V>(v: V): v is V & object {
2121+ return v != null && typeof v === 'object'
2222+}
2323+2424+function is$type<Id extends string, Hash extends string>(
2525+ $type: unknown,
2626+ id: Id,
2727+ hash: Hash,
2828+): $type is $Type<Id, Hash> {
2929+ return hash === 'main'
3030+ ? $type === id
3131+ : // $type === `${id}#${hash}`
3232+ typeof $type === 'string' &&
3333+ $type.length === id.length + 1 + hash.length &&
3434+ $type.charCodeAt(id.length) === 35 /* '#' */ &&
3535+ $type.startsWith(id) &&
3636+ $type.endsWith(hash)
3737+}
3838+3939+export type $TypedObject<
4040+ V,
4141+ Id extends string,
4242+ Hash extends string,
4343+> = V extends {
4444+ $type: $Type<Id, Hash>
4545+}
4646+ ? V
4747+ : V extends { $type?: string }
4848+ ? V extends { $type?: infer T extends $Type<Id, Hash> }
4949+ ? V & { $type: T }
5050+ : never
5151+ : V & { $type: $Type<Id, Hash> }
5252+5353+export function is$typed<V, Id extends string, Hash extends string>(
5454+ v: V,
5555+ id: Id,
5656+ hash: Hash,
5757+): v is $TypedObject<V, Id, Hash> {
5858+ return isObject(v) && '$type' in v && is$type(v.$type, id, hash)
5959+}
6060+6161+export function maybe$typed<V, Id extends string, Hash extends string>(
6262+ v: V,
6363+ id: Id,
6464+ hash: Hash,
6565+): v is V & object & { $type?: $Type<Id, Hash> } {
6666+ return (
6767+ isObject(v) &&
6868+ ('$type' in v ? v.$type === undefined || is$type(v.$type, id, hash) : true)
6969+ )
7070+}
7171+7272+export type Validator<R = unknown> = (v: unknown) => ValidationResult<R>
7373+export type ValidatorParam<V extends Validator> =
7474+ V extends Validator<infer R> ? R : never
7575+7676+/**
7777+ * Utility function that allows to convert a "validate*" utility function into a
7878+ * type predicate.
7979+ */
8080+export function asPredicate<V extends Validator>(validate: V) {
8181+ return function <T>(v: T): v is T & ValidatorParam<V> {
8282+ return validate(v).success
8383+ }
8484+}