1/* Part of SWISH 2 3 Author: Jan Wielemaker 4 E-mail: J.Wielemaker@cs.vu.nl 5 WWW: http://www.swi-prolog.org 6 Copyright (C): 2016-2018, VU University Amsterdam 7 CWI Amsterdam 8 All rights reserved. 9 10 Redistribution and use in source and binary forms, with or without 11 modification, are permitted provided that the following conditions 12 are met: 13 14 1. Redistributions of source code must retain the above copyright 15 notice, this list of conditions and the following disclaimer. 16 17 2. Redistributions in binary form must reproduce the above copyright 18 notice, this list of conditions and the following disclaimer in 19 the documentation and/or other materials provided with the 20 distribution. 21 22 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 POSSIBILITY OF SUCH DAMAGE. 34*/ 35 36:- module(swish_chat, 37 [ chat_broadcast/1, % +Message 38 chat_broadcast/2, % +Message, +Channel 39 chat_to_profile/2, % +ProfileID, :HTML 40 chat_about/2, % +DocID, +Message 41 42 notifications//1, % +Options 43 broadcast_bell//1 % +Options 44 ]). 45:- use_module(library(http/hub)). 46:- use_module(library(http/http_dispatch)). 47:- use_module(library(http/http_session)). 48:- use_module(library(http/http_parameters)). 49:- use_module(library(http/websocket)). 50:- use_module(library(http/json)). 51:- use_module(library(error)). 52:- use_module(library(lists)). 53:- use_module(library(option)). 54:- use_module(library(debug)). 55:- use_module(library(uuid)). 56:- use_module(library(random)). 57:- use_module(library(base64)). 58:- use_module(library(apply)). 59:- use_module(library(broadcast)). 60:- use_module(library(ordsets)). 61:- use_module(library(http/html_write)). 62:- use_module(library(http/http_path)). 63:- if(exists_source(library(user_profile))). 64:- use_module(library(user_profile)). 65:- endif. 66:- use_module(library(aggregate)). 67 68:- use_module(storage). 69:- use_module(gitty). 70:- use_module(config). 71:- use_module(avatar). 72:- use_module(noble_avatar). 73:- use_module(chatstore). 74:- use_module(authenticate). 75:- use_module(pep). 76:- use_module(content_filter). 77 78:- html_meta(chat_to_profile( , )).
94:- multifile swish_config:config/2. 95 96swish_configconfig(hangout, 'Hangout.swinb'). 97swish_configconfig(avatars, svg). % or 'noble' 98 99 100 /******************************* 101 * ESTABLISH WEBSOCKET * 102 *******************************/ 103 104:- http_handler(swish(chat), start_chat, [ id(swish_chat) ]). 105 106:- meta_predicate must_succeed( ).
113start_chat(Request) :- 114 authenticate(Request, Identity), 115 start_chat(Request, [identity(Identity)]). 116 117start_chat(Request, Options) :- 118 authorized(chat(open), Options), 119 ( http_in_session(Session) 120 -> CheckLogin = false 121 ; http_open_session(Session, []), 122 CheckLogin = true 123 ), 124 check_flooding(Session), 125 http_parameters(Request, 126 [ avatar(Avatar, [optional(true)]), 127 nickname(NickName, [optional(true)]), 128 reconnect(Token, [optional(true)]) 129 ]), 130 extend_options([ avatar(Avatar), 131 nick_name(NickName), 132 reconnect(Token), 133 check_login(CheckLogin) 134 ], Options, ChatOptions), 135 debug(chat(websocket), 'Accepting (session ~p)', [Session]), 136 http_upgrade_to_websocket( 137 accept_chat(Session, ChatOptions), 138 [ guarded(false), 139 subprotocols(['v1.chat.swish.swi-prolog.org', chat]) 140 ], 141 Request). 142 143extend_options([], Options, Options). 144extend_options([H|T0], Options, [H|T]) :- 145 ground(H), !, 146 extend_options(T0, Options, T). 147extend_options([_|T0], Options, T) :- 148 extend_options(T0, Options, T).
156check_flooding(Session) :-
157 get_time(Now),
158 ( http_session_retract(websocket(Score, Last))
159 -> Passed is Now-Last,
160 NewScore is Score*(2**(-Passed/60)) + 10
161 ; NewScore = 10,
162 Passed = 0
163 ),
164 debug(chat(flooding), 'Flooding score: ~2f (session ~p)',
165 [NewScore, Session]),
166 http_session_assert(websocket(NewScore, Now)),
167 ( NewScore > 50
168 -> throw(http_reply(resource_error(
169 error(permission_error(reconnect, websocket,
170 Session),
171 websocket(reconnect(Passed, NewScore))))))
172 ; true
173 ).
177accept_chat(Session, Options, WebSocket) :- 178 must_succeed(accept_chat_(Session, Options, WebSocket)). 179 180accept_chat_(Session, Options, WebSocket) :- 181 create_chat_room, 182 ( reconnect_token(WSID, Token, Options), 183 retractall(visitor_status(WSID, lost(_))), 184 existing_visitor(WSID, Session, Token, TmpUser, UserData), 185 hub_add(swish_chat, WebSocket, WSID) 186 -> Reason = rejoined 187 ; hub_add(swish_chat, WebSocket, WSID), 188 must_succeed(create_visitor(WSID, Session, Token, 189 TmpUser, UserData, Options)), 190 Reason = joined 191 ), 192 visitor_count(Visitors), 193 option(check_login(CheckLogin), Options, true), 194 Msg = _{ type:welcome, 195 uid:TmpUser, 196 wsid:WSID, 197 reconnect:Token, 198 visitors:Visitors, 199 check_login:CheckLogin 200 }, 201 hub_send(WSID, json(UserData.put(Msg))), 202 must_succeed(chat_broadcast(UserData.put(_{type:Reason, 203 visitors:Visitors, 204 wsid:WSID}))), 205 gc_visitors, 206 debug(chat(websocket), '~w (session ~p, wsid ~p)', 207 [Reason, Session, WSID]). 208 209 210reconnect_token(WSID, Token, Options) :- 211 option(reconnect(Token), Options), 212 visitor_session(WSID, _, Token), !. 213 214must_succeed(Goal) :- 215 catch(Goal, E, print_message(warning, E)), !. 216must_succeed(Goal) :- 217 print_message(warning, goal_failed(Goal)). 218 219 220 /******************************* 221 * DATA * 222 *******************************/
242:- dynamic 243 visitor_status/2, % WSID, Status 244 visitor_session/3, % WSID, Session, Token 245 session_user/2, % Session, TmpUser 246 visitor_data/2, % TmpUser, Data 247 subscription/3. % WSID, Channel, SubChannel
253visitor(WSID) :- 254 visitor_session(WSID, _Session, _Token), 255 \+ inactive(WSID, 30). 256 257visitor_count(Count) :- 258 aggregate_all(count, visitor(_), Count).
265inactive(WSID, Timeout) :-
266 visitor_status(WSID, lost(Lost)),
267 get_time(Now),
268 Now - Lost > Timeout.
274visitor_session(WSID, Session) :-
275 visitor_session(WSID, Session, _Token).
281wsid_visitor(WSID, Visitor) :- 282 nonvar(WSID), !, 283 visitor_session(WSID, Session), 284 session_user(Session, Visitor). 285wsid_visitor(WSID, Visitor) :- 286 session_user(Session, Visitor), 287 visitor_session(WSID, Session).
294existing_visitor(WSID, Session, Token, TmpUser, UserData) :- 295 visitor_session(WSID, Session, Token), 296 session_user(Session, TmpUser), 297 visitor_data(TmpUser, UserData), !. 298existing_visitor(WSID, Session, Token, _, _) :- 299 retractall(visitor_session(WSID, Session, Token)), 300 fail.
314create_visitor(WSID, Session, Token, TmpUser, UserData, Options) :-
315 generate_key(Token),
316 assertz(visitor_session(WSID, Session, Token)),
317 create_session_user(Session, TmpUser, UserData, Options).
323generate_key(Key) :-
324 length(Codes, 16),
325 maplist(random_between(0,255), Codes),
326 phrase(base64url(Codes), Encoded),
327 atom_codes(Key, Encoded).
340destroy_visitor(WSID) :- 341 must_be(atom, WSID), 342 destroy_reason(WSID, Reason), 343 ( Reason == unload 344 -> reclaim_visitor(WSID) 345 ; get_time(Now), 346 assertz(visitor_status(WSID, lost(Now))) 347 ), 348 visitor_count(Count), 349 chat_broadcast(_{ type:removeUser, 350 wsid:WSID, 351 reason:Reason, 352 visitors:Count 353 }). 354 355destroy_reason(WSID, Reason) :- 356 retract(visitor_status(WSID, unload)), !, 357 Reason = unload. 358destroy_reason(_, close).
365:- dynamic last_gc/1. 366 367gc_visitors :- 368 last_gc(Last), 369 get_time(Now), 370 Now-Last < 300, !. 371gc_visitors :- 372 with_mutex(gc_visitors, gc_visitors_sync). 373 374gc_visitors_sync :- 375 get_time(Now), 376 ( last_gc(Last), 377 Now-Last < 300 378 -> true 379 ; retractall(last_gc(_)), 380 asserta(last_gc(Now)), 381 do_gc_visitors 382 ). 383 384do_gc_visitors :- 385 forall(( visitor_session(WSID, _Session, _Token), 386 inactive(WSID, 5*60) 387 ), 388 reclaim_visitor(WSID)). 389 390reclaim_visitor(WSID) :- 391 debug(chat(gc), 'Reclaiming idle ~p', [WSID]), 392 reclaim_visitor_session(WSID), 393 retractall(visitor_status(WSID, _Status)), 394 unsubscribe(WSID, _). 395 396reclaim_visitor_session(WSID) :- 397 forall(retract(visitor_session(WSID, Session, _Token)), 398 http_session_retractall(websocket(_, _), Session)). 399 400:- if(\+current_predicate(http_session_retractall/2)). 401http_session_retractall(Data, Session) :- 402 retractall(http_session:session_data(Session, Data)). 403:- endif.
412:- listen(http_session(end(SessionID, _Peer)), 413 destroy_session_user(SessionID)). 414 415create_session_user(Session, TmpUser, UserData, _Options) :- 416 session_user(Session, TmpUser), 417 visitor_data(TmpUser, UserData), !. 418create_session_user(Session, TmpUser, UserData, Options) :- 419 uuid(TmpUser), 420 get_visitor_data(UserData, Options), 421 assertz(session_user(Session, TmpUser)), 422 assertz(visitor_data(TmpUser, UserData)). 423 424destroy_session_user(Session) :- 425 forall(visitor_session(WSID, Session, _Token), 426 inform_session_closed(WSID, Session)), 427 retractall(visitor_session(_, Session, _)), 428 forall(retract(session_user(Session, TmpUser)), 429 destroy_visitor_data(TmpUser)). 430 431destroy_visitor_data(TmpUser) :- 432 ( retract(visitor_data(TmpUser, Data)), 433 release_avatar(Data.get(avatar)), 434 fail 435 ; true 436 ). 437 438inform_session_closed(WSID, Session) :- 439 ignore(hub_send(WSID, json(_{type:session_closed}))), 440 session_user(Session, TmpUser), 441 update_visitor_data(TmpUser, _Data, logout).
458update_visitor_data(TmpUser, _Data, logout) :- !, 459 anonymise_user_data(TmpUser, NewData), 460 set_visitor_data(TmpUser, NewData, logout). 461update_visitor_data(TmpUser, Data, Reason) :- 462 profile_reason(Reason), !, 463 ( visitor_data(TmpUser, Old) 464 ; Old = v{} 465 ), 466 copy_profile([name,avatar,email], Data, Old, New), 467 set_visitor_data(TmpUser, New, Reason). 468update_visitor_data(TmpUser, _{name:Name}, 'set-nick-name') :- !, 469 visitor_data(TmpUser, Old), 470 set_nick_name(Old, Name, New), 471 set_visitor_data(TmpUser, New, 'set-nick-name'). 472update_visitor_data(TmpUser, Data, Reason) :- 473 set_visitor_data(TmpUser, Data, Reason). 474 475profile_reason('profile-edit'). 476profile_reason('login'). 477 478copy_profile([], _, Data, Data). 479copy_profile([H|T], New, Data0, Data) :- 480 copy_profile_field(H, New, Data0, Data1), 481 copy_profile(T, New, Data1, Data). 482 483copy_profile_field(avatar, New, Data0, Data) :- !, 484 ( Data1 = Data0.put(avatar,New.get(avatar)) 485 -> Data = Data1.put(avatar_source, profile) 486 ; email_gravatar(New.get(email), Avatar), 487 valid_gravatar(Avatar) 488 -> Data = Data0.put(_{avatar:Avatar,avatar_source:email}) 489 ; Avatar = Data0.get(anonymous_avatar) 490 -> Data = Data0.put(_{avatar:Avatar,avatar_source:client}) 491 ; noble_avatar_url(Avatar, []), 492 Data = Data0.put(_{avatar:Avatar,avatar_source:generated, 493 anonymous_avatar:Avatar 494 }) 495 ). 496copy_profile_field(email, New, Data0, Data) :- !, 497 ( NewMail = New.get(email) 498 -> update_avatar_from_email(NewMail, Data0, Data1), 499 Data = Data1.put(email, NewMail) 500 ; update_avatar_from_email('', Data0, Data1), 501 ( del_dict(email, Data1, _, Data) 502 -> true 503 ; Data = Data1 504 ) 505 ). 506copy_profile_field(F, New, Data0, Data) :- 507 ( Data = Data0.put(F, New.get(F)) 508 -> true 509 ; del_dict(F, Data0, _, Data) 510 -> true 511 ; Data = Data0 512 ). 513 514set_nick_name(Data0, Name, Data) :- 515 Data = Data0.put(_{name:Name, anonymous_name:Name}).
524update_avatar_from_email(_, Data, Data) :- 525 Data.get(avatar_source) == profile, !. 526update_avatar_from_email('', Data0, Data) :- 527 Data0.get(avatar_source) == email, !, 528 noble_avatar_url(Avatar, []), 529 Data = Data0.put(_{avatar:Avatar, anonymous_avatar:Avatar, 530 avatar_source:generated}). 531update_avatar_from_email(Email, Data0, Data) :- 532 email_gravatar(Email, Avatar), 533 valid_gravatar(Avatar), !, 534 Data = Data0.put(avatar, Avatar). 535update_avatar_from_email(_, Data0, Data) :- 536 ( Avatar = Data0.get(anonymous_avatar) 537 -> Data = Data0.put(_{avatar:Avatar, avatar_source:client}) 538 ; noble_avatar_url(Avatar, []), 539 Data = Data0.put(_{avatar:Avatar, anonymous_avatar:Avatar, 540 avatar_source:generated}) 541 ).
547anonymise_user_data(TmpUser, Data) :- 548 visitor_data(TmpUser, Old), 549 ( _{anonymous_name:AName, anonymous_avatar:AAvatar} :< Old 550 -> Data = _{anonymous_name:AName, anonymous_avatar:AAvatar, 551 name:AName, avatar:AAvatar, avatar_source:client} 552 ; _{anonymous_avatar:AAvatar} :< Old 553 -> Data = _{anonymous_avatar:AAvatar, 554 avatar:AAvatar, avatar_source:client} 555 ; _{anonymous_name:AName} :< Old 556 -> noble_avatar_url(Avatar, []), 557 Data = _{anonymous_name:AName, anonymous_avatar:Avatar, 558 name:AName, avatar:Avatar, avatar_source:generated} 559 ), !. 560anonymise_user_data(_, Data) :- 561 noble_avatar_url(Avatar, []), 562 Data = _{anonymous_avatar:Avatar, 563 avatar:Avatar, avatar_source:generated}.
570set_visitor_data(TmpUser, Data, Reason) :-
571 retractall(visitor_data(TmpUser, _)),
572 assertz(visitor_data(TmpUser, Data)),
573 inform_visitor_change(TmpUser, Reason).
582inform_visitor_change(TmpUser, Reason) :- 583 http_in_session(Session), !, 584 public_user_data(TmpUser, Data), 585 forall(visitor_session(WSID, Session), 586 inform_friend_change(WSID, Data, Reason)). 587inform_visitor_change(TmpUser, Reason) :- 588 b_getval(wsid, WSID), 589 public_user_data(TmpUser, Data), 590 inform_friend_change(WSID, Data, Reason). 591 592inform_friend_change(WSID, Data, Reason) :- 593 Message = json(_{ type:"profile", 594 wsid:WSID, 595 reason:Reason 596 }.put(Data)), 597 hub_send(WSID, Message), 598 forall(viewing_same_file(WSID, Friend), 599 ignore(hub_send(Friend, Message))). 600 601viewing_same_file(WSID, Friend) :- 602 subscription(WSID, gitty, File), 603 subscription(Friend, gitty, File), 604 Friend \== WSID.
608subscribe(WSID, Channel) :- 609 subscribe(WSID, Channel, _SubChannel). 610subscribe(WSID, Channel, SubChannel) :- 611 ( subscription(WSID, Channel, SubChannel) 612 -> true 613 ; assertz(subscription(WSID, Channel, SubChannel)) 614 ). 615 616unsubscribe(WSID, Channel) :- 617 unsubscribe(WSID, Channel, _SubChannel). 618unsubscribe(WSID, Channel, SubChannel) :- 619 retractall(subscription(WSID, Channel, SubChannel)).
628sync_gazers(WSID, Files0) :- 629 findall(F, subscription(WSID, gitty, F), Viewing0), 630 sort(Files0, Files), 631 sort(Viewing0, Viewing), 632 ( Files == Viewing 633 -> true 634 ; ord_subtract(Files, Viewing, New), 635 add_gazing(WSID, New), 636 ord_subtract(Viewing, Files, Left), 637 del_gazing(WSID, Left) 638 ). 639 640add_gazing(_, []) :- !. 641add_gazing(WSID, Files) :- 642 inform_me_about_existing_gazers(WSID, Files), 643 inform_existing_gazers_about_newby(WSID, Files). 644 645inform_me_about_existing_gazers(WSID, Files) :- 646 findall(Gazer, files_gazer(Files, Gazer), Gazers), 647 ignore(hub_send(WSID, json(_{type:"gazers", gazers:Gazers}))). 648 649files_gazer(Files, Gazer) :- 650 member(File, Files), 651 subscription(WSID, gitty, File), 652 visitor_session(WSID, Session), 653 session_user(Session, UID), 654 public_user_data(UID, Data), 655 Gazer = _{file:File, uid:UID, wsid:WSID}.put(Data). 656 657inform_existing_gazers_about_newby(WSID, Files) :- 658 forall(member(File, Files), 659 signal_gazer(WSID, File)). 660 661signal_gazer(WSID, File) :- 662 subscribe(WSID, gitty, File), 663 broadcast_event(opened(File), File, WSID). 664 665del_gazing(_, []) :- !. 666del_gazing(WSID, Files) :- 667 forall(member(File, Files), 668 del_gazing1(WSID, File)). 669 670del_gazing1(WSID, File) :- 671 broadcast_event(closed(File), File, WSID), 672 unsubscribe(WSID, gitty, File).
uid
field.
679add_user_details(Message, Enriched) :-
680 public_user_data(Message.uid, Data),
681 Enriched = Message.put(Data).
688public_user_data(UID, Public) :-
689 visitor_data(UID, Data),
690 ( _{name:Name, avatar:Avatar} :< Data
691 -> Public = _{name:Name, avatar:Avatar}
692 ; _{avatar:Avatar} :< Data
693 -> Public = _{avatar:Avatar}
694 ; Public = _{}
695 ).
Data always contains an avatar
key and optionally contains a
name
and email
key. If the avatar is generated there is also
a key avatar_generated
with the value true
.
716get_visitor_data(Data, Options) :- 717 option(identity(Identity), Options), 718 findall(N-V, visitor_property(Identity, Options, N, V), Pairs), 719 dict_pairs(Data, v, Pairs). 720 721visitor_property(Identity, Options, name, Name) :- 722 ( user_property(Identity, name(Name)) 723 -> true 724 ; option(nick_name(Name), Options) 725 ). 726visitor_property(Identity, _, email, Email) :- 727 user_property(Identity, email(Email)). 728visitor_property(Identity, Options, Name, Value) :- 729 ( user_property(Identity, avatar(Avatar)) 730 -> avatar_property(Avatar, profile, Name, Value) 731 ; user_property(Identity, email(Email)), 732 email_gravatar(Email, Avatar), 733 valid_gravatar(Avatar) 734 -> avatar_property(Avatar, email, Name, Value) 735 ; option(avatar(Avatar), Options) 736 -> avatar_property(Avatar, client, Name, Value) 737 ; noble_avatar_url(Avatar, Options), 738 avatar_property(Avatar, generated, Name, Value) 739 ). 740visitor_property(_, Options, anonymous_name, Name) :- 741 option(nick_name(Name), Options). 742visitor_property(_, Options, anonymous_avatar, Avatar) :- 743 option(avatar(Avatar), Options). 744 745 746avatar_property(Avatar, _Source, avatar, Avatar). 747avatar_property(_Avatar, Source, avatar_source, Source). 748 749 750 /******************************* 751 * NOBLE AVATAR * 752 *******************************/ 753 754:- http_handler(swish('avatar/'), reply_avatar, [id(avatar), prefix]).
Not really. A new user gets a new avatar and this is based on whether or not the file exists. Probably we should maintain a db of handed out avatars and their last-use time stamp. How to do that? Current swish stats: 400K avatars, 3.2Gb data.
767reply_avatar(Request) :- 768 option(path_info(Local), Request), 769 ( absolute_file_name(noble_avatar(Local), Path, 770 [ access(read), 771 file_errors(fail) 772 ]) 773 -> true 774 ; create_avatar(Local, Path) 775 ), 776 http_reply_file(Path, [unsafe(true)], Request). 777 778 779noble_avatar_url(HREF, Options) :- 780 option(avatar(HREF), Options), !. 781noble_avatar_url(HREF, _Options) :- 782 swish_config:config(avatars, noble), 783 !, 784 noble_avatar(_Gender, Path, true), 785 file_base_name(Path, File), 786 http_absolute_location(swish(avatar/File), HREF, []). 787noble_avatar_url(HREF, _Options) :- 788 A is random(0x1FFFFF+1), 789 http_absolute_location(icons('avatar.svg'), HREF0, []), 790 format(atom(HREF), '~w#~d', [HREF0, A]). 791 792 793 794 /******************************* 795 * BROADCASTING * 796 *******************************/
807chat_broadcast(Message) :- 808 debug(chat(broadcast), 'Broadcast: ~p', [Message]), 809 hub_broadcast(swish_chat, json(Message)). 810 811chat_broadcast(Message, Channel/SubChannel) :- !, 812 must_be(atom, Channel), 813 must_be(atom, SubChannel), 814 debug(chat(broadcast), 'Broadcast on ~p: ~p', 815 [Channel/SubChannel, Message]), 816 hub_broadcast(swish_chat, json(Message), 817 subscribed(Channel, SubChannel)). 818chat_broadcast(Message, Channel) :- 819 must_be(atom, Channel), 820 debug(chat(broadcast), 'Broadcast on ~p: ~p', [Channel, Message]), 821 hub_broadcast(swish_chat, json(Message), 822 subscribed(Channel)). 823 824subscribed(Channel, WSID) :- 825 subscription(WSID, Channel, _). 826subscribed(Channel, SubChannel, WSID) :- 827 subscription(WSID, Channel, SubChannel). 828subscribed(gitty, SubChannel, WSID) :- 829 swish_config:config(hangout, SubChannel), 830 \+ subscription(WSID, gitty, SubChannel). 831 832 833 /******************************* 834 * CHAT ROOM * 835 *******************************/ 836 837create_chat_room :- 838 current_hub(swish_chat, _), !. 839create_chat_room :- 840 with_mutex(swish_chat, create_chat_room_sync). 841 842create_chat_room_sync :- 843 current_hub(swish_chat, _), !. 844create_chat_room_sync :- 845 hub_create(swish_chat, Room, _{}), 846 thread_create(swish_chat(Room), _, [alias(swish_chat)]). 847 848swish_chat(Room) :- 849 ( catch(swish_chat_event(Room), E, chat_exception(E)) 850 -> true 851 ; print_message(warning, goal_failed(swish_chat_event(Room))) 852 ), 853 swish_chat(Room). 854 855chat_exception('$aborted') :- !. 856chat_exception(E) :- 857 print_message(warning, E). 858 859swish_chat_event(Room) :- 860 thread_get_message(Room.queues.event, Message), 861 ( handle_message(Message, Room) 862 -> true 863 ; print_message(warning, goal_failed(handle_message(Message, Room))) 864 ).
870handle_message(Message, _Room) :- 871 websocket{opcode:text} :< Message, !, 872 atom_json_dict(Message.data, JSON, []), 873 debug(chat(received), 'Received from ~p: ~p', [Message.client, JSON]), 874 WSID = Message.client, 875 setup_call_cleanup( 876 b_setval(wsid, WSID), 877 json_message(JSON, WSID), 878 nb_delete(wsid)). 879handle_message(Message, _Room) :- 880 hub{joined:WSID} :< Message, !, 881 debug(chat(visitor), 'Joined: ~p', [WSID]). 882handle_message(Message, _Room) :- 883 hub{left:WSID, reason:write(Lost)} :< Message, !, 884 ( destroy_visitor(WSID) 885 -> debug(chat(visitor), 'Left ~p due to write error for ~p', 886 [WSID, Lost]) 887 ; true 888 ). 889handle_message(Message, _Room) :- 890 hub{left:WSID} :< Message, !, 891 ( destroy_visitor(WSID) 892 -> debug(chat(visitor), 'Left: ~p', [WSID]) 893 ; true 894 ). 895handle_message(Message, _Room) :- 896 websocket{opcode:close, client:WSID} :< Message, !, 897 debug(chat(visitor), 'Left: ~p', [WSID]), 898 destroy_visitor(WSID). 899handle_message(Message, _Room) :- 900 debug(chat(ignored), 'Ignoring chat message ~p', [Message]).
919json_message(Dict, WSID) :- 920 _{ type: "subscribe", 921 channel:ChannelS, sub_channel:SubChannelS} :< Dict, !, 922 atom_string(Channel, ChannelS), 923 atom_string(SubChannel, SubChannelS), 924 subscribe(WSID, Channel, SubChannel). 925json_message(Dict, WSID) :- 926 _{type: "subscribe", channel:ChannelS} :< Dict, !, 927 atom_string(Channel, ChannelS), 928 subscribe(WSID, Channel). 929json_message(Dict, WSID) :- 930 _{ type: "unsubscribe", 931 channel:ChannelS, sub_channel:SubChannelS} :< Dict, !, 932 atom_string(Channel, ChannelS), 933 atom_string(SubChannel, SubChannelS), 934 unsubscribe(WSID, Channel, SubChannel). 935json_message(Dict, WSID) :- 936 _{type: "unsubscribe", channel:ChannelS} :< Dict, !, 937 atom_string(Channel, ChannelS), 938 unsubscribe(WSID, Channel). 939json_message(Dict, WSID) :- 940 _{type: "unload"} :< Dict, !, % clean close/reload 941 sync_gazers(WSID, []), 942 assertz(visitor_status(WSID, unload)). 943json_message(Dict, WSID) :- 944 _{type: "has-open-files", files:FileDicts} :< Dict, !, 945 maplist(dict_file_name, FileDicts, Files), 946 sync_gazers(WSID, Files). 947json_message(Dict, WSID) :- 948 _{type: "reloaded", file:FileS, commit:Hash} :< Dict, !, 949 atom_string(File, FileS), 950 event_html(reloaded(File), HTML), 951 Message = _{ type:notify, 952 wsid:WSID, 953 html:HTML, 954 event:reloaded, 955 argv:[File,Hash] 956 }, 957 chat_broadcast(Message, gitty/File). 958json_message(Dict, WSID) :- 959 _{type: "set-nick-name", name:Name} :< Dict, !, 960 wsid_visitor(WSID, Visitor), 961 update_visitor_data(Visitor, _{name:Name}, 'set-nick-name'). 962json_message(Dict, WSID) :- 963 _{type: "chat-message", docid:DocID} :< Dict, !, 964 chat_add_user_id(WSID, Dict, Message), 965 ( forbidden(Message, DocID, Why) 966 -> hub_send(WSID, json(json{type:forbidden, 967 action:chat_post, 968 about:DocID, 969 message:Why 970 })) 971 ; chat_relay(Message) 972 ). 973json_message(Dict, _WSID) :- 974 debug(chat(ignored), 'Ignoring JSON message ~p', [Dict]). 975 976dict_file_name(Dict, File) :- 977 atom_string(File, Dict.get(file)).
990forbidden(Message, DocID, Why) :- 991 \+ swish_config:config(chat_spam_protection, false), 992 \+ ws_authorized(chat(post(Message, DocID)), Message.user), !, 993 Why = "Due to frequent spamming we were forced to limit \c 994 posting chat messages to users who are logged in.". 995forbidden(Message, _DocID, Why) :- 996 Text = Message.get(text), 997 string_length(Text, Len), 998 Len > 500, 999 Why = "Chat messages are limited to 500 characters". 1000forbidden(Message, _DocID, Why) :- 1001 Payloads = Message.get(payload), 1002 member(Payload, Payloads), 1003 large_payload(Payload, Why), !. 1004forbidden(Message, _DocID, Why) :- 1005 \+ swish_config:config(chat_spam_protection, false), 1006 eval_content(Message.get(text), _WC, Score), 1007 user_score(Message, Score, Cummulative, _Count), 1008 Score*2 + Cummulative < 0, 1009 !, 1010 Why = "Chat messages must be in English and avoid offensive language". 1011 1012large_payload(Payload, Why) :- 1013 Selections = Payload.get(selection), 1014 member(Selection, Selections), 1015 ( string_length(Selection.get(string), SelLen), SelLen > 500 1016 ; string_length(Selection.get(context), SelLen), SelLen > 500 1017 ), !, 1018 Why = "Selection too long (max. 500 characters)". 1019large_payload(Payload, Why) :- 1020 string_length(Payload.get(query), QLen), QLen > 1000, !, 1021 Why = "Query too long (max. 1000 characters)". 1022 1023user_score(Message, MsgScore, Cummulative, Count) :- 1024 Profile = Message.get(user).get(profile_id), !, 1025 block(Profile, MsgScore, Cummulative, Count). 1026user_score(_, _, 0, 1).
1032:- dynamic 1033 blocked/4. 1034 1035block(User, Score, Cummulative, Count) :- 1036 blocked(User, Score0, Count0, Time), !, 1037 get_time(Now), 1038 Cummulative = Score0*(0.5**((Now-Time)/600)) + Score, 1039 Count is Count0 + 1, 1040 asserta(blocked(User, Cummulative, Count, Now)), 1041 retractall(blocked(User, Score0, Count0, Time)). 1042block(User, Score, Score, 1) :- 1043 get_time(Now), 1044 asserta(blocked(User, Score, 1, Now)). 1045 1046 1047 /******************************* 1048 * CHAT MESSAGES * 1049 *******************************/
1055chat_add_user_id(WSID, Dict, Message) :-
1056 visitor_session(WSID, Session, _Token),
1057 session_user(Session, Visitor),
1058 visitor_data(Visitor, UserData),
1059 User0 = u{avatar:UserData.avatar,
1060 wsid:WSID
1061 },
1062 ( Name = UserData.get(name)
1063 -> User1 = User0.put(name, Name)
1064 ; User1 = User0
1065 ),
1066 ( http_current_session(Session, profile_id(ProfileID))
1067 -> User = User1.put(profile_id, ProfileID)
1068 ; User = User1
1069 ),
1070 Message = Dict.put(user, User).
1077chat_about(DocID, Message) :-
1078 chat_relay(Message.put(docid, DocID)).
1084chat_relay(Message) :-
1085 chat_enrich(Message, Message1),
1086 chat_send(Message1).
1092chat_enrich(Message0, Message) :-
1093 get_time(Now),
1094 uuid(ID),
1095 Message = Message0.put(_{time:Now, id:ID}).
volatile
property it is broadcasted, but not stored.1102chat_send(Message) :- 1103 atom_concat("gitty:", File, Message.docid), 1104 broadcast(swish(chat(Message))), 1105 ( Message.get(volatile) == true 1106 -> true 1107 ; chat_store(Message) 1108 ), 1109 chat_broadcast(Message, gitty/File). 1110 1111 1112 /******************************* 1113 * EVENTS * 1114 *******************************/ 1115 1116:- unlisten(swish(_)), 1117 listen(swish(Event), chat_event(Event)).
http
.1131chat_event(Event) :- 1132 broadcast_event(Event), 1133 http_session_id(Session), 1134 debug(event, 'Event: ~p, session ~q', [Event, Session]), 1135 event_file(Event, File), !, 1136 ( visitor_session(WSID, Session), 1137 subscription(WSID, gitty, File) 1138 -> true 1139 ; visitor_session(WSID, Session) 1140 -> true 1141 ; WSID = undefined 1142 ), 1143 session_broadcast_event(Event, File, Session, WSID). 1144chat_event(logout(_ProfileID)) :- !, 1145 http_session_id(Session), 1146 session_user(Session, User), 1147 update_visitor_data(User, _, logout). 1148chat_event(visitor_count(Count)) :- % request 1149 visitor_count(Count). 1150 1151:- if(current_predicate(current_profile/2)). 1152 1153chat_event(profile(ProfileID)) :- !, 1154 current_profile(ProfileID, Profile), 1155 http_session_id(Session), 1156 session_user(Session, User), 1157 update_visitor_data(User, Profile, login).
1163:- listen(user_profile(modified(ProfileID, Name, _Old, New)), 1164 propagate_profile_change(ProfileID, Name, New)). 1165 1166propagate_profile_change(ProfileID, _, _) :- 1167 http_current_session(Session, profile_id(ProfileID)), 1168 session_user(Session, User), 1169 current_profile(ProfileID, Profile), 1170 update_visitor_data(User, Profile, 'profile-edit'). 1171 1172:- endif.
1178broadcast_event(updated(_File, _From, _To)).
1189broadcast_event(Event, File, WSID) :- 1190 visitor_session(WSID, Session), 1191 session_broadcast_event(Event, File, Session, WSID), !. 1192broadcast_event(_, _, _). 1193 1194session_broadcast_event(Event, File, Session, WSID) :- 1195 session_user(Session, UID), 1196 event_html(Event, HTML), 1197 Event =.. [EventName|Argv], 1198 Message0 = _{ type:notify, 1199 uid:UID, 1200 html:HTML, 1201 event:EventName, 1202 event_argv:Argv, 1203 wsid:WSID 1204 }, 1205 add_user_details(Message0, Message), 1206 chat_broadcast(Message, gitty/File).
1213event_html(Event, HTML) :- 1214 ( phrase(event_message(Event), Tokens) 1215 -> true 1216 ; phrase(html('Unknown-event: ~p'-[Event]), Tokens) 1217 ), 1218 delete(Tokens, nl(_), SingleLine), 1219 with_output_to(string(HTML), print_html(SingleLine)). 1220 1221event_message(created(File)) --> 1222 html([ 'Created ', \file(File) ]). 1223event_message(reloaded(File)) --> 1224 html([ 'Reloaded ', \file(File) ]). 1225event_message(updated(File, _From, _To)) --> 1226 html([ 'Saved ', \file(File) ]). 1227event_message(deleted(File, _From, _To)) --> 1228 html([ 'Deleted ', \file(File) ]). 1229event_message(closed(File)) --> 1230 html([ 'Closed ', \file(File) ]). 1231event_message(opened(File)) --> 1232 html([ 'Opened ', \file(File) ]). 1233event_message(download(File)) --> 1234 html([ 'Opened ', \file(File) ]). 1235event_message(download(Store, FileOrHash, _Format)) --> 1236 { event_file(download(Store, FileOrHash), File) 1237 }, 1238 html([ 'Opened ', \file(File) ]). 1239 1240file(File) --> 1241 html(a(href('/p/'+File), File)).
1247event_file(created(File, _Commit), File). 1248event_file(updated(File, _Commit), File). 1249event_file(deleted(File, _Commit), File). 1250event_file(download(Store, FileOrHash, _Format), File) :- 1251 ( is_gitty_hash(FileOrHash) 1252 -> gitty_commit(Store, FileOrHash, Meta), 1253 File = Meta.name 1254 ; File = FileOrHash 1255 ). 1256 1257 1258 /******************************* 1259 * NOTIFICATION * 1260 *******************************/
1266chat_to_profile(ProfileID, HTML) :- 1267 ( http_current_session(Session, profile_id(ProfileID)), 1268 visitor_session(WSID, Session), 1269 html_string(HTML, String), 1270 hub_send(WSID, json(_{ wsid:WSID, 1271 type:notify, 1272 html:String 1273 })), 1274 debug(notify(chat), 'Notify to ~p: ~p', [ProfileID, String]), 1275 fail 1276 ; true 1277 ). 1278 1279html_string(HTML, String) :- 1280 phrase(html(HTML), Tokens), 1281 delete(Tokens, nl(_), SingleLine), 1282 with_output_to(string(String), print_html(SingleLine)). 1283 1284 1285 1286 1287 /******************************* 1288 * UI * 1289 *******************************/
1296notifications(_Options) --> 1297 { swish_config:config(chat, true) }, !, 1298 html(div(class(chat), 1299 [ div(class('chat-users'), 1300 ul([ class([nav, 'navbar-nav', 'pull-right']), 1301 id(chat) 1302 ], [])), 1303 div(class('user-count'), 1304 [ span(id('user-count'), '?'), 1305 ' users online' 1306 ]) 1307 ])). 1308notifications(_Options) --> 1309 [].
1315broadcast_bell(_Options) --> 1316 { swish_config:config(chat, true), 1317 swish_config:config(hangout, Hangout), 1318 atom_concat('gitty:', Hangout, HangoutID) 1319 }, !, 1320 html([ a([ class(['dropdown-toggle', 'broadcast-bell']), 1321 'data-toggle'(dropdown) 1322 ], 1323 [ span([ id('broadcast-bell'), 1324 'data-document'(HangoutID) 1325 ], []), 1326 b(class(caret), []) 1327 ]), 1328 ul([ class(['dropdown-menu', 'pull-right']), 1329 id('chat-menu') 1330 ], 1331 [ li(a('data-action'('chat-shared'), 1332 'Open hangout')), 1333 li(a('data-action'('chat-about-file'), 1334 'Open chat for current file')) 1335 ]) 1336 ]). 1337broadcast_bell(_Options) --> 1338 []. 1339 1340 1341 /******************************* 1342 * MESSAGES * 1343 *******************************/ 1344 1345:- multifile 1346 prolog:message_context//1. 1347 1348prologmessage_context(websocket(reconnect(Passed, Score))) --> 1349 [ 'WebSocket: too frequent reconnect requests (~1f sec; score = ~1f)'- 1350 [Passed, Score] ]
The SWISH collaboration backbone
We have three levels of identity as enumerated below. Note that these form a hierarchy: a particular user may be logged on using multiple browsers which in turn may have multiple SWISH windows opened.