1+ import { NamedNode , st } from 'rdflib'
2+ import { LiveStore } from "../index" ;
3+ import { newThing } from "../uri" ;
4+ import { ProfileLogic } from "../profile/ProfileLogic" ;
5+ import { determineChatContainer } from "./determineChatContainer" ;
6+
7+
8+ const CHAT_LOCATION_IN_CONTAINER = 'index.ttl#this'
9+
10+ /**
11+ * Chat-related logic
12+ */
13+ export class ChatLogic {
14+
15+ store : LiveStore
16+ ns : any
17+ profile : ProfileLogic
18+
19+ constructor ( store , ns , profile ) {
20+ this . store = store
21+ this . ns = ns
22+ this . profile = profile
23+ }
24+
25+ async setAcl ( chatContainer , me , invitee ) {
26+ // Some servers don't present a Link http response header
27+ // if the container doesn't exist yet, so refetch the container
28+ // now that it has been created:
29+ await this . store . fetcher . load ( chatContainer )
30+
31+ // FIXME: check the Why value on this quad:
32+ const chatAclDoc = this . store . any ( chatContainer , new NamedNode ( 'http://www.iana.org/assignments/link-relations/acl' ) )
33+ if ( ! chatAclDoc ) {
34+ throw new Error ( 'Chat ACL doc not found!' )
35+ }
36+
37+ const aclBody = `
38+ @prefix acl: <http://www.w3.org/ns/auth/acl#>.
39+ <#owner>
40+ a acl:Authorization;
41+ acl:agent <${ me . value } >;
42+ acl:accessTo <.>;
43+ acl:default <.>;
44+ acl:mode
45+ acl:Read, acl:Write, acl:Control.
46+ <#invitee>
47+ a acl:Authorization;
48+ acl:agent <${ invitee . value } >;
49+ acl:accessTo <.>;
50+ acl:default <.>;
51+ acl:mode
52+ acl:Read, acl:Append.
53+ `
54+ const aclResponse = await this . store . fetcher . webOperation ( 'PUT' , chatAclDoc . value , {
55+ data : aclBody ,
56+ contentType : 'text/turtle'
57+ } )
58+ }
59+
60+
61+
62+ private async addToPrivateTypeIndex ( chatThing , me ) {
63+ // Add to private type index
64+ const privateTypeIndex = this . store . any ( me , this . ns . solid ( 'privateTypeIndex' ) ) as NamedNode | null
65+ if ( ! privateTypeIndex ) {
66+ throw new Error ( 'Private type index not found!' )
67+ }
68+ await this . store . fetcher . load ( privateTypeIndex )
69+ const reg = newThing ( privateTypeIndex )
70+ const ins = [
71+ st ( reg , this . ns . rdf ( 'type' ) , this . ns . solid ( 'TypeRegistration' ) , privateTypeIndex . doc ( ) ) ,
72+ st ( reg , this . ns . solid ( 'forClass' ) , this . ns . meeting ( 'LongChat' ) , privateTypeIndex . doc ( ) ) ,
73+ st ( reg , this . ns . solid ( 'instance' ) , chatThing , privateTypeIndex . doc ( ) )
74+ ]
75+ await new Promise ( ( resolve , reject ) => {
76+ this . store . updater . update ( [ ] , ins , function ( _uri , ok , errm ) {
77+ if ( ! ok ) {
78+ reject ( new Error ( errm ) )
79+ } else {
80+ resolve ( null )
81+ }
82+ } )
83+ } )
84+ }
85+
86+ private async findChat ( invitee : NamedNode ) {
87+ const me = await this . profile . loadMe ( )
88+ const podRoot = await this . profile . getPodRoot ( me )
89+ const chatContainer = determineChatContainer ( invitee , podRoot )
90+ let exists = true
91+ try {
92+ await this . store . fetcher . load ( new NamedNode ( chatContainer . value + "index.ttl#this" ) )
93+ } catch ( e ) {
94+ exists = false
95+ }
96+ return { me, chatContainer, exists}
97+ }
98+
99+ private async createChatThing ( chatContainer , me ) {
100+ const created : any = await this . mintNew (
101+ {
102+ me,
103+ newBase : chatContainer . value
104+ } )
105+ return created . newInstance
106+ }
107+
108+ private mintNew ( newPaneOptions ) {
109+ const kb = this . store
110+ var updater = kb . updater
111+ if ( newPaneOptions . me && ! newPaneOptions . me . uri ) {
112+ throw new Error ( 'chat mintNew: Invalid userid ' + newPaneOptions . me )
113+ }
114+
115+ var newInstance = ( newPaneOptions . newInstance =
116+ newPaneOptions . newInstance ||
117+ kb . sym ( newPaneOptions . newBase + CHAT_LOCATION_IN_CONTAINER ) )
118+ var newChatDoc = newInstance . doc ( )
119+
120+ kb . add ( newInstance , this . ns . rdf ( 'type' ) , this . ns . meeting ( 'LongChat' ) , newChatDoc )
121+ kb . add ( newInstance , this . ns . dc ( 'title' ) , 'Chat channel' , newChatDoc )
122+ // @ts -ignore
123+ kb . add ( newInstance , this . ns . dc ( 'created' ) , new Date ( Date . now ( ) ) , newChatDoc )
124+ if ( newPaneOptions . me ) {
125+ kb . add ( newInstance , this . ns . dc ( 'author' ) , newPaneOptions . me , newChatDoc )
126+ }
127+
128+ return new Promise ( function ( resolve , reject ) {
129+ updater . put (
130+ newChatDoc ,
131+ kb . statementsMatching ( undefined , undefined , undefined , newChatDoc ) ,
132+ 'text/turtle' ,
133+ function ( uri2 , ok , message ) {
134+ if ( ok ) {
135+ resolve ( newPaneOptions )
136+ } else {
137+ reject (
138+ new Error (
139+ 'FAILED to save new chat channel at: ' + uri2 + ' : ' + message
140+ )
141+ )
142+ }
143+ }
144+ )
145+ } )
146+ }
147+
148+ /**
149+ * Find (and optionally create) an individual chat between the current user and the given invitee
150+ * @param invitee - The person to chat with
151+ * @param createIfMissing - Whether the chat should be created, if missing
152+ * @returns null if missing, or a node referring to an already existing chat, or the newly created chat
153+ */
154+ async getChat ( invitee : NamedNode , createIfMissing = true ) : Promise < NamedNode | null > {
155+ const { me, chatContainer, exists} = await this . findChat ( invitee )
156+ if ( exists ) {
157+ return new NamedNode ( chatContainer . value + CHAT_LOCATION_IN_CONTAINER )
158+ }
159+
160+ if ( createIfMissing ) {
161+ const chatThing = await this . createChatThing ( chatContainer , me )
162+ await this . sendInvite ( invitee , chatThing )
163+ await this . setAcl ( chatContainer , me , invitee )
164+ await this . addToPrivateTypeIndex ( chatThing , me )
165+ return chatThing
166+ }
167+ return null
168+ }
169+
170+ private async sendInvite ( invitee : NamedNode , chatThing : NamedNode ) {
171+ await this . store . fetcher . load ( invitee . doc ( ) )
172+ const inviteeInbox = this . store . any ( invitee , this . ns . ldp ( 'inbox' ) , undefined , invitee . doc ( ) )
173+ if ( ! inviteeInbox ) {
174+ throw new Error ( `Invitee inbox not found! ${ invitee . value } ` )
175+ }
176+ const inviteBody = `
177+ <> a <http://www.w3.org/ns/pim/meeting#LongChatInvite> ;
178+ ${ this . ns . rdf ( 'seeAlso' ) } <${ chatThing . value } > .
179+ `
180+
181+ const inviteResponse = await this . store . fetcher . webOperation ( 'POST' , inviteeInbox . value , {
182+ data : inviteBody ,
183+ contentType : 'text/turtle'
184+ } )
185+ const locationStr = inviteResponse . headers . get ( 'location' )
186+ if ( ! locationStr ) {
187+ throw new Error ( `Invite sending returned a ${ inviteResponse . status } ` )
188+ }
189+ }
190+
191+ }
0 commit comments