mirror of
https://github.com/urbit/shrub.git
synced 2024-11-28 05:22:27 +03:00
added NPM packages
This commit is contained in:
parent
8f3afbd0ef
commit
41ebbcc82f
1
pkg/npm/README.md
Normal file
1
pkg/npm/README.md
Normal file
@ -0,0 +1 @@
|
||||
Each one of the folders in this directory is published at `@urbit/{folder name}`
|
3
pkg/npm/api/.eslintrc.js
Normal file
3
pkg/npm/api/.eslintrc.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: "@urbit"
|
||||
};
|
85
pkg/npm/api/contacts/index.d.ts
vendored
Normal file
85
pkg/npm/api/contacts/index.d.ts
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
import { Path, Patp } from "..";
|
||||
|
||||
export type ContactUpdate =
|
||||
| ContactUpdateCreate
|
||||
| ContactUpdateDelete
|
||||
| ContactUpdateAdd
|
||||
| ContactUpdateRemove
|
||||
| ContactUpdateEdit
|
||||
| ContactUpdateInitial
|
||||
| ContactUpdateContacts;
|
||||
|
||||
interface ContactUpdateCreate {
|
||||
create: Path;
|
||||
}
|
||||
|
||||
interface ContactUpdateDelete {
|
||||
delete: Path;
|
||||
}
|
||||
|
||||
interface ContactUpdateAdd {
|
||||
add: {
|
||||
path: Path;
|
||||
ship: Patp;
|
||||
contact: Contact;
|
||||
};
|
||||
}
|
||||
|
||||
interface ContactUpdateRemove {
|
||||
remove: {
|
||||
path: Path;
|
||||
ship: Patp;
|
||||
};
|
||||
}
|
||||
|
||||
interface ContactUpdateEdit {
|
||||
edit: {
|
||||
path: Path;
|
||||
ship: Patp;
|
||||
"edit-field": ContactEdit;
|
||||
};
|
||||
}
|
||||
|
||||
interface ContactUpdateInitial {
|
||||
initial: Rolodex;
|
||||
}
|
||||
|
||||
interface ContactUpdateContacts {
|
||||
contacts: {
|
||||
path: Path;
|
||||
contacts: Contacts;
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
type ContactAvatar = ContactAvatarUrl | ContactAvatarOcts;
|
||||
|
||||
export type Rolodex = {
|
||||
[p in Path]: Contacts;
|
||||
};
|
||||
|
||||
export type Contacts = {
|
||||
[p in Patp]: Contact;
|
||||
};
|
||||
|
||||
interface ContactAvatarUrl {
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface ContactAvatarOcts {
|
||||
octs: string;
|
||||
}
|
||||
export interface Contact {
|
||||
nickname: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
website: string;
|
||||
notes: string;
|
||||
color: string;
|
||||
avatar: string | null;
|
||||
}
|
||||
|
||||
export type ContactEdit = {
|
||||
[k in keyof Contact]: Contact[k];
|
||||
};
|
83
pkg/npm/api/contacts/index.ts
Normal file
83
pkg/npm/api/contacts/index.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { Enc, Path, Patp, Poke } from "..";
|
||||
import { Contact, ContactEdit, ContactUpdateCreate, ContactUpdateEdit, ContactUpdateRemove } from "./index.d";
|
||||
import { GroupPolicy, Resource } from "../groups/index.d"
|
||||
|
||||
export const viewAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'contact-view',
|
||||
mark: 'json',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const hookAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'contact-hook',
|
||||
mark: 'contact-action',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const create = (
|
||||
name: string,
|
||||
policy: Enc<GroupPolicy>,
|
||||
title: string,
|
||||
description: string
|
||||
): Poke<ContactUpdateCreate> => viewAction({ // TODO which type is correct?
|
||||
create: {
|
||||
name,
|
||||
policy,
|
||||
title,
|
||||
description
|
||||
}
|
||||
});
|
||||
|
||||
export const share = (
|
||||
recipient: Patp,
|
||||
path: Patp,
|
||||
ship: Patp,
|
||||
contact: Contact
|
||||
): Poke<any> => viewAction({ // TODO type
|
||||
share: {
|
||||
recipient,
|
||||
path,
|
||||
ship,
|
||||
contact
|
||||
}
|
||||
});
|
||||
|
||||
export const remove = (
|
||||
path: Path,
|
||||
ship: Patp
|
||||
): Poke<ContactUpdateRemove> => viewAction({
|
||||
remove: {
|
||||
path,
|
||||
ship
|
||||
}
|
||||
});
|
||||
|
||||
export const edit = (
|
||||
path: Path,
|
||||
ship: Patp,
|
||||
editField: ContactEdit
|
||||
): Poke<ContactUpdateEdit> => hookAction({
|
||||
edit: {
|
||||
path,
|
||||
ship,
|
||||
'edit-field': editField
|
||||
}
|
||||
});
|
||||
|
||||
export const invite = (
|
||||
resource: Resource,
|
||||
ship: Patp,
|
||||
text: string = ''
|
||||
): Poke<any> => viewAction({ // TODO type
|
||||
invite: {
|
||||
resource,
|
||||
ship,
|
||||
text
|
||||
}
|
||||
});
|
||||
|
||||
export const join = (
|
||||
resource: Resource
|
||||
): Poke<any> => viewAction({ // TODO type
|
||||
join: resource
|
||||
});
|
47
pkg/npm/api/graph/index.d.ts
vendored
Normal file
47
pkg/npm/api/graph/index.d.ts
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
import { Patp } from "..";
|
||||
import BigIntOrderedMap from "../lib/BigIntOrderedMap";
|
||||
|
||||
export interface TextContent {
|
||||
text: string;
|
||||
}
|
||||
export interface UrlContent {
|
||||
url: string;
|
||||
}
|
||||
export interface CodeContent {
|
||||
code: {
|
||||
expresssion: string;
|
||||
output: string | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReferenceContent {
|
||||
uid: string;
|
||||
}
|
||||
export interface MentionContent {
|
||||
mention: string;
|
||||
}
|
||||
export type Content =
|
||||
| TextContent
|
||||
| UrlContent
|
||||
| CodeContent
|
||||
| ReferenceContent
|
||||
| MentionContent;
|
||||
|
||||
export interface Post {
|
||||
author: Patp;
|
||||
contents: Content[];
|
||||
hash: string | null;
|
||||
index: string;
|
||||
pending?: boolean;
|
||||
signatures: string[];
|
||||
"time-sent": number;
|
||||
}
|
||||
|
||||
export interface GraphNode {
|
||||
children: Graph;
|
||||
post: Post;
|
||||
}
|
||||
|
||||
export type Graph = BigIntOrderedMap<GraphNode>;
|
||||
|
||||
export type Graphs = { [rid: string]: Graph };
|
364
pkg/npm/api/graph/index.ts
Normal file
364
pkg/npm/api/graph/index.ts
Normal file
@ -0,0 +1,364 @@
|
||||
import _ from 'lodash';
|
||||
import { PatpNoSig, Patp, Poke, Thread, Path, Enc } from '..';
|
||||
import { Content, GraphNode, Post } from './index.d';
|
||||
import { deSig, unixToDa } from '../lib/util';
|
||||
import { makeResource, resourceFromPath } from '../groups/index';
|
||||
import { GroupPolicy } from '../groups';
|
||||
|
||||
export const createBlankNodeWithChildPost = (
|
||||
ship: PatpNoSig,
|
||||
parentIndex: string = '',
|
||||
childIndex: string = '',
|
||||
contents: Content[]
|
||||
): GraphNode => {
|
||||
const date = unixToDa(Date.now()).toString();
|
||||
const nodeIndex = parentIndex + '/' + date;
|
||||
|
||||
const childGraph = {};
|
||||
childGraph[childIndex] = {
|
||||
post: {
|
||||
author: `~${ship}`,
|
||||
index: nodeIndex + '/' + childIndex,
|
||||
'time-sent': Date.now(),
|
||||
contents,
|
||||
hash: null,
|
||||
signatures: []
|
||||
},
|
||||
children: null
|
||||
};
|
||||
|
||||
return {
|
||||
post: {
|
||||
author: `~${ship}`,
|
||||
index: nodeIndex,
|
||||
'time-sent': Date.now(),
|
||||
contents: [],
|
||||
hash: null,
|
||||
signatures: []
|
||||
},
|
||||
children: childGraph
|
||||
};
|
||||
};
|
||||
|
||||
function markPending(nodes: any) {
|
||||
_.forEach(nodes, node => {
|
||||
node.post.author = deSig(node.post.author);
|
||||
node.post.pending = true;
|
||||
markPending(node.children || {});
|
||||
});
|
||||
}
|
||||
|
||||
export const createPost = (
|
||||
ship: PatpNoSig,
|
||||
contents: Content[],
|
||||
parentIndex: string = '',
|
||||
childIndex:string = 'DATE_PLACEHOLDER'
|
||||
): Post => {
|
||||
if (childIndex === 'DATE_PLACEHOLDER') {
|
||||
childIndex = unixToDa(Date.now()).toString();
|
||||
}
|
||||
return {
|
||||
author: `~${ship}`,
|
||||
index: parentIndex + '/' + childIndex,
|
||||
'time-sent': Date.now(),
|
||||
contents,
|
||||
hash: null,
|
||||
signatures: []
|
||||
};
|
||||
};
|
||||
|
||||
function moduleToMark(mod: string): string | undefined {
|
||||
if(mod === 'link') {
|
||||
return 'graph-validator-link';
|
||||
}
|
||||
if(mod === 'publish') {
|
||||
return 'graph-validator-publish';
|
||||
}
|
||||
if(mod === 'chat') {
|
||||
return 'graph-validator-chat';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const storeAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'graph-store',
|
||||
mark: 'graph-update',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const viewAction = <T>(threadName: string, action: T): Thread<T> => ({
|
||||
inputMark: 'graph-view-action',
|
||||
outputMark: 'json',
|
||||
threadName,
|
||||
body: action
|
||||
});
|
||||
|
||||
export const hookAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'graph-push-hook',
|
||||
mark: 'graph-update',
|
||||
json: data
|
||||
});
|
||||
|
||||
|
||||
export const createManagedGraph = (
|
||||
ship: PatpNoSig,
|
||||
name: string,
|
||||
title: string,
|
||||
description: string,
|
||||
group: Path,
|
||||
mod: string
|
||||
): Thread<any> => {
|
||||
const associated = { group: resourceFromPath(group) };
|
||||
const resource = makeResource(`~${ship}`, name);
|
||||
|
||||
return viewAction('graph-create', {
|
||||
create: {
|
||||
resource,
|
||||
title,
|
||||
description,
|
||||
associated,
|
||||
module: mod,
|
||||
mark: moduleToMark(mod)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const createUnmanagedGraph = (
|
||||
ship: PatpNoSig,
|
||||
name: string,
|
||||
title: string,
|
||||
description: string,
|
||||
policy: Enc<GroupPolicy>,
|
||||
mod: string
|
||||
): Thread<any> => {
|
||||
const resource = makeResource(`~${ship}`, name);
|
||||
|
||||
return viewAction('graph-create', {
|
||||
create: {
|
||||
resource,
|
||||
title,
|
||||
description,
|
||||
associated: { policy },
|
||||
module: mod,
|
||||
mark: moduleToMark(mod)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const joinGraph = (
|
||||
ship: Patp,
|
||||
name: string
|
||||
): Thread<any> => {
|
||||
const resource = makeResource(ship, name);
|
||||
return viewAction('graph-join', {
|
||||
join: {
|
||||
resource,
|
||||
ship,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const deleteGraph = (
|
||||
ship: PatpNoSig,
|
||||
name: string
|
||||
): Thread<any> => {
|
||||
const resource = makeResource(`~${ship}`, name);
|
||||
return viewAction('graph-delete', {
|
||||
"delete": {
|
||||
resource
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const leaveGraph = (
|
||||
ship: Patp,
|
||||
name: string
|
||||
): Thread<any> => {
|
||||
const resource = makeResource(ship, name);
|
||||
return viewAction('graph-leave', {
|
||||
"leave": {
|
||||
resource
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const groupifyGraph = (
|
||||
ship: Patp,
|
||||
name: string,
|
||||
toPath?: string
|
||||
): Thread<any> => {
|
||||
const resource = makeResource(ship, name);
|
||||
const to = toPath && resourceFromPath(toPath);
|
||||
|
||||
return viewAction('graph-groupify', {
|
||||
groupify: {
|
||||
resource,
|
||||
to
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const evalCord = (
|
||||
cord: string
|
||||
): Thread<any> => {
|
||||
return ({
|
||||
inputMark: 'graph-view-action',
|
||||
outputMark: 'tang',
|
||||
threadName: 'graph-eval',
|
||||
body: {
|
||||
eval: cord
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const addGraph = (
|
||||
ship: Patp,
|
||||
name: string,
|
||||
graph: any,
|
||||
mark: any
|
||||
): Poke<any> => {
|
||||
return storeAction({
|
||||
'add-graph': {
|
||||
resource: { ship, name },
|
||||
graph,
|
||||
mark
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const addPost = (
|
||||
ship: Patp,
|
||||
name: string,
|
||||
post: Post
|
||||
) => {
|
||||
let nodes = {};
|
||||
nodes[post.index] = {
|
||||
post,
|
||||
children: null
|
||||
};
|
||||
return addNodes(ship, name, nodes);
|
||||
}
|
||||
|
||||
export const addNode = (
|
||||
ship: Patp,
|
||||
name: string,
|
||||
node: GraphNode
|
||||
) => {
|
||||
let nodes = {};
|
||||
nodes[node.post.index] = node;
|
||||
|
||||
return this.addNodes(ship, name, nodes);
|
||||
}
|
||||
|
||||
export const addNodes = (
|
||||
ship: Patp,
|
||||
name: string,
|
||||
nodes: Object
|
||||
): Poke<any> => {
|
||||
const action = {
|
||||
'add-nodes': {
|
||||
resource: { ship, name },
|
||||
nodes
|
||||
}
|
||||
};
|
||||
|
||||
markPending(action['add-nodes'].nodes);
|
||||
action['add-nodes'].resource.ship = action['add-nodes'].resource.ship.slice(1);
|
||||
// this.store.handleEvent({ data: { 'graph-update': action } });// TODO address this.store
|
||||
return hookAction(action);
|
||||
}
|
||||
|
||||
export const removeNodes = (
|
||||
ship: Patp,
|
||||
name: string,
|
||||
indices: string[]
|
||||
): Poke<any> => {
|
||||
return hookAction({
|
||||
'remove-nodes': {
|
||||
resource: { ship, name },
|
||||
indices
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO these abominations
|
||||
// getKeys() {
|
||||
// return this.scry<any>('graph-store', '/keys')
|
||||
// .then((keys) => {
|
||||
// this.store.handleEvent({
|
||||
// data: keys
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// getTags() {
|
||||
// return this.scry<any>('graph-store', '/tags')
|
||||
// .then((tags) => {
|
||||
// this.store.handleEvent({
|
||||
// data: tags
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// getTagQueries() {
|
||||
// return this.scry<any>('graph-store', '/tag-queries')
|
||||
// .then((tagQueries) => {
|
||||
// this.store.handleEvent({
|
||||
// data: tagQueries
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// getGraph(ship: string, resource: string) {
|
||||
// return this.scry<any>('graph-store', `/graph/${ship}/${resource}`)
|
||||
// .then((graph) => {
|
||||
// this.store.handleEvent({
|
||||
// data: graph
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// async getNewest(ship: string, resource: string, count: number, index = '') {
|
||||
// const data = await this.scry<any>('graph-store', `/newest/${ship}/${resource}/${count}${index}`);
|
||||
// this.store.handleEvent({ data });
|
||||
// }
|
||||
|
||||
// async getOlderSiblings(ship: string, resource: string, count: number, index = '') {
|
||||
// const idx = index.split('/').map(decToUd).join('/');
|
||||
// const data = await this.scry<any>('graph-store',
|
||||
// `/node-siblings/older/${ship}/${resource}/${count}${idx}`
|
||||
// );
|
||||
// this.store.handleEvent({ data });
|
||||
// }
|
||||
|
||||
// async getYoungerSiblings(ship: string, resource: string, count: number, index = '') {
|
||||
// const idx = index.split('/').map(decToUd).join('/');
|
||||
// const data = await this.scry<any>('graph-store',
|
||||
// `/node-siblings/younger/${ship}/${resource}/${count}${idx}`
|
||||
// );
|
||||
// this.store.handleEvent({ data });
|
||||
// }
|
||||
|
||||
|
||||
// getGraphSubset(ship: string, resource: string, start: string, end: string) {
|
||||
// return this.scry<any>(
|
||||
// 'graph-store',
|
||||
// `/graph-subset/${ship}/${resource}/${end}/${start}`
|
||||
// ).then((subset) => {
|
||||
// this.store.handleEvent({
|
||||
// data: subset
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// getNode(ship: string, resource: string, index: string) {
|
||||
// const idx = index.split('/').map(numToUd).join('/');
|
||||
// return this.scry<any>(
|
||||
// 'graph-store',
|
||||
// `/node/${ship}/${resource}${idx}`
|
||||
// ).then((node) => {
|
||||
// this.store.handleEvent({
|
||||
// data: node
|
||||
// });
|
||||
// });
|
||||
// }
|
177
pkg/npm/api/groups/index.d.ts
vendored
Normal file
177
pkg/npm/api/groups/index.d.ts
vendored
Normal file
@ -0,0 +1,177 @@
|
||||
import { PatpNoSig, Path, Jug, ShipRank, Enc } from '..';
|
||||
|
||||
export interface RoleTag {
|
||||
tag: 'admin' | 'moderator' | 'janitor';
|
||||
}
|
||||
|
||||
export interface AppTag {
|
||||
app: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export type Tag = AppTag | RoleTag;
|
||||
|
||||
export interface InvitePolicy {
|
||||
invite: {
|
||||
pending: Set<PatpNoSig>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OpenPolicy {
|
||||
open: {
|
||||
banned: Set<PatpNoSig>;
|
||||
banRanks: Set<ShipRank>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Resource {
|
||||
name: string;
|
||||
ship: PatpNoSig; // TODO different declaration than in metadata?
|
||||
}
|
||||
|
||||
export type OpenPolicyDiff =
|
||||
| AllowRanksDiff
|
||||
| BanRanksDiff
|
||||
| AllowShipsDiff
|
||||
| BanShipsDiff;
|
||||
|
||||
export interface AllowRanksDiff {
|
||||
allowRanks: ShipRank[];
|
||||
}
|
||||
|
||||
export interface BanRanksDiff {
|
||||
banRanks: ShipRank[];
|
||||
}
|
||||
|
||||
export interface AllowShipsDiff {
|
||||
allowShips: PatpNoSig[];
|
||||
}
|
||||
|
||||
export interface BanShipsDiff {
|
||||
banShips: PatpNoSig[];
|
||||
}
|
||||
|
||||
export type InvitePolicyDiff = AddInvitesDiff | RemoveInvitesDiff;
|
||||
|
||||
export interface AddInvitesDiff {
|
||||
addInvites: PatpNoSig[];
|
||||
}
|
||||
|
||||
export interface RemoveInvitesDiff {
|
||||
removeInvites: PatpNoSig[];
|
||||
}
|
||||
|
||||
export interface ReplacePolicyDiff {
|
||||
replace: GroupPolicy;
|
||||
}
|
||||
|
||||
export type GroupPolicyDiff =
|
||||
| { open: OpenPolicyDiff }
|
||||
| { invite: InvitePolicyDiff }
|
||||
| ReplacePolicyDiff;
|
||||
|
||||
export type GroupPolicy = OpenPolicy | InvitePolicy;
|
||||
|
||||
export interface TaggedShips {
|
||||
[tag: string]: Set<PatpNoSig>;
|
||||
}
|
||||
|
||||
export interface Tags {
|
||||
role: TaggedShips;
|
||||
[app: string]: TaggedShips;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
members: Set<PatpNoSig>;
|
||||
tags: Tags;
|
||||
policy: GroupPolicy;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export type Groups = {
|
||||
[p in Path]: Group;
|
||||
};
|
||||
|
||||
export interface GroupUpdateInitial {
|
||||
initial: Enc<Groups>;
|
||||
}
|
||||
|
||||
export interface GroupUpdateAddGroup {
|
||||
addGroup: {
|
||||
resource: Resource;
|
||||
policy: Enc<GroupPolicy>;
|
||||
hidden: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GroupUpdateAddMembers {
|
||||
addMembers: {
|
||||
ships: PatpNoSig[];
|
||||
resource: Resource;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GroupUpdateRemoveMembers {
|
||||
removeMembers: {
|
||||
ships: PatpNoSig[];
|
||||
resource: Resource;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GroupUpdateAddTag {
|
||||
addTag: {
|
||||
tag: Tag;
|
||||
resource: Resource;
|
||||
ships: PatpNoSig[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GroupUpdateRemoveTag {
|
||||
removeTag: {
|
||||
tag: Tag;
|
||||
resource: Resource;
|
||||
ships: PatpNoSig[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GroupUpdateChangePolicy {
|
||||
changePolicy: { resource: Resource; diff: GroupPolicyDiff };
|
||||
}
|
||||
|
||||
export interface GroupUpdateRemoveGroup {
|
||||
removeGroup: {
|
||||
resource: Resource;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GroupUpdateExpose {
|
||||
expose: {
|
||||
resource: Resource;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GroupUpdateInitialGroup {
|
||||
initialGroup: {
|
||||
resource: Resource;
|
||||
group: Enc<Group>;
|
||||
};
|
||||
}
|
||||
|
||||
export type GroupUpdate =
|
||||
| GroupUpdateInitial
|
||||
| GroupUpdateAddGroup
|
||||
| GroupUpdateAddMembers
|
||||
| GroupUpdateRemoveMembers
|
||||
| GroupUpdateAddTag
|
||||
| GroupUpdateRemoveTag
|
||||
| GroupUpdateChangePolicy
|
||||
| GroupUpdateRemoveGroup
|
||||
| GroupUpdateExpose
|
||||
| GroupUpdateInitialGroup;
|
||||
|
||||
export type GroupAction = Omit<GroupUpdate, 'initialGroup' | 'initial'>;
|
||||
|
||||
export const groupBunts = {
|
||||
group: (): Group => ({ members: new Set(), tags: { role: {} }, hidden: false, policy: groupBunts.policy() }),
|
||||
policy: (): GroupPolicy => ({ open: { banned: new Set(), banRanks: new Set() } })
|
||||
};
|
108
pkg/npm/api/groups/index.ts
Normal file
108
pkg/npm/api/groups/index.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { Enc, Path, Patp, PatpNoSig, Poke } from "..";
|
||||
import {
|
||||
Group,
|
||||
GroupAction,
|
||||
GroupPolicyDiff,
|
||||
GroupUpdateAddMembers,
|
||||
GroupUpdateAddTag,
|
||||
GroupUpdateChangePolicy,
|
||||
GroupUpdateRemoveGroup,
|
||||
GroupUpdateRemoveMembers,
|
||||
GroupUpdateRemoveTag,
|
||||
Resource,
|
||||
Tag
|
||||
} from "./index.d";
|
||||
|
||||
export const proxyAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'group-push-hook',
|
||||
mark: 'group-update',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const storeAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'group-store',
|
||||
mark: 'group-update',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const remove = (
|
||||
resource: Resource,
|
||||
ships: PatpNoSig[]
|
||||
): Poke<GroupUpdateRemoveMembers> => proxyAction({
|
||||
removeMembers: {
|
||||
resource,
|
||||
ships
|
||||
}
|
||||
});
|
||||
|
||||
export const addTag = (
|
||||
resource: Resource,
|
||||
tag: Tag,
|
||||
ships: Patp[]
|
||||
): Poke<GroupUpdateAddTag> => proxyAction({
|
||||
addTag: {
|
||||
resource,
|
||||
tag,
|
||||
ships
|
||||
}
|
||||
});
|
||||
|
||||
export const removeTag = (
|
||||
tag: Tag,
|
||||
resource: Resource,
|
||||
ships: PatpNoSig[]
|
||||
): Poke<GroupUpdateRemoveTag> => proxyAction({
|
||||
removeTag: {
|
||||
tag,
|
||||
resource,
|
||||
ships
|
||||
}
|
||||
});
|
||||
|
||||
export const add = (
|
||||
resource: Resource,
|
||||
ships: PatpNoSig[]
|
||||
): Poke<GroupUpdateAddMembers> => proxyAction({
|
||||
addMembers: {
|
||||
resource,
|
||||
ships
|
||||
}
|
||||
});
|
||||
|
||||
export const removeGroup = (
|
||||
resource: Resource
|
||||
): Poke<GroupUpdateRemoveGroup> => storeAction({
|
||||
removeGroup: {
|
||||
resource
|
||||
}
|
||||
});
|
||||
|
||||
export const changePolicy = (
|
||||
resource: Resource,
|
||||
diff: GroupPolicyDiff
|
||||
): Poke<GroupUpdateChangePolicy> => proxyAction({
|
||||
changePolicy: {
|
||||
resource,
|
||||
diff
|
||||
}
|
||||
});
|
||||
|
||||
const roleTags = ['janitor', 'moderator', 'admin'];
|
||||
// TODO make this type better?
|
||||
|
||||
export function roleForShip(group: Group, ship: PatpNoSig): string | undefined {
|
||||
return roleTags.reduce((currRole, role) => {
|
||||
const roleShips = group?.tags?.role?.[role];
|
||||
return roleShips && roleShips.has(ship) ? role : currRole;
|
||||
}, undefined as string | undefined);
|
||||
}
|
||||
|
||||
export function resourceFromPath(path: Path): Resource {
|
||||
const [, , ship, name] = path.split('/');
|
||||
return { ship, name }
|
||||
}
|
||||
|
||||
export function makeResource(ship: string, name:string) {
|
||||
return { ship, name };
|
||||
}
|
||||
|
78
pkg/npm/api/hark/index.d.ts
vendored
Normal file
78
pkg/npm/api/hark/index.d.ts
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
import { Content, Post } from "../graph/index.d";
|
||||
import { GroupUpdate } from "../groups/index.d";
|
||||
import BigIntOrderedMap from "../lib/BigIntOrderedMap";
|
||||
|
||||
export type GraphNotifDescription = "link" | "comment" | "note" | "mention";
|
||||
|
||||
export interface UnreadStats {
|
||||
unreads: Set<string> | number;
|
||||
notifications: number;
|
||||
last: number;
|
||||
}
|
||||
|
||||
export interface GraphNotifIndex {
|
||||
graph: string;
|
||||
group: string;
|
||||
description: GraphNotifDescription;
|
||||
module: string;
|
||||
index: string;
|
||||
}
|
||||
|
||||
export interface GroupNotifIndex {
|
||||
group: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ChatNotifIndex {
|
||||
chat: string;
|
||||
mention: boolean;
|
||||
}
|
||||
|
||||
export type NotifIndex =
|
||||
| { graph: GraphNotifIndex }
|
||||
| { group: GroupNotifIndex }
|
||||
| { chat: ChatNotifIndex };
|
||||
|
||||
export type GraphNotificationContents = Post[];
|
||||
|
||||
export type GroupNotificationContents = GroupUpdate[];
|
||||
|
||||
export type ChatNotificationContents = Content[];
|
||||
|
||||
export type NotificationContents =
|
||||
| { graph: GraphNotificationContents }
|
||||
| { group: GroupNotificationContents }
|
||||
| { chat: ChatNotificationContents };
|
||||
|
||||
export interface Notification {
|
||||
read: boolean;
|
||||
time: number;
|
||||
contents: NotificationContents;
|
||||
}
|
||||
|
||||
export interface IndexedNotification {
|
||||
index: NotifIndex;
|
||||
notification: Notification;
|
||||
}
|
||||
|
||||
export type Timebox = IndexedNotification[];
|
||||
|
||||
export type Notifications = BigIntOrderedMap<Timebox>;
|
||||
|
||||
export interface NotificationGraphConfig {
|
||||
watchOnSelf: boolean;
|
||||
mentions: boolean;
|
||||
watching: WatchedIndex[]
|
||||
}
|
||||
|
||||
export interface Unreads {
|
||||
chat: Record<string, UnreadStats>;
|
||||
graph: Record<string, Record<string, UnreadStats>>;
|
||||
group: Record<string, UnreadStats>;
|
||||
}
|
||||
|
||||
interface WatchedIndex {
|
||||
graph: string;
|
||||
index: string;
|
||||
}
|
||||
export type GroupNotificationsConfig = string[];
|
0
pkg/npm/api/hark/index.ts
Normal file
0
pkg/npm/api/hark/index.ts
Normal file
67
pkg/npm/api/index.d.ts
vendored
Normal file
67
pkg/npm/api/index.d.ts
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Martian embassy
|
||||
*/
|
||||
|
||||
// an urbit style path rendered as string
|
||||
export type Path = string;
|
||||
|
||||
// patp including leading sig
|
||||
export type Patp = string;
|
||||
|
||||
// patp excluding leading sig
|
||||
export type PatpNoSig = string;
|
||||
|
||||
// @uvH encoded string
|
||||
export type Serial = string;
|
||||
|
||||
// jug from hoon
|
||||
export type Jug<K,V> = Map<K,Set<V>>;
|
||||
|
||||
// name of app
|
||||
export type AppName = 'chat' | 'link' | 'contacts' | 'publish' | 'graph';
|
||||
|
||||
export function getTagFromFrond<O>(frond: O): keyof O {
|
||||
const tags = Object.keys(frond) as Array<keyof O>;
|
||||
const tag = tags[0];
|
||||
if(!tag) {
|
||||
throw new Error("bad frond");
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
export type ShipRank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn';
|
||||
|
||||
export type Action = 'poke' | 'subscribe' | 'ack' | 'unsubscribe' | 'delete';
|
||||
|
||||
|
||||
export type SetElement<S> = S extends Set<(infer T)> ? T : never;
|
||||
export type MapKey<M> = M extends Map<(infer K), any> ? K : never;
|
||||
export type MapValue<M> = M extends Map<any, (infer V)> ? V : never;
|
||||
|
||||
/**
|
||||
* Turns sets into arrays and maps into objects so we can send them over the wire
|
||||
*/
|
||||
export type Enc<S> =
|
||||
S extends Set<any> ?
|
||||
Enc<SetElement<S>>[] :
|
||||
S extends Map<string, any> ?
|
||||
{ [s: string]: Enc<MapValue<S>> } :
|
||||
S extends object ?
|
||||
{ [K in keyof S]: Enc<S[K]> } :
|
||||
S;
|
||||
|
||||
export type Mark = string;
|
||||
|
||||
export interface Poke<Action> {
|
||||
ship?: string; // This should be handled by the http library, but is part of the spec
|
||||
app: string;
|
||||
mark: Mark;
|
||||
json: Action;
|
||||
}
|
||||
|
||||
export interface Thread<Action> {
|
||||
inputMark: string;
|
||||
outputMark: string;
|
||||
threadName: string;
|
||||
body: Action;
|
||||
}
|
5
pkg/npm/api/index.js
Normal file
5
pkg/npm/api/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import BigIntOrderedMap from './lib/BigIntOrderedMap';
|
||||
|
||||
export {
|
||||
BigIntOrderedMap
|
||||
};
|
85
pkg/npm/api/invite/index.d.ts
vendored
Normal file
85
pkg/npm/api/invite/index.d.ts
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
import { Serial, PatpNoSig, Path } from '..';
|
||||
|
||||
export type InviteUpdate =
|
||||
InviteUpdateInitial
|
||||
| InviteUpdateCreate
|
||||
| InviteUpdateDelete
|
||||
| InviteUpdateInvite
|
||||
| InviteUpdateAccepted
|
||||
| InviteUpdateDecline;
|
||||
|
||||
export interface InviteUpdateInitial {
|
||||
initial: Invites;
|
||||
}
|
||||
|
||||
export interface InviteUpdateCreate {
|
||||
create: {
|
||||
path: Path;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InviteUpdateDelete {
|
||||
delete: {
|
||||
path: Path;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InviteUpdateInvite {
|
||||
invite: {
|
||||
path: Path;
|
||||
uid: Serial;
|
||||
invite: Invite;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InviteUpdateAccepted {
|
||||
accepted: {
|
||||
path: Path;
|
||||
uid: Serial;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InviteUpdateDecline {
|
||||
decline: {
|
||||
path: Path;
|
||||
uid: Serial;
|
||||
};
|
||||
}
|
||||
|
||||
export type InviteAction =
|
||||
InviteActionAccept
|
||||
| InviteActionDecline;
|
||||
|
||||
export interface InviteActionAccept {
|
||||
accept: {
|
||||
term: string,
|
||||
uid: Serial
|
||||
}
|
||||
}
|
||||
|
||||
export interface InviteActionDecline {
|
||||
decline: {
|
||||
term: string,
|
||||
uid: Serial
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// actual datastructures
|
||||
|
||||
|
||||
export type Invites = {
|
||||
[p in Path]: AppInvites;
|
||||
};
|
||||
|
||||
export type AppInvites = {
|
||||
[s in Serial]: Invite;
|
||||
};
|
||||
|
||||
export interface Invite {
|
||||
app: string;
|
||||
path: Path;
|
||||
recipient: PatpNoSig;
|
||||
ship: PatpNoSig;
|
||||
text: string;
|
||||
}
|
28
pkg/npm/api/invite/index.ts
Normal file
28
pkg/npm/api/invite/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { InviteAction, InviteActionAccept, InviteActionDecline } from "./index.d";
|
||||
import { Poke, Serial } from "..";
|
||||
|
||||
export const action = <T>(data: T): Poke<T> => ({
|
||||
app: 'invite-store',
|
||||
mark: 'invite-action',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const accept = (
|
||||
app: string,
|
||||
uid: Serial
|
||||
): Poke<InviteActionAccept> => action({
|
||||
accept: {
|
||||
term: app,
|
||||
uid
|
||||
}
|
||||
});
|
||||
|
||||
export const decline = (
|
||||
app: string,
|
||||
uid: Serial
|
||||
): Poke<InviteActionDecline> => action({
|
||||
decline: {
|
||||
term: app,
|
||||
uid
|
||||
}
|
||||
});
|
233
pkg/npm/api/lib/BigIntOrderedMap.ts
Normal file
233
pkg/npm/api/lib/BigIntOrderedMap.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import { BigInteger } from "big-integer";
|
||||
|
||||
interface NonemptyNode<V> {
|
||||
n: [BigInteger, V];
|
||||
l: MapNode<V>;
|
||||
r: MapNode<V>;
|
||||
}
|
||||
|
||||
type MapNode<V> = NonemptyNode<V> | null;
|
||||
|
||||
/**
|
||||
* An implementation of ordered maps for JS
|
||||
* Plagiarised wholesale from sys/zuse
|
||||
*/
|
||||
export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
||||
private root: MapNode<V> = null;
|
||||
size: number = 0;
|
||||
|
||||
constructor(initial: [BigInteger, V][] = []) {
|
||||
initial.forEach(([key, val]) => {
|
||||
this.set(key, val);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an value for a key
|
||||
*/
|
||||
get(key: BigInteger): V | null {
|
||||
const inner = (node: MapNode<V>) => {
|
||||
if (!node) {
|
||||
return node;
|
||||
}
|
||||
const [k, v] = node.n;
|
||||
if (key.eq(k)) {
|
||||
return v;
|
||||
}
|
||||
if (key.gt(k)) {
|
||||
return inner(node.l);
|
||||
} else {
|
||||
return inner(node.r);
|
||||
}
|
||||
};
|
||||
|
||||
return inner(this.root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put an item by a key
|
||||
*/
|
||||
set(key: BigInteger, value: V): void {
|
||||
|
||||
const inner = (node: MapNode<V>) => {
|
||||
if (!node) {
|
||||
return {
|
||||
n: [key, value],
|
||||
l: null,
|
||||
r: null,
|
||||
};
|
||||
}
|
||||
const [k] = node.n;
|
||||
if (key.eq(k)) {
|
||||
this.size--;
|
||||
return {
|
||||
...node,
|
||||
n: [k, value],
|
||||
};
|
||||
}
|
||||
if (key.gt(k)) {
|
||||
const l = inner(node.l);
|
||||
if (!l) {
|
||||
throw new Error("invariant violation");
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
l,
|
||||
};
|
||||
}
|
||||
const r = inner(node.r);
|
||||
if (!r) {
|
||||
throw new Error("invariant violation");
|
||||
}
|
||||
|
||||
return { ...node, r };
|
||||
};
|
||||
this.size++;
|
||||
this.root = inner(this.root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all entries
|
||||
*/
|
||||
clear() {
|
||||
this.root = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicate testing if map contains key
|
||||
*/
|
||||
has(key: BigInteger): boolean {
|
||||
const inner = (node: MapNode<V>) => {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const [k] = node.n;
|
||||
|
||||
if (k.eq(key)) {
|
||||
return true;
|
||||
}
|
||||
if (key.gt(k)) {
|
||||
return inner(node.l);
|
||||
}
|
||||
return inner(node.r);
|
||||
};
|
||||
return inner(this.root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove value associated with key, returning whether that key
|
||||
* existed in the first place
|
||||
*/
|
||||
delete(key: BigInteger) {
|
||||
const inner = (node: MapNode<V>): [boolean, MapNode<V>] => {
|
||||
if (!node) {
|
||||
return [false, null];
|
||||
}
|
||||
const [k] = node.n;
|
||||
if (k.eq(key)) {
|
||||
return [true, this.nip(node)];
|
||||
}
|
||||
if (key.gt(k)) {
|
||||
const [bool, l] = inner(node.l);
|
||||
return [
|
||||
bool,
|
||||
{
|
||||
...node,
|
||||
l,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const [bool, r] = inner(node.r);
|
||||
return [
|
||||
bool,
|
||||
{
|
||||
...node,
|
||||
r,
|
||||
},
|
||||
];
|
||||
};
|
||||
const [ret, newRoot] = inner(this.root);
|
||||
if(ret) {
|
||||
this.size--;
|
||||
}
|
||||
this.root = newRoot;
|
||||
return ret;
|
||||
}
|
||||
|
||||
private nip(nod: NonemptyNode<V>): MapNode<V> {
|
||||
const inner = (node: NonemptyNode<V>) => {
|
||||
if (!node.l) {
|
||||
return node.r;
|
||||
}
|
||||
if (!node.r) {
|
||||
return node.l;
|
||||
}
|
||||
return {
|
||||
...node.l,
|
||||
r: inner(node.r),
|
||||
};
|
||||
};
|
||||
return inner(nod);
|
||||
}
|
||||
|
||||
peekLargest(): [BigInteger, V] | undefined {
|
||||
const inner = (node: MapNode<V>) => {
|
||||
if(!node) {
|
||||
return undefined;
|
||||
}
|
||||
if(node.l) {
|
||||
return inner(node.l);
|
||||
}
|
||||
return node.n;
|
||||
}
|
||||
return inner(this.root);
|
||||
}
|
||||
|
||||
peekSmallest(): [BigInteger, V] | undefined {
|
||||
const inner = (node: MapNode<V>) => {
|
||||
if(!node) {
|
||||
return undefined;
|
||||
}
|
||||
if(node.r) {
|
||||
return inner(node.r);
|
||||
}
|
||||
return node.n;
|
||||
}
|
||||
return inner(this.root);
|
||||
}
|
||||
|
||||
keys(): BigInteger[] {
|
||||
const list = Array.from(this);
|
||||
return list.map(([key]) => key);
|
||||
}
|
||||
|
||||
forEach(f: (value: V, key: BigInteger) => void) {
|
||||
const list = Array.from(this);
|
||||
return list.forEach(([k,v]) => f(v,k));
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
||||
let result: [BigInteger, V][] = [];
|
||||
const inner = (node: MapNode<V>) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
inner(node.l);
|
||||
result.push(node.n);
|
||||
inner(node.r);
|
||||
};
|
||||
inner(this.root);
|
||||
|
||||
let idx = 0;
|
||||
return {
|
||||
[Symbol.iterator]: this[Symbol.iterator],
|
||||
next: (): IteratorResult<[BigInteger, V]> => {
|
||||
if (idx < result.length) {
|
||||
return { value: result[idx++], done: false };
|
||||
}
|
||||
return { done: true, value: null };
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
211
pkg/npm/api/lib/util.ts
Normal file
211
pkg/npm/api/lib/util.ts
Normal file
@ -0,0 +1,211 @@
|
||||
import _ from "lodash";
|
||||
import f from "lodash/fp";
|
||||
import bigInt, { BigInteger } from "big-integer";
|
||||
import { Resource } from "../groups/index.d";
|
||||
|
||||
const DA_UNIX_EPOCH = bigInt("170141184475152167957503069145530368000"); // `@ud` ~1970.1.1
|
||||
|
||||
const DA_SECOND = bigInt("18446744073709551616"); // `@ud` ~s1
|
||||
|
||||
/**
|
||||
* Returns true if an app uses a graph backend
|
||||
*
|
||||
* @param {string} app The name of the app
|
||||
*
|
||||
* @return {boolean} Whether or not it uses a graph backend
|
||||
*/
|
||||
export function appIsGraph(app: string): boolean {
|
||||
return app === 'publish' || app == 'link';
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a bigint representing an urbit date, returns a unix timestamp.
|
||||
*
|
||||
* @param {BigInteger} da The urbit date
|
||||
*
|
||||
* @return {number} The unix timestamp
|
||||
*/
|
||||
export function daToUnix(da: BigInteger): number {
|
||||
// ported from +time:enjs:format in hoon.hoon
|
||||
const offset = DA_SECOND.divide(bigInt(2000));
|
||||
const epochAdjusted = offset.add(da.subtract(DA_UNIX_EPOCH));
|
||||
|
||||
return Math.round(
|
||||
epochAdjusted.multiply(bigInt(1000)).divide(DA_SECOND).toJSNumber()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a unix timestamp, returns a bigint representing an urbit date
|
||||
*
|
||||
* @param {number} unix The unix timestamp
|
||||
*
|
||||
* @return {BigInteger} The urbit date
|
||||
*/
|
||||
export function unixToDa(unix: number): BigInteger {
|
||||
const timeSinceEpoch = bigInt(unix).multiply(DA_SECOND).divide(bigInt(1000));
|
||||
return DA_UNIX_EPOCH.add(timeSinceEpoch);
|
||||
}
|
||||
|
||||
|
||||
export function makePatDa(patda: string): BigInteger {
|
||||
return bigInt(udToDec(patda));
|
||||
}
|
||||
|
||||
export function udToDec(ud: string): string {
|
||||
return ud.replace(/\./g, "");
|
||||
}
|
||||
|
||||
export function decToUd(str: string): string {
|
||||
return _.trimStart(
|
||||
f.flow(
|
||||
f.split(""),
|
||||
f.reverse,
|
||||
f.chunk(3),
|
||||
f.map(f.flow(f.reverse, f.join(""))),
|
||||
f.reverse,
|
||||
f.join(".")
|
||||
)(str),
|
||||
"0."
|
||||
);
|
||||
}
|
||||
|
||||
export function resourceAsPath(resource: Resource): string {
|
||||
const { name, ship } = resource;
|
||||
return `/ship/~${ship}/${name}`;
|
||||
}
|
||||
|
||||
export function uuid(): string {
|
||||
let str = "0v";
|
||||
str += Math.ceil(Math.random() * 8) + ".";
|
||||
for (let i = 0; i < 5; i++) {
|
||||
let _str = Math.ceil(Math.random() * 10000000).toString(32);
|
||||
_str = ("00000" + _str).substr(-5, 5);
|
||||
str += _str + ".";
|
||||
}
|
||||
|
||||
return str.slice(0, -1);
|
||||
}
|
||||
|
||||
/*
|
||||
Goes from:
|
||||
~2018.7.17..23.15.09..5be5 // urbit @da
|
||||
To:
|
||||
(javascript Date object)
|
||||
*/
|
||||
export function daToDate(st: string): Date {
|
||||
const dub = function (n: string) {
|
||||
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
|
||||
};
|
||||
const da = st.split("..");
|
||||
const bigEnd = da[0].split(".");
|
||||
const lilEnd = da[1].split(".");
|
||||
const ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(
|
||||
lilEnd[0]
|
||||
)}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
|
||||
return new Date(ds);
|
||||
}
|
||||
|
||||
/*
|
||||
Goes from:
|
||||
(javascript Date object)
|
||||
To:
|
||||
~2018.7.17..23.15.09..5be5 // urbit @da
|
||||
*/
|
||||
|
||||
export function dateToDa(d: Date, mil: boolean = false): string {
|
||||
const fil = function (n: number) {
|
||||
return n >= 10 ? n : "0" + n;
|
||||
};
|
||||
return (
|
||||
`~${d.getUTCFullYear()}.` +
|
||||
`${d.getUTCMonth() + 1}.` +
|
||||
`${fil(d.getUTCDate())}..` +
|
||||
`${fil(d.getUTCHours())}.` +
|
||||
`${fil(d.getUTCMinutes())}.` +
|
||||
`${fil(d.getUTCSeconds())}` +
|
||||
`${mil ? "..0000" : ""}`
|
||||
);
|
||||
}
|
||||
|
||||
export function deSig(ship: string): string | null {
|
||||
if (!ship) {
|
||||
return null;
|
||||
}
|
||||
return ship.replace("~", "");
|
||||
}
|
||||
|
||||
// trim patps to match dojo, chat-cli
|
||||
export function cite(ship: string): string {
|
||||
let patp = ship,
|
||||
shortened = "";
|
||||
if (patp === null || patp === "") {
|
||||
return null;
|
||||
}
|
||||
if (patp.startsWith("~")) {
|
||||
patp = patp.substr(1);
|
||||
}
|
||||
// comet
|
||||
if (patp.length === 56) {
|
||||
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56);
|
||||
return shortened;
|
||||
}
|
||||
// moon
|
||||
if (patp.length === 27) {
|
||||
shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27);
|
||||
return shortened;
|
||||
}
|
||||
return `~${patp}`;
|
||||
}
|
||||
|
||||
// encode the string into @ta-safe format, using logic from +wood.
|
||||
// for example, 'some Chars!' becomes '~.some.~43.hars~21.'
|
||||
//
|
||||
export function stringToTa(str: string): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str[i];
|
||||
let add = "";
|
||||
switch (char) {
|
||||
case " ":
|
||||
add = ".";
|
||||
break;
|
||||
case ".":
|
||||
add = "~.";
|
||||
break;
|
||||
case "~":
|
||||
add = "~~";
|
||||
break;
|
||||
default:
|
||||
const charCode = str.charCodeAt(i);
|
||||
if (
|
||||
(charCode >= 97 && charCode <= 122) || // a-z
|
||||
(charCode >= 48 && charCode <= 57) || // 0-9
|
||||
char === "-"
|
||||
) {
|
||||
add = char;
|
||||
} else {
|
||||
// TODO behavior for unicode doesn't match +wood's,
|
||||
// but we can probably get away with that for now.
|
||||
add = "~" + charCode.toString(16) + ".";
|
||||
}
|
||||
}
|
||||
out = out + add;
|
||||
}
|
||||
return "~." + out;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Formats a numbers as a `@ud` inserting dot where needed
|
||||
*/
|
||||
export function numToUd(num: number): string {
|
||||
return f.flow(
|
||||
f.split(''),
|
||||
f.reverse,
|
||||
f.chunk(3),
|
||||
f.reverse,
|
||||
f.map(s => s.join('')),
|
||||
f.join('.')
|
||||
)(num.toString())
|
||||
}
|
55
pkg/npm/api/metadata/index.d.ts
vendored
Normal file
55
pkg/npm/api/metadata/index.d.ts
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
import { AppName, Path, Patp } from '..';
|
||||
|
||||
|
||||
export type MetadataUpdate =
|
||||
MetadataUpdateInitial
|
||||
| MetadataUpdateAdd
|
||||
| MetadataUpdateUpdate
|
||||
| MetadataUpdateRemove;
|
||||
|
||||
export interface MetadataUpdateInitial {
|
||||
associations: ResourceAssociations;
|
||||
}
|
||||
|
||||
export type ResourceAssociations = {
|
||||
[p in Path]: Association;
|
||||
}
|
||||
|
||||
export type MetadataUpdateAdd = {
|
||||
add: Association;
|
||||
}
|
||||
|
||||
export type MetadataUpdateUpdate = {
|
||||
update: Association;
|
||||
}
|
||||
|
||||
export type MetadataUpdateRemove = {
|
||||
remove: Resource & {
|
||||
'group-path': Path;
|
||||
}
|
||||
}
|
||||
|
||||
export type Associations = Record<AppName, AppAssociations>;
|
||||
|
||||
export type AppAssociations = {
|
||||
[p in Path]: Association;
|
||||
}
|
||||
|
||||
export interface Resource {
|
||||
'app-path': Path;
|
||||
'app-name': AppName;
|
||||
}
|
||||
|
||||
export type Association = Resource & {
|
||||
'group-path': Path;
|
||||
metadata: Metadata;
|
||||
};
|
||||
|
||||
export interface Metadata {
|
||||
color: string;
|
||||
creator: Patp;
|
||||
'date-created': string;
|
||||
description: string;
|
||||
title: string;
|
||||
module: string;
|
||||
}
|
55
pkg/npm/api/metadata/index.ts
Normal file
55
pkg/npm/api/metadata/index.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { AppName, Path, PatpNoSig, Poke } from "..";
|
||||
import { Association, Metadata, MetadataUpdateAdd, MetadataUpdateUpdate } from './index.d';
|
||||
|
||||
export const action = <T>(data: T): Poke<T> => ({
|
||||
app: 'metadata-hook',
|
||||
mark: 'metadata-action',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const add = (
|
||||
ship: PatpNoSig,
|
||||
appName: AppName,
|
||||
appPath: Path,
|
||||
groupPath: Path,
|
||||
title: string,
|
||||
description: string,
|
||||
dateCreated: string,
|
||||
color: string,
|
||||
moduleName: string
|
||||
): Poke<MetadataUpdateAdd> => {
|
||||
const creator = `~${ship}`;
|
||||
return action({
|
||||
add: {
|
||||
'group-path': groupPath,
|
||||
resource: {
|
||||
'app-path': appPath,
|
||||
'app-name': appName
|
||||
},
|
||||
metadata: {
|
||||
title,
|
||||
description,
|
||||
color,
|
||||
'date-created': dateCreated,
|
||||
creator,
|
||||
'module': moduleName
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const update = (
|
||||
association: Association,
|
||||
newMetadata: Partial<Metadata>
|
||||
): Poke<MetadataUpdateUpdate> => {
|
||||
return action({
|
||||
add: {
|
||||
'group-path': association['group-path'],
|
||||
resource: {
|
||||
'app-path': association['app-path'],
|
||||
'app-name': association['app-name'],
|
||||
},
|
||||
metadata: {...association.metadata, ...newMetadata }
|
||||
}
|
||||
});
|
||||
}
|
31
pkg/npm/api/package-lock.json
generated
Normal file
31
pkg/npm/api/package-lock.json
generated
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@urbit/api",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.12.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
|
||||
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"big-integer": {
|
||||
"version": "1.6.48",
|
||||
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
|
||||
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.7",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
|
||||
}
|
||||
}
|
||||
}
|
18
pkg/npm/api/package.json
Normal file
18
pkg/npm/api/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@urbit/api",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Tyler Brown Cifu Shuster",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@urbit/eslint-config": "^1.0.0",
|
||||
"big-integer": "^1.6.48",
|
||||
"lodash": "^4.17.20"
|
||||
}
|
||||
}
|
27
pkg/npm/api/tsconfig.json
Normal file
27
pkg/npm/api/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitReturns": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": false,
|
||||
"noImplicitAny": false,
|
||||
"noEmit": true,
|
||||
"target": "es2015",
|
||||
"module": "es2015",
|
||||
"strict": true,
|
||||
"jsx": "react",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
]
|
||||
}
|
186
pkg/npm/eslint-config/index.js
Normal file
186
pkg/npm/eslint-config/index.js
Normal file
@ -0,0 +1,186 @@
|
||||
const env = {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
};
|
||||
|
||||
const rules = {
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"arrow-parens": [
|
||||
"error",
|
||||
"as-needed",
|
||||
{
|
||||
"requireForBlockBody": true
|
||||
}
|
||||
],
|
||||
"arrow-spacing": "error",
|
||||
"block-spacing": ["error", "always"],
|
||||
"brace-style": ["error", "1tbs"],
|
||||
"camelcase": [
|
||||
"error",
|
||||
{
|
||||
"properties": "never"
|
||||
}
|
||||
],
|
||||
"comma-dangle": ["error", "never"],
|
||||
"eol-last": ["error", "always"],
|
||||
"func-name-matching": "error",
|
||||
"indent": [
|
||||
"off",
|
||||
2,
|
||||
{
|
||||
"ArrayExpression": "off",
|
||||
"SwitchCase": 1,
|
||||
"CallExpression": {
|
||||
"arguments": "off"
|
||||
},
|
||||
"FunctionDeclaration": {
|
||||
"parameters": "off"
|
||||
},
|
||||
"FunctionExpression": {
|
||||
"parameters": "off"
|
||||
},
|
||||
"MemberExpression": "off",
|
||||
"ObjectExpression": "off",
|
||||
"ImportDeclaration": "off"
|
||||
}
|
||||
],
|
||||
"handle-callback-err": "off",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"max-lines": [
|
||||
"error",
|
||||
{
|
||||
"max": 300,
|
||||
"skipBlankLines": true,
|
||||
"skipComments": true
|
||||
}
|
||||
],
|
||||
"max-lines-per-function": [
|
||||
"warn",
|
||||
{
|
||||
"skipBlankLines": true,
|
||||
"skipComments": true
|
||||
}
|
||||
],
|
||||
"max-statements-per-line": [
|
||||
"error",
|
||||
{
|
||||
"max": 1
|
||||
}
|
||||
],
|
||||
"new-cap": [
|
||||
"error",
|
||||
{
|
||||
"newIsCap": true,
|
||||
"capIsNew": false
|
||||
}
|
||||
],
|
||||
"new-parens": "error",
|
||||
"no-buffer-constructor": "error",
|
||||
"no-console": "off",
|
||||
"no-extra-semi": "off",
|
||||
"no-fallthrough": "off",
|
||||
"no-func-assign": "off",
|
||||
"no-implicit-coercion": "error",
|
||||
"no-multi-assign": "error",
|
||||
"no-multiple-empty-lines": [
|
||||
"error",
|
||||
{
|
||||
"max": 1
|
||||
}
|
||||
],
|
||||
"no-nested-ternary": "error",
|
||||
"no-param-reassign": "off",
|
||||
"no-return-assign": "error",
|
||||
"no-return-await": "off",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-tabs": "error",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "none",
|
||||
"ignoreRestSiblings": false
|
||||
}
|
||||
],
|
||||
"no-use-before-define": [
|
||||
"error",
|
||||
{
|
||||
"functions": false,
|
||||
"classes": false
|
||||
}
|
||||
],
|
||||
"no-useless-escape": "off",
|
||||
"no-var": "error",
|
||||
"nonblock-statement-body-position": ["error", "below"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"padded-blocks": ["error", "never"],
|
||||
"prefer-arrow-callback": "error",
|
||||
"prefer-const": [
|
||||
"error",
|
||||
{
|
||||
"destructuring": "all",
|
||||
"ignoreReadBeforeAssign": true
|
||||
}
|
||||
],
|
||||
"prefer-template": "off",
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"exceptions": ["!"]
|
||||
}
|
||||
],
|
||||
"space-before-blocks": "error",
|
||||
"unicode-bom": ["error", "never"],
|
||||
"valid-jsdoc": "error",
|
||||
"wrap-iife": ["error", "inside"],
|
||||
"react/jsx-closing-bracket-location": 1,
|
||||
"react/jsx-tag-spacing": 1,
|
||||
"react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }],
|
||||
"react/prop-types": 0
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
"env": env,
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"eslint:recommended",
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "^16.5.2"
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 10,
|
||||
"requireConfigFile": false,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"root": true,
|
||||
"rules": rules,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts", "**/*.tsx"],
|
||||
"env": env,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": { "jsx": true },
|
||||
"ecmaVersion": 10,
|
||||
"requireConfigFile": false,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": rules
|
||||
}
|
||||
]
|
||||
};
|
5
pkg/npm/eslint-config/package-lock.json
generated
Normal file
5
pkg/npm/eslint-config/package-lock.json
generated
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "@urbit/eslint-config",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1
|
||||
}
|
14
pkg/npm/eslint-config/package.json
Normal file
14
pkg/npm/eslint-config/package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@urbit/eslint-config",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Tyler Brown Cifu Shuster",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"eslint": ">= 3"
|
||||
}
|
||||
}
|
42
pkg/npm/http-api/.github/workflows/main.yml
vendored
Normal file
42
pkg/npm/http-api/.github/workflows/main.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
name: CI
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Begin CI...
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Use Node 12
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
|
||||
- name: Use cached node_modules
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: node_modules
|
||||
key: nodeModules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
nodeModules-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Test
|
||||
run: yarn test --ci --coverage --maxWorkers=2
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
env:
|
||||
CI: true
|
0
pkg/npm/http-api/.gitignore
vendored
Normal file
0
pkg/npm/http-api/.gitignore
vendored
Normal file
5
pkg/npm/http-api/.vscode/settings.json
vendored
Normal file
5
pkg/npm/http-api/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.watcherExclude": {
|
||||
"**/node_modules/**": false
|
||||
}
|
||||
}
|
21
pkg/npm/http-api/LICENSE
Normal file
21
pkg/npm/http-api/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Tyler Brown Cifu Shuster
|
||||
|
||||
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.
|
21
pkg/npm/http-api/README.md
Normal file
21
pkg/npm/http-api/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Urbit Connector
|
||||
|
||||
This project allows you to connect to an [Urbit](https://urbit.org) ship via a JavaScript application.
|
||||
|
||||
## Example
|
||||
|
||||
Check out the `example` directory for examples of how to use this code.
|
||||
|
||||
1. Open `example/index.html` in your browser and follow the instructions there, or
|
||||
2. With a ship running in the same fashion as indicated in the file above, run `node example/index.js`
|
||||
|
||||
The code for either of these can be found in `src/example/browser.js` or `src/example/node.js`, depending on your context.
|
||||
|
||||
## Design
|
||||
|
||||
This library is designed to be useful for node applications that communicate with an urbit running either on the local computer or on a remote one.
|
||||
|
||||
The majority of its methods are asynchronous and return Promises. This is due to the non-blocking nature of JavaScript. If used in a React app, response handlers should be bound with `this` to `setState` after a message is received.
|
||||
|
||||
## NOTE
|
||||
You must enable CORS requests on your urbit for this library to work in browser context. Use `+cors-registry` to see domains which have made requests to your urbit, and then approve the needed one, e.g. `|cors-approve http://zod.arvo.network`.
|
17
pkg/npm/http-api/example/browser.js
Normal file
17
pkg/npm/http-api/example/browser.js
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
|
||||
* This devtool is not neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/*!********************************!*\
|
||||
!*** ./src/example/browser.js ***!
|
||||
\********************************/
|
||||
/*! unknown exports (runtime-defined) */
|
||||
/*! runtime requirements: */
|
||||
eval("// import Urbit from '../../dist/browser';\n// window.Urbit = Urbit;\n\n//# sourceURL=webpack://@urbit/http-api/./src/example/browser.js?");
|
||||
/******/ })()
|
||||
;
|
98
pkg/npm/http-api/example/index.html
Normal file
98
pkg/npm/http-api/example/index.html
Normal file
@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Demo</title>
|
||||
<script src="browser.js"></script>
|
||||
<style>
|
||||
@import url("https://rsms.me/inter/inter.css");
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
|
||||
font-weight: 400;
|
||||
}
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 70ch;
|
||||
padding: 2ch;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
#mylog {
|
||||
white-space: pre-wrap;
|
||||
padding: 2ch;
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
}
|
||||
#mylog div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.chunk {
|
||||
border-bottom: 1px dashed currentColor;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<details>
|
||||
<summary>Show instructions</summary>
|
||||
<p>Assuming you are running a fakezod on port 8080, run</p>
|
||||
<code id="instructions">|cors-approve '{window.location.origin}'</code>
|
||||
<p>in its dojo.</p>
|
||||
<p>Press the button to run the code below. Output will be logged. You should see <code>< ~zod: opening airlock</code> in your dojo.</code> Create a chat and send a message to see the events logged.</p>
|
||||
<pre>window.airlock = await Urbit.authenticate({
|
||||
ship: 'zod',
|
||||
url: 'localhost:8080',
|
||||
code: 'lidlut-tabwed-pillex-ridrup',
|
||||
verbose: true
|
||||
});
|
||||
window.airlock.subscribe('chat-view', '/primary', { event: console.log });</pre>
|
||||
</details>
|
||||
|
||||
<button id="blastoff" onclick="blastOff()">Blast Off</button>
|
||||
<pre id="mylog">
|
||||
|
||||
</pre>
|
||||
</body>
|
||||
<script>
|
||||
var baseLogFunction = console.log;
|
||||
console.log = function(){
|
||||
baseLogFunction.apply(console, arguments);
|
||||
var chunk = document.createElement('div');
|
||||
chunk.className = 'chunk';
|
||||
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
for(var i=0;i<args.length;i++){
|
||||
const val = typeof args[i] === 'string' ? args[i] : JSON.stringify(args[i]);
|
||||
var node = createLogNode(val);
|
||||
chunk.appendChild(node);
|
||||
}
|
||||
document.querySelector("#mylog").insertBefore(chunk, document.querySelector("#mylog").firstChild);
|
||||
}
|
||||
|
||||
function createLogNode(message){
|
||||
var node = document.createElement("div");
|
||||
node.className = 'message';
|
||||
var textNode = document.createTextNode(message);
|
||||
node.appendChild(textNode);
|
||||
return node;
|
||||
}
|
||||
|
||||
window.onerror = function(message, url, linenumber) {
|
||||
console.log("JavaScript error: " + message + " on line " +
|
||||
linenumber + " for " + url);
|
||||
}
|
||||
const instructions = document.getElementById('instructions');
|
||||
instructions.innerText = instructions.innerText.replace('{window.location.origin}', window.location.origin);
|
||||
async function blastOff() {
|
||||
window.airlock = await Urbit.authenticate({
|
||||
ship: 'zod',
|
||||
url: 'localhost:8080',
|
||||
code: 'lidlut-tabwed-pillex-ridrup',
|
||||
verbose: true
|
||||
});
|
||||
window.airlock.subscribe('chat-view', '/primary', { event: console.log });
|
||||
document.body.removeChild(document.getElementById('blastoff'))
|
||||
}
|
||||
</script>
|
||||
</html>
|
17
pkg/npm/http-api/example/node.js
Normal file
17
pkg/npm/http-api/example/node.js
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
|
||||
* This devtool is not neither made for production nor for readable output files.
|
||||
* It uses "eval()" calls to create a separate source file in the browser devtools.
|
||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
||||
* or disable the default devtool with "devtool: false".
|
||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
||||
*/
|
||||
/******/ (() => { // webpackBootstrap
|
||||
/*!*****************************!*\
|
||||
!*** ./src/example/node.js ***!
|
||||
\*****************************/
|
||||
/*! unknown exports (runtime-defined) */
|
||||
/*! runtime requirements: */
|
||||
eval("// import Urbit from '../../dist/index';\n// async function blastOff() {\n// const airlock = await Urbit.authenticate({\n// ship: 'zod',\n// url: 'localhost:8080',\n// code: 'lidlut-tabwed-pillex-ridrup',\n// verbose: true\n// });\n// airlock.subscribe('chat-view', '/primary');\n// }\n// blastOff();\n\n//# sourceURL=webpack://@urbit/http-api/./src/example/node.js?");
|
||||
/******/ })()
|
||||
;
|
2
pkg/npm/http-api/index.js
Normal file
2
pkg/npm/http-api/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import Urbit from './dist';
|
||||
export { Urbit as default, Urbit };
|
5591
pkg/npm/http-api/package-lock.json
generated
Normal file
5591
pkg/npm/http-api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
64
pkg/npm/http-api/package.json
Normal file
64
pkg/npm/http-api/package.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "@urbit/http-api",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"description": "Library to interact with an Urbit ship over HTTP",
|
||||
"repository": "github:tylershuster/urbit",
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"browser": "dist/esm/index.js",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=13"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "npm run clean && webpack --config webpack.prod.js && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json",
|
||||
"clean": "rm -rf dist/*"
|
||||
},
|
||||
"peerDependencies": {},
|
||||
"prettier": {
|
||||
"printWidth": 80,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
},
|
||||
"author": "Tyler Brown Cifu Shuster",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
|
||||
"@babel/preset-typescript": "^7.12.1",
|
||||
"@types/eventsource": "^1.1.5",
|
||||
"@types/react": "^16.9.56",
|
||||
"@typescript-eslint/eslint-plugin": "^4.7.0",
|
||||
"@typescript-eslint/parser": "^4.7.0",
|
||||
"@types/browser-or-node": "^1.2.0",
|
||||
"babel-loader": "^8.2.1",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"tslib": "^2.0.3",
|
||||
"typescript": "^3.9.7",
|
||||
"webpack": "^5.4.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"browser-or-node": "^1.3.0",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^5.7.1",
|
||||
"encoding": "^0.1.13",
|
||||
"eventsource": "^1.0.7",
|
||||
"node-fetch": "^2.6.1",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.1.1",
|
||||
"util": "^0.12.3",
|
||||
"xmlhttprequest": "^1.8.0",
|
||||
"xmlhttprequest-ssl": "^1.6.0"
|
||||
}
|
||||
}
|
40
pkg/npm/http-api/src/app/base.ts
Normal file
40
pkg/npm/http-api/src/app/base.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import Urbit from '..';
|
||||
|
||||
export interface UrbitAppInterface {
|
||||
airlock: Urbit;
|
||||
app: string;
|
||||
}
|
||||
|
||||
export default class UrbitApp implements UrbitAppInterface {
|
||||
airlock: Urbit;
|
||||
|
||||
get app(): string {
|
||||
throw new Error('Access app property on base UrbitApp');
|
||||
}
|
||||
|
||||
constructor(airlock: Urbit) {
|
||||
this.airlock = airlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter that barfs if no ship has been passed
|
||||
*/
|
||||
get ship(): string {
|
||||
if (!this.airlock.ship) {
|
||||
throw new Error('No ship specified');
|
||||
}
|
||||
return this.airlock.ship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to allow any app to handle subscriptions.
|
||||
*
|
||||
* @param path Path on app to subscribe to
|
||||
*/
|
||||
subscribe(path: string) {
|
||||
const ship = this.ship;
|
||||
const app = this.app;
|
||||
return this.airlock.subscribe(app, path);
|
||||
}
|
||||
// TODO handle methods that don't exist
|
||||
}
|
3
pkg/npm/http-api/src/example/browser.js
Normal file
3
pkg/npm/http-api/src/example/browser.js
Normal file
@ -0,0 +1,3 @@
|
||||
// import Urbit from '../../dist/browser';
|
||||
|
||||
// window.Urbit = Urbit;
|
14
pkg/npm/http-api/src/example/node.js
Normal file
14
pkg/npm/http-api/src/example/node.js
Normal file
@ -0,0 +1,14 @@
|
||||
// import Urbit from '../../dist/index';
|
||||
|
||||
// async function blastOff() {
|
||||
// const airlock = await Urbit.authenticate({
|
||||
// ship: 'zod',
|
||||
// url: 'localhost:8080',
|
||||
// code: 'lidlut-tabwed-pillex-ridrup',
|
||||
// verbose: true
|
||||
// });
|
||||
|
||||
// airlock.subscribe('chat-view', '/primary');
|
||||
// }
|
||||
|
||||
// blastOff();
|
456
pkg/npm/http-api/src/index.ts
Normal file
456
pkg/npm/http-api/src/index.ts
Normal file
@ -0,0 +1,456 @@
|
||||
import { isBrowser, isNode } from 'browser-or-node';
|
||||
import { Action, Thread } from '../../api';
|
||||
|
||||
import { AuthenticationInterface, SubscriptionInterface, CustomEventHandler, PokeInterface, SubscriptionRequestInterface, headers, UrbitInterface, SSEOptions, ThreadInterface } from './types';
|
||||
import UrbitApp from './app/base';
|
||||
import { uncamelize, hexString } from './utils';
|
||||
|
||||
/**
|
||||
* A class for interacting with an urbit ship, given its URL and code
|
||||
*/
|
||||
export class Urbit implements UrbitInterface {
|
||||
/**
|
||||
* UID will be used for the channel: The current unix time plus a random hex string
|
||||
*/
|
||||
uid: string = `${Math.floor(Date.now() / 1000)}-${hexString(6)}`;
|
||||
|
||||
/**
|
||||
* Last Event ID is an auto-updated index of which events have been sent over this channel
|
||||
*/
|
||||
lastEventId: number = 0;
|
||||
|
||||
lastAcknowledgedEventId: number = 0;
|
||||
|
||||
/**
|
||||
* SSE Client is null for now; we don't want to start polling until it the channel exists
|
||||
*/
|
||||
sseClient: EventSource | null = null;
|
||||
|
||||
/**
|
||||
* Cookie gets set when we log in.
|
||||
*/
|
||||
cookie?: string | undefined;
|
||||
|
||||
/**
|
||||
* A registry of requestId to successFunc/failureFunc
|
||||
*
|
||||
* These functions are registered during a +poke and are executed
|
||||
* in the onServerEvent()/onServerError() callbacks. Only one of
|
||||
* the functions will be called, and the outstanding poke will be
|
||||
* removed after calling the success or failure function.
|
||||
*/
|
||||
|
||||
outstandingPokes: Map<number, object> = new Map();
|
||||
|
||||
/**
|
||||
* A registry of requestId to subscription functions.
|
||||
*
|
||||
* These functions are registered during a +subscribe and are
|
||||
* executed in the onServerEvent()/onServerError() callbacks. The
|
||||
* event function will be called whenever a new piece of data on this
|
||||
* subscription is available, which may be 0, 1, or many times. The
|
||||
* disconnect function may be called exactly once.
|
||||
*/
|
||||
|
||||
outstandingSubscriptions: Map<number, SubscriptionInterface> = new Map();
|
||||
|
||||
/**
|
||||
* Ship can be set, in which case we can do some magic stuff like send chats
|
||||
*/
|
||||
ship?: string | null;
|
||||
|
||||
/**
|
||||
* If verbose, logs output eagerly.
|
||||
*/
|
||||
verbose?: boolean;
|
||||
|
||||
/**
|
||||
* All registered apps, keyed by name
|
||||
*/
|
||||
static apps: Map<string, typeof UrbitApp> = new Map();
|
||||
|
||||
/** This is basic interpolation to get the channel URL of an instantiated Urbit connection. */
|
||||
get channelUrl(): string {
|
||||
return `${this.url}/~/channel/${this.uid}`;
|
||||
}
|
||||
|
||||
get fetchOptions(): any {
|
||||
const headers: headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (!isBrowser) {
|
||||
headers.Cookie = this.cookie;
|
||||
}
|
||||
return {
|
||||
credentials: 'include',
|
||||
headers
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new Urbit connection.
|
||||
*
|
||||
* @param url The URL (with protocol and port) of the ship to be accessed
|
||||
* @param code The access code for the ship at that address
|
||||
*/
|
||||
constructor(
|
||||
public url: string,
|
||||
public code: string
|
||||
) {
|
||||
return this;
|
||||
// We return a proxy so we can set dynamic properties like `Urbit.onChatHook`
|
||||
return new Proxy(this, {
|
||||
get(target: Urbit, property: string) {
|
||||
// First check if this is a regular property
|
||||
if (property in target) {
|
||||
return (target as any)[property];
|
||||
}
|
||||
|
||||
// Then check if it's a registered app
|
||||
const app = Urbit.apps.get(uncamelize(property));
|
||||
if (app) {
|
||||
return new app(target);
|
||||
}
|
||||
|
||||
// Then check to see if we're trying to register an EventSource watcher
|
||||
if (property.startsWith('on')) {
|
||||
const on = uncamelize(property.replace('on', '')).toLowerCase();
|
||||
return ((action: CustomEventHandler) => {
|
||||
target.eventSource().addEventListener('message', (event: MessageEvent) => {
|
||||
if (target.verbose) {
|
||||
console.log(`Received SSE from ${on}: `, event);
|
||||
}
|
||||
if (event.data && JSON.parse(event.data)) {
|
||||
const data: any = JSON.parse(event.data);
|
||||
if (data.json.hasOwnProperty(on)) {
|
||||
action(data.json[on], data.json.response);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* All-in-one hook-me-up.
|
||||
*
|
||||
* Given a ship, url, and code, this returns an airlock connection
|
||||
* that is ready to go. It `|hi`s itself to create the channel,
|
||||
* then opens the channel via EventSource.
|
||||
*
|
||||
* @param AuthenticationInterface
|
||||
*/
|
||||
static async authenticate({ ship, url, code, verbose = false }: AuthenticationInterface) {
|
||||
const airlock = new Urbit(`http://${url}`, code);
|
||||
airlock.verbose = verbose;
|
||||
airlock.ship = ship;
|
||||
await airlock.connect();
|
||||
await airlock.poke({ app: 'hood', mark: 'helm-hi', json: 'opening airlock' });
|
||||
await airlock.eventSource();
|
||||
return airlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the Urbit ship. Nothing can be done until this is called.
|
||||
* That's why we roll it into this.authenticate
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.verbose) {
|
||||
console.log(`password=${this.code} `, isBrowser ? "Connecting in browser context at " + `${this.url}/~/login` : "Connecting from node context");
|
||||
}
|
||||
return fetch(`${this.url}/~/login`, {
|
||||
method: 'post',
|
||||
body: `password=${this.code}`,
|
||||
credentials: 'include',
|
||||
}).then(response => {
|
||||
if (this.verbose) {
|
||||
console.log('Received authentication response', response);
|
||||
}
|
||||
const cookie = response.headers.get('set-cookie');
|
||||
if (!this.ship) {
|
||||
this.ship = new RegExp(/urbauth-~([\w-]+)/).exec(cookie)[1];
|
||||
}
|
||||
if (!isBrowser) {
|
||||
this.cookie = cookie;
|
||||
}
|
||||
}).catch(error => {
|
||||
console.log(XMLHttpRequest);
|
||||
console.log('errored')
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns (and initializes, if necessary) the SSE pipe for the appropriate channel.
|
||||
*/
|
||||
eventSource(): EventSource {
|
||||
if (!this.sseClient || this.sseClient.readyState === this.sseClient.CLOSED) {
|
||||
const sseOptions: SSEOptions = {
|
||||
headers: {}
|
||||
};
|
||||
if (isBrowser) {
|
||||
sseOptions.withCredentials = true;
|
||||
} else if (isNode) {
|
||||
sseOptions.headers.Cookie = this.cookie;
|
||||
}
|
||||
this.sseClient = new EventSource(this.channelUrl, {
|
||||
withCredentials: true
|
||||
});
|
||||
this.sseClient!.addEventListener('message', (event: MessageEvent) => {
|
||||
if (this.verbose) {
|
||||
console.log('Received SSE: ', event);
|
||||
}
|
||||
this.ack(Number(event.lastEventId));
|
||||
if (event.data && JSON.parse(event.data)) {
|
||||
const data: any = JSON.parse(event.data);
|
||||
if (data.response === 'poke' && this.outstandingPokes.has(data.id)) {
|
||||
const funcs = this.outstandingPokes.get(data.id);
|
||||
if (data.hasOwnProperty('ok')) {
|
||||
funcs.success();
|
||||
} else if (data.hasOwnProperty('err')) {
|
||||
funcs.fail(data.err);
|
||||
} else {
|
||||
console.error('Invalid poke response', data);
|
||||
}
|
||||
this.outstandingPokes.delete(data.id);
|
||||
} else if (data.response === 'subscribe' ||
|
||||
(data.response === 'poke' && this.outstandingSubscriptions.has(data.id))) {
|
||||
const funcs = this.outstandingSubscriptions.get(data.id);
|
||||
if (data.hasOwnProperty('err')) {
|
||||
funcs.err(data.err);
|
||||
this.outstandingSubscriptions.delete(data.id);
|
||||
}
|
||||
} else if (data.response === 'diff' && this.outstandingSubscriptions.has(data.id)) {
|
||||
const funcs = this.outstandingSubscriptions.get(data.id);
|
||||
funcs.event(data.json);
|
||||
} else if (data.response === 'quit' && this.outstandingSubscriptions.has(data.id)) {
|
||||
const funcs = this.outstandingSubscriptions.get(data.id);
|
||||
funcs.quit(data);
|
||||
this.outstandingSubscriptions.delete(data.id);
|
||||
} else {
|
||||
console.log('Unrecognized response', data);
|
||||
}
|
||||
// An incoming message, for example:
|
||||
// {
|
||||
// id: 10,
|
||||
// json: {
|
||||
// 'chat-update' : { // This is where we hook our "on" handlers like "onChatUpdate"
|
||||
// message: {
|
||||
// envelope: {
|
||||
// author: 'zod',
|
||||
// letter: {
|
||||
// text: 'hi'
|
||||
// },
|
||||
// number: 10,
|
||||
// uid: 'saludhafhsdf',
|
||||
// when: 124459
|
||||
// },
|
||||
// path: '/~zod/mailbox'
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
});
|
||||
this.sseClient!.addEventListener('error', function(event: Event) {
|
||||
console.error('pipe error', event);
|
||||
});
|
||||
|
||||
}
|
||||
return this.sseClient;
|
||||
}
|
||||
|
||||
addEventListener(callback: (data: any) => void) {
|
||||
return this.eventSource().addEventListener('message', (event: MessageEvent) => {
|
||||
if (event.data && JSON.parse(event.data)) {
|
||||
callback(JSON.parse(event.data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Autoincrements the next event ID for the appropriate channel.
|
||||
*/
|
||||
getEventId(): number {
|
||||
this.lastEventId = Number(this.lastEventId) + 1;
|
||||
return this.lastEventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledges an event.
|
||||
*
|
||||
* @param eventId The event to acknowledge.
|
||||
*/
|
||||
ack(eventId: number): Promise<void | number> {
|
||||
return this.sendMessage('ack', { 'event-id': eventId });
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a wrapper method that can be used to send any action with data.
|
||||
*
|
||||
* Every message sent has some common parameters, like method, headers, and data
|
||||
* structure, so this method exists to prevent duplication.
|
||||
*
|
||||
* @param action The action to send
|
||||
* @param data The data to send with the action
|
||||
*
|
||||
* @returns void | number If successful, returns the number of the message that was sent
|
||||
*/
|
||||
async sendMessage(action: Action, data?: object): Promise<void | number> {
|
||||
|
||||
const id = this.getEventId();
|
||||
if (this.verbose) {
|
||||
console.log(`Sending message ${id}:`, action, data,);
|
||||
}
|
||||
let response: Response | undefined;
|
||||
try {
|
||||
response = await fetch(this.channelUrl, {
|
||||
...this.fetchOptions,
|
||||
method: 'put',
|
||||
body: JSON.stringify([{
|
||||
id,
|
||||
action,
|
||||
...data,
|
||||
}]),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('message error', error);
|
||||
response = undefined;
|
||||
}
|
||||
if (this.verbose) {
|
||||
console.log(`Received from message ${id}: `, response);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pokes a ship with data.
|
||||
*
|
||||
* @param app The app to poke
|
||||
* @param mark The mark of the data being sent
|
||||
* @param json The data to send
|
||||
*/
|
||||
poke(params: PokeInterface): Promise<void | number> {
|
||||
const { app, mark, json, onSuccess, onError } = {onSuccess: () => {}, onError: () => {}, ...params};
|
||||
return new Promise((resolve, reject) => {
|
||||
this
|
||||
.sendMessage('poke', { ship: this.ship, app, mark, json })
|
||||
.then(pokeId => {
|
||||
if (!pokeId) {
|
||||
return reject('Poke failed');
|
||||
}
|
||||
if (!this.sseClient) resolve(pokeId); // A poke may occur before a listener has been opened
|
||||
this.outstandingPokes.set(pokeId, {
|
||||
success: () => {
|
||||
onSuccess();
|
||||
resolve(pokeId);
|
||||
},
|
||||
fail: (event) => {
|
||||
onError();
|
||||
reject(event.err);
|
||||
}
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to a path on an app on a ship.
|
||||
*
|
||||
* @param app The app to subsribe to
|
||||
* @param path The path to which to subscribe
|
||||
* @param handlers Handlers to deal with various events of the subscription
|
||||
*/
|
||||
async subscribe(params: SubscriptionRequestInterface): Promise<void | number> {
|
||||
const { app, path, err, event, quit } = { err: () => {}, event: () => {}, quit: () => {}, ...params };
|
||||
|
||||
const subscriptionId = await this.sendMessage('subscribe', { ship: this.ship, app, path });
|
||||
console.log('subscribed', subscriptionId);
|
||||
|
||||
if (!subscriptionId) return;
|
||||
|
||||
this.outstandingSubscriptions.set(subscriptionId, {
|
||||
err, event, quit
|
||||
});
|
||||
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes to a given subscription.
|
||||
*
|
||||
* @param subscription
|
||||
*/
|
||||
unsubscribe(subscription: string): Promise<void | number> {
|
||||
return this.sendMessage('unsubscribe', { subscription });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the connection to a channel.
|
||||
*/
|
||||
delete(): Promise<void | number> {
|
||||
return this.sendMessage('delete');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param app The app into which to scry
|
||||
* @param path The path at which to scry
|
||||
*/
|
||||
async scry(app: string, path: string): Promise<void | any> {
|
||||
const response = await fetch(`/~/scry/${app}${path}.json`, this.fetchOptions);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param inputMark The mark of the data being sent
|
||||
* @param outputMark The mark of the data being returned
|
||||
* @param threadName The thread to run
|
||||
* @param body The data to send to the thread
|
||||
*/
|
||||
async spider<T>(params: Thread<T>): Promise<T> {
|
||||
const { inputMark, outputMark, threadName, body } = params;
|
||||
const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, {
|
||||
...this.fetchOptions,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
app(appName: string): UrbitApp {
|
||||
const appClass = Urbit.apps.get(appName);
|
||||
if (!appClass) {
|
||||
throw new Error(`App ${appName} not found`);
|
||||
}
|
||||
return new appClass(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to connect to a ship that has its *.arvo.network domain configured.
|
||||
*
|
||||
* @param name Name of the ship e.g. zod
|
||||
* @param code Code to log in
|
||||
*/
|
||||
static async onArvoNetwork(ship: string, code: string): Promise<Urbit> {
|
||||
const url = `https://${ship}.arvo.network`;
|
||||
return await Urbit.authenticate({ ship, url, code });
|
||||
}
|
||||
|
||||
static extend(appClass: any): void {
|
||||
Urbit.apps.set(appClass.app, appClass);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Urbit;
|
45
pkg/npm/http-api/src/types/index.d.ts
vendored
Normal file
45
pkg/npm/http-api/src/types/index.d.ts
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
import { Action, Mark, Poke } from '../../../api/index';
|
||||
|
||||
export interface PokeInterface extends Poke<Mark, any> {
|
||||
onSuccess?: () => void;
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
|
||||
export interface AuthenticationInterface {
|
||||
ship: string;
|
||||
url: string;
|
||||
code: string;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export interface SubscriptionInterface {
|
||||
err?(error: any): void;
|
||||
event?(data: any): void;
|
||||
quit?(data: any): void;
|
||||
}
|
||||
|
||||
export type SubscriptionRequestInterface = SubscriptionInterface & {
|
||||
app: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface headers {
|
||||
'Content-Type': string;
|
||||
Cookie?: string;
|
||||
}
|
||||
|
||||
export interface UrbitInterface {
|
||||
connect(): void;
|
||||
}
|
||||
|
||||
export interface CustomEventHandler {
|
||||
(data: any, response: string): void;
|
||||
}
|
||||
|
||||
export interface SSEOptions {
|
||||
headers?: {
|
||||
Cookie?: string
|
||||
};
|
||||
withCredentials?: boolean;
|
||||
}
|
82
pkg/npm/http-api/src/utils.ts
Normal file
82
pkg/npm/http-api/src/utils.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import * as http from 'http';
|
||||
|
||||
interface HttpResponse {
|
||||
req: http.ClientRequest;
|
||||
res: http.IncomingMessage;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export function request(
|
||||
url: string,
|
||||
options: http.ClientRequestArgs,
|
||||
body?: string
|
||||
): Promise<HttpResponse> {
|
||||
return new Promise<HttpResponse>((resolve, reject) => {
|
||||
const req = http.request(url, options, res => {
|
||||
let data = "";
|
||||
res.on("data", chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve({ req, res, data });
|
||||
});
|
||||
res.on("error", e => {
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
if (body) {
|
||||
req.write(body);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
export function camelize(str: string) {
|
||||
return str
|
||||
.replace(/\s(.)/g, function($1: string) { return $1.toUpperCase(); })
|
||||
.replace(/\s/g, '')
|
||||
.replace(/^(.)/, function($1: string) { return $1.toLowerCase(); });
|
||||
}
|
||||
|
||||
export function uncamelize(str: string, separator = '-') {
|
||||
// Replace all capital letters by separator followed by lowercase one
|
||||
var str = str.replace(/[A-Z]/g, function (letter: string) {
|
||||
return separator + letter.toLowerCase();
|
||||
});
|
||||
return str.replace(new RegExp('^' + separator), '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hex string of given length.
|
||||
*
|
||||
* Poached from StackOverflow.
|
||||
*
|
||||
* @param len Length of hex string to return.
|
||||
*/
|
||||
export function hexString(len: number): string {
|
||||
const maxlen = 8;
|
||||
const min = Math.pow(16, Math.min(len, maxlen) - 1);
|
||||
const max = Math.pow(16, Math.min(len, maxlen)) - 1;
|
||||
const n = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
let r = n.toString(16);
|
||||
while (r.length < len) {
|
||||
r = r + hexString(len - maxlen);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random UID.
|
||||
*
|
||||
* Copied from https://github.com/urbit/urbit/blob/137e4428f617c13f28ed31e520eff98d251ed3e9/pkg/interface/src/lib/util.js#L3
|
||||
*/
|
||||
export function uid(): string {
|
||||
let str = '0v';
|
||||
str += Math.ceil(Math.random() * 8) + '.';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
let _str = Math.ceil(Math.random() * 10000000).toString(32);
|
||||
_str = ('00000' + _str).substr(-5, 5);
|
||||
str += _str + '.';
|
||||
}
|
||||
return str.slice(0, -1);
|
||||
}
|
8
pkg/npm/http-api/test/default.test.ts
Normal file
8
pkg/npm/http-api/test/default.test.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import Urbit from '../src';
|
||||
|
||||
describe('blah', () => {
|
||||
it('works', () => {
|
||||
const connection = new Urbit('~sampel-palnet', '+code');
|
||||
expect(connection).toEqual(2);
|
||||
});
|
||||
});
|
7
pkg/npm/http-api/tsconfig-cjs.json
Normal file
7
pkg/npm/http-api/tsconfig-cjs.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"outDir": "./dist/cjs"
|
||||
},
|
||||
}
|
18
pkg/npm/http-api/tsconfig.json
Normal file
18
pkg/npm/http-api/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "@types"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/esm",
|
||||
"module": "ES2020",
|
||||
"noImplicitAny": true,
|
||||
"target": "ES2020",
|
||||
"pretty": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"strict": false
|
||||
// "lib": ["ES2020"],
|
||||
}
|
||||
}
|
109
pkg/npm/http-api/webpack.prod.js
Normal file
109
pkg/npm/http-api/webpack.prod.js
Normal file
@ -0,0 +1,109 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const shared = {
|
||||
mode: 'production',
|
||||
entry: {
|
||||
app: './src/index.ts'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(j|t)s$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/typescript'],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
],
|
||||
}
|
||||
},
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.ts'],
|
||||
fallback: {
|
||||
fs: false,
|
||||
child_process: false,
|
||||
util: require.resolve("util/"),
|
||||
buffer: require.resolve('buffer/'),
|
||||
assert: false,
|
||||
http: require.resolve('stream-http'),
|
||||
https: require.resolve('stream-http'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
zlib: require.resolve("browserify-zlib"),
|
||||
}
|
||||
},
|
||||
|
||||
optimization: {
|
||||
minimize: false,
|
||||
usedExports: true
|
||||
}
|
||||
};
|
||||
|
||||
const serverConfig = {
|
||||
...shared,
|
||||
target: 'node',
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
library: 'Urbit',
|
||||
libraryExport: 'default'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
XMLHttpRequest: ['xmlhttprequest-ssl', 'XMLHttpRequest'],
|
||||
EventSource: 'eventsource',
|
||||
fetch: ['node-fetch', 'default'],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const browserConfig = {
|
||||
...shared,
|
||||
target: 'web',
|
||||
output: {
|
||||
filename: 'browser.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
library: 'Urbit',
|
||||
libraryExport: 'default'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: 'buffer',
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
const exampleBrowserConfig = {
|
||||
...shared,
|
||||
mode: 'development',
|
||||
entry: {
|
||||
app: './src/example/browser.js'
|
||||
},
|
||||
output: {
|
||||
filename: 'browser.js',
|
||||
path: path.resolve(__dirname, 'example'),
|
||||
}
|
||||
};
|
||||
|
||||
const exampleNodeConfig = {
|
||||
...shared,
|
||||
mode: 'development',
|
||||
target: 'node',
|
||||
entry: {
|
||||
app: './src/example/node.js'
|
||||
},
|
||||
output: {
|
||||
filename: 'node.js',
|
||||
path: path.resolve(__dirname, 'example'),
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = [ serverConfig, browserConfig, exampleBrowserConfig, exampleNodeConfig ];
|
Loading…
Reference in New Issue
Block a user