1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
|
;;; rudel-obby-client.el --- Client functions of the Rudel obby backend
;;
;; Copyright (C) 2008, 2009 Jan Moringen
;;
;; Author: Jan Moringen <scymtym@users.sourceforge.net>
;; Keywords: Rudel, obby, backend, client
;; X-RCS: $Id:$
;;
;; This file is part of Rudel.
;;
;; Rudel is free software: you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; Rudel is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with Rudel. If not, see <http://www.gnu.org/licenses>.
;;; Commentary:
;;
;; This file contains the client part of the obby backend.
;;; History:
;;
;; 0.2 - State machine.
;;
;; 0.1 - Initial revision.
;;; Code:
;;
(require 'eieio)
(require 'jupiter)
(require 'rudel-state-machine)
(require 'rudel-operations)
(require 'rudel-chat)
(require 'rudel-obby-errors)
(require 'rudel-obby-util)
(require 'rudel-obby-state)
;;; Class rudel-obby-client-state-new
;;
(defclass rudel-obby-client-state-new
(rudel-obby-client-connection-state)
()
"Start state of newly established connections.")
(defmethod rudel-obby/obby_welcome
((this rudel-obby-client-state-new) version)
"Handle obby 'welcome' message."
;; Examine announced protocol version.
(with-parsed-arguments ((version number))
(message "Received Obby welcome message (version %d)" version))
;; Start encryption handshake
'encryption-negotiate)
;;; Class rudel-obby-client-state-encryption-negotiate
;;
(defclass rudel-obby-client-state-encryption-negotiate
(rudel-obby-client-connection-state)
()
"Start state of the encryption handshake.")
(defmethod rudel-obby/net6_encryption
((this rudel-obby-client-state-encryption-negotiate) value)
"Handle net6 'encryption' message."
(rudel-send this "net6_encryption_ok")
'encryption-start)
;;; Class rudel-obby-client-connection-encryption-start
;;
(defclass rudel-obby-client-state-encryption-start
(rudel-obby-client-connection-state)
()
"Second state of the encryption handshake.")
(defmethod rudel-obby/net6_encryption_begin
((this rudel-obby-client-state-encryption-start))
"Handle net6 'encryption_begin' message."
;; Start TLS encryption for the connection.
(with-slots (connection) this
(with-slots (socket) connection
(when (rudel-process-object socket :supports-tls)
(rudel-tls-start-tls socket)
(sit-for 1))))
;; The connection is now established
'joining)
(defmethod rudel-obby/net6_encryption_failed
((this rudel-obby-client-state-encryption-start))
"Handle net6 'encryption_failed' message."
;; The connection is now established; without encryption though
'joining)
;;; Class rudel-obby-client-state-joining
;;
(defclass rudel-obby-client-state-joining
(rudel-obby-client-connection-state)
()
"First state after the connection has been properly set up.")
(defmethod rudel-enter ((this rudel-obby-client-state-joining))
"When entering this state, send a login request."
;; Send login request with username and color. This can easily fail
;; (resulting in response 'net6_login_failed') if the username or
;; color is already taken.
(with-slots (info) (oref this connection)
(let ((username (plist-get info :username))
(color (plist-get info :color))
(global-password (plist-get info :global-password))
(user-password (plist-get info :user-password)))
(apply #'rudel-send
this
"net6_client_login"
username (rudel-obby-format-color color)
(append (when global-password
(list global-password))
(when (and global-password user-password)
(list user-password))))))
nil)
(defmethod rudel-obby/obby_sync_init
((this rudel-obby-client-state-joining) count)
"Handle obby 'sync_init' message."
;; Switch to 'synching' state, passing the number of synchronization
;; items.
(with-parsed-arguments ((count number))
(list 'session-synching count)))
(defmethod rudel-obby/net6_login_failed
((this rudel-obby-client-state-joining) reason)
"Handle net6 'login_failed' message."
(with-parsed-arguments ((reason number))
(with-slots (connection) this
(let ((error-data
(cond
;; Invalid username
((= reason rudel-obby-error-username-invalid)
(cons 'rudel-obby-username-invalid nil))
;; Username in use
((= reason rudel-obby-error-username-in-use)
(cons 'rudel-obby-username-in-use nil))
;; Color in use
((= reason rudel-obby-error-color-in-use)
(cons 'rudel-obby-color-in-use nil))
;; Wrong global password
((= reason rudel-obby-error-wrong-global-password)
(cons 'rudel-obby-wrong-global-password nil))
;; Wrong user password
((= reason rudel-obby-error-wrong-user-password)
(cons 'rudel-obby-wrong-user-password nil))
;; Otherwise, signal a generic join error
(t (cons 'rudel-join-error nil)))))
;; Switch to 'join-failed' state, pass the error data.
(list 'join-failed error-data))))
)
;;; Class rudel-obby-client-state-join-failed
;;
(defclass rudel-obby-client-state-join-failed
(rudel-obby-client-connection-state)
((error-symbol :initarg :error-symbol
:type symbol
:documentation
"Error symbol describing the reason for the
login failure.")
(error-data :initarg :error-data
:type list
:documentation
"Additional error data describing the login
failure."))
"State for failed login attempts.")
(defmethod rudel-enter ((this rudel-obby-client-state-join-failed)
error)
"When the state is entered, store the error data passed in ERROR."
(with-slots (error-symbol error-data) this
(setq error-symbol (car error)
error-data (cdr error)))
nil)
;;; Class rudel-obby-client-state idle
;;
(defclass rudel-obby-client-state-idle
(rudel-obby-client-connection-state
rudel-obby-document-handler)
()
"Default state of the connection.")
(defmethod rudel-obby/net6_client_join
((this rudel-obby-client-state-idle)
client-id name encryption user-id color)
"Handle net6 'client_join' message."
(with-parsed-arguments ((client-id number)
(user-id number)
(color color))
(with-slots (connection) this
(with-slots (session) connection
(let ((user (rudel-find-user session user-id
#'eq #'rudel-id)))
(if user
;; If we have such a user object, update its state.
(with-slots ((client-id1 client-id)
(color1 color)
connected
(encryption1 encryption)) user
(setq client-id1 client-id
color1 color
connected t
encryption1 (string= encryption "1"))
;; Run the change hook of the user object.
(object-run-hook-with-args user 'change-hook))
;; Otherwise, create a new user object.
(let ((user (rudel-obby-user
name
:client-id client-id
:user-id user-id
:connected t
:encryption (string= encryption "1")
:color color)))
(rudel-add-user session user))))))
(message "Client joined: %s %s" name color))
nil)
(defmethod rudel-obby/net6_client_part
((this rudel-obby-client-state-idle) client-id)
"Handle net6 'client_part' message."
;; Find the user object, associated to the client id. Remove the
;; client id and change the user's state to disconnected.
(with-parsed-arguments ((client-id number))
(with-slots (connection) this
(with-slots (session) connection
(let ((user (rudel-find-user session client-id
#'eql #'rudel-client-id)))
(if user
(with-slots (client-id connected) user
;; Set slot values.
(setq client-id nil
connected nil)
;; Run the change hook of the user object.
(object-run-hook-with-args user 'change-hook))
(display-warning
'(rudel obby)
(format "Cannot find user for client id: %d"
client-id)
:warning))))))
nil)
(defmethod rudel-obby/obby_user_colour
((this rudel-obby-client-state-idle) user-id color)
"Handle obby 'user_colour' message."
(with-parsed-arguments ((user-id number)
(color color))
;; Find user object and set color.
(with-slots (connection) this
(with-slots (session) connection
(let ((user (rudel-find-user session user-id
#'= #'rudel-id)))
(with-slots ((name :object-name) (color1 :color)) user
;; Set color in user object.
(setq color1 color)
;; Run the change hook of the user object.
(object-run-hook-with-args user 'change-hook)
;; Update overlays.
(rudel-overlay-set-face-attributes
(rudel-overlay-make-face-symbol 'author name)
color1))))))
nil)
(defmethod rudel-obby/obby_document_create
((this rudel-obby-client-state-idle)
owner-id doc-id name suffix encoding)
"Handle obby 'document_create' message."
(with-parsed-arguments ((owner-id number)
(doc-id number)
(suffix number)
(encoding coding-system))
(with-slots (connection) this
(with-slots (session) connection
(let ((owner (rudel-find-user session owner-id
#'= #'rudel-id)))
(rudel-add-document session (rudel-obby-document
name
:subscribed (list owner)
:id doc-id
:owner-id owner-id
:suffix suffix))))
(message "New document: %s" name)))
nil)
(defmethod rudel-obby/obby_document_remove
((this rudel-obby-client-state-idle) doc-id)
"Handle obby 'document_remove' message."
(with-parsed-arguments ((doc-id document-id))
(with-slots (connection) this
(with-slots (session) connection
(let ((document (rudel-find-document
session doc-id
#'equal #'rudel-both-ids)))
(if document
(progn
(rudel-remove-document session document)
(with-slots ((name :object-name)) document
(message "Document removed: %s" name)))
(display-warning
'(rudel obby)
(format "Document not found: %s" doc-id)
:warning))))))
nil)
(defmethod rudel-obby/obby_document/rename
((this rudel-obby-client-state-idle)
document user new-name new-suffix)
"Handle obby 'rename' submessage of the 'obby_document' message."
(with-parsed-arguments ((new-suffix number))
(with-slots ((name :object-name) suffix) document
(setq name new-name
suffix new-suffix)))
nil)
(defmethod rudel-obby/obby_document/subscribe
((this rudel-obby-client-state-idle)
document user-id)
"Handle 'subscribe' submessage of obby 'document' message."
(with-parsed-arguments ((user-id number))
(with-slots (connection) this
(with-slots (session) connection
(let ((user (rudel-find-user session user-id
#'= #'rudel-id)))
(rudel-add-user document user)))))
nil)
(defmethod rudel-obby/obby_document/unsubscribe
((this rudel-obby-client-state-idle)
document user-id)
"Handle 'unsubscribe' submessage of obby 'document' message."
(with-parsed-arguments ((user-id number))
(with-slots (connection) this
(with-slots (session) connection
(let ((user (rudel-find-user session user-id
#'= #'rudel-id)))
(rudel-remove-user document user)))))
nil)
(defmethod rudel-obby/obby_document/record
((this rudel-obby-client-state-idle)
document user-id local-revision remote-revision
action &rest arguments)
"Handle 'record' submessage of obby 'document' message."
(with-parsed-arguments ((user-id number)
(local-revision number)
(remote-revision number))
;; Locate the user.
(let ((user (with-slots (connection) this
(with-slots (session) connection
(rudel-find-user session user-id
#'= #'rudel-id)))))
(if user
(condition-case error
;; Try to dispatch
(rudel-dispatch
this "rudel-obby/obby_document/record/" action
(append (list document user local-revision remote-revision)
arguments))
;; Warn if we failed to locate or execute the
;; method. Return nil in this case, so we remain in the
;; current state.
(rudel-dispatch-error
(progn
(display-warning
'(rudel obby)
(format "%s: no method (%s: %s): `%s:%s'; arguments: %s"
(object-print this) (car error) (cdr error)
"rudel-obby/obby_document/record/" action arguments)
:debug)
nil)))
;; If we did not find the user, warn.
(progn
(display-warning
'(rudel obby)
(format "User not found: %d" user-id)
:warning)
nil))))
)
(defmethod rudel-obby/obby_document/record/ins
((this rudel-obby-client-state-idle)
document user local-revision remote-revision
position data)
"Handle 'ins' submessage of 'record' submessage of obby 'document' message."
(with-parsed-arguments ((position number))
(let ((operation (jupiter-insert
(format "insert-%d-%d"
remote-revision local-revision)
:from position
:data data)))
(with-slots (connection) this
(rudel-remote-operation connection
document user
remote-revision local-revision
operation))))
nil)
(defmethod rudel-obby/obby_document/record/del
((this rudel-obby-client-state-idle)
document user local-revision remote-revision
position length)
"Handle 'del' submessage of 'record' submessage of obby 'document' message."
(with-parsed-arguments ((position number)
(length number))
(let ((operation (jupiter-delete
(format "delete-%d-%d"
remote-revision local-revision)
:from position
:to (+ position length))))
(with-slots (connection) this
(rudel-remote-operation connection
document user
remote-revision local-revision
operation))))
nil)
(defmethod rudel-obby/obby_document/record/split
((this rudel-obby-client-state-idle)
document user local-revision remote-revision
&rest operations)
"Handle 'split' submessage of 'record' submessage of obby 'document' message."
(let ((operation (rudel-message->operation
(cons "split" operations)
local-revision remote-revision)))
(with-slots (connection) this
(rudel-remote-operation connection
document user
remote-revision local-revision
operation)))
nil)
(defmethod rudel-obby/obby_document/record/noop
((this rudel-obby-client-state-idle)
document user local-revision remote-revision)
"Handle 'noop' submessage of 'record' submessage of obby 'document' message."
(let ((operation (jupiter-nop
(format "nop-%d-%d"
remote-revision local-revision))))
(with-slots (connection) this
(rudel-remote-operation connection
document user
remote-revision local-revision
operation)))
nil)
(defmethod rudel-obby/obby_message ((this rudel-obby-client-state-idle)
sender text)
"Handle obby 'message' message"
(with-parsed-arguments ((sender number))
(with-slots (session) (oref this :connection)
(let ((sender (rudel-find-user session sender #'eq #'rudel-id)))
(rudel-chat-dispatch-message sender text))))
nil)
;;; Class rudel-obby-client-state-session-synching
;;
(defclass rudel-obby-client-state-session-synching
(rudel-obby-client-connection-state)
((all-items :initarg :all-items
:type (integer 0)
:documentation
"Total number of synchronization items expected
to receive from the server.")
(remaining-items :initarg :remaining-items
:type (integer 0)
:documentation
"Number of synchronization items not yet
received from the server.")
(have-self :initarg :have-self
:type boolean
:documentation
"Flag that remembers, whether the session has
a 'self' user object."))
"State used for synching session data.")
(defmethod rudel-enter ((this rudel-obby-client-state-session-synching)
num-items)
"When entering state, store number of expected items."
(with-slots (all-items remaining-items have-self) this
(setq all-items num-items
remaining-items num-items
have-self nil))
nil)
(defmethod rudel-obby/net6_client_join
((this rudel-obby-client-state-session-synching)
client-id name encryption user-id color)
"Handle net6 'client_join' message."
(with-parsed-arguments ((client-id number)
(user-id number)
(color color))
(with-slots (connection remaining-items have-self) this
(with-slots (session) connection
;; Construct user object and add it to the session.
(let ((user (rudel-obby-user
name
:client-id client-id
:user-id user-id
:connected t
:encryption (string= encryption "1")
:color color)))
(rudel-add-user session user)
;; The first user object describes the user of this client.
(unless have-self
(with-slots (self) session
(setq self user
have-self t)))))
;; Decrease number of not yet received synchronization items.
(decf remaining-items)))
nil)
(defmethod rudel-obby/obby_sync_usertable_user
((this rudel-obby-client-state-session-synching) user-id name color)
"Handle obby 'sync_usertable_user' message."
(with-parsed-arguments ((user-id number)
(color color))
(with-slots (connection remaining-items) this
(with-slots (session) connection
(rudel-add-user session (rudel-obby-user
name
:user-id user-id
:connected nil
:color color)))
;; Decrease number of not yet received synchronization items.
(decf remaining-items)))
nil)
(defmethod rudel-obby/obby_sync_doclist_document
((this rudel-obby-client-state-session-synching)
owner-id doc-id name suffix encoding &rest subscribed-user-ids)
"Handle obby 'sync_doclist_document' message."
(with-parsed-arguments ((doc-id number)
(owner-id number)
(suffix number)
(encoding coding-system))
(with-slots (connection remaining-items) this
(with-slots (session) connection
;; Retrieve the subscribed users
(let ((subscribed-users
(mapcar
(lambda (user-id)
(with-parsed-arguments ((user-id number))
(rudel-find-user session user-id
#'= #'rudel-id)))
subscribed-user-ids)))
;; Make a new document with the list of subscribed users.
(rudel-add-document session (rudel-obby-document
name
:subscribed subscribed-users
:id doc-id
:owner-id owner-id
:suffix suffix))))
;; Decrease number of not yet received synchronization items.
(decf remaining-items)))
nil)
(defmethod rudel-obby/obby_sync_final
((this rudel-obby-client-state-session-synching))
"Handle obby 'sync_final' message."
'idle)
(defmethod object-print ((this rudel-obby-client-state-session-synching)
&rest strings)
"Append number of remaining items to string representation."
(with-slots (remaining-items) this
(call-next-method this (format " remaining: %d" remaining-items))))
;;; Class rudel-obby-client-state-subscribing
;;
(defclass rudel-obby-client-state-subscribing
(rudel-obby-client-connection-state
rudel-obby-document-handler)
((document :initarg :document
:type rudel-obby-document-child
:documentation
""))
"")
(defmethod rudel-enter ((this rudel-obby-client-state-subscribing)
user document)
"When entering this state, send a subscription request to the server."
(with-slots ((document1 :document)) this
(setq document1 document)
(with-slots ((doc-id :id) owner-id) document1
(with-slots (user-id) user
(rudel-send this "obby_document"
(format "%x %x" owner-id doc-id)
"subscribe"
(format "%x" user-id)))))
nil)
(defmethod rudel-obby/obby_document/sync_init
((this rudel-obby-client-state-subscribing)
document num-bytes)
"Handle obby 'sync_init' message."
(with-parsed-arguments ((num-bytes number))
(with-slots (documents) this
(if (= num-bytes 0)
'idle
(list 'document-synching document num-bytes))))
)
;;; Class rudel-obby-client-state-document-synching
;;
(defclass rudel-obby-client-state-document-synching
(rudel-obby-client-connection-state
rudel-obby-document-handler)
((document :initarg :document
:type rudel-obby-document-child
:documentation
"")
(all-bytes :initarg :all-bytes
:type (integer 0)
:documentation
"")
(remaining-bytes :initarg :remaining-bytes
:type (integer 0)
:documentation
""))
"")
(defmethod rudel-enter ((this rudel-obby-client-state-document-synching)
document num-bytes)
""
(with-slots ((document1 :document) all-bytes remaining-bytes) this
(setq document1 document
all-bytes num-bytes
remaining-bytes num-bytes))
nil)
(defmethod rudel-obby/obby_document/sync_chunk
((this rudel-obby-client-state-document-synching)
document data user-id)
"Handle obby 'sync_chunk' message."
(with-parsed-arguments ((user-id number))
(with-slots (connection remaining-bytes) this
(with-slots (session) connection
(let* ((user (unless (zerop user-id)
(rudel-find-user session user-id
#'= #'rudel-id)))
(operation (rudel-insert-op "bulk-insert"
:from nil
:data data)))
(rudel-remote-operation document user operation)))
;; After all bytes are transferred, go back to idle state.
(decf remaining-bytes (string-bytes data))
(if (= remaining-bytes 0)
'idle
nil)))
)
(defmethod object-print ((this rudel-obby-client-state-document-synching)
&rest strings)
"Append number of remaining items to string representation."
(with-slots (remaining-bytes) this
(call-next-method this (format " remaining: %d" remaining-bytes))))
;;; Class rudel-obby-client-state-they-finalized
;;
(defclass rudel-obby-client-state-they-finalized
(rudel-obby-client-connection-state)
()
"State used to indicate that the connection was closed by the peer.")
;;; Client connection states.
;;
(defvar rudel-obby-client-connection-states
'((new . rudel-obby-client-state-new)
(encryption-negotiate . rudel-obby-client-state-encryption-negotiate)
(encryption-start . rudel-obby-client-state-encryption-start)
(joining . rudel-obby-client-state-joining)
(join-failed . rudel-obby-client-state-join-failed)
(idle . rudel-obby-client-state-idle)
(session-synching . rudel-obby-client-state-session-synching)
(subscribing . rudel-obby-client-state-subscribing)
(document-synching . rudel-obby-client-state-document-synching)
(they-finalized . rudel-obby-client-state-they-finalized))
"Name symbols and classes of connection states.")
;;; Class rudel-obby-connection
;;
(defclass rudel-obby-connection (rudel-obby-socket-owner
rudel-connection
rudel-state-machine)
((info :initarg :info
:type list
:documentation
"Stores connection information for later use.")
(contexts :initarg :contexts
:type hash-table
:documentation
"Contains jupiter context objects for all
documents."))
"Class rudel-obby-connection ")
(defmethod initialize-instance ((this rudel-obby-connection) &rest slots)
;; Initialize slots of THIS
(when (next-method-p)
(call-next-method))
;; Create a new hash-table object to hold jupiter contexts
;; associated to documents.
(with-slots (contexts) this
(setq contexts (make-hash-table :test #'equal)))
;; Register states.
(rudel-register-states this rudel-obby-client-connection-states)
)
(defmethod rudel-register-state ((this rudel-obby-connection)
symbol state)
"Register SYMBOL and STATE and set connection slot of STATE."
;; Associate THIS connection to STATE.
(oset state :connection this)
;; Register STATE.
(when (next-method-p)
(call-next-method))
)
(defmethod rudel-disconnect ((this rudel-obby-connection))
""
(when (next-method-p)
(call-next-method)))
(defmethod rudel-close ((this rudel-obby-connection))
""
;; Move the state machine into an error state.
(rudel-switch this 'they-finalized)
;; Terminate the session.
(with-slots (session) this
(rudel-end session)))
(defmethod rudel-find-context ((this rudel-obby-connection) document)
"Return the jupiter context associated to DOCUMENT in THIS connection."
(with-slots (contexts) this
(gethash (oref document :id) contexts)))
(defmethod rudel-add-context ((this rudel-obby-connection) document)
"Add a jupiter context for DOCUMENT to THIS connection."
(with-slots (contexts) this
(with-slots ((doc-name :object-name) (doc-id :id)) document
(puthash doc-id
(jupiter-context (format "%s" doc-name))
contexts)))
)
(defmethod rudel-remove-context ((this rudel-obby-connection) document)
"Remove the jupiter context associated to DOCUMENT from THIS connection."
(with-slots (contexts) this
(remhash (oref document :id) contexts)))
(defmethod rudel-message ((this rudel-obby-connection) message)
"Dispatch MESSAGE to the current state of THIS object.
If the state has no suitable method, generate a warning, but do
nothing else."
;; Dispatch message to state.
(rudel-accept this message))
(defmethod rudel-change-color- ((this rudel-obby-connection) color)
""
(rudel-send this "obby_user_colour"
(rudel-obby-format-color color)))
(defmethod rudel-publish ((this rudel-obby-connection) document)
""
;; Create a new jupiter context for DOCUMENT.
(rudel-add-context this document)
;; Announce the new document to the server.
(with-slots ((name :object-name) id buffer) document
(rudel-send this "obby_document_create"
(format "%x" id)
name
"UTF-8"
(with-current-buffer buffer
(buffer-string))))
)
(defmethod rudel-unpublish ((this rudel-obby-connection) document)
"Remove DOCUMENT from the obby session THIS is connected to."
;; Request removal of DOCUMENT.
(with-slots ((doc-id :id) owner-id) document
(rudel-send this "obby_document_remove"
(format "%x %x" owner-id doc-id)))
;; Remove the jupiter context for DOCUMENT.
(rudel-remove-context this document)
)
(defmethod rudel-subscribe-to ((this rudel-obby-connection) document)
""
;; Create a new jupiter context for DOCUMENT.
(rudel-add-context this document)
;; Switch to subscribing state and wait until the state goes back to
;; idle.
(with-slots (session) this
(with-slots (self) session
(rudel-switch this 'subscribing self document)))
(lexical-let ((reporter (make-progress-reporter "Subscribing " 0.0 1.0)))
(flet ((display-progress (state)
(cond
;; Syncing document content, we can provide detailed progress.
((and (consp state)
(eq (car state) 'document-synching))
(with-slots (all-bytes remaining-bytes) (cdr state)
(progress-reporter-force-update
reporter
(- 1.0 (/ (float remaining-bytes) (float all-bytes)))
(format "Subscribing (%s) " (car state)))))
;; For other states, we just spin.
((consp state)
(progress-reporter-force-update
reporter 0.5
(format "Subscribing (%s) " (car state))))
;; Done
(t
(progress-reporter-force-update reporter 1.0 "Subscribing ")
(progress-reporter-done reporter)))))
(rudel-state-wait this '(idle) '(they-finalized) #'display-progress)))
;; We receive a notification of our own subscription from the
;; server. Consequently we do not add SELF to the list of subscribed
;; users of DOCUMENT.
)
(defmethod rudel-unsubscribe-from ((this rudel-obby-connection) document)
""
;; Delete the jupiter context for DOCUMENT.
(rudel-remove-context this document)
;; Announce the end of our subscription to the server.
(with-slots (session) this
(with-slots (user-id) (oref session :self)
(with-slots ((doc-id :id) owner-id) document
(rudel-send this "obby_document"
(format "%x %x" owner-id doc-id)
"unsubscribe"
(format "%x" user-id)))))
;; We receive a notification of the end of our own subscription from
;; the server. Consequently we do not remove SELF from the list of
;; subscribed users of DOCUMENT.
)
(defmethod rudel-local-insert ((this rudel-obby-connection)
document position data)
""
(rudel-local-operation
this
document
(jupiter-insert "insert" :from position :data data)))
(defmethod rudel-local-delete ((this rudel-obby-connection)
document position length)
""
(rudel-local-operation
this
document
(jupiter-delete "delete" :from position :to (+ position length))))
(defmethod rudel-local-operation ((this rudel-obby-connection)
document operation)
"Handle OPERATION performed on DOCUMENT by sending a message through THIS connection."
;; Convert character positions in OPERATION to byte positions, since
;; the obby protocol works with byte positions, but Emacs uses
;; character positions.
(with-slots (buffer) document
(rudel-obby-char->byte operation buffer))
;; Find jupiter context for DOCUMENT.
(let ((context (rudel-find-context this document)))
;; Notify the server of the operation.
(with-slots (owner-id (doc-id :id)) document
(with-slots (local-revision remote-revision) context
(apply #'rudel-send
this
"obby_document"
(format "%x %x" owner-id doc-id)
"record"
(format "%x" local-revision)
(format "%x" remote-revision)
(rudel-operation->message operation))))
;; Submit the operation to the jupiter context.
(jupiter-local-operation context operation))
)
(defmethod rudel-remote-operation ((this rudel-obby-connection)
document user
remote-revision local-revision
operation)
"Handle OPERATION received through THIS connection performed by USER on DOCUMENT."
(let* (;; Find jupiter context for DOCUMENT.
(context (rudel-find-context this document))
;; And transform the operation.
(transformed (jupiter-remote-operation
context
remote-revision local-revision
operation)))
;; Convert byte positions in OPERATION to character positions,
;; since the obby protocol works with byte positions, but Emacs
;; uses character positions.
(with-slots (buffer) document
(rudel-obby-byte->char transformed buffer)) ;; TODO operation's responsibility?
;; Apply the transformed operation to the document.
(rudel-remote-operation document user transformed))
)
(provide 'rudel-obby-client)
;;; rudel-obby-client.el ends here
|