mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 08:32:39 +03:00
Merge remote-tracking branch 'origin/release/next-js'
This commit is contained in:
commit
8c6f43f10e
10
.github/workflows/merge.yml
vendored
10
.github/workflows/merge.yml
vendored
@ -15,3 +15,13 @@ jobs:
|
||||
target_branch: release/next-js
|
||||
github_token: ${{ secrets.JANEWAY_BOT_TOKEN }}
|
||||
|
||||
merge-to-group-timer:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Merge master to ops/group-timer"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: devmasx/merge-branch@v1.3.1
|
||||
with:
|
||||
type: now
|
||||
target_branch: ops/group-timer
|
||||
github_token: ${{ secrets.JANEWAY_BOT_TOKEN }}
|
||||
|
20
.github/workflows/ops-group-timer.yml
vendored
Normal file
20
.github/workflows/ops-group-timer.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: group-timer
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'ops/group-timer'
|
||||
jobs:
|
||||
glob:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Create and deploy a glob to ~difmex-passed"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: true
|
||||
- uses: ./.github/actions/glob
|
||||
with:
|
||||
ship: 'difmex-passed'
|
||||
credentials: ${{ secrets.JANEWAY_SERVICE_KEY }}
|
||||
ssh-sec-key: ${{ secrets.JANEWAY_SSH_SEC_KEY }}
|
||||
ssh-pub-key: ${{ secrets.JANEWAY_SSH_PUB_KEY }}
|
||||
|
@ -52,16 +52,13 @@ const tokenizeMessage = (text) => {
|
||||
}
|
||||
messages.push({ url: str });
|
||||
message = [];
|
||||
} else if (urbitOb.isValidPatp(str.replace(/[^a-z\-\~]/g, '')) && !isInCodeBlock) {
|
||||
} else if(urbitOb.isValidPatp(str) && !isInCodeBlock) {
|
||||
if (message.length > 0) {
|
||||
// If we're in the middle of a message, add it to the stack and reset
|
||||
messages.push({ text: message.join(' ') });
|
||||
message = [];
|
||||
}
|
||||
messages.push({ mention: str.replace(/[^a-z\-\~]/g, '') });
|
||||
if (str.replace(/[a-z\-\~]/g, '').length > 0) {
|
||||
messages.push({ text: str.replace(/[a-z\-\~]/g, '') });
|
||||
}
|
||||
messages.push({ mention: str });
|
||||
message = [];
|
||||
|
||||
} else {
|
||||
|
@ -61,6 +61,7 @@ export default class GroupReducer<S extends GroupState> {
|
||||
reduce(json: Cage, state: S) {
|
||||
const data = json.groupUpdate;
|
||||
if (data) {
|
||||
console.log(data);
|
||||
this.initial(data, state);
|
||||
this.addMembers(data, state);
|
||||
this.addTag(data, state);
|
||||
@ -116,6 +117,12 @@ export default class GroupReducer<S extends GroupState> {
|
||||
const resourcePath = resourceAsPath(resource);
|
||||
for (const member of ships) {
|
||||
state.groups[resourcePath].members.add(member);
|
||||
if (
|
||||
'invite' in state.groups[resourcePath].policy &&
|
||||
state.groups[resourcePath].policy.invite.pending.has(member)
|
||||
) {
|
||||
state.groups[resourcePath].policy.invite.pending.delete(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import { Helmet } from 'react-helmet';
|
||||
import useLocalState from "~/logic/state/local";
|
||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||
import { useQuery } from "~/logic/lib/useQuery";
|
||||
import {
|
||||
import {
|
||||
hasTutorialGroup,
|
||||
TUTORIAL_GROUP,
|
||||
TUTORIAL_HOST,
|
||||
@ -46,17 +46,14 @@ export default function LaunchApp(props) {
|
||||
const hashBox = (
|
||||
<Box
|
||||
position={["relative", "absolute"]}
|
||||
fontFamily="mono"
|
||||
left="0"
|
||||
bottom="0"
|
||||
color="washedGray"
|
||||
bg="white"
|
||||
backgroundColor="white"
|
||||
ml={3}
|
||||
mb={3}
|
||||
borderRadius={2}
|
||||
overflow='hidden'
|
||||
fontSize={0}
|
||||
p={2}
|
||||
boxShadow="0 0 0px 1px inset"
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
writeText(props.baseHash);
|
||||
@ -66,7 +63,9 @@ export default function LaunchApp(props) {
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<Text color="gray">{hashText || props.baseHash}</Text>
|
||||
<Box backgroundColor="washedGray" p={2}>
|
||||
<Text mono bold>{hashText || props.baseHash}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@ -87,7 +86,7 @@ export default function LaunchApp(props) {
|
||||
const waiter = useWaitForProps(props);
|
||||
|
||||
const { modal, showModal } = useModal({
|
||||
position: 'relative',
|
||||
position: 'relative',
|
||||
maxWidth: '350px',
|
||||
modal: (dismiss) => {
|
||||
const onDismiss = (e) => {
|
||||
@ -120,7 +119,7 @@ export default function LaunchApp(props) {
|
||||
</Box>
|
||||
<Text lineHeight="tall" fontWeight="medium">Welcome</Text>
|
||||
<Text lineHeight="tall">
|
||||
You have been invited to use Landscape, an interface to chat
|
||||
You have been invited to use Landscape, an interface to chat
|
||||
and interact with communities
|
||||
<br />
|
||||
Would you like a tour of Landscape?
|
||||
@ -183,21 +182,22 @@ export default function LaunchApp(props) {
|
||||
/>
|
||||
<ModalButton
|
||||
icon="Plus"
|
||||
bg="blue"
|
||||
color="#fff"
|
||||
text="Join a Group"
|
||||
bg="washedGray"
|
||||
color="black"
|
||||
text="New Group"
|
||||
style={{ gridColumnStart: 1 }}
|
||||
>
|
||||
<JoinGroup {...props} />
|
||||
</ModalButton>
|
||||
<ModalButton
|
||||
icon="CreateGroup"
|
||||
bg="green"
|
||||
color="#fff"
|
||||
text="Create Group"
|
||||
>
|
||||
<NewGroup {...props} />
|
||||
</ModalButton>
|
||||
<ModalButton
|
||||
icon="Boot"
|
||||
bg="washedGray"
|
||||
color="black"
|
||||
text="Join Group"
|
||||
>
|
||||
<JoinGroup {...props} />
|
||||
</ModalButton>
|
||||
|
||||
<Groups unreads={props.unreads} groups={props.groups} associations={props.associations} />
|
||||
</Box>
|
||||
<Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box>
|
||||
|
@ -85,12 +85,12 @@ function Group(props: GroupProps) {
|
||||
<Col height="100%" justifyContent="space-between">
|
||||
<Text>{title}</Text>
|
||||
<Col>
|
||||
{unreads > 0 &&
|
||||
(<Text gray>{unreads} unread{unreads !== 1 && 's'} </Text>)
|
||||
}
|
||||
{updates > 0 &&
|
||||
(<Text mt="1" color="blue">{updates} update{updates !== 1 && 's'} </Text>)
|
||||
}
|
||||
{unreads > 0 &&
|
||||
(<Text color="lightGray">{unreads}</Text>)
|
||||
}
|
||||
</Col>
|
||||
|
||||
</Col>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Icon, Text } from '@tlon/indigo-react';
|
||||
import { Row, Button, Icon, Text } from '@tlon/indigo-react';
|
||||
import { useModal } from '~/logic/lib/useModal';
|
||||
|
||||
const ModalButton = (props) => {
|
||||
@ -12,19 +12,19 @@ const ModalButton = (props) => {
|
||||
<Button
|
||||
onClick={showModal}
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
cursor='pointer'
|
||||
bg={bg}
|
||||
p={2}
|
||||
bg="white"
|
||||
overflow='hidden'
|
||||
border={0}
|
||||
p={0}
|
||||
borderRadius={2}
|
||||
boxShadow='0 0 0px 1px inset'
|
||||
color='scales.black20'
|
||||
{...rest}
|
||||
>
|
||||
<Row bg={bg} p={2} width='100%' justifyContent="start" alignItems="center">
|
||||
<Icon icon={props.icon} mr={2} color={color}></Icon>
|
||||
<Text color={color} whiteSpace='nowrap'>
|
||||
<Text color={color} fontWeight="medium" whiteSpace='nowrap'>
|
||||
{props.text}
|
||||
</Text>
|
||||
</Text></Row>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
@ -67,6 +67,7 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
||||
const size = node.children ? node.children.size : 0;
|
||||
const contents = node.post.contents;
|
||||
const hostname = URLparser.exec(contents[1].url) ? URLparser.exec(contents[1].url)[4] : null;
|
||||
const href = URLparser.exec(contents[1].url) ? contents[1].url : `http://${contents[1].url}`
|
||||
|
||||
const baseUrl = props.baseUrl || `/~404/${resource}`;
|
||||
|
||||
@ -120,7 +121,7 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
||||
<RemoteContent
|
||||
ref={r => { remoteRef.current = r }}
|
||||
renderUrl={false}
|
||||
url={contents[1].url}
|
||||
url={href}
|
||||
text={contents[0].text}
|
||||
unfold={true}
|
||||
onLoad={onMeasure}
|
||||
@ -145,7 +146,7 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
||||
}}
|
||||
/>
|
||||
<Text color="gray" p={2} flexShrink={0}>
|
||||
<Anchor target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }} href={contents[1].url}>
|
||||
<Anchor target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }} href={href}>
|
||||
<Box display='flex'>
|
||||
<Icon icon='ArrowExternal' mr={1} />{hostname}
|
||||
</Box>
|
||||
|
@ -5,7 +5,7 @@ import { Box } from '@tlon/indigo-react';
|
||||
export function Body(
|
||||
props: { children: ReactNode } & Parameters<typeof Box>[0]
|
||||
) {
|
||||
const { children, ...boxProps } = props;
|
||||
const { children, border, ...boxProps } = props;
|
||||
return (
|
||||
<Box fontSize={0} px={[0, 3]} pb={[0, 3]} height="100%" width="100%">
|
||||
<Box
|
||||
@ -13,11 +13,11 @@ export function Body(
|
||||
height="100%"
|
||||
width="100%"
|
||||
borderRadius={2}
|
||||
border={[0, 1]}
|
||||
border={border ? border : [0, 1]}
|
||||
borderColor={['washedGray', 'washedGray']}
|
||||
{...boxProps}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
@ -41,6 +41,7 @@ const Candidate = ({ title, selected, onClick }): ReactElement => (
|
||||
<CandidateBox
|
||||
onClick={onClick}
|
||||
selected={selected}
|
||||
cursor="pointer"
|
||||
borderColor="washedGray"
|
||||
color="black"
|
||||
fontSize={0}
|
||||
|
@ -8,10 +8,10 @@ interface LoadingProps {
|
||||
}
|
||||
export function Loading({ text }: LoadingProps) {
|
||||
return (
|
||||
<Body>
|
||||
<Body border="0">
|
||||
<Center height="100%">
|
||||
<LoadingSpinner />
|
||||
{Boolean(text) && <Text>{text}</Text>}
|
||||
{Boolean(text) && <Text ml={4}>{text}</Text>}
|
||||
</Center>
|
||||
</Body>
|
||||
);
|
||||
|
@ -78,6 +78,7 @@ const Candidate = ({ title, detail, selected, onClick }): ReactElement => (
|
||||
bg="white"
|
||||
color="black"
|
||||
fontSize={0}
|
||||
cursor="pointer"
|
||||
p={1}
|
||||
width="100%"
|
||||
>
|
||||
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
import { Box, Row, Icon, Text } from '@tlon/indigo-react';
|
||||
import defaultApps from '~/logic/lib/default-apps';
|
||||
import Sigil from '~/logic/lib/sigil';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import { uxToHex, cite } from '~/logic/lib/util';
|
||||
|
||||
export class OmniboxResult extends Component {
|
||||
constructor(props) {
|
||||
@ -87,25 +87,29 @@ export class OmniboxResult extends Component {
|
||||
}
|
||||
onClick={navigate}
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
ref={this.result}
|
||||
>
|
||||
<Box display="flex" verticalAlign="middle" maxWidth="60%" flexShrink={0}>
|
||||
{graphic}
|
||||
<Text
|
||||
display="inline-block"
|
||||
verticalAlign="middle"
|
||||
mono={(icon == 'profile' && text.startsWith('~'))}
|
||||
color={this.state.hovered || selected === link ? 'white' : 'black'}
|
||||
maxWidth="60%"
|
||||
style={{ flexShrink: 0 }}
|
||||
mr='1'
|
||||
>
|
||||
{text}
|
||||
{text.startsWith("~") ? cite(text) : text}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text pr='2'
|
||||
display="inline-block"
|
||||
verticalAlign="middle"
|
||||
color={this.state.hovered || selected === link ? 'white' : 'black'}
|
||||
width='100%'
|
||||
minWidth={0}
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="pre"
|
||||
overflow="hidden"
|
||||
maxWidth="40%"
|
||||
textAlign='right'
|
||||
>
|
||||
{subtext}
|
||||
|
@ -151,7 +151,7 @@ export function GraphPermissions(props: GraphPermissionsProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const schema = formSchema(Array.from(group.members));
|
||||
const schema = formSchema(Array.from(group?.members ?? []));
|
||||
|
||||
return (
|
||||
<Formik
|
||||
|
@ -22,6 +22,7 @@ import { isChannelAdmin, isHost } from '~/logic/lib/group';
|
||||
|
||||
interface ChannelPopoverRoutesProps {
|
||||
baseUrl: string;
|
||||
rootUrl: string;
|
||||
association: Association;
|
||||
group: Group;
|
||||
groups: Groups;
|
||||
@ -51,8 +52,8 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
||||
};
|
||||
const handleArchive = async () => {
|
||||
const [,,,name] = association.resource.split('/');
|
||||
await api.graph.deleteGraph(name);
|
||||
history.push(props.baseUrl);
|
||||
api.graph.deleteGraph(name);
|
||||
return history.push(props.rootUrl);
|
||||
};
|
||||
|
||||
const canAdmin = isChannelAdmin(group, association.resource);
|
||||
|
@ -206,7 +206,7 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
resource={groupAssociation.group}
|
||||
/>;
|
||||
} else {
|
||||
summary = (<Box p="4"><Text fontSize="0" color='gray'>
|
||||
summary = (<Box p="4"><Text color='gray'>
|
||||
Create or select a channel to get started
|
||||
</Text></Box>);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { ReactElement, ReactNode } from 'react';
|
||||
import { Icon, Box, Col, Text } from '@tlon/indigo-react';
|
||||
import { Icon, Box, Col, Row, Text } from '@tlon/indigo-react';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import urbitOb from 'urbit-ob';
|
||||
@ -12,7 +12,7 @@ import GlobalApi from '~/logic/api/global';
|
||||
import { isWriter } from '~/logic/lib/group';
|
||||
import { getItemTitle } from '~/logic/lib/util';
|
||||
|
||||
const TruncatedBox = styled(Box)`
|
||||
const TruncatedText = styled(RichText)`
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@ -46,11 +46,13 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
? getItemTitle(association)
|
||||
: association?.metadata?.title;
|
||||
|
||||
let recipient = false;
|
||||
let recipient = "";
|
||||
|
||||
if (urbitOb.isValidPatp(title)) {
|
||||
recipient = title;
|
||||
title = (props.contacts?.[title]?.nickname) ? props.contacts[title].nickname : title;
|
||||
} else {
|
||||
recipient = Array.from(group.members).map(e => `~${e}`).join(", ")
|
||||
}
|
||||
|
||||
const [, , ship, resource] = rid.split('/');
|
||||
@ -88,7 +90,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
>
|
||||
<Link to={`/~landscape${workspace}`}> {'<- Back'}</Link>
|
||||
</Box>
|
||||
<Box px={1} mr={2} minWidth={0} display="flex">
|
||||
<Box px={1} mr={2} minWidth={0} display="flex" flexShrink={0}>
|
||||
<Text
|
||||
mono={urbitOb.isValidPatp(title)}
|
||||
fontSize='2'
|
||||
@ -99,28 +101,30 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
overflow="hidden"
|
||||
whiteSpace="pre"
|
||||
minWidth={0}
|
||||
flexShrink={0}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
<TruncatedBox
|
||||
display={['none', 'block']}
|
||||
<Row
|
||||
display={['none', 'flex']}
|
||||
verticalAlign="middle"
|
||||
maxWidth='60%'
|
||||
flexShrink={1}
|
||||
minWidth={0}
|
||||
title={association?.metadata?.description}
|
||||
color="gray"
|
||||
>
|
||||
<RichText
|
||||
<TruncatedText
|
||||
display={(workspace === '/messages' && (urbitOb.isValidPatp(title))) ? 'none' : 'inline-block'}
|
||||
mono={(workspace === '/messages' && !(urbitOb.isValidPatp(title)))}
|
||||
color="gray"
|
||||
minWidth={0}
|
||||
width="100%"
|
||||
mb="0"
|
||||
disableRemoteContent
|
||||
>
|
||||
{(workspace === '/messages') ? recipient : association?.metadata?.description}
|
||||
</RichText>
|
||||
</TruncatedBox>
|
||||
</TruncatedText>
|
||||
</Row>
|
||||
<Box flexGrow={1} />
|
||||
{canWrite && (
|
||||
<Link to={resourcePath('/new')} style={{ flexShrink: '0' }}>
|
||||
|
@ -1,50 +1,2 @@
|
||||
import { Enc, Path, Patp, Poke } from "..";
|
||||
import {
|
||||
Contact,
|
||||
ContactUpdateAdd,
|
||||
ContactUpdateEdit,
|
||||
ContactUpdateRemove,
|
||||
ContactEditField,
|
||||
ContactShare,
|
||||
ContactUpdate,
|
||||
} from "./index.d";
|
||||
|
||||
export const storeAction = <T extends ContactUpdate>(data: T): Poke<T> => ({
|
||||
app: "contact-store",
|
||||
mark: "contact-action",
|
||||
json: data,
|
||||
});
|
||||
|
||||
export const add = (ship: Patp, contact: Contact): Poke<ContactUpdateAdd> => {
|
||||
contact["last-updated"] = Date.now();
|
||||
|
||||
return storeAction({
|
||||
add: { ship, contact },
|
||||
});
|
||||
};
|
||||
|
||||
export const remove = (ship: Patp): Poke<ContactUpdateRemove> =>
|
||||
storeAction({
|
||||
remove: { ship },
|
||||
});
|
||||
|
||||
export const share = (recipient: Patp): Poke<ContactShare> => ({
|
||||
app: "contact-push-hook",
|
||||
mark: "contact-action",
|
||||
json: { share: recipient },
|
||||
});
|
||||
|
||||
export const edit = (
|
||||
path: Path,
|
||||
ship: Patp,
|
||||
editField: ContactEditField
|
||||
): Poke<ContactUpdateEdit> =>
|
||||
storeAction({
|
||||
edit: {
|
||||
path,
|
||||
ship,
|
||||
"edit-field": editField,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
export * from './types';
|
||||
export * from './lib';
|
94
pkg/npm/api/contacts/lib.ts
Normal file
94
pkg/npm/api/contacts/lib.ts
Normal file
@ -0,0 +1,94 @@
|
||||
|
||||
import { Path, Patp, Poke, resourceAsPath } from "../lib";
|
||||
import {
|
||||
Contact,
|
||||
ContactUpdateAdd,
|
||||
ContactUpdateEdit,
|
||||
ContactUpdateRemove,
|
||||
ContactEditField,
|
||||
ContactShare,
|
||||
ContactUpdate,
|
||||
ContactUpdateAllowShips,
|
||||
ContactUpdateAllowGroup,
|
||||
ContactUpdateSetPublic,
|
||||
} from "./types";
|
||||
|
||||
const storeAction = <T extends ContactUpdate>(data: T): Poke<T> => ({
|
||||
app: "contact-store",
|
||||
mark: "contact-action",
|
||||
json: data,
|
||||
});
|
||||
|
||||
export { storeAction as contactStoreAction };
|
||||
|
||||
export const addContact = (ship: Patp, contact: Contact): Poke<ContactUpdateAdd> => {
|
||||
contact["last-updated"] = Date.now();
|
||||
|
||||
return storeAction({
|
||||
add: { ship, contact },
|
||||
});
|
||||
};
|
||||
|
||||
export const removeContact = (ship: Patp): Poke<ContactUpdateRemove> =>
|
||||
storeAction({
|
||||
remove: { ship },
|
||||
});
|
||||
|
||||
export const share = (recipient: Patp): Poke<ContactShare> => ({
|
||||
app: "contact-push-hook",
|
||||
mark: "contact-action",
|
||||
json: { share: recipient },
|
||||
});
|
||||
|
||||
export const editContact = (
|
||||
ship: Patp,
|
||||
editField: ContactEditField
|
||||
): Poke<ContactUpdateEdit> =>
|
||||
storeAction({
|
||||
edit: {
|
||||
ship,
|
||||
"edit-field": editField,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
export const allowShips = (
|
||||
ships: Patp[]
|
||||
): Poke<ContactUpdateAllowShips> => storeAction({
|
||||
allow: {
|
||||
ships
|
||||
}
|
||||
});
|
||||
|
||||
export const allowGroup = (
|
||||
ship: string,
|
||||
name: string
|
||||
): Poke<ContactUpdateAllowGroup> => storeAction({
|
||||
allow: {
|
||||
group: resourceAsPath({ ship, name })
|
||||
}
|
||||
});
|
||||
|
||||
export const setPublic = (
|
||||
setPublic: any
|
||||
): Poke<ContactUpdateSetPublic> => {
|
||||
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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -1,47 +1,49 @@
|
||||
import { Path, Patp } from "..";
|
||||
import {Resource} from "../groups/update.d";
|
||||
import { Resource } from "../groups";
|
||||
|
||||
export type ContactUpdate =
|
||||
| ContactUpdateAdd
|
||||
| ContactUpdateRemove
|
||||
| ContactUpdateEdit
|
||||
| ContactUpdateInitial
|
||||
| ContactUpdateAllowGroup
|
||||
| ContactUpdateAllowShips
|
||||
| ContactUpdateSetPublic;
|
||||
|
||||
interface ContactUpdateAdd {
|
||||
export interface ContactUpdateAdd {
|
||||
add: {
|
||||
ship: Patp;
|
||||
contact: Contact;
|
||||
};
|
||||
}
|
||||
|
||||
interface ContactUpdateRemove {
|
||||
export interface ContactUpdateRemove {
|
||||
remove: {
|
||||
ship: Patp;
|
||||
};
|
||||
}
|
||||
|
||||
interface ContactUpdateEdit {
|
||||
export interface ContactUpdateEdit {
|
||||
edit: {
|
||||
path: Path;
|
||||
ship: Patp;
|
||||
"edit-field": ContactEditField;
|
||||
timestamp: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ContactUpdateAllowShips {
|
||||
export interface ContactUpdateAllowShips {
|
||||
allow: {
|
||||
ships: Patp[];
|
||||
}
|
||||
}
|
||||
|
||||
interface ContactUpdateAllowGroup {
|
||||
export interface ContactUpdateAllowGroup {
|
||||
allow: {
|
||||
group: Path;
|
||||
}
|
||||
}
|
||||
|
||||
interface ContactUpdateSetPublic {
|
||||
export interface ContactUpdateSetPublic {
|
||||
'set-public': boolean;
|
||||
}
|
||||
|
||||
@ -49,7 +51,7 @@ export interface ContactShare {
|
||||
share: Patp;
|
||||
}
|
||||
|
||||
interface ContactUpdateInitial {
|
||||
export interface ContactUpdateInitial {
|
||||
initial: Rolodex;
|
||||
}
|
||||
|
||||
@ -57,6 +59,8 @@ export type Rolodex = {
|
||||
[p in Patp]: Contact;
|
||||
};
|
||||
|
||||
export type Contacts = Rolodex;
|
||||
|
||||
export interface Contact {
|
||||
nickname: string;
|
||||
bio: string;
|
@ -1,370 +1,2 @@
|
||||
import _ from 'lodash';
|
||||
import { PatpNoSig, Patp, Poke, Thread, Path, Enc } from '..';
|
||||
import { Content, GraphNode, Post, GraphNodePoke, GraphChildrenPoke } from './index.d';
|
||||
import { deSig, unixToDa } from '../lib/util';
|
||||
import { makeResource, resourceFromPath } from '../groups/index';
|
||||
import { GroupPolicy } from '../groups/update.d';
|
||||
|
||||
export const createBlankNodeWithChildPost = (
|
||||
ship: PatpNoSig,
|
||||
parentIndex: string = '',
|
||||
childIndex: string = '',
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const storeAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'graph-store',
|
||||
mark: 'graph-update',
|
||||
json: data
|
||||
});
|
||||
|
||||
export { storeAction as graphStoreAction };
|
||||
|
||||
const viewAction = <T>(threadName: string, action: T): Thread<T> => ({
|
||||
inputMark: 'graph-view-action',
|
||||
outputMark: 'json',
|
||||
threadName,
|
||||
body: action
|
||||
});
|
||||
|
||||
export { viewAction as graphViewAction };
|
||||
|
||||
const hookAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'graph-push-hook',
|
||||
mark: 'graph-update',
|
||||
json: data
|
||||
});
|
||||
|
||||
export { hookAction as graphHookAction };
|
||||
|
||||
|
||||
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 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
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
export * from './lib';
|
||||
export * from './types';
|
271
pkg/npm/api/graph/lib.ts
Normal file
271
pkg/npm/api/graph/lib.ts
Normal file
@ -0,0 +1,271 @@
|
||||
import _ from 'lodash';
|
||||
import { GroupPolicy, makeResource, resourceFromPath } from '../groups';
|
||||
|
||||
import { deSig, unixToDa } from '../lib';
|
||||
import { Enc, Path, Patp, PatpNoSig, Poke, Thread } from '../lib/types';
|
||||
import { Content, GraphChildrenPoke, GraphNode, GraphNodePoke, Post } from './types';
|
||||
|
||||
export const createBlankNodeWithChildPost = (
|
||||
ship: PatpNoSig,
|
||||
parentIndex: string = '',
|
||||
childIndex: string = '',
|
||||
contents: Content[]
|
||||
): any => { // TODO should be GraphNode
|
||||
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): void => {
|
||||
_.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;
|
||||
}
|
||||
|
||||
const storeAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'graph-store',
|
||||
mark: 'graph-update',
|
||||
json: data
|
||||
});
|
||||
|
||||
export { storeAction as graphStoreAction };
|
||||
|
||||
const viewAction = <T>(threadName: string, action: T): Thread<T> => ({
|
||||
inputMark: 'graph-view-action',
|
||||
outputMark: 'json',
|
||||
threadName,
|
||||
body: action
|
||||
});
|
||||
|
||||
export { viewAction as graphViewAction };
|
||||
|
||||
const hookAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'graph-push-hook',
|
||||
mark: 'graph-update',
|
||||
json: data
|
||||
});
|
||||
|
||||
export { hookAction as graphHookAction };
|
||||
|
||||
|
||||
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> => 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<any> => viewAction('graph-join', {
|
||||
join: {
|
||||
resource: makeResource(ship, name),
|
||||
ship,
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteGraph = (
|
||||
ship: PatpNoSig,
|
||||
name: string
|
||||
): Thread<any> => viewAction('graph-delete', {
|
||||
"delete": {
|
||||
resource: makeResource(`~${ship}`, name)
|
||||
}
|
||||
});
|
||||
|
||||
export const leaveGraph = (
|
||||
ship: Patp,
|
||||
name: string
|
||||
): Thread<any> => viewAction('graph-leave', {
|
||||
"leave": {
|
||||
resource: makeResource(ship, name)
|
||||
}
|
||||
});
|
||||
|
||||
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 addNodes = (
|
||||
ship: Patp,
|
||||
name: string,
|
||||
nodes: Object
|
||||
): Poke<any> => {
|
||||
const action = {
|
||||
'add-nodes': {
|
||||
resource: { ship, name },
|
||||
nodes
|
||||
}
|
||||
};
|
||||
|
||||
return hookAction(action);
|
||||
};
|
||||
|
||||
export const addPost = (
|
||||
ship: Patp,
|
||||
name: string,
|
||||
post: Post
|
||||
) => {
|
||||
let nodes: Record<string, GraphNode> = {};
|
||||
nodes[post.index] = {
|
||||
post,
|
||||
children: null
|
||||
};
|
||||
return addNodes(ship, name, nodes);
|
||||
}
|
||||
|
||||
export const addNode = (
|
||||
ship: Patp,
|
||||
name: string,
|
||||
node: GraphNode
|
||||
): Poke<any> => {
|
||||
let nodes: Record<string, GraphNode> = {};
|
||||
nodes[node.post.index] = node;
|
||||
|
||||
return addNodes(ship, name, nodes);
|
||||
}
|
||||
|
||||
|
||||
export const removeNodes = (
|
||||
ship: Patp,
|
||||
name: string,
|
||||
indices: string[]
|
||||
): Poke<any> => hookAction({
|
||||
'remove-nodes': {
|
||||
resource: { ship, name },
|
||||
indices
|
||||
}
|
||||
});
|
@ -9,8 +9,8 @@ export interface UrlContent {
|
||||
}
|
||||
export interface CodeContent {
|
||||
code: {
|
||||
expresssion: string;
|
||||
output: string | undefined;
|
||||
expression: string;
|
||||
output: string[] | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ export interface GraphChildrenPoke {
|
||||
}
|
||||
|
||||
export interface GraphNode {
|
||||
children: Graph;
|
||||
children: Graph | null;
|
||||
post: Post;
|
||||
}
|
||||
|
2
pkg/npm/api/groups/index.d.ts
vendored
2
pkg/npm/api/groups/index.d.ts
vendored
@ -1,2 +0,0 @@
|
||||
export * from './update.d';
|
||||
export * from './view.d';
|
@ -1,117 +1,2 @@
|
||||
import { Enc, Path, Patp, PatpNoSig, Poke } from "..";
|
||||
import {
|
||||
Group,
|
||||
GroupAction,
|
||||
GroupPolicyDiff,
|
||||
GroupUpdateAddMembers,
|
||||
GroupUpdateAddTag,
|
||||
GroupUpdateChangePolicy,
|
||||
GroupUpdateRemoveGroup,
|
||||
GroupUpdateRemoveMembers,
|
||||
GroupUpdateRemoveTag,
|
||||
Resource,
|
||||
Tag
|
||||
} from "./index.d";
|
||||
import { GroupPolicy } from "./update";
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
export 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 };
|
||||
}
|
||||
|
||||
export const groupBunts = {
|
||||
group: (): Group => ({ members: new Set(), tags: { role: {} }, hidden: false, policy: groupBunts.policy() }),
|
||||
policy: (): GroupPolicy => ({ open: { banned: new Set(), banRanks: new Set() } })
|
||||
};
|
||||
|
||||
export const joinError = ['no-perms', 'strange'] as const;
|
||||
export const joinResult = ['done', ...joinError] as const;
|
||||
export const joinProgress = ['start', 'added', ...joinResult] as const;
|
||||
export * from './types';
|
||||
export * from './lib';
|
215
pkg/npm/api/groups/lib.ts
Normal file
215
pkg/npm/api/groups/lib.ts
Normal file
@ -0,0 +1,215 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
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 proxyAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'group-push-hook',
|
||||
mark: 'group-update',
|
||||
json: data
|
||||
});
|
||||
|
||||
const storeAction = <T extends GroupUpdate>(data: T): Poke<T> => ({
|
||||
app: 'group-store',
|
||||
mark: 'group-update',
|
||||
json: data
|
||||
});
|
||||
|
||||
export { storeAction as groupStoreAction };
|
||||
|
||||
const viewAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'group-view',
|
||||
mark: 'group-view-action',
|
||||
json: data
|
||||
});
|
||||
|
||||
export { viewAction as groupViewAction };
|
||||
|
||||
export const viewThread = <T>(thread: string, action: T): Thread<T> => ({
|
||||
inputMark: 'group-view-action',
|
||||
outputMark: 'json',
|
||||
threadName: thread,
|
||||
body: action
|
||||
});
|
||||
|
||||
export const removeMembers = (
|
||||
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 addMembers = (
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
export const join = (
|
||||
ship: string,
|
||||
name: string
|
||||
): Poke<any> => viewAction({
|
||||
join: {
|
||||
resource: makeResource(ship, name),
|
||||
ship
|
||||
}
|
||||
});
|
||||
|
||||
export const createGroup = (
|
||||
name: string,
|
||||
policy: Enc<GroupPolicy>,
|
||||
title: string,
|
||||
description: string
|
||||
): Thread<any> => viewThread('group-create', {
|
||||
create: {
|
||||
name,
|
||||
policy,
|
||||
title,
|
||||
description
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteGroup = (
|
||||
ship: string,
|
||||
name: string
|
||||
): Thread<any> => viewThread('group-delete', {
|
||||
remove: makeResource(ship, name)
|
||||
});
|
||||
|
||||
export const leaveGroup = (
|
||||
ship: string,
|
||||
name: string
|
||||
): Thread<any> => viewThread('group-leave', {
|
||||
leave: makeResource(ship, name)
|
||||
});
|
||||
|
||||
export const invite = (
|
||||
ship: string,
|
||||
name: string,
|
||||
ships: Patp[],
|
||||
description: string
|
||||
): Thread<any> => viewThread('group-invite', {
|
||||
invite: {
|
||||
resource: makeResource(ship, name),
|
||||
ships,
|
||||
description
|
||||
}
|
||||
});
|
||||
|
||||
export const roleTags = ['janitor', 'moderator', 'admin'];
|
||||
// TODO make this type better?
|
||||
|
||||
export const groupBunts = {
|
||||
group: (): Group => ({ members: new Set(), tags: { role: {} }, hidden: false, policy: groupBunts.policy() }),
|
||||
policy: (): GroupPolicy => ({ open: { banned: new Set(), banRanks: new Set() } })
|
||||
};
|
||||
|
||||
export const joinError = ['no-perms', 'strange'] as const;
|
||||
export const joinResult = ['done', ...joinError] as const;
|
||||
export const joinProgress = ['start', 'added', ...joinResult] as const;
|
||||
|
||||
export const roleForShip = (
|
||||
group: Group,
|
||||
ship: PatpNoSig
|
||||
): RoleTags | undefined => {
|
||||
return roleTags.reduce((currRole, role) => {
|
||||
const roleShips = group?.tags?.role?.[role];
|
||||
return roleShips && roleShips.has(ship) ? role : currRole;
|
||||
}, undefined as RoleTags | undefined);
|
||||
}
|
||||
|
||||
export const resourceFromPath = (path: Path): Resource => {
|
||||
const [, , ship, name] = path.split('/');
|
||||
return { ship, name };
|
||||
}
|
||||
|
||||
export const makeResource = (ship: string, name: string) => {
|
||||
return { ship, name };
|
||||
}
|
||||
|
||||
export const isWriter = (group: Group, resource: string, ship: string) => {
|
||||
const writers: Set<string> | undefined = _.get(
|
||||
group,
|
||||
['tags', 'graph', resource, 'writers'],
|
||||
undefined
|
||||
);
|
||||
const admins = group?.tags?.role?.admin ?? new Set();
|
||||
if (_.isUndefined(writers)) {
|
||||
return true;
|
||||
} else {
|
||||
return writers.has(ship) || admins.has(ship);
|
||||
}
|
||||
}
|
||||
|
||||
export const isChannelAdmin = (
|
||||
group: Group,
|
||||
resource: string,
|
||||
ship: string
|
||||
): boolean => {
|
||||
const role = roleForShip(group, ship.slice(1));
|
||||
|
||||
return (
|
||||
isHost(resource, ship) ||
|
||||
role === 'admin' ||
|
||||
role === 'moderator'
|
||||
);
|
||||
}
|
||||
|
||||
export const isHost = (
|
||||
resource: string,
|
||||
ship: string
|
||||
): boolean => {
|
||||
const [, , host] = resource.split('/');
|
||||
|
||||
return ship === host;
|
||||
}
|
2
pkg/npm/api/groups/types.ts
Normal file
2
pkg/npm/api/groups/types.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './update';
|
||||
export * from './view';
|
@ -38,33 +38,33 @@ export type OpenPolicyDiff =
|
||||
| AllowShipsDiff
|
||||
| BanShipsDiff;
|
||||
|
||||
interface AllowRanksDiff {
|
||||
export interface AllowRanksDiff {
|
||||
allowRanks: ShipRank[];
|
||||
}
|
||||
|
||||
interface BanRanksDiff {
|
||||
export interface BanRanksDiff {
|
||||
banRanks: ShipRank[];
|
||||
}
|
||||
|
||||
interface AllowShipsDiff {
|
||||
export interface AllowShipsDiff {
|
||||
allowShips: PatpNoSig[];
|
||||
}
|
||||
|
||||
interface BanShipsDiff {
|
||||
export interface BanShipsDiff {
|
||||
banShips: PatpNoSig[];
|
||||
}
|
||||
|
||||
export type InvitePolicyDiff = AddInvitesDiff | RemoveInvitesDiff;
|
||||
|
||||
interface AddInvitesDiff {
|
||||
export interface AddInvitesDiff {
|
||||
addInvites: PatpNoSig[];
|
||||
}
|
||||
|
||||
interface RemoveInvitesDiff {
|
||||
export interface RemoveInvitesDiff {
|
||||
removeInvites: PatpNoSig[];
|
||||
}
|
||||
|
||||
interface ReplacePolicyDiff {
|
||||
export interface ReplacePolicyDiff {
|
||||
replace: GroupPolicy;
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ export type GroupPolicyDiff =
|
||||
|
||||
export type GroupPolicy = OpenPolicy | InvitePolicy;
|
||||
|
||||
interface TaggedShips {
|
||||
export interface TaggedShips {
|
||||
[tag: string]: Set<PatpNoSig>;
|
||||
}
|
||||
|
||||
@ -95,11 +95,11 @@ export type Groups = {
|
||||
[p in Path]: Group;
|
||||
};
|
||||
|
||||
interface GroupUpdateInitial {
|
||||
export interface GroupUpdateInitial {
|
||||
initial: Enc<Groups>;
|
||||
}
|
||||
|
||||
interface GroupUpdateAddGroup {
|
||||
export interface GroupUpdateAddGroup {
|
||||
addGroup: {
|
||||
resource: Resource;
|
||||
policy: Enc<GroupPolicy>;
|
||||
@ -107,21 +107,21 @@ interface GroupUpdateAddGroup {
|
||||
};
|
||||
}
|
||||
|
||||
interface GroupUpdateAddMembers {
|
||||
export interface GroupUpdateAddMembers {
|
||||
addMembers: {
|
||||
ships: PatpNoSig[];
|
||||
resource: Resource;
|
||||
};
|
||||
}
|
||||
|
||||
interface GroupUpdateRemoveMembers {
|
||||
export interface GroupUpdateRemoveMembers {
|
||||
removeMembers: {
|
||||
ships: PatpNoSig[];
|
||||
resource: Resource;
|
||||
};
|
||||
}
|
||||
|
||||
interface GroupUpdateAddTag {
|
||||
export interface GroupUpdateAddTag {
|
||||
addTag: {
|
||||
tag: Tag;
|
||||
resource: Resource;
|
||||
@ -129,7 +129,7 @@ interface GroupUpdateAddTag {
|
||||
};
|
||||
}
|
||||
|
||||
interface GroupUpdateRemoveTag {
|
||||
export interface GroupUpdateRemoveTag {
|
||||
removeTag: {
|
||||
tag: Tag;
|
||||
resource: Resource;
|
||||
@ -137,23 +137,23 @@ interface GroupUpdateRemoveTag {
|
||||
};
|
||||
}
|
||||
|
||||
interface GroupUpdateChangePolicy {
|
||||
export interface GroupUpdateChangePolicy {
|
||||
changePolicy: { resource: Resource; diff: GroupPolicyDiff };
|
||||
}
|
||||
|
||||
interface GroupUpdateRemoveGroup {
|
||||
export interface GroupUpdateRemoveGroup {
|
||||
removeGroup: {
|
||||
resource: Resource;
|
||||
};
|
||||
}
|
||||
|
||||
interface GroupUpdateExpose {
|
||||
export interface GroupUpdateExpose {
|
||||
expose: {
|
||||
resource: Resource;
|
||||
};
|
||||
}
|
||||
|
||||
interface GroupUpdateInitialGroup {
|
||||
export interface GroupUpdateInitialGroup {
|
||||
initialGroup: {
|
||||
resource: Resource;
|
||||
group: Enc<Group>;
|
@ -0,0 +1,2 @@
|
||||
export * from './types';
|
||||
export * from './lib';
|
250
pkg/npm/api/hark/lib.ts
Normal file
250
pkg/npm/api/hark/lib.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import f from 'lodash/fp';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
|
||||
import { Poke } from '../lib/types';
|
||||
import { GraphNotifDescription, GraphNotificationContents, GraphNotifIndex, IndexedNotification, NotifIndex, Unreads } from './types';
|
||||
import { decToUd } from '../lib';
|
||||
import { Association } from '../metadata/types';
|
||||
|
||||
export const harkAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'hark-store',
|
||||
mark: 'hark-action',
|
||||
json: data
|
||||
});
|
||||
|
||||
const graphHookAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'hark-graph-hook',
|
||||
mark: 'hark-graph-hook-action',
|
||||
json: data
|
||||
});
|
||||
|
||||
export { graphHookAction as harkGraphHookAction };
|
||||
|
||||
const groupHookAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'hark-group-hook',
|
||||
mark: 'hark-group-hook-action',
|
||||
json: data
|
||||
});
|
||||
|
||||
export { groupHookAction as harkGroupHookAction };
|
||||
|
||||
export const actOnNotification = (
|
||||
frond: string,
|
||||
intTime: BigInteger,
|
||||
index: NotifIndex
|
||||
): Poke<unknown> => harkAction({
|
||||
[frond]: {
|
||||
time: decToUd(intTime.toString()),
|
||||
index
|
||||
}
|
||||
});
|
||||
|
||||
export const getParentIndex = (
|
||||
idx: GraphNotifIndex,
|
||||
contents: GraphNotificationContents
|
||||
): string | undefined => {
|
||||
const origIndex = contents[0].index.slice(1).split('/');
|
||||
const ret = (i: string[]) => `/${i.join('/')}`;
|
||||
switch (idx.description) {
|
||||
case 'link':
|
||||
return '/';
|
||||
case 'comment':
|
||||
return ret(origIndex.slice(0, 1));
|
||||
case 'note':
|
||||
return '/';
|
||||
case 'mention':
|
||||
return undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const setMentions = (
|
||||
mentions: boolean
|
||||
): Poke<unknown> => graphHookAction({
|
||||
'set-mentions': mentions
|
||||
});
|
||||
|
||||
export const setWatchOnSelf = (
|
||||
watchSelf: boolean
|
||||
): Poke<unknown> => graphHookAction({
|
||||
'set-watch-on-self': watchSelf
|
||||
});
|
||||
|
||||
export const setDoNotDisturb = (
|
||||
dnd: boolean
|
||||
): Poke<unknown> => harkAction({
|
||||
'set-dnd': dnd
|
||||
});
|
||||
|
||||
export const archive = (
|
||||
time: BigInteger,
|
||||
index: NotifIndex
|
||||
): Poke<unknown> => actOnNotification('archive', time, index);
|
||||
|
||||
export const read = (
|
||||
time: BigInteger,
|
||||
index: NotifIndex
|
||||
): Poke<unknown> => actOnNotification('read-note', time, index);
|
||||
|
||||
export const readIndex = (
|
||||
index: NotifIndex
|
||||
): Poke<unknown> => harkAction({
|
||||
'read-index': index
|
||||
});
|
||||
|
||||
export const unread = (
|
||||
time: BigInteger,
|
||||
index: NotifIndex
|
||||
): Poke<unknown> => actOnNotification('unread-note', time, index);
|
||||
|
||||
export const markCountAsRead = (
|
||||
association: Association,
|
||||
parent: string,
|
||||
description: GraphNotifDescription
|
||||
): Poke<unknown> => harkAction({
|
||||
'read-count': {
|
||||
graph: {
|
||||
graph: association.resource,
|
||||
group: association.group,
|
||||
module: association.metadata.module,
|
||||
description: description,
|
||||
index: parent
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const markEachAsRead = (
|
||||
association: Association,
|
||||
parent: string,
|
||||
child: string,
|
||||
description: GraphNotifDescription,
|
||||
module: string
|
||||
): Poke<unknown> => harkAction({
|
||||
'read-each': {
|
||||
index: {
|
||||
graph: {
|
||||
graph: association.resource,
|
||||
group: association.group,
|
||||
description: description,
|
||||
module: module,
|
||||
index: parent
|
||||
}
|
||||
},
|
||||
target: child
|
||||
}
|
||||
});
|
||||
|
||||
export const dec = (
|
||||
index: NotifIndex,
|
||||
ref: string
|
||||
): Poke<unknown> => harkAction({
|
||||
dec: {
|
||||
index,
|
||||
ref
|
||||
}
|
||||
});
|
||||
|
||||
export const seen = () => harkAction({ seen: null });
|
||||
|
||||
export const readAll = () => harkAction({ 'read-all': null });
|
||||
|
||||
export const ignoreGroup = (
|
||||
group: string
|
||||
): Poke<unknown> => groupHookAction({
|
||||
ignore: group
|
||||
});
|
||||
|
||||
export const ignoreGraph = (
|
||||
graph: string,
|
||||
index: string
|
||||
): Poke<unknown> => graphHookAction({
|
||||
ignore: {
|
||||
graph,
|
||||
index
|
||||
}
|
||||
});
|
||||
|
||||
export const listenGroup = (
|
||||
group: string
|
||||
): Poke<unknown> => groupHookAction({
|
||||
listen: group
|
||||
});
|
||||
|
||||
export const listenGraph = (
|
||||
graph: string,
|
||||
index: string
|
||||
): Poke<unknown> => graphHookAction({
|
||||
listen: {
|
||||
graph,
|
||||
index
|
||||
}
|
||||
});
|
||||
|
||||
export const mute = (
|
||||
notif: IndexedNotification
|
||||
): Poke<any> | {} => {
|
||||
if('graph' in notif.index && 'graph' in notif.notification.contents) {
|
||||
const { index } = notif;
|
||||
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
|
||||
if(!parentIndex) {
|
||||
return {};
|
||||
}
|
||||
return ignoreGraph(index.graph.graph, parentIndex);
|
||||
}
|
||||
if('group' in notif.index) {
|
||||
const { group } = notif.index.group;
|
||||
return ignoreGroup(group);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export const unmute = (
|
||||
notif: IndexedNotification
|
||||
): Poke<any> | {} => {
|
||||
if('graph' in notif.index && 'graph' in notif.notification.contents) {
|
||||
const { index } = notif;
|
||||
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
|
||||
if(!parentIndex) {
|
||||
return {};
|
||||
}
|
||||
return listenGraph(index.graph.graph, parentIndex);
|
||||
}
|
||||
if('group' in notif.index) {
|
||||
return listenGroup(notif.index.group.group);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export const getLastSeen = (
|
||||
unreads: Unreads,
|
||||
path: string,
|
||||
index: string
|
||||
): BigInteger | undefined => {
|
||||
const lastSeenIdx = unreads.graph?.[path]?.[index]?.unreads;
|
||||
if (!(typeof lastSeenIdx === 'string')) {
|
||||
return bigInt.zero;
|
||||
}
|
||||
return f.flow(f.split('/'), f.last, x => (x ? bigInt(x) : undefined))(
|
||||
lastSeenIdx
|
||||
);
|
||||
}
|
||||
|
||||
export const getUnreadCount = (
|
||||
unreads: Unreads,
|
||||
path: string,
|
||||
index: string
|
||||
): number => {
|
||||
const graphUnreads = unreads.graph?.[path]?.[index]?.unreads ?? 0;
|
||||
return typeof graphUnreads === 'number' ? graphUnreads : graphUnreads.size;
|
||||
}
|
||||
|
||||
export const getNotificationCount = (
|
||||
unreads: Unreads,
|
||||
path: string
|
||||
): number => {
|
||||
const unread = unreads.graph?.[path] || {};
|
||||
return Object.keys(unread)
|
||||
.map(index => unread[index]?.notifications || 0)
|
||||
.reduce(f.add, 0);
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { Post } from "../graph/index.d";
|
||||
import { GroupUpdate } from "../groups/index.d";
|
||||
import { Post } from "../graph/types";
|
||||
import { GroupUpdate } from "../groups/types";
|
||||
import BigIntOrderedMap from "../lib/BigIntOrderedMap";
|
||||
|
||||
export type GraphNotifDescription = "link" | "comment" | "note" | "mention";
|
||||
export type GraphNotifDescription = "link" | "comment" | "note" | "mention" | "message";
|
||||
|
||||
export interface UnreadStats {
|
||||
unreads: Set<string> | number;
|
7
pkg/npm/api/index.d.ts
vendored
7
pkg/npm/api/index.d.ts
vendored
@ -1,7 +0,0 @@
|
||||
export * from './contacts/index.d'
|
||||
export * from './graph/index.d';
|
||||
export * from './groups/index.d';
|
||||
export * from './hark/index.d';
|
||||
export * from './invite/index.d';
|
||||
export * from './lib/index.d';
|
||||
export * from './metadata/index.d';
|
@ -1,9 +1,3 @@
|
||||
import BigIntOrderedMap from './lib/BigIntOrderedMap';
|
||||
|
||||
export {
|
||||
BigIntOrderedMap
|
||||
};
|
||||
|
||||
export * from './contacts';
|
||||
export * from './graph';
|
||||
export * from './groups';
|
||||
@ -11,4 +5,5 @@ export * from './hark';
|
||||
export * from './invite';
|
||||
export * from './metadata';
|
||||
export * from './settings';
|
||||
export * from './index.d';
|
||||
export * from './lib';
|
||||
export * from './lib/BigIntOrderedMap';
|
@ -1,28 +1,2 @@
|
||||
import { InviteUpdate, InviteUpdateAccept, InviteUpdateDecline } from "./index.d";
|
||||
import { Poke, Serial } from "..";
|
||||
|
||||
export const action = <T extends InviteUpdate>(data: T): Poke<T> => ({
|
||||
app: 'invite-store',
|
||||
mark: 'invite-action',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const accept = (
|
||||
app: string,
|
||||
uid: Serial
|
||||
): Poke<InviteUpdateAccept> => action({
|
||||
accept: {
|
||||
term: app,
|
||||
uid
|
||||
}
|
||||
});
|
||||
|
||||
export const decline = (
|
||||
app: string,
|
||||
uid: Serial
|
||||
): Poke<InviteUpdateDecline> => action({
|
||||
decline: {
|
||||
term: app,
|
||||
uid
|
||||
}
|
||||
});
|
||||
export * from './types';
|
||||
export * from './lib';
|
28
pkg/npm/api/invite/lib.ts
Normal file
28
pkg/npm/api/invite/lib.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Poke, Serial } from "..";
|
||||
import { InviteUpdate, InviteUpdateAccept, InviteUpdateDecline } from "./types";
|
||||
|
||||
export const inviteAction = <T extends InviteUpdate>(data: T): Poke<T> => ({
|
||||
app: 'invite-store',
|
||||
mark: 'invite-action',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const accept = (
|
||||
app: string,
|
||||
uid: Serial
|
||||
): Poke<InviteUpdateAccept> => inviteAction({
|
||||
accept: {
|
||||
term: app,
|
||||
uid
|
||||
}
|
||||
});
|
||||
|
||||
export const decline = (
|
||||
app: string,
|
||||
uid: Serial
|
||||
): Poke<InviteUpdateDecline> => inviteAction({
|
||||
decline: {
|
||||
term: app,
|
||||
uid
|
||||
}
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import { Serial, PatpNoSig, Path } from '..';
|
||||
import { Resource } from "../groups/update.d";
|
||||
import { Resource } from "../groups";
|
||||
|
||||
export type InviteUpdate =
|
||||
InviteUpdateInitial
|
||||
@ -10,30 +10,30 @@ export type InviteUpdate =
|
||||
| InviteUpdateAccepted
|
||||
| InviteUpdateDecline;
|
||||
|
||||
interface InviteUpdateAccept {
|
||||
export interface InviteUpdateAccept {
|
||||
accept: {
|
||||
term: string;
|
||||
uid: Serial;
|
||||
}
|
||||
}
|
||||
|
||||
interface InviteUpdateInitial {
|
||||
export interface InviteUpdateInitial {
|
||||
initial: Invites;
|
||||
}
|
||||
|
||||
interface InviteUpdateCreate {
|
||||
export interface InviteUpdateCreate {
|
||||
create: {
|
||||
term: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface InviteUpdateDelete {
|
||||
export interface InviteUpdateDelete {
|
||||
delete: {
|
||||
term: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface InviteUpdateInvite {
|
||||
export interface InviteUpdateInvite {
|
||||
invite: {
|
||||
term: string;
|
||||
uid: Serial;
|
||||
@ -41,14 +41,14 @@ interface InviteUpdateInvite {
|
||||
};
|
||||
}
|
||||
|
||||
interface InviteUpdateAccepted {
|
||||
export interface InviteUpdateAccepted {
|
||||
accepted: {
|
||||
term: string;
|
||||
uid: Serial;
|
||||
};
|
||||
}
|
||||
|
||||
interface InviteUpdateDecline {
|
||||
export interface InviteUpdateDecline {
|
||||
decline: {
|
||||
term: string;
|
||||
uid: Serial;
|
2
pkg/npm/api/lib/index.ts
Normal file
2
pkg/npm/api/lib/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './lib';
|
||||
export * from './types';
|
@ -1,23 +1,14 @@
|
||||
import _ from "lodash";
|
||||
import f from "lodash/fp";
|
||||
import bigInt, { BigInteger } from "big-integer";
|
||||
import { Resource } from "../groups/index.d";
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@ -136,28 +127,49 @@ export function deSig(ship: string): string | null {
|
||||
}
|
||||
|
||||
// trim patps to match dojo, chat-cli
|
||||
export function cite(ship: string): string {
|
||||
export function cite(ship: string) {
|
||||
let patp = ship,
|
||||
shortened = "";
|
||||
if (patp === null || patp === "") {
|
||||
return "";
|
||||
shortened = '';
|
||||
if (patp === null || patp === '') {
|
||||
return null;
|
||||
}
|
||||
if (patp.startsWith("~")) {
|
||||
if (patp.startsWith('~')) {
|
||||
patp = patp.substr(1);
|
||||
}
|
||||
// comet
|
||||
if (patp.length === 56) {
|
||||
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 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);
|
||||
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 ux = f.flow(
|
||||
f.chunk(4),
|
||||
f.map(x => _.dropWhile(x, (y: unknown) => y === 0).join('')),
|
||||
f.join('.')
|
||||
)(hex.split(''));
|
||||
return `0x${ux}`;
|
||||
};
|
||||
|
||||
|
||||
// encode the string into @ta-safe format, using logic from +wood.
|
||||
// for example, 'some Chars!' becomes '~.some.~43.hars~21.'
|
||||
//
|
||||
@ -209,3 +221,20 @@ export function numToUd(num: number): string {
|
||||
f.join('.')
|
||||
)(num.toString())
|
||||
}
|
||||
|
||||
export const buntPost = (): Post => ({
|
||||
author: '',
|
||||
contents: [],
|
||||
hash: null,
|
||||
index: '',
|
||||
signatures: [],
|
||||
'time-sent': 0
|
||||
});
|
||||
|
||||
export function makeNodeMap(posts: Post[]): Record<string, GraphNode> {
|
||||
const nodes: Record<string, GraphNode> = {};
|
||||
posts.forEach((p: Post) => {
|
||||
nodes[String(p.index)] = { children: null, post: p };
|
||||
});
|
||||
return nodes;
|
||||
}
|
@ -20,7 +20,7 @@ export type Serial = string;
|
||||
export type Jug<K,V> = Map<K,Set<V>>;
|
||||
|
||||
// name of app
|
||||
export type AppName = 'chat' | 'link' | 'contacts' | 'publish' | 'graph';
|
||||
export type AppName = 'chat' | 'link' | 'contacts' | 'publish' | 'graph' | 'groups';
|
||||
|
||||
export type ShipRank = 'czar' | 'king' | 'duke' | 'earl' | 'pawn';
|
||||
|
||||
@ -54,6 +54,11 @@ export interface Poke<Action> {
|
||||
json: Action;
|
||||
}
|
||||
|
||||
export interface Scry {
|
||||
app: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface Thread<Action> {
|
||||
inputMark: string;
|
||||
outputMark: string;
|
@ -1,43 +1,2 @@
|
||||
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 = (
|
||||
appName: AppName,
|
||||
resource: string,
|
||||
group: string,
|
||||
metadata: Metadata,
|
||||
): Poke<MetadataUpdateAdd> => {
|
||||
return action({
|
||||
add: {
|
||||
group,
|
||||
resource: {
|
||||
resource,
|
||||
'app-name': appName
|
||||
},
|
||||
metadata
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const update = (
|
||||
association: Association,
|
||||
newMetadata: Partial<Metadata>
|
||||
): Poke<MetadataUpdateAdd> => {
|
||||
const { resource, metadata, group } = association;
|
||||
return action({
|
||||
add: {
|
||||
group,
|
||||
resource: {
|
||||
resource,
|
||||
'app-name': association['app-name'],
|
||||
},
|
||||
metadata: {...metadata, ...newMetadata }
|
||||
}
|
||||
});
|
||||
}
|
||||
export * from './types';
|
||||
export * from './lib';
|
77
pkg/npm/api/metadata/lib.ts
Normal file
77
pkg/npm/api/metadata/lib.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { AppName, Path, Poke, uxToHex, PatpNoSig } from "../lib";
|
||||
import { Association, Metadata, MetadataUpdate, MetadataUpdateAdd, MetadataUpdateRemove } from './types';
|
||||
|
||||
export const metadataAction = <T extends MetadataUpdate>(data: T): Poke<T> => ({
|
||||
app: 'metadata-push-hook',
|
||||
mark: 'metadata-update',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const add = (
|
||||
ship: PatpNoSig,
|
||||
appName: AppName,
|
||||
resource: Path,
|
||||
group: Path,
|
||||
title: string,
|
||||
description: string,
|
||||
dateCreated: string,
|
||||
color: string,
|
||||
moduleName: string,
|
||||
): Poke<MetadataUpdateAdd> => metadataAction({
|
||||
add: {
|
||||
group,
|
||||
resource: {
|
||||
resource,
|
||||
app: appName
|
||||
},
|
||||
metadata: {
|
||||
title,
|
||||
description,
|
||||
color,
|
||||
'date-created': dateCreated,
|
||||
creator: `~${ship}`,
|
||||
'module': moduleName,
|
||||
picture: '',
|
||||
preview: false,
|
||||
vip: ''
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export { add as metadataAdd };
|
||||
|
||||
export const remove = (
|
||||
appName: AppName,
|
||||
resource: string,
|
||||
group: string
|
||||
): Poke<MetadataUpdateRemove> => metadataAction({
|
||||
remove: {
|
||||
group,
|
||||
resource: {
|
||||
resource,
|
||||
app: appName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export { remove as metadataRemove };
|
||||
|
||||
export const update = (
|
||||
association: Association,
|
||||
newMetadata: Partial<Metadata>
|
||||
): Poke<MetadataUpdateAdd> => {
|
||||
const metadata = { ...association.metadata, ...newMetadata };
|
||||
metadata.color = uxToHex(metadata.color);
|
||||
return metadataAction({
|
||||
add: {
|
||||
group: association.group,
|
||||
resource: {
|
||||
resource: association.resource,
|
||||
app: association.app
|
||||
},
|
||||
metadata
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { update as metadataUpdate };
|
@ -1,4 +1,4 @@
|
||||
import { AppName, Path, Patp } from '..';
|
||||
import { AppName, Path, Patp } from "../lib";
|
||||
|
||||
export type MetadataUpdate =
|
||||
MetadataUpdateInitial
|
||||
@ -6,28 +6,34 @@ export type MetadataUpdate =
|
||||
| MetadataUpdateUpdate
|
||||
| MetadataUpdateRemove;
|
||||
|
||||
interface MetadataUpdateInitial {
|
||||
export interface MetadataUpdateInitial {
|
||||
associations: ResourceAssociations;
|
||||
}
|
||||
|
||||
type ResourceAssociations = {
|
||||
export type ResourceAssociations = {
|
||||
[p in Path]: Association;
|
||||
}
|
||||
|
||||
type MetadataUpdateAdd = {
|
||||
export type MetadataUpdateAdd = {
|
||||
add: AssociationPoke;
|
||||
}
|
||||
|
||||
type MetadataUpdateUpdate = {
|
||||
export type MetadataUpdateUpdate = {
|
||||
update: AssociationPoke;
|
||||
}
|
||||
|
||||
type MetadataUpdateRemove = {
|
||||
remove: MdResource & {
|
||||
group: Path;
|
||||
export type MetadataUpdateRemove = {
|
||||
remove: {
|
||||
resource: MdResource;
|
||||
group: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface MdResource {
|
||||
resource: string;
|
||||
app: AppName;
|
||||
}
|
||||
|
||||
export interface MetadataUpdatePreview {
|
||||
group: string;
|
||||
channels: Associations;
|
||||
@ -42,10 +48,7 @@ export type AppAssociations = {
|
||||
[p in Path]: Association;
|
||||
}
|
||||
|
||||
interface MdResource {
|
||||
resource: Path;
|
||||
'app-name': AppName;
|
||||
}
|
||||
|
||||
|
||||
export type Association = MdResource & {
|
||||
group: Path;
|
@ -7,15 +7,18 @@
|
||||
"url": "ssh://git@github.com/urbit/urbit.git",
|
||||
"directory": "pkg/npm/api"
|
||||
},
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "npm run clean && tsc -p tsconfig.json",
|
||||
"clean": "rm -rf dist/*"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@urbit/eslint-config": "^1.0.0",
|
||||
"big-integer": "^1.6.48",
|
||||
"lodash": "^4.17.20"
|
||||
|
0
pkg/npm/api/s3/index.ts
Normal file
0
pkg/npm/api/s3/index.ts
Normal file
47
pkg/npm/api/s3/lib.ts
Normal file
47
pkg/npm/api/s3/lib.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Poke } from '../lib/types';
|
||||
import { S3Update, S3UpdateAccessKeyId, S3UpdateAddBucket, S3UpdateCurrentBucket, S3UpdateEndpoint, S3UpdateRemoveBucket, S3UpdateSecretAccessKey } from './types';
|
||||
|
||||
const s3Action = <T extends S3Update>(
|
||||
data: any
|
||||
): Poke<T> => ({
|
||||
app: 's3-store',
|
||||
mark: 's3-action',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const setCurrentBucket = (
|
||||
bucket: string
|
||||
): Poke<S3UpdateCurrentBucket> => s3Action({
|
||||
'set-current-bucket': bucket
|
||||
});
|
||||
|
||||
export const addBucket = (
|
||||
bucket: string
|
||||
): Poke<S3UpdateAddBucket> => s3Action({
|
||||
'add-bucket': bucket
|
||||
});
|
||||
|
||||
export const removeBucket = (
|
||||
bucket: string
|
||||
): Poke<S3UpdateRemoveBucket> => s3Action({
|
||||
'remove-bucket': bucket
|
||||
});
|
||||
|
||||
export const setEndpoint = (
|
||||
endpoint: string
|
||||
): Poke<S3UpdateEndpoint> => s3Action({
|
||||
'set-endpoint': endpoint
|
||||
});
|
||||
|
||||
export const setAccessKeyId = (
|
||||
accessKeyId: string
|
||||
): Poke<S3UpdateAccessKeyId> => s3Action({
|
||||
'set-access-key-id': accessKeyId
|
||||
});
|
||||
|
||||
export const setSecretAccessKey = (
|
||||
secretAccessKey: string
|
||||
): Poke<S3UpdateSecretAccessKey> => s3Action({
|
||||
'set-secret-access-key': secretAccessKey
|
||||
});
|
||||
|
60
pkg/npm/api/s3/types.ts
Normal file
60
pkg/npm/api/s3/types.ts
Normal file
@ -0,0 +1,60 @@
|
||||
export interface S3Credentials {
|
||||
endpoint: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
}
|
||||
|
||||
export interface S3Configuration {
|
||||
buckets: Set<string>;
|
||||
currentBucket: string;
|
||||
}
|
||||
|
||||
export interface S3State {
|
||||
configuration: S3Configuration;
|
||||
credentials: S3Credentials | null;
|
||||
}
|
||||
|
||||
export interface S3UpdateCredentials {
|
||||
credentials: S3Credentials;
|
||||
}
|
||||
|
||||
export interface S3UpdateConfiguration {
|
||||
configuration: {
|
||||
buckets: string[];
|
||||
currentBucket: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface S3UpdateCurrentBucket {
|
||||
setCurrentBucket: string;
|
||||
}
|
||||
|
||||
export interface S3UpdateAddBucket {
|
||||
addBucket: string;
|
||||
}
|
||||
|
||||
export interface S3UpdateRemoveBucket {
|
||||
removeBucket: string;
|
||||
}
|
||||
|
||||
export interface S3UpdateEndpoint {
|
||||
setEndpoint: string;
|
||||
}
|
||||
|
||||
export interface S3UpdateAccessKeyId {
|
||||
setAccessKeyId: string;
|
||||
}
|
||||
|
||||
export interface S3UpdateSecretAccessKey {
|
||||
setSecretAccessKey: string;
|
||||
}
|
||||
|
||||
export type S3Update =
|
||||
S3UpdateCredentials
|
||||
| S3UpdateConfiguration
|
||||
| S3UpdateCurrentBucket
|
||||
| S3UpdateAddBucket
|
||||
| S3UpdateRemoveBucket
|
||||
| S3UpdateEndpoint
|
||||
| S3UpdateAccessKeyId
|
||||
| S3UpdateSecretAccessKey;
|
@ -0,0 +1,2 @@
|
||||
export * from './types';
|
||||
export * from './lib';
|
50
pkg/npm/api/settings/lib.ts
Normal file
50
pkg/npm/api/settings/lib.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Poke } from "../lib";
|
||||
import { PutBucket, Key, Bucket, DelBucket, Value, PutEntry, DelEntry, SettingsUpdate } from './types';
|
||||
|
||||
export const action = <T extends SettingsUpdate>(data: T): Poke<T> => ({
|
||||
app: 'settings-store',
|
||||
mark: 'settings-event',
|
||||
json: data
|
||||
});
|
||||
|
||||
export const putBucket = (
|
||||
key: Key,
|
||||
bucket: Bucket
|
||||
): Poke<PutBucket> => action({
|
||||
'put-bucket': {
|
||||
'bucket-key': key,
|
||||
'bucket': bucket
|
||||
}
|
||||
});
|
||||
|
||||
export const delBucket = (
|
||||
key: Key
|
||||
): Poke<DelBucket> => action({
|
||||
'del-bucket': {
|
||||
'bucket-key': key
|
||||
}
|
||||
});
|
||||
|
||||
export const putEntry = (
|
||||
bucket: Key,
|
||||
key: Key,
|
||||
value: Value
|
||||
): Poke<PutEntry> => action({
|
||||
'put-entry': {
|
||||
'bucket-key': bucket,
|
||||
'entry-key': key,
|
||||
value: value
|
||||
}
|
||||
});
|
||||
|
||||
export const delEntry = (
|
||||
bucket: Key,
|
||||
key: Key
|
||||
): Poke<DelEntry> => action({
|
||||
'del-entry': {
|
||||
'bucket-key': bucket,
|
||||
'entry-key': key
|
||||
}
|
||||
});
|
||||
|
||||
export * from './types';
|
@ -3,20 +3,20 @@ export type Value = string | boolean | number;
|
||||
export type Bucket = Map<string, Value>;
|
||||
export type Settings = Map<string, Bucket>;
|
||||
|
||||
interface PutBucket {
|
||||
export interface PutBucket {
|
||||
"put-bucket": {
|
||||
"bucket-key": Key;
|
||||
"bucket": Bucket;
|
||||
};
|
||||
}
|
||||
|
||||
interface DelBucket {
|
||||
export interface DelBucket {
|
||||
"del-bucket": {
|
||||
"bucket-key": Key;
|
||||
};
|
||||
}
|
||||
|
||||
interface PutEntry {
|
||||
export interface PutEntry {
|
||||
"put-entry": {
|
||||
"bucket-key": Key;
|
||||
"entry-key": Key;
|
||||
@ -24,22 +24,22 @@ interface PutEntry {
|
||||
};
|
||||
}
|
||||
|
||||
interface DelEntry {
|
||||
export interface DelEntry {
|
||||
"del-entry": {
|
||||
"bucket-key": Key;
|
||||
"entry-key": Key;
|
||||
};
|
||||
}
|
||||
|
||||
interface AllData {
|
||||
export interface AllData {
|
||||
"all": Settings;
|
||||
}
|
||||
|
||||
interface BucketData {
|
||||
export interface BucketData {
|
||||
"bucket": Bucket;
|
||||
}
|
||||
|
||||
interface EntryData {
|
||||
export interface EntryData {
|
||||
"entry": Value;
|
||||
}
|
||||
|
@ -1,25 +1,19 @@
|
||||
{
|
||||
"include": ["*.ts"],
|
||||
"exclude": ["node_modules", "dist", "@types"],
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitReturns": true,
|
||||
"outDir": "./dist",
|
||||
"module": "ESNext",
|
||||
"noImplicitAny": true,
|
||||
"target": "ESNext",
|
||||
"pretty": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": false,
|
||||
"noImplicitAny": false,
|
||||
"noEmit": true,
|
||||
"target": "es2015",
|
||||
"module": "es2015",
|
||||
"strict": true,
|
||||
"jsx": "react",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*"
|
||||
],
|
||||
"exclude": [ "node_modules" ]
|
||||
}
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"strict": false,
|
||||
"noErrorTruncation": true,
|
||||
"allowJs": true,
|
||||
}
|
||||
}
|
1
pkg/npm/http-api/.gitignore
vendored
1
pkg/npm/http-api/.gitignore
vendored
@ -0,0 +1 @@
|
||||
example/*.js
|
@ -1,17 +0,0 @@
|
||||
/*
|
||||
* 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?");
|
||||
/******/ })()
|
||||
;
|
@ -1,17 +0,0 @@
|
||||
/*
|
||||
* 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?");
|
||||
/******/ })()
|
||||
;
|
@ -1,2 +0,0 @@
|
||||
import Urbit from './dist';
|
||||
export { Urbit as default, Urbit };
|
@ -8,20 +8,15 @@
|
||||
"url": "ssh://git@github.com/urbit/urbit.git",
|
||||
"directory": "pkg/npm/http-api"
|
||||
},
|
||||
"main": "dist/cjs/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"browser": "dist/esm/index.js",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/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",
|
||||
"build": "npm run clean && tsc -p tsconfig.json",
|
||||
"clean": "rm -rf dist/*"
|
||||
},
|
||||
"peerDependencies": {},
|
||||
@ -38,31 +33,29 @@
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
|
||||
"@babel/preset-typescript": "^7.12.1",
|
||||
"@types/browser-or-node": "^1.2.0",
|
||||
"@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",
|
||||
"util": "^0.12.3",
|
||||
"webpack": "^5.4.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@microsoft/fetch-event-source": "^2.0.0",
|
||||
"@urbit/api": "file:../api",
|
||||
"browser-or-node": "^1.3.0",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^5.7.1",
|
||||
"encoding": "^0.1.13",
|
||||
"eventsource": "^1.0.7",
|
||||
"buffer": "^6.0.3",
|
||||
"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"
|
||||
"stream-http": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
372
pkg/npm/http-api/src/Urbit.ts
Normal file
372
pkg/npm/http-api/src/Urbit.ts
Normal file
@ -0,0 +1,372 @@
|
||||
import { isBrowser, isNode } from 'browser-or-node';
|
||||
import { Action, Scry, Thread } from '@urbit/api';
|
||||
import { fetchEventSource, EventSourceMessage } from '@microsoft/fetch-event-source';
|
||||
|
||||
import { AuthenticationInterface, SubscriptionInterface, CustomEventHandler, PokeInterface, SubscriptionRequestInterface, headers, UrbitInterface, SSEOptions, PokeHandlers } from './types';
|
||||
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
|
||||
*/
|
||||
sseClientInitialized: boolean = false;
|
||||
|
||||
/**
|
||||
* 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, PokeHandlers> = 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;
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the SSE pipe for the appropriate channel.
|
||||
*/
|
||||
eventSource(): void{
|
||||
if (!this.sseClientInitialized) {
|
||||
const sseOptions: SSEOptions = {
|
||||
headers: {}
|
||||
};
|
||||
if (isBrowser) {
|
||||
sseOptions.withCredentials = true;
|
||||
} else if (isNode) {
|
||||
sseOptions.headers.Cookie = this.cookie;
|
||||
}
|
||||
fetchEventSource(this.channelUrl, {
|
||||
// withCredentials: true,
|
||||
onmessage: (event: EventSourceMessage) => {
|
||||
if (this.verbose) {
|
||||
console.log('Received SSE: ', event);
|
||||
}
|
||||
this.ack(Number(event.id));
|
||||
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.onSuccess();
|
||||
} else if (data.hasOwnProperty('err')) {
|
||||
funcs.onError(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);
|
||||
}
|
||||
}
|
||||
},
|
||||
onerror: (error) => {
|
||||
console.error('pipe error', error);
|
||||
}
|
||||
});
|
||||
this.sseClientInitialized = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T>(params: PokeInterface<T>): 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.sseClientInitialized) resolve(pokeId); // A poke may occur before a listener has been opened
|
||||
this.outstandingPokes.set(pokeId, {
|
||||
onSuccess: () => {
|
||||
onSuccess();
|
||||
resolve(pokeId);
|
||||
},
|
||||
onError: (event) => {
|
||||
onError(event);
|
||||
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 });
|
||||
|
||||
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(params: Scry): Promise<void | any> {
|
||||
const { app, path } = params;
|
||||
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 thread<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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Urbit;
|
@ -1,41 +0,0 @@
|
||||
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;
|
||||
// @ts-ignore
|
||||
return this.airlock.subscribe(app, path);
|
||||
}
|
||||
// TODO handle methods that don't exist
|
||||
}
|
@ -1,457 +1,3 @@
|
||||
import { isBrowser, isNode } from 'browser-or-node';
|
||||
import { Action, Thread } from '../../api';
|
||||
|
||||
import { AuthenticationInterface, SubscriptionInterface, CustomEventHandler, PokeInterface, SubscriptionRequestInterface, headers, UrbitInterface, SSEOptions, PokeHandlers } 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, PokeHandlers> = 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`
|
||||
// @ts-ignore
|
||||
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.onSuccess();
|
||||
} else if (data.hasOwnProperty('err')) {
|
||||
funcs.onError(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<T>(params: PokeInterface<T>): 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, {
|
||||
onSuccess: () => {
|
||||
onSuccess();
|
||||
resolve(pokeId);
|
||||
},
|
||||
onError: (event) => {
|
||||
onError(event);
|
||||
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;
|
||||
export * from './types';
|
||||
import Urbit from './Urbit';
|
||||
export { Urbit as default };
|
66
pkg/npm/http-api/src/types.ts
Normal file
66
pkg/npm/http-api/src/types.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Action, Poke, Scry, Thread } from '@urbit/api';
|
||||
|
||||
export interface PokeHandlers {
|
||||
onSuccess?: () => void;
|
||||
onError?: (e: any) => void;
|
||||
}
|
||||
|
||||
export type PokeInterface<T> = PokeHandlers & Poke<T>;
|
||||
|
||||
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 {
|
||||
uid: string;
|
||||
lastEventId: number;
|
||||
lastAcknowledgedEventId: number;
|
||||
sseClientInitialized: boolean;
|
||||
cookie?: string | undefined;
|
||||
outstandingPokes: Map<number, PokeHandlers>;
|
||||
outstandingSubscriptions: Map<number, SubscriptionInterface>;
|
||||
verbose?: boolean;
|
||||
ship?: string | null;
|
||||
connect(): void;
|
||||
connect(): Promise<void>;
|
||||
eventSource(): void;
|
||||
getEventId(): number;
|
||||
ack(eventId: number): Promise<void | number>;
|
||||
sendMessage(action: Action, data?: object): Promise<void | number>;
|
||||
poke<T>(params: PokeInterface<T>): Promise<void | number>;
|
||||
subscribe(params: SubscriptionRequestInterface): Promise<void | number>;
|
||||
unsubscribe(subscription: string): Promise<void | number>;
|
||||
delete(): Promise<void | number>;
|
||||
scry(params: Scry): Promise<void | any>;
|
||||
thread<T>(params: Thread<T>): Promise<T>;
|
||||
}
|
||||
|
||||
export interface CustomEventHandler {
|
||||
(data: any, response: string): void;
|
||||
}
|
||||
|
||||
export interface SSEOptions {
|
||||
headers?: {
|
||||
Cookie?: string
|
||||
};
|
||||
withCredentials?: boolean;
|
||||
}
|
47
pkg/npm/http-api/src/types/index.d.ts
vendored
47
pkg/npm/http-api/src/types/index.d.ts
vendored
@ -1,47 +0,0 @@
|
||||
import { Action, Mark, Poke } from '../../../api/index';
|
||||
|
||||
export interface PokeHandlers {
|
||||
onSuccess?: () => void;
|
||||
onError?: (e: any) => void;
|
||||
}
|
||||
|
||||
export type PokeInterface<T> = PokeHandlers & Poke<T>;
|
||||
|
||||
|
||||
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;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"outDir": "./dist/cjs"
|
||||
},
|
||||
}
|
@ -1,18 +1,19 @@
|
||||
{
|
||||
"include": ["src/**/*.ts"],
|
||||
"include": ["src/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "@types"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/esm",
|
||||
"module": "ES2020",
|
||||
"outDir": "./dist",
|
||||
"module": "ESNext",
|
||||
"noImplicitAny": true,
|
||||
"target": "ES2020",
|
||||
"target": "ESNext",
|
||||
"pretty": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"strict": false
|
||||
// "lib": ["ES2020"],
|
||||
"strict": false,
|
||||
"noErrorTruncation": true,
|
||||
"allowJs": true,
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
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