Remove groups API from PersonaStore
[platform/upstream/folks.git] / backends / telepathy / tpf-persona-store.vala
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  *
4  * This library is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation, either version 2.1 of the License, or
7  * (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this library.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  * Authors:
18  *       Travis Reitter <travis.reitter@collabora.co.uk>
19  */
20
21 using GLib;
22 using Gee;
23 using Tp;
24 using Tp.ContactFeature;
25 using Folks;
26
27 public class Tpf.PersonaStore : Folks.PersonaStore
28 {
29   private string[] undisplayed_groups = { "publish", "stored", "subscribe" };
30
31   private HashTable<string, Persona> _personas;
32   /* universal, contact owner handles (not channel-specific) */
33   private HashMap<uint, Persona> handle_persona_map;
34   private HashMap<Channel, HashSet<Persona>> channel_group_personas_map;
35   private HashMap<Channel, HashSet<uint>> channel_group_incoming_adds;
36   private HashMap<string, HashSet<Tpf.Persona>> group_outgoing_adds;
37   private HashMap<string, HashSet<Tpf.Persona>> group_outgoing_removes;
38   private HashMap<string, Channel> standard_channels_unready;
39   private HashMap<string, Channel> group_channels_unready;
40   private HashMap<string, Channel> groups;
41   private Channel publish;
42   private Channel stored;
43   private Channel subscribe;
44   private Connection conn;
45   private TpLowlevel ll;
46   private AccountManager account_manager;
47
48   internal signal void group_members_changed (string group,
49       GLib.List<Persona>? added, GLib.List<Persona>? removed);
50   internal signal void group_removed (string group, GLib.Error? error);
51
52   [Property(nick = "basis account",
53       blurb = "Telepathy account this store is based upon")]
54   public Account account { get; construct; }
55   public override string type_id { get; private set; }
56   public override string id { get; private set; }
57   public override HashTable<string, Persona> personas
58     {
59       get { return this._personas; }
60     }
61
62   public PersonaStore (Account account)
63     {
64       Object (account: account);
65
66       this.type_id = "telepathy";
67       this.id = account.get_object_path (account);
68
69       this._personas = new HashTable<string, Persona> (str_hash,
70           str_equal);
71       this.conn = null;
72       this.handle_persona_map = new HashMap<uint, Persona> ();
73       this.channel_group_personas_map = new HashMap<Channel, HashSet<Persona>> (
74           );
75       this.channel_group_incoming_adds = new HashMap<Channel, HashSet<uint>> ();
76       this.group_outgoing_adds = new HashMap<string, HashSet<Tpf.Persona>> ();
77       this.group_outgoing_removes = new HashMap<string, HashSet<Tpf.Persona>> (
78           );
79       this.publish = null;
80       this.stored = null;
81       this.subscribe = null;
82       this.standard_channels_unready = new HashMap<string, Channel> ();
83       this.group_channels_unready = new HashMap<string, Channel> ();
84       this.groups = new HashMap<string, Channel> ();
85       this.ll = new TpLowlevel ();
86       this.account_manager = AccountManager.dup ();
87
88       this.account_manager.account_disabled.connect ((a) =>
89         {
90           if (this.account == a)
91             this.removed ();
92         });
93       this.account_manager.account_removed.connect ((a) =>
94         {
95           if (this.account == a)
96             this.removed ();
97         });
98       this.account_manager.account_validity_changed.connect ((a, valid) =>
99         {
100           if (!valid && this.account == a)
101             this.removed ();
102         });
103
104       this.account.status_changed.connect (this.account_status_changed_cb);
105
106       Tp.ConnectionStatusReason reason;
107       var status = this.account.get_connection_status (out reason);
108       /* immediately handle accounts which are not currently being disconnected
109        */
110       if (status != Tp.ConnectionStatus.DISCONNECTED)
111         {
112           this.account_status_changed_cb (Tp.ConnectionStatus.DISCONNECTED,
113               status, reason, null, null);
114         }
115     }
116
117   private void account_status_changed_cb (ConnectionStatus old_status,
118       ConnectionStatus new_status, ConnectionStatusReason reason,
119       string? dbus_error_name, GLib.HashTable? details)
120     {
121       if (new_status != Tp.ConnectionStatus.CONNECTED)
122         return;
123
124       var conn = this.account.get_connection ();
125       conn.call_when_ready (this.connection_ready_cb);
126     }
127
128   private void connection_ready_cb (Connection conn, GLib.Error? error)
129     {
130       this.ll.connection_connect_to_new_group_channels (conn,
131           this.new_group_channels_cb);
132
133       this.add_standard_channel (conn, "publish");
134       this.add_standard_channel (conn, "stored");
135       this.add_standard_channel (conn, "subscribe");
136       this.conn = conn;
137     }
138
139   private void new_group_channels_cb (void *data)
140     {
141       var channel = (Channel) data;
142       if (channel == null)
143         {
144           warning ("error creating channel for NewChannels signal");
145           return;
146         }
147
148       this.set_up_new_group_channel (channel);
149       this.channel_group_changes_resolve (channel);
150     }
151
152   private void channel_group_changes_resolve (Channel channel)
153     {
154       var group = channel.get_identifier ();
155
156       var change_maps = new HashMap<HashSet<Tpf.Persona>, bool> ();
157       if (this.group_outgoing_adds[group] != null)
158         change_maps.set (this.group_outgoing_adds[group], true);
159
160       if (this.group_outgoing_removes[group] != null)
161         change_maps.set (this.group_outgoing_removes[group], false);
162
163       if (change_maps.size < 1)
164         return;
165
166       foreach (var entry in change_maps)
167         {
168           var changes = entry.key;
169
170           foreach (var persona in changes)
171             {
172               try
173                 {
174                   this.ll.channel_group_change_membership (channel,
175                       (Handle) persona.contact.handle, entry.value);
176                 }
177               catch (GLib.Error e)
178                 {
179                   warning ("failed to change persona %s group %s membership to "
180                       + "%s",
181                       persona.uid, group, entry.value ? "true" : "false");
182                 }
183             }
184
185           changes.clear ();
186         }
187     }
188
189   private void set_up_new_standard_channel (Channel channel)
190     {
191       /* hold a ref to the channel here until it's ready, so it doesn't
192        * disappear */
193       this.standard_channels_unready[channel.get_identifier ()] = channel;
194
195       channel.notify["channel-ready"].connect ((s, p) =>
196         {
197           var c = (Channel) s;
198           var name = c.get_identifier ();
199
200           if (name == "publish")
201             {
202               this.publish = c;
203
204               c.group_members_changed.connect (
205                   this.publish_channel_group_members_changed_cb);
206             }
207           else if (name == "stored")
208             {
209               this.stored = c;
210
211               c.group_members_changed.connect (
212                   this.stored_channel_group_members_changed_cb);
213             }
214           else if (name == "subscribe")
215             {
216               this.subscribe = c;
217
218               c.group_members_changed.connect (
219                   this.subscribe_channel_group_members_changed_cb);
220             }
221
222           this.standard_channels_unready.remove (name);
223
224           c.invalidated.connect (this.channel_invalidated_cb);
225
226           unowned IntSet members = c.group_get_members ();
227           if (members != null)
228             {
229               this.channel_group_pend_incoming_adds (c, members.to_array (),
230                   true);
231             }
232         });
233     }
234
235   private void publish_channel_group_members_changed_cb (Channel channel,
236       string message,
237       /* FIXME: Array<uint> => Array<Handle>; parser bug */
238       Array<uint>? added,
239       Array<uint>? removed,
240       Array<uint>? local_pending,
241       Array<uint>? remote_pending,
242       uint actor,
243       uint reason)
244     {
245       if (added != null)
246         this.channel_group_pend_incoming_adds (channel, added, true);
247
248       /* we refuse to send these contacts our presence, so remove them */
249       for (var i = 0; i < removed.length; i++)
250         {
251           var handle = removed.index (i);
252           this.ignore_by_handle_if_needed (handle);
253         }
254
255       /* FIXME: continue for the other arrays */
256     }
257
258   private void stored_channel_group_members_changed_cb (Channel channel,
259       string message,
260       /* FIXME: Array<uint> => Array<Handle>; parser bug */
261       Array<uint>? added,
262       Array<uint>? removed,
263       Array<uint>? local_pending,
264       Array<uint>? remote_pending,
265       uint actor,
266       uint reason)
267     {
268       if (added != null)
269         {
270           this.channel_group_pend_incoming_adds (channel, added, true);
271         }
272
273       for (var i = 0; i < removed.length; i++)
274         {
275           var handle = removed.index (i);
276           this.ignore_by_handle_if_needed (handle);
277         }
278     }
279
280   private void subscribe_channel_group_members_changed_cb (Channel channel,
281       string message,
282       /* FIXME: Array<uint> => Array<Handle>; parser bug */
283       Array<uint>? added,
284       Array<uint>? removed,
285       Array<uint>? local_pending,
286       Array<uint>? remote_pending,
287       uint actor,
288       uint reason)
289     {
290       if (added != null)
291         {
292           this.channel_group_pend_incoming_adds (channel, added, true);
293
294           /* expose ourselves to anyone we can see */
295           if (this.publish != null)
296             {
297               this.channel_group_pend_incoming_adds (this.publish, added, true);
298             }
299         }
300
301       /* these contacts refused to send us their presence, so remove them */
302       for (var i = 0; i < removed.length; i++)
303         {
304           var handle = removed.index (i);
305           this.ignore_by_handle_if_needed (handle);
306         }
307
308       /* FIXME: continue for the other arrays */
309     }
310
311   private void channel_invalidated_cb (Proxy proxy, uint domain, int code,
312       string message)
313     {
314       var channel = (Channel) proxy;
315
316       this.channel_group_personas_map.remove (channel);
317       this.channel_group_incoming_adds.remove (channel);
318
319       if (proxy == this.publish)
320         this.publish = null;
321       else if (proxy == this.subscribe)
322         this.subscribe = null;
323       else
324         {
325           var error = new GLib.Error ((Quark) domain, code, "%s", message);
326           var name = channel.get_identifier ();
327           this.group_removed (name, error);
328           this.groups.remove (name);
329         }
330     }
331
332   private void ignore_by_handle_if_needed (uint handle)
333     {
334       unowned Tp.IntSet members;
335
336       if (this.subscribe != null)
337         {
338           members = this.subscribe.group_get_members ();
339           if (members.is_member (handle))
340             return;
341
342           members = this.subscribe.group_get_remote_pending ();
343           if (members.is_member (handle))
344             return;
345         }
346
347       if (this.publish != null)
348         {
349           members = this.publish.group_get_members ();
350           if (members.is_member (handle))
351             return;
352         }
353
354       var persona = this.handle_persona_map[handle];
355       this.ignore_persona (persona);
356     }
357
358   private void ignore_persona (Tpf.Persona? persona)
359     {
360       if (persona == null)
361         return;
362
363       foreach (var entry in this.channel_group_incoming_adds)
364         {
365           var channel = (Channel) entry.key;
366           var members = this.channel_group_personas_map[channel];
367           if (members != null)
368             members.remove (persona);
369         }
370
371       foreach (var entry in this.group_outgoing_adds)
372         {
373           var name = (string) entry.key;
374           var members = this.group_outgoing_adds[name];
375           if (members != null)
376             members.remove (persona);
377         }
378
379       var personas = new GLib.List<Persona> ();
380       personas.append (persona);
381       this.personas_removed (personas);
382       this._personas.remove (persona.iid);
383     }
384
385   /**
386    * Remove the given persona from the server entirely
387    */
388   public override void remove_persona (Folks.Persona persona)
389     {
390       var tp_persona = (Tpf.Persona) persona;
391
392       try
393         {
394           this.ll.channel_group_change_membership (this.stored,
395               (Handle) tp_persona.contact.handle, false);
396         }
397       catch (GLib.Error e)
398         {
399           warning ("failed to remove persona '%s' (%s) from stored list: %s",
400               tp_persona.uid, tp_persona.alias, e.message);
401         }
402
403       try
404         {
405           this.ll.channel_group_change_membership (this.subscribe,
406               (Handle) tp_persona.contact.handle, false);
407         }
408       catch (GLib.Error e)
409         {
410           warning ("failed to remove persona '%s' (%s) from subscribe list: %s",
411               tp_persona.uid, tp_persona.alias, e.message);
412         }
413
414       try
415         {
416           this.ll.channel_group_change_membership (this.publish,
417               (Handle) tp_persona.contact.handle, false);
418         }
419       catch (GLib.Error e)
420         {
421           warning ("failed to remove persona '%s' (%s) from publish list: %s",
422               tp_persona.uid, tp_persona.alias, e.message);
423         }
424
425       var personas = new GLib.List<Persona> ();
426       personas.append (tp_persona);
427       this.personas_removed (personas);
428     }
429
430   /* Only non-group contact list channels should use create_personas == true,
431    * since the exposed set of Personas are meant to be filtered by them */
432   private void channel_group_pend_incoming_adds (Channel channel,
433       Array<uint> adds,
434       bool create_personas)
435     {
436       var adds_length = adds != null ? adds.length : 0;
437       if (adds_length >= 1)
438         {
439           /* this won't complete before we would add the personas to the group,
440            * so we have to buffer the contact handles below */
441           if (create_personas)
442             this.create_personas_from_channel_handles_async (channel, adds);
443
444           for (var i = 0; i < adds.length; i++)
445             {
446               var channel_handle = (Handle) adds.index (i);
447               var contact_handle = channel.group_get_handle_owner (
448                 channel_handle);
449               var persona = this.handle_persona_map[contact_handle];
450               if (persona == null)
451                 {
452                   HashSet<uint>? contact_handles =
453                       this.channel_group_incoming_adds[channel];
454                   if (contact_handles == null)
455                     {
456                       contact_handles = new HashSet<uint> ();
457                       this.channel_group_incoming_adds[channel] =
458                           contact_handles;
459                     }
460                   contact_handles.add (contact_handle);
461                 }
462             }
463         }
464
465       this.channel_groups_add_new_personas ();
466     }
467
468   private void set_up_new_group_channel (Channel channel)
469     {
470       /* hold a ref to the channel here until it's ready, so it doesn't
471        * disappear */
472       this.group_channels_unready[channel.get_identifier ()] = channel;
473
474       channel.notify["channel-ready"].connect ((s, p) =>
475         {
476           var c = (Channel) s;
477           var name = c.get_identifier ();
478
479           this.groups[name] = c;
480           this.group_channels_unready.remove (name);
481
482           c.invalidated.connect (this.channel_invalidated_cb);
483           c.group_members_changed.connect (
484             this.group_channel_group_members_changed_cb);
485
486           unowned IntSet members = c.group_get_members ();
487           if (members != null)
488             {
489               this.channel_group_pend_incoming_adds (c, members.to_array (),
490                 false);
491             }
492         });
493     }
494
495   private void group_channel_group_members_changed_cb (Channel channel,
496       string message,
497       /* FIXME: Array<uint> => Array<Handle>; parser bug */
498       Array<uint>? added,
499       Array<uint>? removed,
500       Array<uint>? local_pending,
501       Array<uint>? remote_pending,
502       uint actor,
503       uint reason)
504     {
505       if (added != null)
506         this.channel_group_pend_incoming_adds (channel, added, false);
507
508       /* FIXME: continue for the other arrays */
509     }
510
511   internal async void change_group_membership (Folks.Persona persona,
512       string group, bool is_member)
513     {
514       var tp_persona = (Tpf.Persona) persona;
515       var channel = this.groups[group];
516       var change_map = is_member ? this.group_outgoing_adds :
517         this.group_outgoing_removes;
518       var change_set = change_map[group];
519
520       if (change_set == null)
521         {
522           change_set = new HashSet<Tpf.Persona> ();
523           change_map[group] = change_set;
524         }
525       change_set.add (tp_persona);
526
527       if (channel == null)
528         {
529           /* the changes queued above will be resolve in the NewChannels handler
530            */
531           this.ll.connection_create_group_async (this.account.get_connection (),
532               group);
533         }
534       else
535         {
536           /* the channel is already ready, so resolve immediately */
537           this.channel_group_changes_resolve (channel);
538         }
539     }
540
541   private void change_standard_contact_list_membership (Tp.Channel channel,
542       Folks.Persona persona, bool is_member)
543     {
544       var tp_persona = (Tpf.Persona) persona;
545
546       try
547         {
548           this.ll.channel_group_change_membership (channel,
549               (Handle) tp_persona.contact.handle, is_member);
550         }
551       catch (GLib.Error e)
552         {
553           warning ("failed to change persona %s contact list %s " +
554               "membership to %s",
555               persona.uid, channel.get_identifier (),
556               is_member ? "true" : "false");
557         }
558     }
559
560   private async Channel? add_standard_channel (Connection conn, string name)
561     {
562       Channel? channel = null;
563
564       /* FIXME: handle the error GLib.Error from this function */
565       try
566         {
567           channel = yield this.ll.connection_open_contact_list_channel_async (
568               conn, name);
569         }
570       catch (GLib.Error e)
571         {
572           warning ("failed to add channel '%s': %s\n", name, e.message);
573
574           /* XXX: assuming there's no decent way to recover from this */
575
576           return null;
577         }
578
579       this.set_up_new_standard_channel (channel);
580
581       return channel;
582     }
583
584   /* FIXME: Array<uint> => Array<Handle>; parser bug */
585   private void create_personas_from_channel_handles_async (Channel channel,
586       Array<uint> channel_handles)
587     {
588       ContactFeature[] features =
589         {
590           ALIAS,
591           /* XXX: also avatar token? */
592           PRESENCE
593         };
594
595       Handle[] contact_handles = {};
596       for (var i = 0; i < channel_handles.length; i++)
597         {
598           var channel_handle = (Handle) channel_handles.index (i);
599           var contact_handle = channel.group_get_handle_owner (channel_handle);
600
601           if (this.handle_persona_map[contact_handle] == null)
602             contact_handles += contact_handle;
603         }
604
605       /* FIXME: we have to use 'this' as the weak object because the
606         * weak object gets passed into the underlying callback as the
607         * object instance; there may be a way to fix this with the
608         * instance_pos directive, but I couldn't get it to work */
609       if (contact_handles.length > 0)
610         this.conn.get_contacts_by_handle (contact_handles, features,
611             this.get_contacts_by_handle_cb, this);
612     }
613
614   private void get_contacts_by_handle_cb (Connection connection,
615       uint n_contacts,
616       [CCode (array_length = false)]
617       Contact[] contacts,
618       uint n_failed,
619       [CCode (array_length = false)]
620       Handle[] failed,
621       GLib.Error error,
622       GLib.Object weak_object)
623     {
624       if (n_failed >= 1)
625         warning ("failed to retrieve contacts for handles:");
626
627       for (var i = 0; i < n_failed; i++)
628         {
629           Handle h = failed[i];
630           warning ("    %u", (uint) h);
631         }
632
633       /* we have to manually pass the length since we don't get it */
634       this.add_new_personas_from_contacts (contacts, n_contacts);
635     }
636
637   private async GLib.List<Tpf.Persona>? create_personas_from_contact_ids (
638       string[] contact_ids) throws GLib.Error
639     {
640       ContactFeature[] features =
641         {
642           ALIAS,
643           /* XXX: also avatar token? */
644           PRESENCE
645         };
646
647       if (contact_ids.length > 0)
648         {
649           unowned GLib.List<Tp.Contact> contacts =
650               yield this.ll.connection_get_contacts_by_id_async (
651                   this.conn, contact_ids, features);
652
653           GLib.List<Persona> personas = new GLib.List<Persona> ();
654           uint err_count = 0;
655           string err_format = "";
656           unowned GLib.List<Tp.Contact> l;
657           for (l = contacts; l != null; l = l.next)
658             {
659               var contact = l.data;
660               try
661                 {
662                   var persona = new Tpf.Persona (contact, this);
663                   personas.prepend (persona);
664                 }
665               catch (Tp.Error e)
666                 {
667                   if (err_count == 0)
668                     err_format = "failed to create %u personas:\n";
669
670                   err_format = "%s        '%s' (%p): %s\n".printf (
671                     err_format, contact.alias, contact, e.message);
672                   err_count++;
673                 }
674             }
675
676           if (err_count > 0)
677             {
678               throw new Folks.PersonaStoreError.CREATE_FAILED (err_format,
679                   err_count);
680             }
681
682           return personas;
683         }
684
685       return null;
686     }
687
688   private void add_new_personas_from_contacts (Contact[] contacts,
689       uint n_contacts)
690     {
691       var personas_new = new HashTable<string, Persona> (str_hash, str_equal);
692       for (var i = 0; i < n_contacts; i++)
693         {
694           var contact = contacts[i];
695
696           try
697             {
698               var persona = new Tpf.Persona (contact, this);
699               if (this._personas.lookup (persona.iid) == null)
700                 {
701                   personas_new.insert (persona.iid, persona);
702
703                   this._personas.insert (persona.iid, persona);
704                   this.handle_persona_map[contact.get_handle ()] = persona;
705                 }
706             }
707           catch (Tp.Error e)
708             {
709               warning ("failed to create persona from contact '%s' (%p)",
710                   contact.alias, contact);
711             }
712         }
713
714       this.channel_groups_add_new_personas ();
715
716       if (personas_new.size () >= 1)
717         {
718           GLib.List<Persona> personas = personas_new.get_values ();
719           this.personas_added (personas);
720         }
721     }
722
723   private void channel_groups_add_new_personas ()
724     {
725       foreach (var entry in this.channel_group_incoming_adds)
726         {
727           var channel = (Channel) entry.key;
728           var members_added = new GLib.List<Persona> ();
729
730           HashSet<Persona> members = this.channel_group_personas_map[channel];
731           if (members == null)
732             members = new HashSet<Persona> ();
733
734           var contact_handles = entry.value;
735           if (contact_handles != null && contact_handles.size > 0)
736             {
737               var contact_handles_added = new HashSet<uint> ();
738               foreach (var contact_handle in contact_handles)
739                 {
740                   var persona = this.handle_persona_map[contact_handle];
741                   if (persona != null)
742                     {
743                       members.add (persona);
744                       members_added.prepend (persona);
745                       contact_handles_added.add (contact_handle);
746                     }
747                 }
748
749               foreach (var handle in contact_handles_added)
750                 contact_handles.remove (handle);
751             }
752
753           if (members.size > 0)
754             this.channel_group_personas_map[channel] = members;
755
756           var name = channel.get_identifier ();
757           if (this.group_is_display_group (name) &&
758               members_added.length () > 0)
759             {
760               members_added.reverse ();
761               this.group_members_changed (name, members_added, null);
762             }
763         }
764     }
765
766   private bool group_is_display_group (string group)
767     {
768       for (var i = 0; i < this.undisplayed_groups.length; i++)
769         {
770           if (this.undisplayed_groups[i] == group)
771             return false;
772         }
773
774       return true;
775     }
776
777   public override async Folks.Persona? add_persona_from_details (
778       HashTable<string, string> details) throws Folks.PersonaStoreError
779     {
780       var contact_id = details.lookup ("contact");
781       if (contact_id == null)
782         {
783           throw new PersonaStoreError.INVALID_ARGUMENT (
784               "persona store (%s, %s) requires the following details:\n" +
785               "    contact (provided: '%s')\n",
786               this.type_id, this.id, contact_id);
787         }
788
789       string[] contact_ids = new string[1];
790       contact_ids[0] = contact_id;
791
792       try
793         {
794           var personas = yield create_personas_from_contact_ids (
795               contact_ids);
796
797           if (personas != null && personas.length () == 1)
798             {
799               var persona = personas.data;
800
801               if (this.subscribe != null)
802                 change_standard_contact_list_membership (subscribe, persona,
803                     true);
804
805               if (this.publish != null)
806                 {
807                   var flags = publish.group_get_flags ();
808                   if ((flags & ChannelGroupFlags.CAN_ADD) ==
809                       ChannelGroupFlags.CAN_ADD)
810                     {
811                       change_standard_contact_list_membership (publish, persona,
812                           true);
813                     }
814                 }
815
816               return persona;
817             }
818           else
819             {
820               warning ("requested a single persona, but got %u back",
821                   personas == null ? 0 : personas.length ());
822             }
823         }
824       catch (GLib.Error e)
825         {
826           warning ("failed to add a persona from details: %s", e.message);
827         }
828
829       return null;
830     }
831 }