36
37:- module(swish_chat,
38 [ chat_broadcast/1, 39 chat_broadcast/2, 40 chat_to_profile/2, 41 chat_about/2, 42
43 notifications//1, 44 broadcast_bell//1 45 ]). 46:- use_module(library(http/hub)). 47:- use_module(library(http/http_dispatch)). 48:- use_module(library(http/http_session)). 49:- use_module(library(http/http_parameters)). 50:- use_module(library(http/http_cors)). 51:- use_module(library(http/websocket)). 52:- use_module(library(http/json)). 53:- use_module(library(error)). 54:- use_module(library(lists)). 55:- use_module(library(option)). 56:- use_module(library(debug)). 57:- use_module(library(uuid)). 58:- use_module(library(random)). 59:- use_module(library(base64)). 60:- use_module(library(apply)). 61:- use_module(library(broadcast)). 62:- use_module(library(ordsets)). 63:- use_module(library(http/html_write)). 64:- use_module(library(http/http_path)). 65:- if(exists_source(library(user_profile))). 66:- use_module(library(user_profile)). 67:- endif. 68:- use_module(library(aggregate)). 69:- use_module(library(redis)). 70:- use_module(library(solution_sequences)). 71
72:- use_module(storage). 73:- use_module(gitty). 74:- use_module(config). 75:- use_module(avatar). 76:- use_module(noble_avatar). 77:- use_module(chatstore). 78:- use_module(authenticate). 79:- use_module(pep). 80:- use_module(content_filter). 81:- use_module(swish_redis). 82
83:- html_meta(chat_to_profile(+, html)). 84
98
99:- multifile swish_config:config/2. 100
101swish_config:config(hangout, 'Hangout.swinb').
102swish_config:config(avatars, svg). 103swish_config:config(session_lost_timeout, 300).
104
105
106 109
110:- http_handler(swish(chat), start_chat, [ id(swish_chat) ]). 111
112:- meta_predicate must_succeed(0). 113
118
119start_chat(Request) :-
120 memberchk(method(options), Request),
121 !,
122 cors_enable(Request,
123 [ methods([get])
124 ]),
125 format('~n').
126start_chat(Request) :-
127 cors_enable,
128 authenticate(Request, Identity),
129 start_chat(Request, [identity(Identity)]).
130
131start_chat(Request, Options) :-
132 authorized(chat(open), Options),
133 ( http_in_session(Session)
134 -> CheckLogin = false
135 ; http_open_session(Session, []),
136 CheckLogin = true
137 ),
138 check_flooding(Session),
139 http_parameters(Request,
140 [ avatar(Avatar, [optional(true)]),
141 nickname(NickName, [optional(true)]),
142 reconnect(Token, [optional(true)])
143 ]),
144 extend_options([ avatar(Avatar),
145 nick_name(NickName),
146 reconnect(Token),
147 check_login(CheckLogin)
148 ], Options, ChatOptions),
149 debug(chat(websocket), 'Accepting (session ~p)', [Session]),
150 http_upgrade_to_websocket(
151 accept_chat(Session, ChatOptions),
152 [ guarded(false),
153 subprotocols(['v1.chat.swish.swi-prolog.org', chat])
154 ],
155 Request).
156
157extend_options([], Options, Options).
158extend_options([H|T0], Options, [H|T]) :-
159 ground(H),
160 !,
161 extend_options(T0, Options, T).
162extend_options([_|T0], Options, T) :-
163 extend_options(T0, Options, T).
164
165
170
171check_flooding(Session) :-
172 get_time(Now),
173 ( http_session_retract(websocket(Score, Last))
174 -> Passed is Now-Last,
175 NewScore is Score*(2**(-Passed/60)) + 10
176 ; NewScore = 10,
177 Passed = 0
178 ),
179 debug(chat(flooding), 'Flooding score: ~2f (session ~p)',
180 [NewScore, Session]),
181 http_session_assert(websocket(NewScore, Now)),
182 ( NewScore > 50
183 -> throw(http_reply(resource_error(
184 error(permission_error(reconnect, websocket,
185 Session),
186 websocket(reconnect(Passed, NewScore))))))
187 ; true
188 ).
189
197
198accept_chat(Session, Options, WebSocket) :-
199 must_succeed(accept_chat_(Session, Options, WebSocket)),
200 Long is 100*24*3600, 201 http_set_session(Session, timeout(Long)).
202
203accept_chat_(Session, Options, WebSocket) :-
204 create_chat_room,
205 ( option(reconnect(Token), Options),
206 http_session_data(wsid(WSID, Token), Session),
207 wsid_status_del_lost(WSID),
208 existing_visitor(WSID, Session, TmpUser, UserData),
209 must_succeed(hub_add(swish_chat, WebSocket, WSID))
210 -> Reason = rejoined
211 ; must_succeed(hub_add(swish_chat, WebSocket, WSID)),
212 random_key(16, Token),
213 http_session_asserta(wsid(WSID, Token), Session),
214 must_succeed(create_visitor(WSID, Session, TmpUser, UserData, Options)),
215 Reason = joined
216 ),
217 gc_visitors,
218 visitor_count(Visitors),
219 option(check_login(CheckLogin), Options, true),
220 Msg0 = _{ type:welcome,
221 uid:TmpUser,
222 wsid:WSID,
223 reconnect:Token,
224 visitors:Visitors,
225 check_login:CheckLogin
226 },
227 add_redis_consumer(Msg0, Msg),
228 AckMsg = UserData.put(Msg),
229 ( hub_send(WSID, json(AckMsg))
230 -> must_succeed(chat_broadcast(UserData.put(_{type:Reason,
231 visitors:Visitors,
232 wsid:WSID}))),
233 debug(chat(websocket), '~w (session ~p, wsid ~p)',
234 [Reason, Session, WSID])
235 ; Reason = joined
236 -> debug(chat(websocket), 'Failed to acknowledge join for ~p in ~p',
237 [WSID, Session]),
238 http_session_retractall(wsid(WSID, Token), Session),
239 reclaim_visitor(WSID),
240 fail
241 ; debug(chat(websocket), 'Failed to acknowledge rejoin for ~p in ~p',
242 [WSID, Session]),
243 fail
244 ).
245
246add_redis_consumer(Msg0, Msg) :-
247 use_redis,
248 redis_consumer(Consumer),
249 !,
250 Msg = Msg0.put(consumer, Consumer).
251add_redis_consumer(Msg, Msg).
252
253must_succeed(Goal) :-
254 catch_with_backtrace(Goal, E, (print_message(warning, E), fail)),
255 !.
256must_succeed(Goal) :-
257 print_message(warning, goal_failed(Goal)),
258 fail.
259
260
261 264
300
301
338
339:- dynamic
340 wsid_status_db/2, 341 wsid_session_db/2, 342 session_user_db/2, 343 visitor_data_db/2, 344 subscription_db/3. 345
346
353
354redis_key(Which, Server, Key) :-
355 swish_config(redis, Server),
356 swish_config(redis_prefix, Prefix),
357 Which =.. List,
358 atomic_list_concat([Prefix, chat | List], :, Key).
359
360redis_key_ro(Which, Server, Key) :-
361 swish_config(redis_ro, Server),
362 !,
363 swish_config(redis_prefix, Prefix),
364 Which =.. List,
365 atomic_list_concat([Prefix, chat | List], :, Key).
366redis_key_ro(Which, Server, Key) :-
367 redis_key(Which, Server, Key).
368
369use_redis :-
370 swish_config(redis, _).
371
372
384
385wsid_status(WSID, Status) :-
386 redis_key_ro(unload(WSID), Server, UnloadKey),
387 !,
388 redis_key_ro(lost(WSID), Server, LostKey),
389 redis(Server,
390 [ get(UnloadKey) -> Unload,
391 get(LostKey) -> Lost
392 ]),
393 ( number(Lost),
394 Status = lost(Lost)
395 ; Unload \== nil
396 -> Status = unload
397 ).
398wsid_status(WSID, Status) :-
399 wsid_status_db(WSID, Status).
400
401wsid_status_del(WSID) :-
402 redis_key(unload(WSID), Server, UnloadKey),
403 !,
404 redis_key(lost(WSID), Server, LostKey),
405 redis(Server,
406 [ del(UnloadKey),
407 del(LostKey)
408 ]).
409wsid_status_del(WSID) :-
410 retractall(wsid_status_db(WSID, _Status)).
411
412wsid_status_del_lost(WSID) :-
413 redis_key(lost(WSID), Server, Key),
414 !,
415 redis(Server, del(Key)).
416wsid_status_del_lost(WSID) :-
417 retractall(wsid_status_db(WSID, lost(_))).
418
419wsid_status_set_lost(WSID, Time) :-
420 redis_key(lost(WSID), Server, Key),
421 !,
422 redis(Server, set(Key, Time)).
423wsid_status_set_lost(WSID, Time) :-
424 assertz(wsid_status_db(WSID, lost(Time))).
425
426wsid_status_set_unload(WSID) :-
427 redis_key(unload(WSID), Server, Key),
428 !,
429 redis(Server, set(Key, true)).
430wsid_status_set_unload(WSID) :-
431 assertz(wsid_status_db(WSID, unload)).
432
436
437wsid_status_del_unload(WSID) :-
438 redis_key_ro(unload(WSID), ROServer, Key),
439 !,
440 ( redis(ROServer, get(Key), true)
441 -> redis_key(unload(WSID), RWServer, Key),
442 redis(RWServer, del(Key))
443 ).
444wsid_status_del_unload(WSID) :-
445 retract(wsid_status_db(WSID, unload)),
446 !.
447
459
460register_wsid_session(WSID, Session) :-
461 redis_key(wsid, Server, SetKey),
462 redis_key(session(WSID), Server, SessionKey),
463 !,
464 redis_consumer(Consumer),
465 redis(Server, sadd(SetKey, WSID)),
466 redis(Server, set(SessionKey, at(Consumer,Session) as prolog)).
467register_wsid_session(WSID, Session) :-
468 assertz(wsid_session_db(WSID, Session)).
469
476
477wsid_session(WSID, Session) :-
478 wsid_session(WSID, Session, _Consumer).
479
480wsid_session(WSID, Session, Consumer) :-
481 use_redis,
482 !,
483 ( nonvar(Session)
484 -> http_session_data(wsid(WSID,_Token), Session)
485 ; current_wsid(WSID),
486 redis_key_ro(session(WSID), Server, SessionKey),
487 redis(Server, get(SessionKey), at(Consumer,Session))
488 ).
489wsid_session(WSID, Session, single) :-
490 wsid_session_db(WSID, Session).
491
495
496wsid_session_reclaim(WSID, Session) :-
497 redis_key_ro(session(WSID), ROServer, SessionKey),
498 redis_key(wsid, WRServer, SetKey),
499 !,
500 redis(ROServer, get(SessionKey), At),
501 arg(2, At, Session), 502 redis(WRServer, srem(SetKey, WSID)),
503 redis(WRServer, del(SessionKey)).
504wsid_session_reclaim(WSID, Session) :-
505 retract(wsid_session_db(WSID, Session)).
506
508
509wsid_session_reclaim_all(WSID, _Session) :-
510 redis_key(wsid, Server, SetKey),
511 !,
512 redis(Server, srem(SetKey, WSID)),
513 redis_key(session(WSID), Server, SessionKey),
514 redis(Server, del(SessionKey)).
515wsid_session_reclaim_all(WSID, Session) :-
516 retractall(wsid_session_db(WSID, Session)).
517
518wsid_session_del_session(Session) :-
519 use_redis,
520 !,
521 ( wsid_session(WSID, Session),
522 wsid_session_reclaim(WSID, Session),
523 fail
524 ; true
525 ).
526wsid_session_del_session(Session) :-
527 retractall(wsid_session_db(_, Session)).
528
532
533current_wsid(WSID) :-
534 nonvar(WSID),
535 !,
536 redis_key_ro(wsid, Server, SetKey),
537 redis(Server, sismember(SetKey, WSID), 1).
538current_wsid(WSID) :-
539 redis_key_ro(wsid, Server, SetKey),
540 redis_sscan(Server, SetKey, List, []),
541 member(WSID, List).
542
547
548session_user(Session, TmpUser) :-
549 http_current_session(Session, swish_user(TmpUser)).
550
551session_user_create(Session, User) :-
552 http_session_asserta(swish_user(User), Session).
553
554session_user_del(Session, User) :-
555 http_session_retract(swish_user(User), Session).
556
558
559visitor_data(Visitor, Data) :-
560 redis_key(visitor(Visitor), Server, Key),
561 !,
562 redis_get_hash(Server, Key, Data).
563visitor_data(Visitor, Data) :-
564 visitor_data_db(Visitor, Data).
565
566visitor_data_set(Visitor, Data) :-
567 redis_key(visitor(Visitor), Server, Key),
568 !,
569 redis_set_hash(Server, Key, Data).
570visitor_data_set(Visitor, Data) :-
571 retractall(visitor_data_db(Visitor, _)),
572 assertz(visitor_data_db(Visitor, Data)).
573
574visitor_data_del(Visitor, Data) :-
575 redis_key(visitor(Visitor), Server, Key),
576 !,
577 redis_get_hash(Server, Key, Data),
578 redis(Server, del(Key)).
579visitor_data_del(Visitor, Data) :-
580 retract(visitor_data_db(Visitor, Data)).
581
589
590subscription(WSID, Channel, SubChannel) :-
591 use_redis,
592 !,
593 ( nonvar(WSID), nonvar(Channel), nonvar(SubChannel)
594 -> redis_key_ro(subscription(WSID), Server, WsKey),
595 redis(Server, sismember(WsKey, Channel-SubChannel as prolog), 1)
596 ; nonvar(SubChannel)
597 -> redis_key_ro(channel(SubChannel), Server, ChKey),
598 redis_sscan(Server, ChKey, List, []),
599 member(WSID-Channel, List)
600 ; ( nonvar(WSID)
601 -> true
602 ; current_wsid(WSID)
603 ),
604 redis_key_ro(subscription(WSID), Server, WsKey),
605 redis_sscan(Server, WsKey, List, []),
606 member(Channel-SubChannel, List)
607 ).
608subscription(WSID, Channel, SubChannel) :-
609 subscription_db(WSID, Channel, SubChannel).
610
615
616subscribe(WSID, Channel) :-
617 subscribe(WSID, Channel, _SubChannel).
618
619subscribe(WSID, Channel, SubChannel) :-
620 use_redis,
621 !,
622 redis_key(channel(SubChannel), Server, ChKey),
623 redis_key(subscription(WSID), Server, WsKey),
624 redis(Server, sadd(ChKey, WSID-Channel as prolog)),
625 redis(Server, sadd(WsKey, Channel-SubChannel as prolog)).
626subscribe(WSID, Channel, SubChannel) :-
627 ( subscription(WSID, Channel, SubChannel)
628 -> true
629 ; assertz(subscription_db(WSID, Channel, SubChannel))
630 ).
631
636
637unsubscribe(WSID, Channel) :-
638 unsubscribe(WSID, Channel, _SubChannel).
639
640unsubscribe(WSID, Channel, SubChannel) :-
641 use_redis,
642 !,
643 ( ( nonvar(WSID), nonvar(Channel), nonvar(SubChannel)
644 -> true
645 ; subscription(WSID, Channel, SubChannel)
646 ),
647 redis_unsubscribe(WSID, Channel, SubChannel),
648 fail
649 ; true
650 ).
651unsubscribe(WSID, Channel, SubChannel) :-
652 retractall(subscription_db(WSID, Channel, SubChannel)).
653
654redis_unsubscribe(WSID, Channel, SubChannel) :-
655 redis_key(channel(SubChannel), Server, ChKey),
656 redis_key(subscription(WSID), Server, WsKey),
657 redis(Server, srem(ChKey, WSID-Channel as prolog)),
658 redis(Server, srem(WsKey, Channel-SubChannel as prolog)).
659
660
661 664
676
677visitor(WSID) :-
678 visitor(WSID, _).
679
680visitor(WSID, Consumer) :-
681 wsid_session(WSID, _Session, Consumer),
682 \+ pending_visitor(WSID, 30).
683
684visitor_count(Count) :-
685 use_redis,
686 !,
687 active_wsid_count(Count).
688visitor_count(Count) :-
689 aggregate_all(count, visitor(_), Count).
690
695
696pending_visitor(WSID, Timeout) :-
697 wsid_status(WSID, lost(Lost)),
698 get_time(Now),
699 Now - Lost > Timeout.
700
704
705wsid_visitor(WSID, Visitor) :-
706 nonvar(WSID),
707 !,
708 wsid_session(WSID, Session),
709 session_user(Session, Visitor).
710wsid_visitor(WSID, Visitor) :-
711 session_user(Session, Visitor),
712 wsid_session(WSID, Session).
713
718
719existing_visitor(WSID, Session, TmpUser, UserData) :-
720 wsid_session(WSID, Session),
721 session_user(Session, TmpUser),
722 visitor_data(TmpUser, UserData),
723 !.
724existing_visitor(WSID, Session, _, _) :-
725 wsid_session_reclaim_all(WSID, Session),
726 fail.
727
743
744create_visitor(WSID, Session, TmpUser, UserData, Options) :-
745 register_wsid_session(WSID, Session),
746 create_session_user(Session, TmpUser, UserData, Options).
747
751
752random_key(Len, Key) :-
753 length(Codes, Len),
754 maplist(random_between(0,255), Codes),
755 phrase(base64url(Codes), Encoded),
756 atom_codes(Key, Encoded).
757
768
769destroy_visitor(WSID) :-
770 must_be(atom, WSID),
771 update_session_timeout(WSID),
772 destroy_reason(WSID, Reason),
773 ( Reason == unload
774 -> reclaim_visitor(WSID)
775 ; get_time(Now),
776 wsid_status_set_lost(WSID, Now)
777 ),
778 visitor_count(Count),
779 debug(chat(visitor), '~p left. Broadcasting ~d visitors', [WSID,Count]),
780 chat_broadcast(_{ type:removeUser,
781 wsid:WSID,
782 reason:Reason,
783 visitors:Count
784 }).
785
786destroy_reason(WSID, Reason) :-
787 wsid_status_del_unload(WSID),
788 !,
789 Reason = unload.
790destroy_reason(_, close).
791
798
799update_session_timeout(WSID) :-
800 wsid_session(WSID, Session),
801 !,
802 ( wsid_session(WSID2, Session),
803 WSID2 \== WSID
804 -> true
805 ; debug(chat(websocket), 'Websocket ~p was last in session ~p',
806 [ WSID, Session]),
807 http_set_session(Session, timeout(900))
808 ).
809update_session_timeout(_).
810
818
819:- dynamic gc_status/1. 820
821gc_visitors :-
822 swish_config(session_lost_timeout, TMO),
823 gc_status(Status),
824 ( Status == running
825 -> true
826 ; Status = completed(When),
827 get_time(Now),
828 Now-When > TMO
829 -> fail
830 ; retractall(gc_status(completed(_)))
831 ),
832 !.
833gc_visitors :-
834 swish_config(session_lost_timeout, TMO),
835 catch(thread_create(gc_visitors_sync(TMO), _Id,
836 [ alias('swish_chat_gc_visitors'),
837 detached(true)
838 ]),
839 error(permission_error(create, thread, _), _),
840 true).
841
842gc_visitors_sync(TMO) :-
843 setup_call_cleanup(
844 asserta(gc_status(running), Ref),
845 ( do_gc_visitors(TMO),
846 get_time(Now),
847 asserta(gc_status(completed(Now)))
848 ),
849 erase(Ref)).
850
851do_gc_visitors(TMO) :-
852 findall(WSID-Consumer,
853 active_visitor(TMO, WSID, Consumer),
854 Pairs),
855 transaction(
856 ( retractall(active_wsid(_,_)),
857 forall(member(WSID-Consumer, Pairs),
858 assertz(active_wsid(WSID, Consumer))))).
859
860active_visitor(TMO, WSID, Consumer) :-
861 wsid_session(WSID, _Session, Consumer),
862 ( valid_visitor(WSID, TMO, Consumer)
863 -> true
864 ; reclaim_visitor(WSID),
865 fail
866 ).
867
868valid_visitor(WSID, _TMO, _Consumer) :-
869 hub_member(swish_chat, WSID),
870 !.
871valid_visitor(WSID, TMO, _Consumer) :-
872 wsid_status(WSID, lost(Lost)),
873 !,
874 get_time(Now),
875 Now - Lost < TMO.
876valid_visitor(_WSID, _TMO, Consumer) :-
877 use_redis,
878 !,
879 \+ redis_consumer(Consumer).
880
886
887reclaim_visitor(WSID) :-
888 debug(chat(gc), 'Reclaiming idle ~p', [WSID]),
889 reclaim_wsid_session(WSID),
890 wsid_status_del(WSID),
891 unsubscribe(WSID, _).
892
893reclaim_wsid_session(WSID) :-
894 ( wsid_session_reclaim(WSID, Session)
895 -> http_session_retractall(websocket(_, _), Session)
896 ; true
897 ).
898
904
905:- listen(http_session(end(SessionID, _Peer)),
906 destroy_session_user(SessionID)). 907
908create_session_user(Session, TmpUser, UserData, _Options) :-
909 session_user(Session, TmpUser),
910 visitor_data(TmpUser, UserData),
911 !.
912create_session_user(Session, TmpUser, UserData, Options) :-
913 uuid(TmpUser),
914 get_visitor_data(UserData, Options),
915 session_user_create(Session, TmpUser),
916 visitor_data_set(TmpUser, UserData).
917
918destroy_session_user(Session) :-
919 forall(wsid_session(WSID, Session, _Token),
920 inform_session_closed(WSID, Session)),
921 wsid_session_del_session(Session),
922 forall(session_user_del(Session, TmpUser),
923 destroy_visitor_data(TmpUser)).
924
925destroy_visitor_data(TmpUser) :-
926 ( visitor_data_del(TmpUser, Data),
927 release_avatar(Data.get(avatar)),
928 fail
929 ; true
930 ).
931
932inform_session_closed(WSID, Session) :-
933 ignore(hub_send(WSID, json(_{type:session_closed}))),
934 session_user(Session, TmpUser),
935 update_visitor_data(TmpUser, _Data, logout).
936
937
951
952update_visitor_data(TmpUser, _Data, logout) :-
953 !,
954 anonymise_user_data(TmpUser, NewData),
955 set_visitor_data(TmpUser, NewData, logout).
956update_visitor_data(TmpUser, Data, Reason) :-
957 profile_reason(Reason),
958 !,
959 ( visitor_data(TmpUser, Old)
960 ; Old = v{}
961 ),
962 copy_profile([name,avatar,email], Data, Old, New),
963 set_visitor_data(TmpUser, New, Reason).
964update_visitor_data(TmpUser, _{name:Name}, 'set-nick-name') :-
965 !,
966 visitor_data(TmpUser, Old),
967 set_nick_name(Old, Name, New),
968 set_visitor_data(TmpUser, New, 'set-nick-name').
969update_visitor_data(TmpUser, Data, Reason) :-
970 set_visitor_data(TmpUser, Data, Reason).
971
972profile_reason('profile-edit').
973profile_reason('login').
974
975copy_profile([], _, Data, Data).
976copy_profile([H|T], New, Data0, Data) :-
977 copy_profile_field(H, New, Data0, Data1),
978 copy_profile(T, New, Data1, Data).
979
980copy_profile_field(avatar, New, Data0, Data) :-
981 !,
982 ( Data1 = Data0.put(avatar,New.get(avatar))
983 -> Data = Data1.put(avatar_source, profile)
984 ; email_gravatar(New.get(email), Avatar),
985 valid_gravatar(Avatar)
986 -> Data = Data0.put(_{avatar:Avatar,avatar_source:email})
987 ; Avatar = Data0.get(anonymous_avatar)
988 -> Data = Data0.put(_{avatar:Avatar,avatar_source:client})
989 ; noble_avatar_url(Avatar, []),
990 Data = Data0.put(_{avatar:Avatar,avatar_source:generated,
991 anonymous_avatar:Avatar
992 })
993 ).
994copy_profile_field(email, New, Data0, Data) :-
995 !,
996 ( NewMail = New.get(email)
997 -> update_avatar_from_email(NewMail, Data0, Data1),
998 Data = Data1.put(email, NewMail)
999 ; update_avatar_from_email('', Data0, Data1),
1000 ( del_dict(email, Data1, _, Data)
1001 -> true
1002 ; Data = Data1
1003 )
1004 ).
1005copy_profile_field(F, New, Data0, Data) :-
1006 ( Data = Data0.put(F, New.get(F))
1007 -> true
1008 ; del_dict(F, Data0, _, Data)
1009 -> true
1010 ; Data = Data0
1011 ).
1012
1013set_nick_name(Data0, Name, Data) :-
1014 Data = Data0.put(_{name:Name, anonymous_name:Name}).
1015
1022
1023update_avatar_from_email(_, Data, Data) :-
1024 Data.get(avatar_source) == profile,
1025 !.
1026update_avatar_from_email('', Data0, Data) :-
1027 Data0.get(avatar_source) == email,
1028 !,
1029 noble_avatar_url(Avatar, []),
1030 Data = Data0.put(_{avatar:Avatar, anonymous_avatar:Avatar,
1031 avatar_source:generated}).
1032update_avatar_from_email(Email, Data0, Data) :-
1033 email_gravatar(Email, Avatar),
1034 valid_gravatar(Avatar),
1035 !,
1036 Data = Data0.put(avatar, Avatar).
1037update_avatar_from_email(_, Data0, Data) :-
1038 ( Avatar = Data0.get(anonymous_avatar)
1039 -> Data = Data0.put(_{avatar:Avatar, avatar_source:client})
1040 ; noble_avatar_url(Avatar, []),
1041 Data = Data0.put(_{avatar:Avatar, anonymous_avatar:Avatar,
1042 avatar_source:generated})
1043 ).
1044
1048
1049anonymise_user_data(TmpUser, Data) :-
1050 visitor_data(TmpUser, Old),
1051 ( _{anonymous_name:AName, anonymous_avatar:AAvatar} :< Old
1052 -> Data = _{anonymous_name:AName, anonymous_avatar:AAvatar,
1053 name:AName, avatar:AAvatar, avatar_source:client}
1054 ; _{anonymous_avatar:AAvatar} :< Old
1055 -> Data = _{anonymous_avatar:AAvatar,
1056 avatar:AAvatar, avatar_source:client}
1057 ; _{anonymous_name:AName} :< Old
1058 -> noble_avatar_url(Avatar, []),
1059 Data = _{anonymous_name:AName, anonymous_avatar:Avatar,
1060 name:AName, avatar:Avatar, avatar_source:generated}
1061 ),
1062 !.
1063anonymise_user_data(_, Data) :-
1064 noble_avatar_url(Avatar, []),
1065 Data = _{anonymous_avatar:Avatar,
1066 avatar:Avatar, avatar_source:generated}.
1067
1072
1073set_visitor_data(TmpUser, Data, Reason) :-
1074 visitor_data_set(TmpUser, Data),
1075 inform_visitor_change(TmpUser, Reason).
1076
1083
1084inform_visitor_change(TmpUser, Reason) :-
1085 http_in_session(Session),
1086 !,
1087 public_user_data(TmpUser, Data),
1088 forall(wsid_session(WSID, Session),
1089 inform_friend_change(WSID, Data, Reason)).
1090inform_visitor_change(TmpUser, Reason) :-
1091 nb_current(wsid, WSID),
1092 !,
1093 public_user_data(TmpUser, Data),
1094 inform_friend_change(WSID, Data, Reason).
1095inform_visitor_change(_, _).
1096
1097inform_friend_change(WSID, Data, Reason) :-
1098 Message = json(_{ type:"profile",
1099 wsid:WSID,
1100 reason:Reason
1101 }.put(Data)),
1102 send_friends(WSID, Message).
1103
1110
1111sync_gazers(WSID, Files0) :-
1112 findall(F, subscription(WSID, gitty, F), Viewing0),
1113 sort(Files0, Files),
1114 sort(Viewing0, Viewing),
1115 ( Files == Viewing
1116 -> true
1117 ; ord_subtract(Files, Viewing, New),
1118 add_gazing(WSID, New),
1119 ord_subtract(Viewing, Files, Left),
1120 del_gazing(WSID, Left)
1121 ).
1122
1123add_gazing(_, []) :- !.
1124add_gazing(WSID, Files) :-
1125 inform_me_about_existing_gazers(WSID, Files),
1126 inform_existing_gazers_about_newby(WSID, Files).
1127
1128inform_me_about_existing_gazers(WSID, Files) :-
1129 hub_member(swish_chat, WSID),
1130 !,
1131 findall(Gazer, files_gazer(Files, Gazer), Gazers),
1132 ignore(hub_send(WSID, json(_{type:"gazers", gazers:Gazers}))).
1133inform_me_about_existing_gazers(_, _).
1134
1135files_gazer(Files, Gazer) :-
1136 member(File, Files),
1137 subscription(WSID, gitty, File),
1138 wsid_session(WSID, Session),
1139 session_user(Session, UID),
1140 public_user_data(UID, Data),
1141 Gazer = _{file:File, uid:UID, wsid:WSID}.put(Data).
1142
1143inform_existing_gazers_about_newby(WSID, Files) :-
1144 forall(member(File, Files),
1145 signal_gazer(WSID, File)).
1146
1147signal_gazer(WSID, File) :-
1148 subscribe(WSID, gitty, File),
1149 broadcast_event(opened(File), File, WSID).
1150
1151del_gazing(_, []) :- !.
1152del_gazing(WSID, Files) :-
1153 forall(member(File, Files),
1154 del_gazing1(WSID, File)).
1155
1156del_gazing1(WSID, File) :-
1157 broadcast_event(closed(File), File, WSID),
1158 unsubscribe(WSID, gitty, File).
1159
1164
1165add_user_details(Message, Enriched) :-
1166 public_user_data(Message.uid, Data),
1167 Enriched = Message.put(Data).
1168
1173
1174public_user_data(UID, Public) :-
1175 visitor_data(UID, Data),
1176 ( _{name:Name, avatar:Avatar} :< Data
1177 -> Public = _{name:Name, avatar:Avatar}
1178 ; _{avatar:Avatar} :< Data
1179 -> Public = _{avatar:Avatar}
1180 ; Public = _{}
1181 ).
1182
1201
1202get_visitor_data(Data, Options) :-
1203 option(identity(Identity), Options),
1204 findall(N-V, visitor_property(Identity, Options, N, V), Pairs),
1205 dict_pairs(Data, v, Pairs).
1206
1207visitor_property(Identity, Options, name, Name) :-
1208 ( user_property(Identity, name(Name))
1209 -> true
1210 ; option(nick_name(Name), Options)
1211 ).
1212visitor_property(Identity, _, email, Email) :-
1213 user_property(Identity, email(Email)).
1214visitor_property(Identity, Options, Name, Value) :-
1215 ( user_property(Identity, avatar(Avatar))
1216 -> avatar_property(Avatar, profile, Name, Value)
1217 ; user_property(Identity, email(Email)),
1218 email_gravatar(Email, Avatar),
1219 valid_gravatar(Avatar)
1220 -> avatar_property(Avatar, email, Name, Value)
1221 ; option(avatar(Avatar), Options)
1222 -> avatar_property(Avatar, client, Name, Value)
1223 ; noble_avatar_url(Avatar, Options),
1224 avatar_property(Avatar, generated, Name, Value)
1225 ).
1226visitor_property(_, Options, anonymous_name, Name) :-
1227 option(nick_name(Name), Options).
1228visitor_property(_, Options, anonymous_avatar, Avatar) :-
1229 option(avatar(Avatar), Options).
1230
1231
1232avatar_property(Avatar, _Source, avatar, Avatar).
1233avatar_property(_Avatar, Source, avatar_source, Source).
1234
1235
1236 1239
1240:- http_handler(swish('avatar/'), reply_avatar, [id(avatar), prefix]). 1241
1252
1253reply_avatar(Request) :-
1254 cors_enable,
1255 option(path_info(Local), Request),
1256 ( absolute_file_name(noble_avatar(Local), Path,
1257 [ access(read),
1258 file_errors(fail)
1259 ])
1260 -> true
1261 ; create_avatar(Local, Path)
1262 ),
1263 http_reply_file(Path, [unsafe(true)], Request).
1264
1265
1266noble_avatar_url(HREF, Options) :-
1267 option(avatar(HREF), Options),
1268 !.
1269noble_avatar_url(HREF, _Options) :-
1270 swish_config:config(avatars, noble),
1271 !,
1272 noble_avatar(_Gender, Path, true),
1273 file_base_name(Path, File),
1274 http_absolute_location(swish(avatar/File), HREF, []).
1275noble_avatar_url(HREF, _Options) :-
1276 A is random(0x1FFFFF+1),
1277 http_absolute_location(icons('avatar.svg'), HREF0, []),
1278 format(atom(HREF), '~w#~d', [HREF0, A]).
1279
1280
1281
1282 1285
1296
1297chat_broadcast(Message) :-
1298 use_redis,
1299 !,
1300 redis(swish, publish(swish:chat, chat(Message) as prolog)).
1301chat_broadcast(Message) :-
1302 chat_broadcast_local(Message).
1303
1304chat_broadcast(Message, Channel) :-
1305 use_redis,
1306 !,
1307 redis(swish, publish(swish:chat, chat(Message, Channel) as prolog)).
1308chat_broadcast(Message, Channel) :-
1309 chat_broadcast_local(Message, Channel).
1310
1311
1312chat_broadcast_local(Message) :-
1313 debug(chat(broadcast), 'Broadcast: ~p', [Message]),
1314 hub_broadcast(swish_chat, json(Message)).
1315
1316chat_broadcast_local(Message, Channel/SubChannel) :-
1317 !,
1318 must_be(atom, Channel),
1319 must_be(atom, SubChannel),
1320 debug(chat(broadcast), 'Broadcast on ~p: ~p',
1321 [Channel/SubChannel, Message]),
1322 hub_broadcast(swish_chat, json(Message),
1323 subscribed(Channel, SubChannel)).
1324chat_broadcast_local(Message, Channel) :-
1325 must_be(atom, Channel),
1326 debug(chat(broadcast), 'Broadcast on ~p: ~p', [Channel, Message]),
1327 hub_broadcast(swish_chat, json(Message),
1328 subscribed(Channel)).
1329
1335
1336subscribed(Channel, WSID) :-
1337 subscription(WSID, Channel, _).
1338subscribed(Channel, SubChannel, WSID) :-
1339 subscription(WSID, Channel, SubChannel).
1340subscribed(gitty, SubChannel, WSID) :-
1341 swish_config:config(hangout, SubChannel),
1342 \+ subscription(WSID, gitty, SubChannel).
1343
1347
1348send_friends(WSID, Message) :-
1349 use_redis,
1350 !,
1351 redis(swish, publish(swish:chat, send_friends(WSID, Message) as prolog)).
1352send_friends(WSID, Message) :-
1353 send_friends_local(WSID, Message).
1354
1355send_friends_local(WSID, Message) :-
1356 hub_send_if_on_me(WSID, Message),
1357 forall(distinct(viewing_same_file(WSID, Friend)),
1358 ignore(hub_send_if_on_me(Friend, Message))).
1359
1360hub_send_if_on_me(WSID, Message) :-
1361 hub_member(swish_chat, WSID),
1362 !,
1363 hub_send(WSID, Message).
1364hub_send_if_on_me(_, _).
1365
1366viewing_same_file(WSID, Friend) :-
1367 subscription(WSID, gitty, File),
1368 subscription(Friend, gitty, File),
1369 Friend \== WSID.
1370
1371
1372 1375
1376:- initialization
1377 listen(redis(_, 'swish:chat', Message),
1378 chat_message(Message)). 1379
1380chat_message(chat(Message)) :-
1381 update_visitors(Message),
1382 chat_broadcast_local(Message).
1383chat_message(chat(Message, Channel)) :-
1384 chat_broadcast_local(Message, Channel).
1385chat_message(send_friends(WSID, Message)) :-
1386 send_friends_local(WSID, Message).
1387
1393
1394:- dynamic
1395 ( last_wsid_sync/1,
1396 active_wsid/2
1397 ) as volatile. 1398
1399update_visitors(Msg),
1400 _{type:removeUser, wsid:WSID} :< Msg =>
1401 retractall(active_wsid(WSID, _)).
1402update_visitors(Msg),
1403 _{type:joined, wsid:WSID} :< Msg,
1404 \+ active_wsid(WSID, _) =>
1405 asserta(active_wsid(WSID, Msg.get(consumer, -))).
1406update_visitors(Msg),
1407 _{type:rejoined, wsid:WSID} :< Msg,
1408 \+ active_wsid(WSID, _) =>
1409 asserta(active_wsid(WSID, Msg.get(consumer, -))).
1410update_visitors(_) =>
1411 true.
1412
1413active_wsid_count(Count) :-
1414 predicate_property(active_wsid(_,_), number_of_clauses(Count)),
1415 !.
1416active_wsid_count(0).
1417
1418active_wsid_count(Consumer, Count) :-
1419 aggregate(count, WSID^active_wsid(WSID, Consumer), Count).
1420
1421
1422 1425
1426create_chat_room :-
1427 current_hub(swish_chat, _),
1428 !.
1429create_chat_room :-
1430 with_mutex(swish_chat, create_chat_room_sync).
1431
1432create_chat_room_sync :-
1433 current_hub(swish_chat, _),
1434 !.
1435create_chat_room_sync :-
1436 hub_create(swish_chat, Room, _{}),
1437 thread_create(swish_chat(Room), _, [alias(swish_chat)]).
1438
1439swish_chat(Room) :-
1440 ( catch_with_backtrace(swish_chat_event(Room), E, chat_exception(E))
1441 -> true
1442 ; print_message(warning, goal_failed(swish_chat_event(Room)))
1443 ),
1444 swish_chat(Room).
1445
1446chat_exception('$aborted') :- !.
1447chat_exception(E) :-
1448 print_message(warning, E).
1449
1450swish_chat_event(Room) :-
1451 thread_get_message(Room.queues.event, Message),
1452 ( handle_message(Message, Room)
1453 -> true
1454 ; print_message(warning, goal_failed(handle_message(Message, Room)))
1455 ).
1456
1462
1463handle_message(Message, _Room) :-
1464 websocket{opcode:text} :< Message,
1465 !,
1466 atom_json_dict(Message.data, JSON, []),
1467 debug(chat(received), 'Received from ~p: ~p', [Message.client, JSON]),
1468 WSID = Message.client,
1469 ( current_wsid(WSID)
1470 -> setup_call_cleanup(
1471 b_setval(wsid, WSID),
1472 json_message(JSON, WSID),
1473 nb_delete(wsid))
1474 ; debug(chat(visitor), 'Ignored ~p (WSID ~p unknown)', [Message, WSID])
1475 ).
1476handle_message(Message, _Room) :-
1477 hub{joined:WSID} :< Message,
1478 !,
1479 debug(chat(visitor), 'Joined: ~p', [WSID]).
1480handle_message(Message, _Room) :-
1481 hub{left:WSID, reason:write(Lost)} :< Message,
1482 !,
1483 ( destroy_visitor(WSID)
1484 -> debug(chat(visitor), 'Left ~p due to write error for ~p',
1485 [WSID, Lost])
1486 ; true
1487 ).
1488handle_message(Message, _Room) :-
1489 hub{left:WSID} :< Message,
1490 !,
1491 ( destroy_visitor(WSID)
1492 -> debug(chat(visitor), 'Left: ~p', [WSID])
1493 ; true
1494 ).
1495handle_message(Message, _Room) :-
1496 websocket{opcode:close, client:WSID} :< Message,
1497 !,
1498 debug(chat(visitor), 'Left: ~p', [WSID]),
1499 destroy_visitor(WSID).
1500handle_message(Message, _Room) :-
1501 debug(chat(ignored), 'Ignoring chat message ~p', [Message]).
1502
1503
1519
1520json_message(Dict, WSID) :-
1521 _{ type: "subscribe",
1522 channel:ChannelS, sub_channel:SubChannelS} :< Dict,
1523 !,
1524 atom_string(Channel, ChannelS),
1525 atom_string(SubChannel, SubChannelS),
1526 subscribe(WSID, Channel, SubChannel).
1527json_message(Dict, WSID) :-
1528 _{type: "subscribe", channel:ChannelS} :< Dict,
1529 !,
1530 atom_string(Channel, ChannelS),
1531 subscribe(WSID, Channel).
1532json_message(Dict, WSID) :-
1533 _{ type: "unsubscribe",
1534 channel:ChannelS, sub_channel:SubChannelS} :< Dict,
1535 !,
1536 atom_string(Channel, ChannelS),
1537 atom_string(SubChannel, SubChannelS),
1538 unsubscribe(WSID, Channel, SubChannel).
1539json_message(Dict, WSID) :-
1540 _{type: "unsubscribe", channel:ChannelS} :< Dict,
1541 !,
1542 atom_string(Channel, ChannelS),
1543 unsubscribe(WSID, Channel).
1544json_message(Dict, WSID) :-
1545 _{type: "unload"} :< Dict, 1546 !,
1547 sync_gazers(WSID, []),
1548 wsid_status_set_unload(WSID).
1549json_message(Dict, WSID) :-
1550 _{type: "has-open-files", files:FileDicts} :< Dict,
1551 !,
1552 maplist(dict_file_name, FileDicts, Files),
1553 sync_gazers(WSID, Files).
1554json_message(Dict, WSID) :-
1555 _{type: "reloaded", file:FileS, commit:Hash} :< Dict,
1556 !,
1557 atom_string(File, FileS),
1558 event_html(reloaded(File), HTML),
1559 Message = _{ type:notify,
1560 wsid:WSID,
1561 html:HTML,
1562 event:reloaded,
1563 argv:[File,Hash]
1564 },
1565 chat_broadcast(Message, gitty/File).
1566json_message(Dict, WSID) :-
1567 _{type: "set-nick-name", name:Name} :< Dict,
1568 !,
1569 wsid_visitor(WSID, Visitor),
1570 update_visitor_data(Visitor, _{name:Name}, 'set-nick-name').
1571json_message(Dict, WSID) :-
1572 _{type: "chat-message", docid:DocID} :< Dict,
1573 !,
1574 chat_add_user_id(WSID, Dict, Message),
1575 ( forbidden(Message, DocID, Why)
1576 -> hub_send(WSID, json(json{type:forbidden,
1577 action:chat_post,
1578 about:DocID,
1579 message:Why
1580 }))
1581 ; chat_relay(Message)
1582 ).
1583json_message(Dict, _WSID) :-
1584 debug(chat(ignored), 'Ignoring JSON message ~p', [Dict]).
1585
1586dict_file_name(Dict, File) :-
1587 atom_string(File, Dict.get(file)).
1588
1599
1600forbidden(Message, DocID, Why) :-
1601 \+ swish_config:config(chat_spam_protection, false),
1602 \+ ws_authorized(chat(post(Message, DocID)), Message.user),
1603 !,
1604 Why = "Due to frequent spamming we were forced to limit \c
1605 posting chat messages to users who are logged in.".
1606forbidden(Message, _DocID, Why) :-
1607 Text = Message.get(text),
1608 string_length(Text, Len),
1609 Len > 500,
1610 Why = "Chat messages are limited to 500 characters".
1611forbidden(Message, _DocID, Why) :-
1612 Payloads = Message.get(payload),
1613 member(Payload, Payloads),
1614 large_payload(Payload, Why),
1615 !.
1616forbidden(Message, _DocID, Why) :-
1617 \+ swish_config:config(chat_spam_protection, false),
1618 eval_content(Message.get(text), _WC, Score),
1619 user_score(Message, Score, Cummulative, _Count),
1620 Score*2 + Cummulative < 0,
1621 !,
1622 Why = "Chat messages must be in English and avoid offensive language".
1623
1624large_payload(Payload, Why) :-
1625 Selections = Payload.get(selection),
1626 member(Selection, Selections),
1627 ( string_length(Selection.get(string), SelLen), SelLen > 500
1628 ; string_length(Selection.get(context), SelLen), SelLen > 500
1629 ),
1630 !,
1631 Why = "Selection too long (max. 500 characters)".
1632large_payload(Payload, Why) :-
1633 string_length(Payload.get(query), QLen), QLen > 1000,
1634 !,
1635 Why = "Query too long (max. 1000 characters)".
1636
1637user_score(Message, MsgScore, Cummulative, Count) :-
1638 Profile = Message.get(user).get(profile_id),
1639 !,
1640 block(Profile, MsgScore, Cummulative, Count).
1641user_score(_, _, 0, 1).
1642
1646
1647:- dynamic
1648 blocked/4. 1649
1650block(User, Score, Cummulative, Count) :-
1651 blocked(User, Score0, Count0, Time),
1652 !,
1653 get_time(Now),
1654 Cummulative = Score0*(0.5**((Now-Time)/600)) + Score,
1655 Count is Count0 + 1,
1656 asserta(blocked(User, Cummulative, Count, Now)),
1657 retractall(blocked(User, Score0, Count0, Time)).
1658block(User, Score, Score, 1) :-
1659 get_time(Now),
1660 asserta(blocked(User, Score, 1, Now)).
1661
1662
1663 1666
1670
1671chat_add_user_id(WSID, Dict, Message) :-
1672 wsid_session(WSID, Session),
1673 session_user(Session, Visitor),
1674 visitor_data(Visitor, UserData),
1675 User0 = u{avatar:UserData.avatar,
1676 wsid:WSID
1677 },
1678 ( Name = UserData.get(name)
1679 -> User1 = User0.put(name, Name)
1680 ; User1 = User0
1681 ),
1682 ( http_current_session(Session, profile_id(ProfileID))
1683 -> User = User1.put(profile_id, ProfileID)
1684 ; User = User1
1685 ),
1686 Message = Dict.put(user, User).
1687
1688
1692
1693chat_about(DocID, Message) :-
1694 chat_relay(Message.put(docid, DocID)).
1695
1699
1700chat_relay(Message) :-
1701 chat_enrich(Message, Message1),
1702 chat_send(Message1).
1703
1707
1708chat_enrich(Message0, Message) :-
1709 get_time(Now),
1710 uuid(ID),
1711 Message = Message0.put(_{time:Now, id:ID}).
1712
1717
1718chat_send(Message) :-
1719 atom_concat("gitty:", File, Message.docid),
1720 broadcast(swish(chat(Message))),
1721 ( Message.get(volatile) == true
1722 -> true
1723 ; chat_store(Message)
1724 ),
1725 chat_broadcast(Message, gitty/File).
1726
1727
1728 1731
1732:- unlisten(swish(_)),
1733 listen(swish(Event), chat_event(Event)). 1734
1746
1747chat_event(Event) :-
1748 broadcast_event(Event),
1749 http_session_id(Session),
1750 debug(event, 'Event: ~p, session ~q', [Event, Session]),
1751 event_file(Event, File),
1752 !,
1753 ( wsid_session(WSID, Session),
1754 subscription(WSID, gitty, File)
1755 -> true
1756 ; wsid_session(WSID, Session)
1757 -> true
1758 ; WSID = undefined
1759 ),
1760 session_broadcast_event(Event, File, Session, WSID).
1761chat_event(logout(_ProfileID)) :-
1762 !,
1763 http_session_id(Session),
1764 session_user(Session, User),
1765 update_visitor_data(User, _, logout).
1766chat_event(visitor_count(Count)) :- 1767 visitor_count(Count).
1768chat_event(visitor_count(Cluster, Local)) :- 1769 visitor_count(Cluster),
1770 ( use_redis,
1771 redis_consumer(Consumer)
1772 -> ( active_wsid_count(Consumer, Local)
1773 -> true
1774 ; Local = 0
1775 )
1776 ; Local = Cluster
1777 ).
1778
1779:- if(current_predicate(current_profile/2)). 1780
1781chat_event(profile(ProfileID)) :-
1782 !,
1783 current_profile(ProfileID, Profile),
1784 http_session_id(Session),
1785 session_user(Session, User),
1786 update_visitor_data(User, Profile, login).
1787
1791
1792:- listen(user_profile(modified(ProfileID, Name, _Old, New)),
1793 propagate_profile_change(ProfileID, Name, New)). 1794
1795propagate_profile_change(ProfileID, _, _) :-
1796 http_current_session(Session, profile_id(ProfileID)),
1797 session_user(Session, User),
1798 current_profile(ProfileID, Profile),
1799 update_visitor_data(User, Profile, 'profile-edit').
1800
1801:- endif. 1802
1806
1807broadcast_event(updated(_File, _Commit)).
1808
1809
1817
1818broadcast_event(Event, File, WSID) :-
1819 wsid_session(WSID, Session),
1820 session_broadcast_event(Event, File, Session, WSID),
1821 !.
1822broadcast_event(_, _, _).
1823
1824session_broadcast_event(Event, File, Session, WSID) :-
1825 session_user(Session, UID),
1826 event_html(Event, HTML),
1827 Event =.. [EventName|Argv],
1828 Message0 = _{ type:notify,
1829 uid:UID,
1830 html:HTML,
1831 event:EventName,
1832 event_argv:Argv,
1833 wsid:WSID
1834 },
1835 add_user_details(Message0, Message),
1836 chat_broadcast(Message, gitty/File).
1837
1842
1843event_html(Event, HTML) :-
1844 ( phrase(event_message(Event), Tokens)
1845 -> true
1846 ; phrase(html('Unknown-event: ~p'-[Event]), Tokens)
1847 ),
1848 delete(Tokens, nl(_), SingleLine),
1849 with_output_to(string(HTML), print_html(SingleLine)).
1850
1851event_message(created(File)) -->
1852 html([ 'Created ', \file(File) ]).
1853event_message(reloaded(File)) -->
1854 html([ 'Reloaded ', \file(File) ]).
1855event_message(updated(File, _Commit)) -->
1856 html([ 'Saved ', \file(File) ]).
1857event_message(deleted(File, _From, _To)) -->
1858 html([ 'Deleted ', \file(File) ]).
1859event_message(closed(File)) -->
1860 html([ 'Closed ', \file(File) ]).
1861event_message(opened(File)) -->
1862 html([ 'Opened ', \file(File) ]).
1863event_message(download(File)) -->
1864 html([ 'Opened ', \file(File) ]).
1865event_message(download(Store, FileOrHash, Format)) -->
1866 { event_file(download(Store, FileOrHash, Format), File)
1867 },
1868 html([ 'Opened ', \file(File) ]).
1869
1870file(File) -->
1871 html(a(href('/p/'+File), File)).
1872
1876
1877event_file(created(File, _Commit), File).
1878event_file(updated(File, _Commit), File).
1879event_file(deleted(File, _Commit), File).
1880event_file(download(Store, FileOrHash, _Format), File) :-
1881 ( is_gitty_hash(FileOrHash)
1882 -> gitty_commit(Store, FileOrHash, Meta),
1883 File = Meta.name
1884 ; File = FileOrHash
1885 ).
1886
1887
1888 1891
1895
1896chat_to_profile(ProfileID, HTML) :-
1897 ( http_current_session(Session, profile_id(ProfileID)),
1898 wsid_session(WSID, Session),
1899 html_string(HTML, String),
1900 hub_send(WSID, json(_{ wsid:WSID,
1901 type:notify,
1902 html:String
1903 })),
1904 debug(notify(chat), 'Notify to ~p: ~p', [ProfileID, String]),
1905 fail
1906 ; true
1907 ).
1908
1909html_string(HTML, String) :-
1910 phrase(html(HTML), Tokens),
1911 delete(Tokens, nl(_), SingleLine),
1912 with_output_to(string(String), print_html(SingleLine)).
1913
1914
1915
1916
1917 1920
1925
1926notifications(_Options) -->
1927 { swish_config:config(chat, true) },
1928 !,
1929 html(div(class(chat),
1930 [ div(class('chat-users'),
1931 ul([ class([nav, 'navbar-nav', 'pull-right']),
1932 id(chat)
1933 ], [])),
1934 div(class('user-count'),
1935 [ span(id('user-count'), '?'),
1936 ' users online'
1937 ])
1938 ])).
1939notifications(_Options) -->
1940 [].
1941
1945
1946broadcast_bell(_Options) -->
1947 { swish_config:config(chat, true),
1948 swish_config:config(hangout, Hangout),
1949 atom_concat('gitty:', Hangout, HangoutID)
1950 },
1951 !,
1952 html([ a([ class(['dropdown-toggle', 'broadcast-bell']),
1953 'data-toggle'(dropdown)
1954 ],
1955 [ span([ id('broadcast-bell'),
1956 'data-document'(HangoutID)
1957 ], []),
1958 b(class(caret), [])
1959 ]),
1960 ul([ class(['dropdown-menu', 'pull-right']),
1961 id('chat-menu')
1962 ],
1963 [ li(a('data-action'('chat-shared'),
1964 'Open hangout')),
1965 li(a('data-action'('chat-about-file'),
1966 'Open chat for current file'))
1967 ])
1968 ]).
1969broadcast_bell(_Options) -->
1970 [].
1971
1972
1973 1976
1977:- multifile
1978 prolog:message_context//1. 1979
1980prolog:message_context(websocket(reconnect(Passed, Score))) -->
1981 [ 'WebSocket: too frequent reconnect requests (~1f sec; score = ~1f)'-
1982 [Passed, Score] ]