feat: initial implementation of chat in terminal from stream.place

This commit is contained in:
Stevan Freeborn committed 2026-08-26 21:32:59 -05:00
commit 689304656f
39 files changed
+4243

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
node_modules/
.env
.session-store.json
.state-store.json
dist/
+19
View File
@@ -0,0 +1,19 @@
# Copyright (c) 2026 Stevan Freeborn
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 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.
+70
View File
@@ -0,0 +1,70 @@
[![npm version](https://img.shields.io/npm/v/@stevanfreeborn/streamplace-term-chat.svg)](https://www.npmjs.com/package/@stevanfreeborn/streamplace-term-chat)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
# Streamplace Chat Client
A lightweight, terminal-based live chat client for Streamplace powered by AT Protocol OAuth and Bluesky Jetstream.
## Features
- Secure browser login flow with persistent session storage.
- Subscribes to live `place.stream.chat.message` events.
- Efficient handle/DID resolution to minimize network overhead.
- Auto-reconnecting WebSocket for uninterrupted stream chat.
## Quick Start
Run directly using `npx`:
```bash
# Connect to your own stream chat room
npx @stevanfreeborn/streamplace-term-chat your-handle.bsky.social
# Connect to another streamer's chat room
npx @stevanfreeborn/streamplace-term-chat your-handle.bsky.social --streamer streamer-handle.bsky.social
```
## Global Installation
Install globally via npm to use the streamplace-term-chat anywhere on your system:
```sh
npm install -g @stevanfreeborn/streamplace-term-chat
```
## From Source
### Prerequisites
- Node.js (v20+ recommended)
- npm
### Quick Start
Clone the repository and install dependencies:
```bash
git clone https://github.com/StevanFreeborn/stream.place-chat-client.git
cd stream.place-chat-client
npm install
```
Start the chat client:
```bash
# Connect to your own stream chat
npm start -- <your-handle.bsky.social>
# Connect to a target streamer's chat room
npm start -- <your-handle.bsky.social> --streamer <streamer.bsky.social>
```
Chat commands:
- Type your message and hit `Enter` to post.
- `/help`: Show available in-chat commands.
- `/quit` or `/exit`: Gracefully disconnect and exit.
## License
[MIT](LICENSE.md)
+2
View File
@@ -0,0 +1,2 @@
STREAMER_DID=
JETSTREAM_URL=
+25
View File
@@ -0,0 +1,25 @@
{
"version": 1,
"lexicons": [
"app.bsky.actor.profile",
"app.bsky.richtext.facet"
],
"resolutions": {
"app.bsky.actor.profile": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.profile",
"cid": "bafyreia6umzg3a6d7mjbow4p57tviey45muohklhgsvjoamcctoiusr4pe"
},
"app.bsky.richtext.facet": {
"uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.richtext.facet",
"cid": "bafyreidg56eo7zynf6ihz4xb627vwoqf5idnevkmwp7sxc4tijg6xngbu4"
},
"com.atproto.label.defs": {
"uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.label.defs",
"cid": "bafyreidp2wpcbzl2qiob2uwoc7lhntojx3tjcr553mmahw2f5cumtm3wpm"
},
"com.atproto.repo.strongRef": {
"uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.strongRef",
"cid": "bafyreifrkdbnkvfjujntdaeigolnrjj3srrs53tfixjhmacclps72qlov4"
}
}
}
+75
View File
@@ -0,0 +1,75 @@
{
"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
}
+90
View File
@@ -0,0 +1,90 @@
{
"id": "app.bsky.richtext.facet",
"defs": {
"tag": {
"type": "object",
"required": [
"tag"
],
"properties": {
"tag": {
"type": "string",
"maxLength": 640,
"maxGraphemes": 64
}
},
"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')."
},
"link": {
"type": "object",
"required": [
"uri"
],
"properties": {
"uri": {
"type": "string",
"format": "uri"
}
},
"description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL."
},
"main": {
"type": "object",
"required": [
"index",
"features"
],
"properties": {
"index": {
"ref": "#byteSlice",
"type": "ref"
},
"features": {
"type": "array",
"items": {
"refs": [
"#mention",
"#link",
"#tag"
],
"type": "union"
}
}
},
"description": "Annotation of a sub-string within rich text."
},
"mention": {
"type": "object",
"required": [
"did"
],
"properties": {
"did": {
"type": "string",
"format": "did"
}
},
"description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID."
},
"byteSlice": {
"type": "object",
"required": [
"byteStart",
"byteEnd"
],
"properties": {
"byteEnd": {
"type": "integer",
"minimum": 0
},
"byteStart": {
"type": "integer",
"minimum": 0
}
},
"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."
}
},
"$type": "com.atproto.lexicon.schema",
"lexicon": 1
}
+190
View File
@@ -0,0 +1,190 @@
{
"id": "com.atproto.label.defs",
"defs": {
"label": {
"type": "object",
"required": [
"src",
"uri",
"val",
"cts"
],
"properties": {
"cid": {
"type": "string",
"format": "cid",
"description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
},
"cts": {
"type": "string",
"format": "datetime",
"description": "Timestamp when this label was created."
},
"exp": {
"type": "string",
"format": "datetime",
"description": "Timestamp at which this label expires (no longer applies)."
},
"neg": {
"type": "boolean",
"description": "If true, this is a negation label, overwriting a previous label."
},
"sig": {
"type": "bytes",
"description": "Signature of dag-cbor encoded label."
},
"src": {
"type": "string",
"format": "did",
"description": "DID of the actor who created this label."
},
"uri": {
"type": "string",
"format": "uri",
"description": "AT URI of the record, repository (account), or other resource that this label applies to."
},
"val": {
"type": "string",
"maxLength": 128,
"description": "The short string name of the value or type of this label."
},
"ver": {
"type": "integer",
"description": "The AT Protocol version of the label object."
}
},
"description": "Metadata tag on an atproto resource (eg, repo or record)."
},
"selfLabel": {
"type": "object",
"required": [
"val"
],
"properties": {
"val": {
"type": "string",
"maxLength": 128,
"description": "The short string name of the value or type of this label."
}
},
"description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel."
},
"labelValue": {
"type": "string",
"knownValues": [
"!hide",
"!warn",
"!no-unauthenticated",
"porn",
"sexual",
"nudity",
"graphic-media",
"bot"
]
},
"selfLabels": {
"type": "object",
"required": [
"values"
],
"properties": {
"values": {
"type": "array",
"items": {
"ref": "#selfLabel",
"type": "ref"
},
"maxLength": 10
}
},
"description": "Metadata tags on an atproto record, published by the author within the record."
},
"labelValueDefinition": {
"type": "object",
"required": [
"identifier",
"severity",
"blurs",
"locales"
],
"properties": {
"blurs": {
"type": "string",
"description": "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.",
"knownValues": [
"content",
"media",
"none"
]
},
"locales": {
"type": "array",
"items": {
"ref": "#labelValueDefinitionStrings",
"type": "ref"
}
},
"severity": {
"type": "string",
"description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
"knownValues": [
"inform",
"alert",
"none"
]
},
"adultOnly": {
"type": "boolean",
"description": "Does the user need to have adult content enabled in order to configure this label?"
},
"identifier": {
"type": "string",
"maxLength": 100,
"description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
"maxGraphemes": 100
},
"defaultSetting": {
"type": "string",
"default": "warn",
"description": "The default setting for this label.",
"knownValues": [
"ignore",
"warn",
"hide"
]
}
},
"description": "Declares a label value and its expected interpretations and behaviors."
},
"labelValueDefinitionStrings": {
"type": "object",
"required": [
"lang",
"name",
"description"
],
"properties": {
"lang": {
"type": "string",
"format": "language",
"description": "The code of the language these strings are written in."
},
"name": {
"type": "string",
"maxLength": 640,
"description": "A short human-readable name for the label.",
"maxGraphemes": 64
},
"description": {
"type": "string",
"maxLength": 100000,
"description": "A longer description of what the label means and why it might be applied.",
"maxGraphemes": 10000
}
},
"description": "Strings which describe the label in the UI, localized into a specific language."
}
},
"$type": "com.atproto.lexicon.schema",
"lexicon": 1
}
+25
View File
@@ -0,0 +1,25 @@
{
"id": "com.atproto.repo.strongRef",
"defs": {
"main": {
"type": "object",
"required": [
"uri",
"cid"
],
"properties": {
"cid": {
"type": "string",
"format": "cid"
},
"uri": {
"type": "string",
"format": "at-uri"
}
}
}
},
"$type": "com.atproto.lexicon.schema",
"lexicon": 1,
"description": "A URI with a content-hash fingerprint."
}
+59
View File
@@ -0,0 +1,59 @@
{
"lexicon": 1,
"id": "place.stream.chat.message",
"defs": {
"main": {
"type": "record",
"description": "Record containing a Streamplace chat message.",
"key": "tid",
"record": {
"type": "object",
"required": ["text", "createdAt", "streamer"],
"properties": {
"text": {
"type": "string",
"maxLength": 3000,
"maxGraphemes": 300,
"description": "The primary message content. May be an empty string, if there are embeds."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Client-declared timestamp when this message was originally created."
},
"facets": {
"type": "array",
"description": "Annotations of text (mentions, URLs, etc)",
"items": {
"type": "ref",
"ref": "place.stream.richtext.facet"
}
},
"streamer": {
"type": "string",
"format": "did",
"description": "The DID of the streamer whose chat this is."
},
"reply": {
"type": "ref",
"ref": "#replyRef"
}
}
}
},
"replyRef": {
"type": "object",
"required": ["root", "parent"],
"properties": {
"root": {
"type": "ref",
"ref": "com.atproto.repo.strongRef"
},
"parent": {
"type": "ref",
"ref": "com.atproto.repo.strongRef"
}
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
{
"lexicon": 1,
"id": "place.stream.richtext.facet",
"defs": {
"main": {
"type": "object",
"description": "Annotation of a sub-string within rich text in a livestream chat message.",
"required": ["index", "features"],
"properties": {
"index": {
"type": "ref",
"ref": "app.bsky.richtext.facet#byteSlice"
},
"features": {
"type": "array",
"items": {
"type": "union",
"refs": [
"app.bsky.richtext.facet#mention",
"app.bsky.richtext.facet#link"
]
}
}
}
}
}
}
+2340
View File
File diff suppressed because it is too large. Load diff
+66
View File
@@ -0,0 +1,66 @@
{
"name": "@stevanfreeborn/streamplace-term-chat",
"version": "0.0.0",
"description": "Terminal chat client for Streamplace built on AT Protocol and Jetstream",
"main": "./dist/index.ts",
"types": "./dist/index.d.ts",
"type": "module",
"bin": {
"streamplace-term-chat": "./dist/index.js"
},
"files": [
"dist",
"README.md",
"LICENSE.md"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "tsx --env-file=.env src/index.ts",
"start": "node --env-file=.env dist/index.js",
"typecheck": "tsc --noEmit",
"build": "tsc",
"prepublishOnly": "npm run typecheck && npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/stevanfreeborn/streamplace-term-chat.git"
},
"bugs": {
"url": "https://github.com/stevanfreeborn/streamplace-term-chat/issues"
},
"homepage": "https://github.com/stevanfreeborn/streamplace-term-chat#readme",
"keywords": [
"atproto",
"streamplace",
"chat",
"cli",
"bluesky",
"jetstream",
"terminal"
],
"author": {
"name": "Stevan Freeborn",
"url": "https://stevanfreeborn.com",
"email": "me@stevanfreeborn.com"
},
"license": "MIT",
"devDependencies": {
"@types/node": "^26.4.0",
"@types/ws": "^8.18.1",
"tsx": "^4.23.12",
"typescript": "^7.0.2"
},
"dependencies": {
"@atcute/identity-resolver": "^2.0.1",
"@atproto/lex": "^0.3.8",
"@atproto/oauth-client-node": "^0.5.4",
"open": "^11.0.1",
"ws": "^8.21.3"
},
"allowScripts": {
"esbuild@0.28.2": true,
"core-js@3.50.0": true
}
}
+399
View File
@@ -0,0 +1,399 @@
#!/usr/bin/env node
import http from "node:http";
import open from "open";
import os from 'os';
import {
buildAtprotoLoopbackClientMetadata,
NodeOAuthClient,
type NodeSavedSession,
type NodeSavedState,
} from "@atproto/oauth-client-node";
import { Client } from "@atproto/lex";
import * as place from "./lexicons/place.js";
import WebSocket from "ws";
import {
CompositeDidDocumentResolver,
PlcDidDocumentResolver,
WebDidDocumentResolver,
LocalActorResolver,
WellKnownHandleResolver,
DohJsonHandleResolver,
CompositeHandleResolver,
} from "@atcute/identity-resolver";
import readline from "readline";
import path from "node:path";
import fs from "node:fs/promises";
const c = {
gray: (str: string) => `\x1b[90m${str}\x1b[0m`,
bold: (str: string) => `\x1b[1m${str}\x1b[22m`,
cyan: (str: string) => `\x1b[36m${str}\x1b[39m`,
green: (str: string) => `\x1b[32m${str}\x1b[39m`,
magenta: (str: string) => `\x1b[35m${str}\x1b[39m`,
yellow: (str: string) => `\x1b[33m${str}\x1b[39m`,
red: (str: string) => `\x1b[31m${str}\x1b[39m`,
};
const DEFAULT_JETSTREAM_URL =
"wss://jetstream.us-east.bsky.network/xrpc/network.bsky.jetstream.subscribeEvents?collections=place.stream.chat.message";
const CONFIG_DIR = path.join(os.homedir(), ".streamplace-term-chat");
await fs.mkdir(CONFIG_DIR, { recursive: true });
const SESSION_FILE = path.join(CONFIG_DIR, ".session-store.json");
const STATE_FILE = path.join(CONFIG_DIR, ".state-store.json");
async function loadJsonStore<T>(filePath: string): Promise<Map<string, T>> {
try {
const data = await fs.readFile(filePath, "utf-8");
return new Map(Object.entries(JSON.parse(data)));
} catch {
return new Map();
}
}
async function saveJsonStore<T>(
filePath: string,
store: Map<string, T>,
): Promise<void> {
try {
const obj = Object.fromEntries(store.entries());
await fs.writeFile(filePath, JSON.stringify(obj, null, 2), "utf-8");
} catch (err) {
console.error(`Failed to save store to ${filePath}:`, err);
}
}
const stateStore = await loadJsonStore<NodeSavedState>(STATE_FILE);
const sessionStore = await loadJsonStore<NodeSavedSession>(SESSION_FILE);
const handleResolver = new CompositeHandleResolver({
methods: {
dns: new DohJsonHandleResolver({
dohUrl: "https://mozilla.cloudflare-dns.com/dns-query",
}),
http: new WellKnownHandleResolver(),
},
});
const didResolver = new CompositeDidDocumentResolver({
methods: {
plc: new PlcDidDocumentResolver(),
web: new WebDidDocumentResolver(),
},
});
const actorResolver = new LocalActorResolver({
handleResolver,
didDocumentResolver: didResolver,
});
const handleCache = new Map<string, string>();
async function resolveHandle(did: string): Promise<string> {
const cached = handleCache.get(did);
if (cached) {
return cached;
}
try {
const actor = await actorResolver.resolve(did as any);
const identifier = actor.handle ?? actor.did;
handleCache.set(did, identifier);
return identifier;
} catch {
return did;
}
}
const oauthClient = new NodeOAuthClient({
clientMetadata: buildAtprotoLoopbackClientMetadata({
scope: "atproto repo:place.stream.chat.message?action=create",
redirect_uris: ["http://127.0.0.1:3000/callback"],
}),
stateStore: {
async get(key: string) {
return stateStore.get(key);
},
async set(key: string, value: NodeSavedState) {
stateStore.set(key, value);
await saveJsonStore(STATE_FILE, stateStore);
},
async del(key: string) {
stateStore.delete(key);
await saveJsonStore(STATE_FILE, stateStore);
},
},
sessionStore: {
async get(key: string) {
return sessionStore.get(key);
},
async set(key: string, value: NodeSavedSession) {
sessionStore.set(key, value);
await saveJsonStore(SESSION_FILE, sessionStore);
},
async del(key: string) {
sessionStore.delete(key);
await saveJsonStore(SESSION_FILE, sessionStore);
},
},
});
async function resolveToDid(input: string): Promise<string> {
if (input.startsWith("did:")) {
return input;
}
const resolved = await actorResolver.resolve(input as any);
return resolved.did;
}
async function login(handleOrDid: string) {
let userDid: string;
try {
userDid = await resolveToDid(handleOrDid);
} catch (err) {
console.error(c.red(`Failed to resolve handle "${handleOrDid}":`), err);
throw err;
}
try {
const session = await oauthClient.restore(userDid);
if (session) {
console.log(
c.green(`\nRestored existing session for @${handleOrDid} (${userDid})`),
);
return session;
}
} catch { }
console.log(c.gray(`\nInitiating browser login for @${handleOrDid}...`));
const authUrl = await oauthClient.authorize(handleOrDid, {
scope: "atproto repo:place.stream.chat.message?action=create",
});
const params = await new Promise<URLSearchParams>((resolve, reject) => {
const server = http.createServer((req, res) => {
if (!req.url) {
return;
}
const url = new URL(req.url, "http://127.0.0.1:3000");
if (url.pathname === "/callback") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(
"<h1>Authorized! You can close this tab and return to your terminal.</h1>",
);
resolve(url.searchParams);
server.close();
}
});
server.listen(3000, "127.0.0.1", () => {
console.log(
c.gray(
"Listening on http://127.0.0.1:3000/callback for OAuth redirect...",
),
);
open(authUrl.toString());
});
server.on("error", reject);
});
const { session } = await oauthClient.callback(params);
return session;
}
async function main() {
const args = process.argv.slice(2);
const handleArg = args.find((arg) => !arg.startsWith("--"));
const streamerFlagIndex = args.indexOf("--streamer");
const streamerInput =
streamerFlagIndex !== -1
? args[streamerFlagIndex + 1]
: process.env.STREAMER_DID;
if (!handleArg || args.includes("--help") || args.includes("-h")) {
console.log(
c.gray(`
Streamplace Chat Client
Usage:
npm start -- <your-handle> [--streamer <streamer-handle-or-did>]
Options:
--streamer Specify target streamer handle/DID to view and post chat messages.
Defaults to your own handle if omitted.
Examples:
npm start -- alice.bsky.social
npm start -- alice.bsky.social --streamer streamer.bsky.social
`),
);
process.exit(0);
}
const session = await login(handleArg);
const client = new Client(session);
let targetStreamerDid = session.did as string;
if (streamerInput) {
if (streamerInput.startsWith("did:")) {
targetStreamerDid = streamerInput;
} else {
try {
const resolved = await actorResolver.resolve(streamerInput as any);
targetStreamerDid = resolved.did;
} catch (err) {
console.error(
c.red(
`Could not resolve streamer handle "${streamerInput}". Using user's DID.`,
),
);
}
}
}
const connectedAsHandle = await resolveHandle(session.did);
const targetStreamerHandle = await resolveHandle(targetStreamerDid);
console.clear();
console.log(
c.green(c.bold("Connected as:")) +
` @${connectedAsHandle} (${session.did})`,
);
console.log(
c.yellow(c.bold("Chat room streamer:")) +
` @${targetStreamerHandle} (${targetStreamerDid})`,
);
console.log(
c.gray("Type your message and hit Enter. Commands: /quit, /help\n"),
);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: c.cyan(c.bold("> ")),
});
let ws: WebSocket | null = null;
let isConnecting = false;
function connectWs() {
if (isConnecting) return;
isConnecting = true;
ws = new WebSocket(process.env.JETSTREAM_URL || DEFAULT_JETSTREAM_URL);
ws.on("open", () => {
isConnecting = false;
rl.prompt();
});
ws.on("message", async (msg) => {
try {
const jsonMsg = JSON.parse(msg.toString());
if (jsonMsg.payload?.record?.streamer !== targetStreamerDid) {
return;
}
const createdAt = new Date(jsonMsg.payload.record.createdAt);
const timestamp = createdAt.toLocaleTimeString("en-US", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
});
const senderDid = jsonMsg.payload.did;
const senderIdentifier = await resolveHandle(senderDid);
const messageText = jsonMsg.payload.record.text;
const formattedLine = `${c.gray(`[${timestamp}]`)} ${c.magenta("[SP]")} ${c.cyan(
c.bold(senderIdentifier),
)}: ${messageText}`;
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
console.log(formattedLine);
rl.prompt(true);
} catch { }
});
ws.on("error", (err) => {
readline.clearLine(process.stdout, 0);
readline.cursorTo(process.stdout, 0);
console.error(`[WebSocket Error] ${err.message}`);
rl.prompt(true);
});
ws.on("close", () => {
isConnecting = false;
setTimeout(connectWs, 3000);
});
}
connectWs();
rl.on("line", async (line) => {
process.stdout.moveCursor(0, -1);
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
const text = line.trim();
if (!text) {
rl.prompt();
return;
}
if (text === "/quit" || text === "/exit") {
rl.close();
return;
}
if (text === "/help") {
console.log("Commands: /quit, /exit, /help");
rl.prompt();
return;
}
try {
await client.create(place.stream.chat.message as any, {
streamer: targetStreamerDid,
text: text,
createdAt: new Date().toISOString(),
});
} catch (err) {
if (err instanceof Error) {
console.error(c.red(`Failed to send message: ${err.message}`));
}
}
rl.prompt();
});
rl.on("close", () => {
if (ws) {
ws.removeAllListeners();
ws.close();
}
console.log(c.gray("\nExiting chat. Goodbye!"));
process.exit(0);
});
}
main().catch((err) => {
if (err instanceof Error) {
console.error(c.red(`Fatal error: ${err.message}`));
}
process.exit(1);
});
+5
View File
@@ -0,0 +1,5 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as bsky from './app/bsky.js'
+6
View File
@@ -0,0 +1,6 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as actor from './bsky/actor.js'
export * as richtext from './bsky/richtext.js'
+5
View File
@@ -0,0 +1,5 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as profile from './actor/profile.js'
+125
View File
@@ -0,0 +1,125 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
import { l } from '@atproto/lex'
import * as LabelDefs from './..\\..\\..\\com\\atproto\\label\\defs.defs.js'
import * as RepoStrongRef from './..\\..\\..\\com\\atproto\\repo\\strongRef.defs.js'
const $nsid = 'app.bsky.actor.profile'
type $nsid = typeof $nsid
export { $nsid }
/** A declaration of a Bluesky account profile. */
type Main = {
$type: 'app.bsky.actor.profile'
/**
* Small image to be displayed next to posts from account. AKA, 'profile picture'
*/
avatar?: l.BlobRef
/**
* Larger horizontal image to display behind profile view.
*/
banner?: l.BlobRef
/**
* Self-label values, specific to the Bluesky application, on the overall account.
*/
labels?: l.$Typed<LabelDefs.SelfLabels> | l.Unknown$TypedObject
website?: l.UriString
/**
* Free-form pronouns text.
*/
pronouns?: string
createdAt?: l.DatetimeString
pinnedPost?: RepoStrongRef.Main
/**
* Free-form profile description text.
*/
description?: string
displayName?: string
joinedViaStarterPack?: RepoStrongRef.Main
}
export type { Main }
/** A declaration of a Bluesky account profile. */
const main = /*#__PURE__*/ l.record<'literal:self', Main>(
'literal:self',
$nsid,
/*#__PURE__*/ l.object({
avatar: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.blob({
accept: ['image/png', 'image/jpeg'],
maxSize: 1000000,
}),
),
banner: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.blob({
accept: ['image/png', 'image/jpeg'],
maxSize: 1000000,
}),
),
labels: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.typedUnion(
[
/*#__PURE__*/ l.typedRef<LabelDefs.SelfLabels>(
(() => LabelDefs.selfLabels) as any,
),
],
false,
),
),
website: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.string({ format: 'uri' }),
),
pronouns: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.string({ maxLength: 200, maxGraphemes: 20 }),
),
createdAt: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.string({ format: 'datetime' }),
),
pinnedPost: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.ref<RepoStrongRef.Main>(
(() => RepoStrongRef.main) as any,
),
),
description: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.string({ maxLength: 2560, maxGraphemes: 256 }),
),
displayName: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.string({ maxLength: 640, maxGraphemes: 64 }),
),
joinedViaStarterPack: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.ref<RepoStrongRef.Main>(
(() => RepoStrongRef.main) as any,
),
),
}),
)
export { main }
const $type = $nsid
type $type = typeof $type
export { $type }
export const $isTypeOf = /*#__PURE__*/ main.isTypeOf.bind(main)
export const $build = /*#__PURE__*/ main.build.bind(main)
export const $assert = /*#__PURE__*/ main.assert.bind(main)
export const $check = /*#__PURE__*/ main.check.bind(main)
export const $cast = /*#__PURE__*/ main.cast.bind(main)
export const $ifMatches = /*#__PURE__*/ main.ifMatches.bind(main)
export const $matches = /*#__PURE__*/ main.matches.bind(main)
export const $parse = /*#__PURE__*/ main.parse.bind(main)
export const $safeParse = /*#__PURE__*/ main.safeParse.bind(main)
export const $validate = /*#__PURE__*/ main.validate.bind(main)
export const $safeValidate = /*#__PURE__*/ main.safeValidate.bind(main)
+6
View File
@@ -0,0 +1,6 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * from './profile.defs.js'
export { main as default } from './profile.defs.js'
+5
View File
@@ -0,0 +1,5 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as facet from './richtext/facet.js'
@@ -0,0 +1,126 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
import { l } from '@atproto/lex'
const $nsid = 'app.bsky.richtext.facet'
type $nsid = typeof $nsid
export { $nsid }
/** 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'). */
type Tag = { $type?: 'app.bsky.richtext.facet#tag'; tag: string }
export type { Tag }
/** 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'). */
const tag = /*#__PURE__*/ l.typedObject<Tag>(
$nsid,
'tag',
/*#__PURE__*/ l.object({
tag: /*#__PURE__*/ l.string({ maxLength: 640, maxGraphemes: 64 }),
}),
)
export { tag }
/** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. */
type Link = { $type?: 'app.bsky.richtext.facet#link'; uri: l.UriString }
export type { Link }
/** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. */
const link = /*#__PURE__*/ l.typedObject<Link>(
$nsid,
'link',
/*#__PURE__*/ l.object({ uri: /*#__PURE__*/ l.string({ format: 'uri' }) }),
)
export { link }
/** Annotation of a sub-string within rich text. */
type Main = {
$type?: 'app.bsky.richtext.facet'
index: ByteSlice
features: (
l.$Typed<Mention> | l.$Typed<Link> | l.$Typed<Tag> | l.Unknown$TypedObject
)[]
}
export type { Main }
/** Annotation of a sub-string within rich text. */
const main = /*#__PURE__*/ l.typedObject<Main>(
$nsid,
'main',
/*#__PURE__*/ l.object({
index: /*#__PURE__*/ l.ref<ByteSlice>((() => byteSlice) as any),
features: /*#__PURE__*/ l.array(
/*#__PURE__*/ l.typedUnion(
[
/*#__PURE__*/ l.typedRef<Mention>((() => mention) as any),
/*#__PURE__*/ l.typedRef<Link>((() => link) as any),
/*#__PURE__*/ l.typedRef<Tag>((() => tag) as any),
],
false,
),
),
}),
)
export { main }
const $type = $nsid
type $type = typeof $type
export { $type }
export const $isTypeOf = /*#__PURE__*/ main.isTypeOf.bind(main)
export const $build = /*#__PURE__*/ main.build.bind(main)
export const $assert = /*#__PURE__*/ main.assert.bind(main)
export const $check = /*#__PURE__*/ main.check.bind(main)
export const $cast = /*#__PURE__*/ main.cast.bind(main)
export const $ifMatches = /*#__PURE__*/ main.ifMatches.bind(main)
export const $matches = /*#__PURE__*/ main.matches.bind(main)
export const $parse = /*#__PURE__*/ main.parse.bind(main)
export const $safeParse = /*#__PURE__*/ main.safeParse.bind(main)
export const $validate = /*#__PURE__*/ main.validate.bind(main)
export const $safeValidate = /*#__PURE__*/ main.safeValidate.bind(main)
/** Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. */
type Mention = { $type?: 'app.bsky.richtext.facet#mention'; did: l.DidString }
export type { Mention }
/** Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID. */
const mention = /*#__PURE__*/ l.typedObject<Mention>(
$nsid,
'mention',
/*#__PURE__*/ l.object({ did: /*#__PURE__*/ l.string({ format: 'did' }) }),
)
export { mention }
/** 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. */
type ByteSlice = {
$type?: 'app.bsky.richtext.facet#byteSlice'
byteEnd: number
byteStart: number
}
export type { ByteSlice }
/** 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. */
const byteSlice = /*#__PURE__*/ l.typedObject<ByteSlice>(
$nsid,
'byteSlice',
/*#__PURE__*/ l.object({
byteEnd: /*#__PURE__*/ l.integer({ minimum: 0 }),
byteStart: /*#__PURE__*/ l.integer({ minimum: 0 }),
}),
)
export { byteSlice }
+6
View File
@@ -0,0 +1,6 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * from './facet.defs.js'
export { main as default } from './facet.defs.js'
+5
View File
@@ -0,0 +1,5 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as atproto from './com/atproto.js'
+6
View File
@@ -0,0 +1,6 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as label from './atproto/label.js'
export * as repo from './atproto/repo.js'
+5
View File
@@ -0,0 +1,5 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as defs from './label/defs.js'
+256
View File
@@ -0,0 +1,256 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
import { l } from '@atproto/lex'
const $nsid = 'com.atproto.label.defs'
type $nsid = typeof $nsid
export { $nsid }
/** Metadata tag on an atproto resource (eg, repo or record). */
type Label = {
$type?: 'com.atproto.label.defs#label'
/**
* Optionally, CID specifying the specific version of 'uri' resource this label applies to.
*/
cid?: l.CidString
/**
* Timestamp when this label was created.
*/
cts: l.DatetimeString
/**
* Timestamp at which this label expires (no longer applies).
*/
exp?: l.DatetimeString
/**
* If true, this is a negation label, overwriting a previous label.
*/
neg?: boolean
/**
* Signature of dag-cbor encoded label.
*/
sig?: Uint8Array
/**
* DID of the actor who created this label.
*/
src: l.DidString
/**
* AT URI of the record, repository (account), or other resource that this label applies to.
*/
uri: l.UriString
/**
* The short string name of the value or type of this label.
*/
val: string
/**
* The AT Protocol version of the label object.
*/
ver?: number
}
export type { Label }
/** Metadata tag on an atproto resource (eg, repo or record). */
const label = /*#__PURE__*/ l.typedObject<Label>(
$nsid,
'label',
/*#__PURE__*/ l.object({
cid: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string({ format: 'cid' })),
cts: /*#__PURE__*/ l.string({ format: 'datetime' }),
exp: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.string({ format: 'datetime' }),
),
neg: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
sig: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.bytes()),
src: /*#__PURE__*/ l.string({ format: 'did' }),
uri: /*#__PURE__*/ l.string({ format: 'uri' }),
val: /*#__PURE__*/ l.string({ maxLength: 128 }),
ver: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.integer()),
}),
)
export { label }
/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */
type SelfLabel = {
$type?: 'com.atproto.label.defs#selfLabel'
/**
* The short string name of the value or type of this label.
*/
val: string
}
export type { SelfLabel }
/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */
const selfLabel = /*#__PURE__*/ l.typedObject<SelfLabel>(
$nsid,
'selfLabel',
/*#__PURE__*/ l.object({ val: /*#__PURE__*/ l.string({ maxLength: 128 }) }),
)
export { selfLabel }
type LabelValue =
| '!hide'
| '!warn'
| '!no-unauthenticated'
| 'porn'
| 'sexual'
| 'nudity'
| 'graphic-media'
| 'bot'
| l.UnknownString
export type { LabelValue }
const labelValue = /*#__PURE__*/ l.string<{
knownValues: [
'!hide',
'!warn',
'!no-unauthenticated',
'porn',
'sexual',
'nudity',
'graphic-media',
'bot',
]
}>()
export { labelValue }
/** Metadata tags on an atproto record, published by the author within the record. */
type SelfLabels = {
$type?: 'com.atproto.label.defs#selfLabels'
values: SelfLabel[]
}
export type { SelfLabels }
/** Metadata tags on an atproto record, published by the author within the record. */
const selfLabels = /*#__PURE__*/ l.typedObject<SelfLabels>(
$nsid,
'selfLabels',
/*#__PURE__*/ l.object({
values: /*#__PURE__*/ l.array(
/*#__PURE__*/ l.ref<SelfLabel>((() => selfLabel) as any),
{ maxLength: 10 },
),
}),
)
export { selfLabels }
/** Declares a label value and its expected interpretations and behaviors. */
type LabelValueDefinition = {
$type?: 'com.atproto.label.defs#labelValueDefinition'
/**
* 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.
*/
blurs: 'content' | 'media' | 'none' | l.UnknownString
locales: LabelValueDefinitionStrings[]
/**
* How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.
*/
severity: 'inform' | 'alert' | 'none' | l.UnknownString
/**
* Does the user need to have adult content enabled in order to configure this label?
*/
adultOnly?: boolean
/**
* The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).
*/
identifier: string
/**
* The default setting for this label.
*/
defaultSetting?: 'ignore' | 'warn' | 'hide' | l.UnknownString
}
export type { LabelValueDefinition }
/** Declares a label value and its expected interpretations and behaviors. */
const labelValueDefinition = /*#__PURE__*/ l.typedObject<LabelValueDefinition>(
$nsid,
'labelValueDefinition',
/*#__PURE__*/ l.object({
blurs: /*#__PURE__*/ l.string<{
knownValues: ['content', 'media', 'none']
}>(),
locales: /*#__PURE__*/ l.array(
/*#__PURE__*/ l.ref<LabelValueDefinitionStrings>(
(() => labelValueDefinitionStrings) as any,
),
),
severity: /*#__PURE__*/ l.string<{
knownValues: ['inform', 'alert', 'none']
}>(),
adultOnly: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
identifier: /*#__PURE__*/ l.string({ maxLength: 100, maxGraphemes: 100 }),
defaultSetting: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.withDefault(
/*#__PURE__*/ l.string<{ knownValues: ['ignore', 'warn', 'hide'] }>(),
'warn',
),
),
}),
)
export { labelValueDefinition }
/** Strings which describe the label in the UI, localized into a specific language. */
type LabelValueDefinitionStrings = {
$type?: 'com.atproto.label.defs#labelValueDefinitionStrings'
/**
* The code of the language these strings are written in.
*/
lang: l.LanguageString
/**
* A short human-readable name for the label.
*/
name: string
/**
* A longer description of what the label means and why it might be applied.
*/
description: string
}
export type { LabelValueDefinitionStrings }
/** Strings which describe the label in the UI, localized into a specific language. */
const labelValueDefinitionStrings =
/*#__PURE__*/ l.typedObject<LabelValueDefinitionStrings>(
$nsid,
'labelValueDefinitionStrings',
/*#__PURE__*/ l.object({
lang: /*#__PURE__*/ l.string({ format: 'language' }),
name: /*#__PURE__*/ l.string({ maxLength: 640, maxGraphemes: 64 }),
description: /*#__PURE__*/ l.string({
maxLength: 100000,
maxGraphemes: 10000,
}),
}),
)
export { labelValueDefinitionStrings }
+5
View File
@@ -0,0 +1,5 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * from './defs.defs.js'
+5
View File
@@ -0,0 +1,5 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as strongRef from './repo/strongRef.js'
@@ -0,0 +1,48 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
import { l } from '@atproto/lex'
const $nsid = 'com.atproto.repo.strongRef'
type $nsid = typeof $nsid
export { $nsid }
type Main = {
$type?: 'com.atproto.repo.strongRef'
cid: l.CidString
uri: l.AtUriString
}
export type { Main }
const main = /*#__PURE__*/ l.typedObject<Main>(
$nsid,
'main',
/*#__PURE__*/ l.object({
cid: /*#__PURE__*/ l.string({ format: 'cid' }),
uri: /*#__PURE__*/ l.string({ format: 'at-uri' }),
}),
)
export { main }
const $type = $nsid
type $type = typeof $type
export { $type }
export const $isTypeOf = /*#__PURE__*/ main.isTypeOf.bind(main)
export const $build = /*#__PURE__*/ main.build.bind(main)
export const $assert = /*#__PURE__*/ main.assert.bind(main)
export const $check = /*#__PURE__*/ main.check.bind(main)
export const $cast = /*#__PURE__*/ main.cast.bind(main)
export const $ifMatches = /*#__PURE__*/ main.ifMatches.bind(main)
export const $matches = /*#__PURE__*/ main.matches.bind(main)
export const $parse = /*#__PURE__*/ main.parse.bind(main)
export const $safeParse = /*#__PURE__*/ main.safeParse.bind(main)
export const $validate = /*#__PURE__*/ main.validate.bind(main)
export const $safeValidate = /*#__PURE__*/ main.safeValidate.bind(main)
@@ -0,0 +1,6 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * from './strongRef.defs.js'
export { main as default } from './strongRef.defs.js'
+5
View File
@@ -0,0 +1,5 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as stream from './place/stream.js'
+6
View File
@@ -0,0 +1,6 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as chat from './stream/chat.js'
export * as richtext from './stream/richtext.js'
+5
View File
@@ -0,0 +1,5 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as message from './chat/message.js'
@@ -0,0 +1,105 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
import { l } from '@atproto/lex'
import * as RichtextFacet from './..\\richtext\\facet.defs.js'
import * as RepoStrongRef from './..\\..\\..\\com\\atproto\\repo\\strongRef.defs.js'
const $nsid = 'place.stream.chat.message'
type $nsid = typeof $nsid
export { $nsid }
/** Record containing a Streamplace chat message. */
type Main = {
$type: 'place.stream.chat.message'
/**
* The primary message content. May be an empty string, if there are embeds.
*/
text: string
/**
* Client-declared timestamp when this message was originally created.
*/
createdAt: l.DatetimeString
/**
* Annotations of text (mentions, URLs, etc)
*/
facets?: RichtextFacet.Main[]
/**
* The DID of the streamer whose chat this is.
*/
streamer: l.DidString
reply?: ReplyRef
}
export type { Main }
/** Record containing a Streamplace chat message. */
const main = /*#__PURE__*/ l.record<'tid', Main>(
'tid',
$nsid,
/*#__PURE__*/ l.object({
text: /*#__PURE__*/ l.string({ maxLength: 3000, maxGraphemes: 300 }),
createdAt: /*#__PURE__*/ l.string({ format: 'datetime' }),
facets: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.array(
/*#__PURE__*/ l.ref<RichtextFacet.Main>(
(() => RichtextFacet.main) as any,
),
),
),
streamer: /*#__PURE__*/ l.string({ format: 'did' }),
reply: /*#__PURE__*/ l.optional(
/*#__PURE__*/ l.ref<ReplyRef>((() => replyRef) as any),
),
}),
)
export { main }
const $type = $nsid
type $type = typeof $type
export { $type }
export const $isTypeOf = /*#__PURE__*/ main.isTypeOf.bind(main)
export const $build = /*#__PURE__*/ main.build.bind(main)
export const $assert = /*#__PURE__*/ main.assert.bind(main)
export const $check = /*#__PURE__*/ main.check.bind(main)
export const $cast = /*#__PURE__*/ main.cast.bind(main)
export const $ifMatches = /*#__PURE__*/ main.ifMatches.bind(main)
export const $matches = /*#__PURE__*/ main.matches.bind(main)
export const $parse = /*#__PURE__*/ main.parse.bind(main)
export const $safeParse = /*#__PURE__*/ main.safeParse.bind(main)
export const $validate = /*#__PURE__*/ main.validate.bind(main)
export const $safeValidate = /*#__PURE__*/ main.safeValidate.bind(main)
type ReplyRef = {
$type?: 'place.stream.chat.message#replyRef'
root: RepoStrongRef.Main
parent: RepoStrongRef.Main
}
export type { ReplyRef }
const replyRef = /*#__PURE__*/ l.typedObject<ReplyRef>(
$nsid,
'replyRef',
/*#__PURE__*/ l.object({
root: /*#__PURE__*/ l.ref<RepoStrongRef.Main>(
(() => RepoStrongRef.main) as any,
),
parent: /*#__PURE__*/ l.ref<RepoStrongRef.Main>(
(() => RepoStrongRef.main) as any,
),
}),
)
export { replyRef }
@@ -0,0 +1,6 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * from './message.defs.js'
export { main as default } from './message.defs.js'
+5
View File
@@ -0,0 +1,5 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * as facet from './richtext/facet.js'
@@ -0,0 +1,69 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
import { l } from '@atproto/lex'
import * as RichtextFacet from './..\\..\\..\\app\\bsky\\richtext\\facet.defs.js'
const $nsid = 'place.stream.richtext.facet'
type $nsid = typeof $nsid
export { $nsid }
/** Annotation of a sub-string within rich text in a livestream chat message. */
type Main = {
$type?: 'place.stream.richtext.facet'
index: RichtextFacet.ByteSlice
features: (
| l.$Typed<RichtextFacet.Mention>
| l.$Typed<RichtextFacet.Link>
| l.Unknown$TypedObject
)[]
}
export type { Main }
/** Annotation of a sub-string within rich text in a livestream chat message. */
const main = /*#__PURE__*/ l.typedObject<Main>(
$nsid,
'main',
/*#__PURE__*/ l.object({
index: /*#__PURE__*/ l.ref<RichtextFacet.ByteSlice>(
(() => RichtextFacet.byteSlice) as any,
),
features: /*#__PURE__*/ l.array(
/*#__PURE__*/ l.typedUnion(
[
/*#__PURE__*/ l.typedRef<RichtextFacet.Mention>(
(() => RichtextFacet.mention) as any,
),
/*#__PURE__*/ l.typedRef<RichtextFacet.Link>(
(() => RichtextFacet.link) as any,
),
],
false,
),
),
}),
)
export { main }
const $type = $nsid
type $type = typeof $type
export { $type }
export const $isTypeOf = /*#__PURE__*/ main.isTypeOf.bind(main)
export const $build = /*#__PURE__*/ main.build.bind(main)
export const $assert = /*#__PURE__*/ main.assert.bind(main)
export const $check = /*#__PURE__*/ main.check.bind(main)
export const $cast = /*#__PURE__*/ main.cast.bind(main)
export const $ifMatches = /*#__PURE__*/ main.ifMatches.bind(main)
export const $matches = /*#__PURE__*/ main.matches.bind(main)
export const $parse = /*#__PURE__*/ main.parse.bind(main)
export const $safeParse = /*#__PURE__*/ main.safeParse.bind(main)
export const $validate = /*#__PURE__*/ main.validate.bind(main)
export const $safeValidate = /*#__PURE__*/ main.safeValidate.bind(main)
@@ -0,0 +1,6 @@
/*
* THIS FILE WAS GENERATED BY "@atproto/lex". DO NOT EDIT.
*/
export * from './facet.defs.js'
export { main as default } from './facet.defs.js'
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"module": "nodenext",
"target": "esnext",
"types": [
"node"
],
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": false,
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true
}
}