diff --git a/gear/contacts/index.ts b/gear/contacts/index.ts new file mode 100644 index 00000000..341e8171 --- /dev/null +++ b/gear/contacts/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './lib'; \ No newline at end of file diff --git a/gear/contacts/lib.ts b/gear/contacts/lib.ts new file mode 100644 index 00000000..26b08c6b --- /dev/null +++ b/gear/contacts/lib.ts @@ -0,0 +1,109 @@ + +import { Patp, Poke, Scry } from '../lib'; +import { + Contact, + ContactUpdateAdd, + ContactUpdateEdit, + ContactUpdateRemove, + ContactEditField, + ContactShare, + ContactUpdate, + ContactUpdateAllowShips, + ContactUpdateAllowGroup, + ContactUpdateSetPublic +} from './types'; + +export const CONTACT_UPDATE_VERSION = 0; + +const storeAction = (data: T, version: number = CONTACT_UPDATE_VERSION): Poke => ({ + app: 'contact-store', + mark: `contact-update-${version}`, + json: data +}); + +export { storeAction as contactStoreAction }; + +export const addContact = (ship: Patp, contact: Contact): Poke => { + contact['last-updated'] = Date.now(); + + return storeAction({ + add: { ship, contact } + }); +}; + +export const removeContact = (ship: Patp): Poke => + storeAction({ + remove: { ship } + }); + +export const share = (recipient: Patp, version: number = CONTACT_UPDATE_VERSION): Poke => ({ + app: 'contact-push-hook', + mark: 'contact-share', + json: { share: recipient } +}); + +export const editContact = ( + ship: Patp, + editField: ContactEditField +): Poke => + storeAction({ + edit: { + ship, + 'edit-field': editField, + timestamp: Date.now() + } + }); + +export const allowShips = ( + ships: Patp[] +): Poke => storeAction({ + allow: { + ships + } +}); + +export const allowGroup = ( + ship: string, + name: string +): Poke => storeAction({ + allow: { + group: { ship, name } + } +}); + +export const setPublic = ( + setPublic: any +): Poke => { + return storeAction({ + 'set-public': setPublic + }); +}; + +export const retrieve = ( + ship: string +) => { + const resource = { ship, name: '' }; + return { + app: 'contact-pull-hook', + mark: 'pull-hook-action', + json: { + add: { + resource, + ship + } + } + }; +}; + +export const fetchIsAllowed = ( + entity: string, + name: string, + ship: string, + personal: boolean +): Scry => { + const isPersonal = personal ? 'true' : 'false'; + return { + app: 'contact-store', + path: `/is-allowed/${entity}/${name}/${ship}/${isPersonal}` + }; +}; diff --git a/gear/contacts/types.ts b/gear/contacts/types.ts new file mode 100644 index 00000000..201aa243 --- /dev/null +++ b/gear/contacts/types.ts @@ -0,0 +1,82 @@ +import { Path, Patp } from '../lib'; +import { Resource } from '../groups'; + +export type ContactUpdate = + | ContactUpdateAdd + | ContactUpdateRemove + | ContactUpdateEdit + | ContactUpdateInitial + | ContactUpdateAllowGroup + | ContactUpdateAllowShips + | ContactUpdateSetPublic; + + export interface ContactUpdateAdd { + add: { + ship: Patp; + contact: Contact; + }; +} + +export interface ContactUpdateRemove { + remove: { + ship: Patp; + }; +} + +export interface ContactUpdateEdit { + edit: { + ship: Patp; + 'edit-field': ContactEditField; + timestamp: number; + }; +} + +export interface ContactUpdateAllowShips { + allow: { + ships: Patp[]; + } +} + +export interface ContactUpdateAllowGroup { + allow: { + group: Resource; + } +} + +export interface ContactUpdateSetPublic { + 'set-public': boolean; +} + +export interface ContactShare { + share: Patp; +} + +export interface ContactUpdateInitial { + initial: Rolodex; +} + +export type Rolodex = { + [p in Patp]: Contact; +}; + +export type Contacts = Rolodex; + +export interface Contact { + nickname: string; + bio: string; + status: string; + color: string; + avatar: string | null; + cover: string | null; + groups: Path[]; + 'last-updated': number; +} + +type ContactKeys = keyof Contact; + +export type ContactEditFieldPrim = Exclude; + +export type ContactEditField = Partial> & { + 'add-group'?: Resource; + 'remove-group'?: Resource; +}; diff --git a/gear/deps.d.ts b/gear/deps.d.ts new file mode 100644 index 00000000..0099f33f --- /dev/null +++ b/gear/deps.d.ts @@ -0,0 +1,8 @@ + +declare module 'urbit-ob' { + + /** + * Convert a @p-encoded string to a decimal-encoded string. + */ + function patp2dec(name: string): string +} diff --git a/gear/docket/index.ts b/gear/docket/index.ts new file mode 100644 index 00000000..e06143cf --- /dev/null +++ b/gear/docket/index.ts @@ -0,0 +1,2 @@ +export * from './lib'; +export * from './types'; diff --git a/gear/docket/lib.ts b/gear/docket/lib.ts new file mode 100644 index 00000000..f5c3e255 --- /dev/null +++ b/gear/docket/lib.ts @@ -0,0 +1,65 @@ +import { Poke, Scry } from '../lib'; +import { Chad } from './types'; + +export function chadIsRunning(chad: Chad) { + return 'glob' in chad || 'site' in chad; +} + +export const scryCharges: Scry = { + app: 'docket', + path: '/charges' +}; + +export const scryDockets: Scry = { + app: 'docket', + path: '/dockets' +}; + +export const scryTreaties: Scry = { + app: 'treaty', + path: '/treaties' +}; + +export const scryDefaultAlly: Scry = { + app: 'treaty', + path: '/default-ally' +}; + +export const scryAllies: Scry = { + app: 'treaty', + path: '/allies' +}; + +export const scryAllyTreaties = (ship: string): Scry => ({ + app: 'treaty', + path: `/treaties/${ship}` +}); + +/** + * Uninstall a desk, and remove docket + */ +export function docketUninstall(desk: string): Poke { + return { + app: 'docket', + mark: 'docket-uninstall', + json: desk + }; +} + +export function docketInstall(ship: string, desk: string): Poke { + return { + app: 'docket', + mark: 'docket-install', + json: `${ship}/${desk}` + }; +} + +export function allyShip(ship: string): Poke { + return { + app: 'treaty', + mark: 'ally-update-0', + json: { + add: ship + } + }; +} diff --git a/gear/docket/types.ts b/gear/docket/types.ts new file mode 100644 index 00000000..c35399b5 --- /dev/null +++ b/gear/docket/types.ts @@ -0,0 +1,135 @@ +import { Cass } from '../hood'; +export type DeskStatus = 'active' | 'suspended'; + +export type DocketHref = DocketHrefSite | DocketHrefGlob; + +export interface DocketHrefGlob { + glob: { + base: string; + + } +} + +export interface DocketHrefSite { + site: string; +} + +export interface Docket { + title: string; + info?: string; + color: string; + href: DocketHref; + website: string; + license: string; + version: string; + image?: string; +} + +export interface Charge extends Docket { + chad: Chad; +} + +export type Chad = HungChad | GlobChad | SiteChad | InstallChad | SuspendChad; + +export interface HungChad { + hung: string; +} + +export interface GlobChad { + glob: null; +} +export interface SiteChad { + site: null; +} +export interface InstallChad { + install: null; + +} +export interface SuspendChad { + suspend: null; +} + +export interface Treaty extends Docket { + ship: string; + desk: string; + cass: Cass; + hash: string; +} + +export interface Charges { + [desk: string]: Charge; +} + +export interface Treaties { + [ref: string]: Treaty; +} + +export type Charter = string[]; + +export interface Allies { + [ship: string]: Charter; +} + +export interface Provider { + shipName: string; + nickname?: string; + status?: string; +} + +export type ChargeUpdate = ChargeUpdateInitial | ChargeUpdateAdd | ChargeUpdateDel; + +export interface ChargeUpdateInitial { + initial: { + [desk: string]: Charge; + } +} + +export interface ChargeUpdateAdd { + 'add-charge': { + desk: string; + charge: Charge; + } +} + +export interface ChargeUpdateDel { + 'del-charge': string; +} + +export type AllyUpdate = AllyUpdateIni | AllyUpdateAdd | AllyUpdateDel | AllyUpdateNew; + +export interface AllyUpdateIni { + ini: { + [ship: string]: string[]; + } +} + +export interface AllyUpdateAdd { + add: string; +} + +export interface AllyUpdateDel { + del: string; +} + +export interface AllyUpdateNew { + new: { + ship: string; + alliance: string[]; + } +} + +export type TreatyUpdate = TreatyUpdateIni | TreatyUpdateAdd | TreatyUpdateDel; + +export interface TreatyUpdateIni { + ini: { + [foreignDesk: string]: Treaty; + } +} + +export interface TreatyUpdateAdd { + add: Treaty; +} + +export interface TreatyUpdateDel { + del: string; +} diff --git a/gear/graph/index.ts b/gear/graph/index.ts new file mode 100644 index 00000000..4fed660f --- /dev/null +++ b/gear/graph/index.ts @@ -0,0 +1,2 @@ +export * from './lib'; +export * from './types'; \ No newline at end of file diff --git a/gear/graph/lib.ts b/gear/graph/lib.ts new file mode 100644 index 00000000..ce187df6 --- /dev/null +++ b/gear/graph/lib.ts @@ -0,0 +1,534 @@ +import { GroupPolicy, makeResource, Resource, resourceFromPath } from '../groups'; + +import { decToUd, deSig, unixToDa, Scry } from '../lib'; +import { Enc, Path, Patp, PatpNoSig, Poke, Thread } from '../lib/types'; +import { Content, GraphChildrenPoke, GraphNode, GraphNodePoke, Post } from './types'; +import { patp2dec } from 'urbit-ob'; + +export const GRAPH_UPDATE_VERSION = 3; + +export const createBlankNodeWithChildPost = ( + ship: PatpNoSig, + parentIndex = '', + childIndex = '', + contents: Content[] +): GraphNodePoke => { + const date = unixToDa(Date.now()).toString(); + const nodeIndex = parentIndex + '/' + date; + + const childGraph: GraphChildrenPoke = {}; + 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 + }; +}; + +export const markPending = (nodes: any): any => { + Object.keys(nodes).forEach((key) => { + nodes[key].post.author = deSig(nodes[key].post.author); + nodes[key].post.pending = true; + if (nodes[key].children) { + nodes[key].children = markPending(nodes[key].children); + } + }); + return nodes; +}; + +export const createPost = ( + ship: PatpNoSig, + contents: Content[], + parentIndex = '', + childIndex = '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; +} + +const storeAction = (data: T, version: number = GRAPH_UPDATE_VERSION): Poke => ({ + app: 'graph-store', + mark: `graph-update-${version}`, + json: data +}); + +export { storeAction as graphStoreAction }; + +const viewAction = (threadName: string, action: T): Thread => ({ + inputMark: 'graph-view-action', + outputMark: 'json', + threadName, + body: action +}); + +export { viewAction as graphViewAction }; + +const hookAction = (data: T, version: number = GRAPH_UPDATE_VERSION): Poke => ({ + app: 'graph-push-hook', + mark: `graph-update-${version}`, + json: data +}); + +const dmAction = (data: T): Poke => ({ + app: 'dm-hook', + mark: 'dm-hook-action', + json: data +}); + +export { hookAction as graphHookAction }; + +export const createManagedGraph = ( + ship: PatpNoSig, + name: string, + title: string, + description: string, + group: Path, + mod: string +): Thread => { + 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, + mod: string +): Thread => viewAction('graph-create', { + create: { + resource: makeResource(`~${ship}`, name), + title, + description, + associated: { policy }, + module: mod, + mark: moduleToMark(mod) + } +}); + +export const joinGraph = ( + ship: Patp, + name: string +): Thread => viewAction('graph-join', { + join: { + resource: makeResource(ship, name), + ship + } +}); + +export const deleteGraph = ( + ship: PatpNoSig, + name: string +): Thread => viewAction('graph-delete', { + 'delete': { + resource: makeResource(`~${ship}`, name) + } +}); + +export const leaveGraph = ( + ship: Patp, + name: string +): Thread => viewAction('graph-leave', { + 'leave': { + resource: makeResource(ship, name) + } +}); + +export const groupifyGraph = ( + ship: Patp, + name: string, + toPath?: string +): Thread => { + const resource = makeResource(ship, name); + const to = toPath && resourceFromPath(toPath); + + return viewAction('graph-groupify', { + groupify: { + resource, + to + } + }); +}; + +export const evalCord = ( + cord: string +): Thread => { + 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 => { + return storeAction({ + 'add-graph': { + resource: { ship, name }, + graph, + mark + } + }); +}; + +export const addNodes = ( + ship: Patp, + name: string, + nodes: Object +): Thread => ({ + inputMark: `graph-update-${GRAPH_UPDATE_VERSION}`, + outputMark: 'graph-view-action', + threadName: 'graph-add-nodes', + body: { + 'add-nodes': { + resource: { ship, name }, + nodes + } + } +}); + +export const addPost = ( + ship: Patp, + name: string, + post: Post +): Thread => { + const nodes: Record = {}; + nodes[post.index] = { + post, + children: null + }; + return addNodes(ship, name, nodes); +}; + +export const addNode = ( + ship: Patp, + name: string, + node: GraphNodePoke +): Thread => { + const nodes: Record = {}; + nodes[node.post.index] = node; + + return addNodes(ship, name, nodes); +}; + +export const createGroupFeed = ( + group: Resource, + vip: any = '' +): Thread => ({ + inputMark: 'graph-view-action', + outputMark: 'resource', + threadName: 'graph-create-group-feed', + body: { + 'create-group-feed': { + resource: group, + vip + } + } +}); + +export const disableGroupFeed = ( + group: Resource +): Thread => ({ + inputMark: 'graph-view-action', + outputMark: 'json', + threadName: 'graph-disable-group-feed', + body: { + 'disable-group-feed': { + resource: group + } + } +}); + +/** + * Set dm-hook to screen new DMs or not + * + */ +export const setScreen = (screen: boolean): Poke => dmAction({ screen }); + +/** + * Accept a pending DM request + * + * @param ship the ship to accept + */ +export const acceptDm = (ship: string) => dmAction({ + accept: ship +}); + +/** + * Decline a pending DM request + * + * @param ship the ship to accept + */ +export const declineDm = (ship: string) => dmAction({ + decline: ship +}); + +/** + * Remove posts from a set of indices + * + */ +export const removePosts = ( + ship: Patp, + name: string, + indices: string[] +): Poke => hookAction({ + 'remove-posts': { + resource: { ship, name }, + indices + } +}); + +/** + * Remove a DM message from our inbox + * + * @remarks + * This does not remove the message from the recipients inbox + */ +export const removeDmMessage = ( + our: Patp, + index: string +): Poke => ({ + app: 'graph-store', + mark: `graph-update-${GRAPH_UPDATE_VERSION}`, + json: { + 'remove-posts': { + resource: { ship: our, name: 'dm-inbox' }, + indices: [index] + } + } +}); + +/** + * Send a DM to a ship + * + * @param our sender + * @param ship recipient + * @param contents contents of message + */ +export const addDmMessage = (our: PatpNoSig, ship: Patp, contents: Content[]): Poke => { + const post = createPost(our, contents, `/${patp2dec(ship)}`); + const node: GraphNode = { + post, + children: null + }; + return { + app: 'dm-hook', + mark: `graph-update-${GRAPH_UPDATE_VERSION}`, + json: { + 'add-nodes': { + resource: { ship: `~${our}`, name: 'dm-inbox' }, + nodes: { + [post.index]: node + } + } + } + }; +}; + +const encodeIndex = (idx: string) => idx.split('/').map(decToUd).join('/'); + +/** + * Fetch all graph keys + */ +export const getKeys = (): Scry => ({ + app: 'graph-store', + path: '/keys' +}); + +/** + * Fetch newest (larger keys) nodes in a graph under some index + * + * @param ship ship of graph + * @param name name of graph + * @param count number of nodes to load + * @param index index to query + */ +export const getNewest = ( + ship: string, + name: string, + count: number, + index = '' +): Scry => ({ + app: 'graph-store', + path: `/graph/${ship}/${name}/node/siblings` + + `/newest/lone/${count}${encodeIndex(index)}` +}); + +/** + * Fetch nodes in a graph that are older (key is smaller) and direct + * siblings of some index + * + * @param ship ship of graph + * @param name name of graph + * @param count number of nodes to load + * @param index index to query + */ +export const getOlderSiblings = ( + ship: string, + name: string, + count: number, + index: string +): Scry => ({ + app: 'graph-store', + path: `/graph/${ship}/${name}/node/siblings/older/lone/${count}${encodeIndex(index)}` +}); + +/** + * Fetch nodes in a graph that are younger (key is larger) and direct + * siblings of some index + * + * @param ship ship of graph + * @param name name of graph + * @param count number of nodes to load + * @param index index to query + */ +export const getYoungerSiblings = ( + ship: string, + name: string, + count: number, + index: string +): Scry => ({ + app: 'graph-store', + path: `/graph/${ship}/${name}/node/siblings/newer/lone/${count}${encodeIndex(index)}` +}); + +/** + * Fetch all nodes in a graph under some index, without loading children + * + * @param ship ship of graph + * @param name name of graph + * @param index index to query + */ +export const getShallowChildren = (ship: string, name: string, index = '') => ({ + app: 'graph-store', + path: `/graph/${ship}/${name}/node/children/lone/~/~${encodeIndex(index)}` +}); + +/** + * Fetch newest nodes in a graph as a flat map, including children, + * optionally starting at a specified key + * + * @param ship ship of graph + * @param name name of graph + * @param count number of nodes to load + * @param start key to start at + * + */ +export const getDeepOlderThan = ( + ship: string, + name: string, + count: number, + start = '' +) => ({ + app: 'graph-store', + path: `/graph/${ship}/${name}/node/siblings` + + `/${start.length > 0 ? 'older' : 'newest'}` + + `/kith/${count}${encodeIndex(start)}` +}); + +/** + * Fetch a flat map of a nodes ancestors and firstborn children + * + * @param ship ship of graph + * @param name name of graph + * @param index index to query + * + */ +export const getFirstborn = ( + ship: string, + name: string, + index: string +): Scry => ({ + app: 'graph-store', + path: `/graph/${ship}/${name}/node/firstborn${encodeIndex(index)}` +}); + +/** + * Fetch a single node, and all it's children + * + * @param ship ship of graph + * @param name name of graph + * @param index index to query + * + */ +export const getNode = ( + ship: string, + name: string, + index: string +): Scry => ({ + app: 'graph-store', + path: `/graph/${ship}/${name}/node/index/kith${encodeIndex(index)}` +}); + +/** + * Fetch entire graph + * + * @param ship ship of graph + * @param name name of graph + * + */ +export const getGraph = ( + ship: string, + name: string +): Scry => ({ + app: 'graph-store', + path: `/graph/${ship}/${name}` +}); diff --git a/gear/graph/types.ts b/gear/graph/types.ts new file mode 100644 index 00000000..2e8816a3 --- /dev/null +++ b/gear/graph/types.ts @@ -0,0 +1,94 @@ +import { Patp } from '../lib'; +import { BigIntOrderedMap } from '../lib/BigIntOrderedMap'; +import { BigIntArrayOrderedMap } from '../lib/BigIntArrayOrderedMap'; + +export interface TextContent { + text: string; +} +export interface UrlContent { + url: string; +} +export interface CodeContent { + code: { + expression: string; + output: string[] | undefined; + } +} + +export interface ReferenceContent { + reference: AppReference | GraphReference | GroupReference; +} + +export interface GraphReference { + graph: { + graph: string; + group: string; + index: string; + } +} + +export interface GroupReference { + group: string; +} + +export interface AppReference { + app: { + ship: string; + desk: string; + path: string; + } +} + +export interface MentionContent { + mention: string; + emphasis?: 'bold' | 'italic'; +} +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 GraphNodePoke { + post: Post; + children: GraphChildrenPoke | null; +} + +export interface GraphChildrenPoke { + [k: string]: GraphNodePoke; +} + +export interface GraphNode { + children: Graph | null; + post: Post; +} + +export interface FlatGraphNode { + children: null; + post: Post; +} + +export type Graph = BigIntOrderedMap; + +export type Graphs = { [rid: string]: Graph }; + +export type FlatGraph = BigIntArrayOrderedMap; + +export type FlatGraphs = { [rid: string]: FlatGraph }; + +export type ThreadGraphs = { + [rid: string]: { + [index: string]: FlatGraph; + } +}; diff --git a/gear/groups/index.ts b/gear/groups/index.ts new file mode 100644 index 00000000..341e8171 --- /dev/null +++ b/gear/groups/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './lib'; \ No newline at end of file diff --git a/gear/groups/lib.ts b/gear/groups/lib.ts new file mode 100644 index 00000000..7e93ad3a --- /dev/null +++ b/gear/groups/lib.ts @@ -0,0 +1,226 @@ +import { deSig } from '../index'; +import { Enc, Path, Patp, PatpNoSig, Poke, Thread } from '../lib/types'; +import { Group, GroupPolicy, GroupPolicyDiff, GroupUpdateAddMembers, GroupUpdateAddTag, GroupUpdateChangePolicy, GroupUpdateRemoveGroup, GroupUpdateRemoveMembers, GroupUpdateRemoveTag, Resource, RoleTags, Tag } from './types'; +import { GroupUpdate } from './update'; + +export const GROUP_UPDATE_VERSION = 0; + +export const proxyAction = (data: T, version: number = GROUP_UPDATE_VERSION): Poke => ({ + app: 'group-push-hook', + mark: `group-update-${version}`, + json: data +}); + +const storeAction = (data: T, version: number = GROUP_UPDATE_VERSION): Poke => ({ + app: 'group-store', + mark: `group-update-${version}`, + json: data +}); + +export { storeAction as groupStoreAction }; + +const viewAction = (data: T): Poke => ({ + app: 'group-view', + mark: 'group-view-action', + json: data +}); + +export { viewAction as groupViewAction }; + +export const viewThread = (thread: string, action: T): Thread => ({ + inputMark: 'group-view-action', + outputMark: 'json', + threadName: thread, + body: action +}); + +export const removeMembers = ( + resource: Resource, + ships: PatpNoSig[] +): Poke => proxyAction({ + removeMembers: { + resource, + ships + } +}); + +export const addTag = ( + resource: Resource, + tag: Tag, + ships: Patp[] +): Poke => proxyAction({ + addTag: { + resource, + tag, + ships + } +}); + +export const removeTag = ( + tag: Tag, + resource: Resource, + ships: PatpNoSig[] +): Poke => proxyAction({ + removeTag: { + tag, + resource, + ships + } +}); + +export const addMembers = ( + resource: Resource, + ships: PatpNoSig[] +): Poke => proxyAction({ + addMembers: { + resource, + ships + } +}); + +export const removeGroup = ( + resource: Resource +): Poke => storeAction({ + removeGroup: { + resource + } +}); + +export const changePolicy = ( + resource: Resource, + diff: Enc +): Poke> => proxyAction({ + changePolicy: { + resource, + diff + } +}); + +export const join = ( + ship: string, + name: string, + app: "groups" | "graph", + autojoin: boolean, + share: boolean +): Poke => viewAction({ + join: { + resource: makeResource(ship, name), + ship, + shareContact: share || false, + app, + autojoin + } +}); + +export const createGroup = ( + name: string, + policy: Enc, + title: string, + description: string +): Thread => viewThread('group-create', { + create: { + name, + policy, + title, + description + } +}); + +export const deleteGroup = ( + ship: string, + name: string +): Thread => viewThread('group-delete', { + remove: makeResource(ship, name) +}); + +export const leaveGroup = ( + ship: string, + name: string +): Thread => viewThread('group-leave', { + leave: makeResource(ship, name) +}); + +export const invite = ( + ship: string, + name: string, + ships: Patp[], + description: string +): Thread => viewThread('group-invite', { + invite: { + resource: makeResource(ship, name), + ships, + description + } +}); + +export const abortJoin = ( + resource: string +): Poke => viewAction({ + abort: resource +}); + +export const roleTags = ['janitor', 'moderator', 'admin']; +// TODO make this type better? + +export const groupBunts = { + group: (): Group => ({ members: [], tags: { role: {} }, hidden: false, policy: groupBunts.policy() }), + policy: (): GroupPolicy => ({ open: { banned: [], banRanks: [] } }) +}; + +export const joinError = ['no-perms', 'strange', 'abort'] as const; +export const joinResult = ['done', ...joinError] as const; +export const joinLoad = ['start', 'added', 'metadata'] as const; +export const joinProgress = [...joinLoad, ...joinResult] as const; + +export function roleForShip( + group: Group, + ship: PatpNoSig +): RoleTags | undefined { + return roleTags.reduce((currRole, role) => { + const roleShips = group?.tags?.role?.[role]; + return roleShips && roleShips.includes(ship) ? role : currRole; + }, undefined as RoleTags | 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 }; +} + +export const isWriter = (group: Group, resource: string, ship: string) => { + const graph = group?.tags?.graph; + const writers: string[] | undefined = graph && (graph[resource] as any)?.writers; + const admins = group?.tags?.role?.admin ?? []; + if (typeof writers === 'undefined') { + return true; + } else { + return [...writers].includes(ship) || admins.includes(ship); + } +}; + +export function isChannelAdmin( + group: Group, + resource: string, + ship: string +): boolean { + const role = roleForShip(group, deSig(ship)); + + return ( + isHost(resource, ship) || + role === 'admin' || + role === 'moderator' + ); +} + +export function isHost( + resource: string, + ship: string +): boolean { + const [, , host] = resource.split('/'); + + return ship === host; +} diff --git a/gear/groups/types.ts b/gear/groups/types.ts new file mode 100644 index 00000000..6197c4f9 --- /dev/null +++ b/gear/groups/types.ts @@ -0,0 +1,2 @@ +export * from './update'; +export * from './view'; \ No newline at end of file diff --git a/gear/groups/update.ts b/gear/groups/update.ts new file mode 100644 index 00000000..a25c9a63 --- /dev/null +++ b/gear/groups/update.ts @@ -0,0 +1,175 @@ +import { PatpNoSig, Path, ShipRank, Enc } from '../lib'; +import { roleTags } from './index'; + +export type RoleTags = typeof roleTags[number]; +interface RoleTag { + tag: 'admin' | 'moderator' | 'janitor'; +} + +interface AppTag { + app: string; + resource: string; + tag: string; +} + +export type Tag = AppTag | RoleTag; + +export interface InvitePolicy { + invite: { + pending: PatpNoSig[]; + }; +} + +export interface OpenPolicy { + open: { + banned: PatpNoSig[]; + banRanks: ShipRank[]; + }; +} + +export interface Resource { + name: string; + ship: PatpNoSig; +} + +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]: PatpNoSig[]; +} + +export interface Tags { + role: TaggedShips; + [app: string]: TaggedShips; +} + +export interface Group { + members: PatpNoSig[]; + tags: Tags; + policy: GroupPolicy; + hidden: boolean; +} + +export type Groups = { + [p in Path]: Group; +}; + +export interface GroupUpdateInitial { + initial: Enc; +} + +export interface GroupUpdateAddGroup { + addGroup: { + resource: Resource; + policy: Enc; + 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; + }; +} + +export type GroupUpdate = + | GroupUpdateInitial + | GroupUpdateAddGroup + | GroupUpdateAddMembers + | GroupUpdateRemoveMembers + | GroupUpdateAddTag + | GroupUpdateRemoveTag + | GroupUpdateChangePolicy + | GroupUpdateRemoveGroup + | GroupUpdateExpose + | GroupUpdateInitialGroup; + +export type GroupAction = Omit; diff --git a/gear/groups/view.ts b/gear/groups/view.ts new file mode 100644 index 00000000..c6e432b2 --- /dev/null +++ b/gear/groups/view.ts @@ -0,0 +1,30 @@ +import { joinError, joinProgress, joinResult } from "."; +import {Patp} from "../lib"; + +export type JoinError = typeof joinError[number]; + +export type JoinResult = typeof joinResult[number]; + + +export type JoinProgress = typeof joinProgress[number]; + +export interface JoinRequest { + /** + * Whether to display the join request or not + */ + hidden: boolean; + /** + * Timestamp of when the request started + */ + started: number; + ship: Patp; + progress: JoinProgress; + shareContact: boolean; + autojoin: boolean; + app: 'graph' | 'groups'; + invite: string[]; +} + +export interface JoinRequests { + [rid: string]: JoinRequest; +} diff --git a/gear/hark/index.ts b/gear/hark/index.ts new file mode 100644 index 00000000..341e8171 --- /dev/null +++ b/gear/hark/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './lib'; \ No newline at end of file diff --git a/gear/hark/lib.ts b/gear/hark/lib.ts new file mode 100644 index 00000000..a470c671 --- /dev/null +++ b/gear/hark/lib.ts @@ -0,0 +1,163 @@ +import { BigInteger } from 'big-integer'; + +import { Poke } from '../lib/types'; +import { + HarkBin, + HarkBinId, + HarkBody, + HarkLid, + HarkPlace +} from './types'; +import { decToUd } from '../lib'; + +export const harkAction = (data: T): Poke => ({ + app: 'hark-store', + mark: 'hark-action', + json: data +}); + +const graphHookAction = (data: T): Poke => ({ + app: 'hark-graph-hook', + mark: 'hark-graph-hook-action', + json: data +}); + +export { graphHookAction as harkGraphHookAction }; + +const groupHookAction = (data: T): Poke => ({ + app: 'hark-group-hook', + mark: 'hark-group-hook-action', + json: data +}); + +export { groupHookAction as harkGroupHookAction }; + +export const actOnNotification = ( + frond: string, + intTime: BigInteger, + bin: HarkBin +): Poke => + harkAction({ + [frond]: { + time: decToUd(intTime.toString()), + bin + } + }); + +export const setMentions = (mentions: boolean): Poke => + graphHookAction({ + 'set-mentions': mentions + }); + +export const setWatchOnSelf = (watchSelf: boolean): Poke => + graphHookAction({ + 'set-watch-on-self': watchSelf + }); + +export const setDoNotDisturb = (dnd: boolean): Poke => + harkAction({ + 'set-dnd': dnd + }); + +export const addNote = (bin: HarkBin, body: HarkBody) => + harkAction({ + 'add-note': { + bin, + body + } + }); + +export const archive = (bin: HarkBin, lid: HarkLid): Poke => + harkAction({ + archive: { + lid, + bin + } + }); + +export const opened = harkAction({ + opened: null +}); + +export const markCountAsRead = (place: HarkPlace): Poke => + harkAction({ + 'read-count': place + }); + +export const markEachAsRead = ( + place: HarkPlace, + path: string +): Poke => + harkAction({ + 'read-each': { + place, + path + } + }); + +export const seen = () => harkAction({ seen: null }); + +export const readAll = harkAction({ 'read-all': null }); +export const archiveAll = harkAction({ 'archive-all': null }); + +export const ignoreGroup = (group: string): Poke => + groupHookAction({ + ignore: group + }); + +export const ignoreGraph = (graph: string, index: string): Poke => + graphHookAction({ + ignore: { + graph, + index + } + }); + +export const listenGroup = (group: string): Poke => + groupHookAction({ + listen: group + }); + +export const listenGraph = (graph: string, index: string): Poke => + graphHookAction({ + listen: { + graph, + index + } + }); + +/** + * Read all graphs belonging to a particular group + */ +export const readGroup = (group: string) => + harkAction({ + 'read-group': group + }); + +/** + * Read all unreads in a graph + */ +export const readGraph = (graph: string) => + harkAction({ + 'read-graph': graph + }); + +export function harkBinToId(bin: HarkBin): HarkBinId { + const { place, path } = bin; + return `${place.desk}${place.path}${path}`; +} + +export function harkBinEq(a: HarkBin, b: HarkBin): boolean { + return ( + a.place.path === b.place.path && + a.place.desk === b.place.desk && + a.path === b.path + ); +} + +export function harkLidToId(lid: HarkLid): string { + if('time' in lid) { + return `archive-${lid.time}`; + } + return Object.keys(lid)[0]; +} diff --git a/gear/hark/types.ts b/gear/hark/types.ts new file mode 100644 index 00000000..e52912cb --- /dev/null +++ b/gear/hark/types.ts @@ -0,0 +1,58 @@ + +export interface HarkStats { + count: number; + each: string[]; + last: number; +} + +export interface Timebox { + [binId: string]: Notification; +} + +export type HarkContent = { ship: string; } | { text: string; }; + +export interface HarkBody { + title: HarkContent[]; + time: number; + content: HarkContent[]; + link: string; + binned: string; +} + +export interface HarkPlace { + desk: string; + path: string; +} + +export interface HarkBin { + path: string; + place: HarkPlace; +} + +export type HarkLid = + { unseen: null; } +| { seen: null; } +| { time: string; }; + +export type HarkBinId = string; +export interface Notification { + bin: HarkBin; + time: number; + body: HarkBody[]; +} + +export interface NotificationGraphConfig { + watchOnSelf: boolean; + mentions: boolean; + watching: WatchedIndex[] +} + +export interface Unreads { + [path: string]: HarkStats; +} + +interface WatchedIndex { + graph: string; + index: string; +} +export type GroupNotificationsConfig = string[]; diff --git a/gear/hood/index.ts b/gear/hood/index.ts new file mode 100644 index 00000000..e06143cf --- /dev/null +++ b/gear/hood/index.ts @@ -0,0 +1,2 @@ +export * from './lib'; +export * from './types'; diff --git a/gear/hood/lib.ts b/gear/hood/lib.ts new file mode 100644 index 00000000..02d86868 --- /dev/null +++ b/gear/hood/lib.ts @@ -0,0 +1,127 @@ +import { Poke, Scry } from '../lib'; +import { Pike } from './types'; + +export const getPikes: Scry = { + app: 'hood', + path: '/kiln/pikes' +}; + +/** + * Install a foreign desk + */ +export function kilnInstall( + ship: string, + desk: string, + local?: string +): Poke { + return { + app: 'hood', + mark: 'kiln-install', + json: { + ship, + desk, + local: local || desk + } + }; +} + +/** + * Sync with a foreign desk + */ +export function kilnSync( + ship: string, + desk: string, + local?: string +): Poke { + return { + app: 'hood', + mark: 'kiln-sync', + json: { + ship, + desk, + local: local || desk + } + }; +} + +/** + * Unsync with a foreign desk + */ +export function kilnUnsync( + ship: string, + desk: string, + local?: string +): Poke { + return { + app: 'hood', + mark: 'kiln-unsync', + json: { + ship, + desk, + local: local || desk + } + }; +} + +/** + * Uninstall a desk + */ +export function kilnUninstall( + desk: string +): Poke { + return { + app: 'hood', + mark: 'kiln-uninstall', + json: desk + }; +} + +export function kilnSuspend( + desk: string +): Poke { + return { + app: 'hood', + mark: 'kiln-suspend', + json: desk + }; +} + +export function kilnRevive( + desk: string +): Poke { + return { + app: 'hood', + mark: 'kiln-revive', + json: desk + }; +} + +export function kilnBump(): Poke { + return { + app: 'hood', + mark: 'kiln-bump', + json: null, + }; +} + +export function kilnPause(desk: string) { + return { + app: 'hood', + mark: 'kiln-pause', + json: desk + }; +} + +export function kilnResume(desk: string) { + return { + app: 'hood', + mark: 'kiln-resume', + json: desk + }; +} + +export const scryLag: Scry = ({ app: 'hood', path: '/kiln/lag' }); + +export function getPikePublisher(pike: Pike) { + return pike.sync?.ship; +} diff --git a/gear/hood/types.ts b/gear/hood/types.ts new file mode 100644 index 00000000..17676b47 --- /dev/null +++ b/gear/hood/types.ts @@ -0,0 +1,208 @@ + +/** + * A pending commit, awaiting a future kelvin version + */ +interface Woof { + aeon: number; + weft: Weft; +} + +interface Rein { + /** + * Agents not in manifest that should be running + */ + add: string[]; + /** + * Agents in manifest that should not be running + */ + sub: string[]; +} + +export interface Rail { + /** + * Original publisher of desk, if available + */ + publisher: string | null; + /** + * Ship of foreign vat + */ + ship: string; + /** + * Desk of foreign vat + */ + desk: string; + /** + * Aeon (version number) that we currently have synced + */ + aeon: number; + next: Woof[]; + paused: boolean; +} + +/** + * A tracker of a foreign {@link Vat} + * + */ +export interface Arak { + rein: Rein; + rail: Rail | null; +} + +/** + * A component's kelvin version + */ +export interface Weft { + /** + * Name of the component + * + * @remarks + * Usually %zuse, %hoon, or %lull + */ + name: string; + /** + * Kelvin version + * + */ + kelvin: number; +} + +export interface KilnDiffBlock { + block: { + desk: string; + arak: Arak; + weft: Weft; + blockers: string[]; + }; +} + +export interface KilnDiffReset { + reset: { + desk: string; + arak: Arak; + }; +} + +export interface KilnDiffMerge { + merge: { + desk: string; + arak: Arak; + }; +} + +export interface KilnDiffMergeSunk { + 'merge-sunk': { + desk: string; + arak: Arak; + tang: string; + }; +} + +export interface KilnDiffMergeFail { + 'merge-fail': { + desk: string; + arak: Arak; + tang: string; + }; +} + +export type KilnDiff = + | KilnDiffBlock + | KilnDiffReset + | KilnDiffMerge + | KilnDiffMergeSunk + | KilnDiffMergeFail; + +/** + * Cases for revision + * + */ +export interface Cass { + /** + * Revision number + */ + ud: number; + /** + * Timestamp of revision, as stringifed `@da` + * + * @remarks + * If \@da is outside valid positive unix timestamp, value will be zero + */ + da: string; +} + +/** + * A local desk installation + */ +export interface Vat { + /** + * Desk that this Vat describes + */ + desk: string; + /** + * Hash of the desk, rendered as `@uv` + * + * @remarks + * Equivalent to + * ```hoon + * .^(@uv %cz /=desk=) + * ``` + */ + hash: string; + /** + * Current revision + */ + cass: Cass; + /** + * Foreign sync + */ + arak: Arak; +} + +export interface Vats { + [desk: string]: Vat; +} +/** + * TODO: crisp one-liner describing a Pike + */ +export interface Pike { + /** + * Hash of the desk, rendered as `@uv` + * + * @remarks + * Equivalent to + * ```hoon + * .^(@uv %cz /=desk=) + * ``` + */ + hash: string; + sync: { + /** + * Source desk for this Pike + */ + desk: string; + /** + * Source ship for this Pike + */ + ship: string; + } | null; + /** + * {@link Weft}s associated with this Pike + */ + wefts: Weft[]; + /** + * how live is this pike? + * + * live - app is running + * held - app is not running, but is trying to run. this state can be entered + * in two main ways: + * - when installing an app but it hasn't finished downloading (or it did + * but failed to install for some reason) + * - when user forced a kelvin upgrade by suspending desks. + * dead - app is not running + */ + zest: "live" | "dead" | "held"; +} + +export interface Pikes { + [desk: string]: Pike; +} diff --git a/gear/index.ts b/gear/index.ts new file mode 100644 index 00000000..d3f28beb --- /dev/null +++ b/gear/index.ts @@ -0,0 +1,28 @@ +export * from './contacts'; +export * as contacts from './contacts'; +export * from './graph'; +export * as graph from './graph'; +export * from './groups'; +export * as groups from './groups'; +export * from './hark'; +export * as hark from './hark'; +export * from './invite'; +// this conflicts with /groups/lib invite +// export * as invite from './invite'; +export * from './metadata'; +export * as metadata from './metadata'; +export * from './settings'; +export * as settings from './settings'; +export * from './s3'; +export * as s3 from './s3'; +export * from './lib'; +export * from './lib/BigIntOrderedMap'; +export * from './lib/BigIntArrayOrderedMap'; +export * as hood from './hood'; +export * from './hood'; +export * as docket from './docket'; +export * from './docket'; + +// TODO: Userspace Permissions +// export * from './permissions'; +// export * as permissions from './permissions'; diff --git a/gear/invite/index.ts b/gear/invite/index.ts new file mode 100644 index 00000000..341e8171 --- /dev/null +++ b/gear/invite/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './lib'; \ No newline at end of file diff --git a/gear/invite/lib.ts b/gear/invite/lib.ts new file mode 100644 index 00000000..a5a5ca94 --- /dev/null +++ b/gear/invite/lib.ts @@ -0,0 +1,28 @@ +import { Poke, Serial } from "../lib"; +import { InviteUpdate, InviteUpdateAccept, InviteUpdateDecline } from "./types"; + +export const inviteAction = (data: T): Poke => ({ + app: 'invite-store', + mark: 'invite-action', + json: data +}); + +export const accept = ( + app: string, + uid: Serial +): Poke => inviteAction({ + accept: { + term: app, + uid + } +}); + +export const decline = ( + app: string, + uid: Serial +): Poke => inviteAction({ + decline: { + term: app, + uid + } +}); diff --git a/gear/invite/types.ts b/gear/invite/types.ts new file mode 100644 index 00000000..b3515bb4 --- /dev/null +++ b/gear/invite/types.ts @@ -0,0 +1,75 @@ +import { Serial, PatpNoSig, Path } from '../lib'; +import { Resource } from "../groups"; + +export type InviteUpdate = + InviteUpdateInitial +| InviteUpdateCreate +| InviteUpdateDelete +| InviteUpdateInvite +| InviteUpdateAccept +| InviteUpdateAccepted +| InviteUpdateDecline; + +export interface InviteUpdateAccept { + accept: { + term: string; + uid: Serial; + } +} + +export interface InviteUpdateInitial { + initial: Invites; +} + +export interface InviteUpdateCreate { + create: { + term: string; + }; +} + +export interface InviteUpdateDelete { + delete: { + term: string; + }; +} + +export interface InviteUpdateInvite { + invite: { + term: string; + uid: Serial; + invite: Invite; + }; +} + +export interface InviteUpdateAccepted { + accepted: { + term: string; + uid: Serial; + }; +} + +export interface InviteUpdateDecline { + 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; + recipient: PatpNoSig; + resource: Resource; + ship: PatpNoSig; + text: string; +} diff --git a/gear/lib/BigIntArrayOrderedMap.ts b/gear/lib/BigIntArrayOrderedMap.ts new file mode 100644 index 00000000..79e998c5 --- /dev/null +++ b/gear/lib/BigIntArrayOrderedMap.ts @@ -0,0 +1,152 @@ +import produce, { immerable, castDraft, setAutoFreeze, enablePatches } from 'immer'; +import bigInt, { BigInteger } from 'big-integer'; + +setAutoFreeze(false); + +enablePatches(); + +export function stringToArr(str: string) { + return str.split('/').slice(1).map((ind) => { + return bigInt(ind); + }); +} + +export function arrToString(arr: BigInteger[]) { + let string = ''; + arr.forEach((key) => { + string = string + `/${key.toString()}`; + }); + return string; +} + +function sorted(a: BigInteger[], b: BigInteger[], reversed = false) { + const getSort = sortBigIntArr(a, b); + if (reversed) { + return getSort * -1; + } else { + return getSort; + } +} + +export function sortBigIntArr(a: BigInteger[], b: BigInteger[]) { + const aLen = a.length; + const bLen = b.length; + + const aCop = a.slice(0); + const bCop = b.slice(0); + aCop.reverse(); + bCop.reverse(); + + let i = 0; + while (i < aLen && i < bLen) { + if (aCop[i].lt(bCop[i])) { + return 1; + } else if (aCop[i].gt(bCop[i])) { + return -1; + } else { + i++; + } + } + + return bLen - aLen; +} + +export class BigIntArrayOrderedMap implements Iterable<[BigInteger[], V]> { + root: Record = {} + cachedIter: [BigInteger[], V][] | null = null; + [immerable] = true; + reversed = false; + + constructor(items: [BigInteger[], V][] = [], reversed = false) { + items.forEach(([key, val]) => { + this.set(key, val); + }); + this.reversed = reversed; + } + + get size() { + return Object.keys(this.root).length; + } + + get(key: BigInteger[]) { + return this.root[arrToString(key)] ?? null; + } + + gas(items: [BigInteger[], V][]) { + return produce(this, (draft) => { + items.forEach(([key, value]) => { + draft.root[arrToString(key)] = castDraft(value); + }); + draft.generateCachedIter(); + }, + (patches) => { + // console.log(`gassed with ${JSON.stringify(patches, null, 2)}`); + }); + } + + set(key: BigInteger[], value: V) { + return produce(this, (draft) => { + draft.root[arrToString(key)] = castDraft(value); + draft.cachedIter = null; + }); + } + + clear() { + return produce(this, (draft) => { + draft.cachedIter = []; + draft.root = {}; + }); + } + + has(key: BigInteger[]) { + return arrToString(key) in this.root; + } + + delete(key: BigInteger[]) { + const result = produce(this, (draft) => { + delete draft.root[arrToString(key)]; + draft.cachedIter = null; + }); + return result; + } + + [Symbol.iterator](): IterableIterator<[BigInteger[], V]> { + let idx = 0; + const result = this.generateCachedIter(); + 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 }; + } + }; + } + + peekLargest() { + const sorted = Array.from(this); + return sorted[0] as [BigInteger[], V] | null; + } + + peekSmallest() { + const sorted = Array.from(this); + return sorted[sorted.length - 1] as [BigInteger[], V] | null; + } + + keys() { + return Array.from(this).map(([k,v]) => k); + } + + generateCachedIter() { + if(this.cachedIter) { + return [...this.cachedIter]; + } + const result = Object.keys(this.root).map((key) => { + return [stringToArr(key), this.root[key]] as [BigInteger[], V]; + }).sort(([a], [b]) => sorted(a, b, this.reversed)); + this.cachedIter = result; + return [...result]; + } +} + diff --git a/gear/lib/BigIntOrderedMap.ts b/gear/lib/BigIntOrderedMap.ts new file mode 100644 index 00000000..5e3661d1 --- /dev/null +++ b/gear/lib/BigIntOrderedMap.ts @@ -0,0 +1,117 @@ +import produce, { immerable, castDraft, setAutoFreeze, enablePatches } from 'immer'; +import bigInt, { BigInteger } from 'big-integer'; + +setAutoFreeze(false); + +enablePatches(); + +function sortBigInt(a: BigInteger, b: BigInteger) { + if (a.lt(b)) { + return 1; + } else if (a.eq(b)) { + return 0; + } else { + return -1; + } +} +export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { + root: Record = {} + cachedIter: [BigInteger, V][] | null = null; + [immerable] = true; + + constructor(items: [BigInteger, V][] = []) { + items.forEach(([key, val]) => { + this.set(key, val); + }); + } + + get size() { + if(this.cachedIter) { + return this.cachedIter.length; + } + return this.generateCachedIter().length; + } + + get(key: BigInteger) { + return this.root[key.toString()] ?? null; + } + + gas(items: [BigInteger, V][]) { + return produce(this, (draft) => { + items.forEach(([key, value]) => { + draft.root[key.toString()] = castDraft(value); + }); + draft.cachedIter = null; + }, + (patches) => { + // console.log(`gassed with ${JSON.stringify(patches, null, 2)}`); + }); + } + + set(key: BigInteger, value: V) { + return produce(this, (draft) => { + draft.root[key.toString()] = castDraft(value); + draft.cachedIter = null; + }); + } + + clear() { + return produce(this, (draft) => { + draft.cachedIter = []; + draft.root = {}; + }); + } + + has(key: BigInteger) { + return key.toString() in this.root; + } + + delete(key: BigInteger) { + const result = produce(this, (draft) => { + delete draft.root[key.toString()]; + draft.cachedIter = null; + }); + return result; + } + + [Symbol.iterator](): IterableIterator<[BigInteger, V]> { + let idx = 0; + const result = this.generateCachedIter(); + 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 }; + } + }; + } + + peekLargest() { + const sorted = Array.from(this); + return sorted[0] as [BigInteger, V] | null; + } + + peekSmallest() { + const sorted = Array.from(this); + return sorted[sorted.length - 1] as [BigInteger, V] | null; + } + + keys() { + return Array.from(this).map(([k,v]) => k); + } + + generateCachedIter() { + if(this.cachedIter) { + return [...this.cachedIter]; + } + const result = Object.keys(this.root).map((key) => { + const num = bigInt(key); + return [num, this.root[key]] as [BigInteger, V]; + }).sort(([a], [b]) => sortBigInt(a,b)); + this.cachedIter = result; + return [...result]; + } +} + diff --git a/gear/lib/index.ts b/gear/lib/index.ts new file mode 100644 index 00000000..4fed660f --- /dev/null +++ b/gear/lib/index.ts @@ -0,0 +1,2 @@ +export * from './lib'; +export * from './types'; \ No newline at end of file diff --git a/gear/lib/lib.ts b/gear/lib/lib.ts new file mode 100644 index 00000000..c57d0a25 --- /dev/null +++ b/gear/lib/lib.ts @@ -0,0 +1,259 @@ +import bigInt, { BigInteger } from "big-integer"; + +import { Resource } from "../groups/types"; +import { Post, GraphNode } from "../graph/types"; + +const DA_UNIX_EPOCH = bigInt("170141184475152167957503069145530368000"); // `@ud` ~1970.1.1 + +const DA_SECOND = bigInt("18446744073709551616"); // `@ud` ~s1 + +function chunk(arr: T[], size: number): T[][] { + let chunk: T[] = []; + let newArray = [chunk]; + + for (let i = 0;i < arr.length;i++) { + if (chunk.length < size) { + chunk.push(arr[i]) + } else { + chunk = [arr[i]] + newArray.push(chunk) + } + } + + return newArray; +} + +function dropWhile(arr: T[], pred: (x: T) => boolean): T[] { + const newArray = arr.slice(); + + for (const item of arr) { + if (pred(item)) { + newArray.shift(); + } else { + return newArray; + } + } + + return newArray; +} + +/** + * 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 { + const transform = chunk(str.split('').reverse(), 3) + .map(group => group.reverse().join('')) + .reverse() + .join('.') + return transform.replace(/^[0\.]+/g, ''); +} + +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 preSig(ship: string): string { + if (!ship) { + return ''; + } + + if (ship.trim().startsWith('~')) { + return ship.trim(); + } + + return '~'.concat(ship.trim()); +} + +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) { + 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}`; +} + + +export function uxToHex(ux: string) { + if (ux.length > 2 && ux.substr(0, 2) === '0x') { + const value = ux.substr(2).replace('.', '').padStart(6, '0'); + return value; + } + + const value = ux.replace('.', '').padStart(6, '0'); + return value; +} + +export const hexToUx = (hex: string): string => { + const nonZeroChars = dropWhile(hex.split(''), y => y === '0'); + const ux = chunk(nonZeroChars.reverse(), 4).map(x => { + return x.reverse().join(''); + }).reverse().join('.') || '0'; + + return `0x${ux}`; +}; + + +// 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; +} + +export const buntPost = (): Post => ({ + author: '', + contents: [], + hash: null, + index: '', + signatures: [], + 'time-sent': 0 +}); + +export function makeNodeMap(posts: Post[]): Record { + const nodes: Record = {}; + posts.forEach((p: Post) => { + nodes[String(p.index)] = { children: null, post: p }; + }); + return nodes; +} diff --git a/gear/lib/types.ts b/gear/lib/types.ts new file mode 100644 index 00000000..4c701ea9 --- /dev/null +++ b/gear/lib/types.ts @@ -0,0 +1,67 @@ +/** + * Martian embassy + */ + +import { BigIntOrderedMap } from "./BigIntOrderedMap"; + +// 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 = Map>; + +// name of app +export type AppName = 'chat' | 'link' | 'contacts' | 'publish' | 'graph' | 'groups'; + +export type ShipRank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn'; + +export type Action = 'poke' | 'subscribe' | 'ack' | 'unsubscribe' | 'delete'; + + +export type SetElement = S extends Set<(infer T)> ? T : never; +export type MapKey = M extends Map<(infer K), any> ? K : never; +export type MapValue = M extends Map ? V : never; + +/** + * Turns sets into arrays and maps into objects so we can send them over the wire + */ +export type Enc = + S extends Set ? + Enc>[] : + S extends Map ? + { [s: string]: Enc> } : + S extends object ? + { [K in keyof S]: Enc } : + S extends BigIntOrderedMap ? + { [index: string]: T } : + S; + +export type Mark = string; + +export interface Poke { + ship?: string; // This should be handled by the http library, but is part of the spec + app: string; + mark: Mark; + json: Action; +} + +export interface Scry { + app: string; + path: string; +} + +export interface Thread { + inputMark: string; + outputMark: string; + threadName: string; + body: Action; +} diff --git a/gear/metadata/index.ts b/gear/metadata/index.ts new file mode 100644 index 00000000..341e8171 --- /dev/null +++ b/gear/metadata/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './lib'; \ No newline at end of file diff --git a/gear/metadata/lib.ts b/gear/metadata/lib.ts new file mode 100644 index 00000000..ace81e8a --- /dev/null +++ b/gear/metadata/lib.ts @@ -0,0 +1,99 @@ +import { Path, Poke, uxToHex, PatpNoSig } from '../lib'; +import { MdAppName, Association, Metadata, MetadataUpdate, MetadataUpdateAdd, MetadataUpdateRemove, MetadataEditField, MetadataUpdateEdit } from './types'; + +export const METADATA_UPDATE_VERSION = 2; + +export const metadataAction = (data: T, version: number = METADATA_UPDATE_VERSION): Poke => ({ + app: 'metadata-push-hook', + mark: `metadata-update-${version}`, + json: data +}); + +export const add = ( + ship: PatpNoSig, + appName: MdAppName, + resource: Path, + group: Path, + title: string, + description: string, + dateCreated: string, + color: string, + moduleName: string +): Poke => metadataAction({ + add: { + group, + resource: { + resource, + 'app-name': appName + }, + metadata: { + title, + description, + color, + 'date-created': dateCreated, + creator: `~${ship}`, + config: { graph: moduleName }, + picture: '', + hidden: false, + preview: false, + vip: '' + } + } +}); + +export { add as metadataAdd }; + +export const remove = ( + appName: MdAppName, + resource: string, + group: string +): Poke => metadataAction({ + remove: { + group, + resource: { + resource, + 'app-name': appName + } + } +}); + +export { remove as metadataRemove }; + +export const edit = ( + association: Association, + edit: MetadataEditField +): Poke => metadataAction({ + edit: { + group: association.group, + resource: { + resource: association.resource, + 'app-name': association['app-name'] + }, + edit + } +}); + +export { edit as metadataEdit }; + +/** + * @deprecated use {@link edit} instead + */ +export const update = ( + association: Association, + newMetadata: Partial +): Poke => { + const metadata = { ...association.metadata, ...newMetadata }; + metadata.color = uxToHex(metadata.color); + return metadataAction({ + add: { + group: association.group, + resource: { + resource: association.resource, + 'app-name': association['app-name'] + }, + metadata + } + }); +}; + +export { update as metadataUpdate }; diff --git a/gear/metadata/types.ts b/gear/metadata/types.ts new file mode 100644 index 00000000..828c83b1 --- /dev/null +++ b/gear/metadata/types.ts @@ -0,0 +1,101 @@ +import { Path, Patp } from '../lib'; + +export type MdAppName = 'groups' | 'graph'; + +export type MetadataUpdate = + MetadataUpdateInitial +| MetadataUpdateAdd +| MetadataUpdateUpdate +| MetadataUpdateRemove +| MetadataUpdateEdit; + +export interface MetadataUpdateInitial { + associations: ResourceAssociations; +} + +export type ResourceAssociations = { + [p in Path]: Association; +} + +export type MetadataUpdateAdd = { + add: AssociationPoke; +} + +export type MetadataUpdateUpdate = { + update: AssociationPoke; +} + +export interface MetadataUpdateEdit { + edit: { + resource: MdResource; + group: string; + edit: MetadataEditField; + } +} + +export type MetadataEditField = Partial>; + +export type MetadataUpdateRemove = { + remove: { + resource: MdResource; + group: string; + } +} + +export interface MdResource { + resource: string; + 'app-name': MdAppName; +} + +export interface MetadataUpdatePreview { + group: string; + channels: Associations; + 'channel-count': number; + members: number; + metadata: Metadata; +} + +export type Associations = { + groups: AppAssociations + graph: AppAssociations; +} + +export type AppAssociations = { + [p in Path]: Association; +} + +export type Association = MdResource & { + group: Path; + metadata: Metadata; +}; + +export interface AssociationPoke { + group: Path; + resource: MdResource; + metadata: Metadata; +} + +export interface Metadata { + color: string; + creator: Patp; + 'date-created': string; + description: string; + title: string; + config: C; + hidden: boolean; + picture: string; + preview: boolean; + vip: PermVariation; +} + +export type MetadataConfig = GroupConfig | GraphConfig; + +export interface GraphConfig { + graph: string; +} + +export interface GroupConfig { + group: undefined | {} | MdResource; +} + +export type PermVariation = '' | ' ' | 'reader-comments' | 'member-metadata' | 'host-feed' | 'admin-feed'; diff --git a/gear/settings/index.ts b/gear/settings/index.ts new file mode 100644 index 00000000..341e8171 --- /dev/null +++ b/gear/settings/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './lib'; \ No newline at end of file diff --git a/gear/settings/lib.ts b/gear/settings/lib.ts new file mode 100644 index 00000000..94fca9d5 --- /dev/null +++ b/gear/settings/lib.ts @@ -0,0 +1,78 @@ +import { Poke, Scry } from '../lib'; +import { PutBucket, Key, Bucket, DelBucket, Value, PutEntry, DelEntry, SettingsUpdate } from './types'; + +export const action = (data: T): Poke => ({ + app: 'settings-store', + mark: 'settings-event', + json: data +}); + +export const putBucket = ( + desk: string, + key: Key, + bucket: Bucket +): Poke => action({ + 'put-bucket': { + desk, + 'bucket-key': key, + 'bucket': bucket + } +}); + +export const delBucket = ( + desk: string, + key: Key +): Poke => action({ + 'del-bucket': { + desk, + 'bucket-key': key + } +}); + +export const putEntry = ( + desk: string, + bucket: Key, + key: Key, + value: Value +): Poke => action({ + 'put-entry': { + desk, + 'bucket-key': bucket, + 'entry-key': key, + value: value + } +}); + +export const delEntry = ( + desk: string, + bucket: Key, + key: Key +): Poke => action({ + 'del-entry': { + desk, + 'bucket-key': bucket, + 'entry-key': key + } +}); + +export const getAll: Scry = { + app: 'settings-store', + path: '/all' +}; + +export const getBucket = (desk: string, bucket: string) => ({ + app: 'settings-store', + path: `/bucket/${bucket}` +}); + +export const getEntry = (desk: string, bucket: string, entry: string) => ({ + app: 'settings-store', + path: `/entry/${desk}/${bucket}/${entry}` +}); + +export const getDeskSettings = (desk: string) => ({ + app: 'settings-store', + path: `/desk/${desk}` +}); + +export * from './types'; diff --git a/gear/settings/types.ts b/gear/settings/types.ts new file mode 100644 index 00000000..af02c70b --- /dev/null +++ b/gear/settings/types.ts @@ -0,0 +1,64 @@ +export type Key = string; +export type Value = string | string[] | boolean | number; +export type Bucket = { [key: string]: Value; }; +export type DeskSettings = { [bucket: string]: Bucket; }; +export type Settings = { [desk: string]: Settings; } + +export interface PutBucket { + 'put-bucket': { + desk: string; + 'bucket-key': Key; + 'bucket': Bucket; + }; +} + +export interface DelBucket { + 'del-bucket': { + desk: string; + 'bucket-key': Key; + }; +} + +export interface PutEntry { + 'put-entry': { + 'bucket-key': Key; + 'entry-key': Key; + 'value'?: Value; + }; +} + +export interface DelEntry { + 'del-entry': { + desk: string; + 'bucket-key': Key; + 'entry-key': Key; + }; +} + +export interface AllData { + 'all': Settings; +} + +export interface DeskData { + desk: DeskSettings; +} + +export interface BucketData { + 'bucket': Bucket; +} + +export interface EntryData { + 'entry': Value; +} + +export type SettingsUpdate = + | PutBucket + | DelBucket + | PutEntry + | DelEntry; + +export type SettingsData = + | AllData + | BucketData + | EntryData + | DeskData; diff --git a/gear/storage/index.ts b/gear/storage/index.ts new file mode 100644 index 00000000..4fed660f --- /dev/null +++ b/gear/storage/index.ts @@ -0,0 +1,2 @@ +export * from './lib'; +export * from './types'; \ No newline at end of file diff --git a/gear/storage/lib.ts b/gear/storage/lib.ts new file mode 100644 index 00000000..165789ea --- /dev/null +++ b/gear/storage/lib.ts @@ -0,0 +1,47 @@ +import { Poke } from '../lib/types'; +import { StorageUpdate, StorageUpdateCurrentBucket, StorageUpdateAddBucket, StorageUpdateRemoveBucket, StorageUpdateEndpoint, StorageUpdateAccessKeyId, StorageUpdateSecretAccessKey } from './types'; + +const s3Action = ( + data: any +): Poke => ({ + app: 's3-store', + mark: 's3-action', + json: data +}); + +export const setCurrentBucket = ( + bucket: string +): Poke => s3Action({ + 'set-current-bucket': bucket +}); + +export const addBucket = ( + bucket: string +): Poke => s3Action({ + 'add-bucket': bucket +}); + +export const removeBucket = ( + bucket: string +): Poke => s3Action({ + 'remove-bucket': bucket +}); + +export const setEndpoint = ( + endpoint: string +): Poke => s3Action({ + 'set-endpoint': endpoint +}); + +export const setAccessKeyId = ( + accessKeyId: string +): Poke => s3Action({ + 'set-access-key-id': accessKeyId +}); + +export const setSecretAccessKey = ( + secretAccessKey: string +): Poke => s3Action({ + 'set-secret-access-key': secretAccessKey +}); + diff --git a/gear/storage/types.ts b/gear/storage/types.ts new file mode 100644 index 00000000..6c5cb93e --- /dev/null +++ b/gear/storage/types.ts @@ -0,0 +1,60 @@ +export interface StorageCredentials { + endpoint: string; + accessKeyId: string; + secretAccessKey: string; +} + +export interface StorageConfiguration { + buckets: Set; + currentBucket: string; +} + +export interface StorageState { + configuration: StorageConfiguration; + credentials: StorageCredentials | null; +} + +export interface StorageUpdateCredentials { + credentials: StorageCredentials; +} + +export interface StorageUpdateConfiguration { + configuration: { + buckets: string[]; + currentBucket: string; + } +} + +export interface StorageUpdateCurrentBucket { + setCurrentBucket: string; +} + +export interface StorageUpdateAddBucket { + addBucket: string; +} + +export interface StorageUpdateRemoveBucket { + removeBucket: string; +} + +export interface StorageUpdateEndpoint { + setEndpoint: string; +} + +export interface StorageUpdateAccessKeyId { + setAccessKeyId: string; +} + +export interface StorageUpdateSecretAccessKey { + setSecretAccessKey: string; +} + +export type StorageUpdate = + StorageUpdateCredentials +| StorageUpdateConfiguration +| StorageUpdateCurrentBucket +| StorageUpdateAddBucket +| StorageUpdateRemoveBucket +| StorageUpdateEndpoint +| StorageUpdateAccessKeyId +| StorageUpdateSecretAccessKey;