1:- module(slack_client, [
    2        slack_start_listener/0,
    3        slack_chat/2,
    4        slack_send/1,
    5        slack_ping/0,
    6        is_thread_running/1,
    7        slack_ensure_im/2,
    8        name_to_id/2
    9        ]).

slack_client - Provides a websocket API to write slack clients and bots

*/

   14:- if(exists_source(library(dicts))).   15 :- use_module(library(dicts)).   16:- endif.   17
   18:- use_module(library(http/http_open)).   19:- use_module(library(http/http_client)).   20:- use_module(library(http/http_json)).   21:- use_module(library(url)).   22:- use_module(library(http/json)).   23:- use_module(library(http/json_convert)).   24:- use_module(library(http/websocket)).   25
   26:- if(exists_source(library(logicmoo_common))).   27 :- use_module(library(logicmoo_common)).   28:- endif.   29
   30:- if(exists_source(library(dictoo))).   31 :- use_module(library(dictoo)).   32:- endif.   33
   34:- if(exists_source(library(udt))).   35% :- use_module(library(udt)).
   36:- endif.   37                
   38% dbgM(O):- term_to_atom(O,A), eggdrop:say("dmiles",A),!.
   39dbgM(O):- dbgM('~N% ~p.~n',[O]).
   40dbgM(F,O):- format(user_error,F,O),flush_output(user_error).
   41
   42:- use_module(library(eggdrop)).   43:- egg_go.   44
   45is_thread_running(ID):-
   46  is_thread(ID), thread_property(ID,status(What)),!,
   47   (What==running->true;(thread_join(ID,_ ),!,fail)).
   48
   49
   50:- dynamic(slack_token/1).   51
   52% ===============================================
   53% How this module might find your token:
   54% ===============================================
   55
   56% 1st - Checks for a local declaration 
   57%  (if the next line is uncommented and replaced by a real token )
   58% slack_token('xoxb-01234567890-xxxxxxxxxxxxxxxx').
   59slack_token('xoxb-1030744384576-1077790097232-HYegGh0O4y8SUnuiF6FSCrDC').
   60
   61% 2nd - Checks for a local file called ".slack_auth.pl" for slack_token/1 as above
   62:- if(( \+ slack_token(_) , exists_file('.slack_auth.pl'))).   63:- include('.slack_auth.pl').   64:- endif.   65
   66% 3rd - Checks env for SLACK_API_TOKEN
   67%  ( defined by# export SLACK_API_TOKEN=xoxb-01234567890-xxxxxxxxxxxxxxxx )
   68:- if(( \+ slack_token(_))).   69:- getenv('SLACK_API_TOKEN',Was)->asserta(slack_token(Was));true.   70:- endif.   71
   72% 4th - Checks users config directory for file called ".slack_auth.pl"  slack_token/1 as above
   73:- if(( \+ slack_token(_) , exists_file('~/.slack_auth.pl'))).   74:- include('~/.slack_auth.pl').   75:- endif.   76
   77:- if(( \+ slack_token(_))).   78:- throw(missing(slack_token(_))).   79:- endif.   80
   81
   82
   83
   84:- oo_class_begin(slack_client).   85
   86% url	A WebSocket Message Server URL.
   87:- oo_class_field(url).   88
   89% self	The authenticated bot user.
   90:- oo_inner_class_begin(clients).   91
   92slack_client:clients:new(Ref):- throw(clients:new(Ref)).
   93
   94:- oo_inner_class_end(clients).   95
   96
   97
   98% self	The authenticated bot user.
   99:- oo_inner_class_begin(self).  100:- oo_inner_class_end(self).  101
  102% self	The authenticated bot user.
  103:- oo_inner_class_begin(self).  104:- oo_inner_class_end(self).  105
  106
  107% team	Details on the authenticated user's team.
  108:- oo_inner_class_begin(team).  109:- oo_inner_class_end(team).  110
  111% users	A hash of user objects by user ID.
  112:- oo_inner_class_begin(users).  113:- oo_inner_class_end(users).  114
  115
  116% channels	A hash of channel objects, one for every channel visible to the authenticated user.
  117:- oo_inner_class_begin(channels).  118:- oo_inner_class_end(channels).  119
  120% groups	A hash of group objects, one for every group the authenticated user is in.
  121:- oo_inner_class_begin(self).  122:- oo_inner_class_end(self).  123
  124% ims	A hash of IM objects, one for every direct message channel visible to the authenticated user.
  125:- oo_inner_class_begin(groups).  126:- oo_inner_class_end(groups).  127
  128% bots	Details of the integrations set up on this team.
  129:- oo_inner_class_begin(bots).  130:- oo_inner_class_end(bots).  131
  132% text	textual utils.
  133:- oo_inner_class_begin(text).  134:- oo_inner_class_end(text).  135
  136% debug	Debugger fidling.
  137:- oo_inner_class_begin(self).  138:- oo_inner_class_end(self).  139
  140% events	Registered callbacks.
  141:- oo_inner_class_begin(events).  142:- oo_inner_class_end(events).  143
  144% files	registered storage.
  145:- oo_inner_class_begin(files).  146:- oo_inner_class_end(files).  147
  148:- oo_class_end(slack_client).  149
  150:- dynamic(tmpd:slack_info/3).  151
  152% ===============================================
  153% Utility functions
  154% ===============================================
  155
  156slack_token_string(S):-slack_token(T),atom_string(T,S).
  157
  158slack_get_websocket_url(URL):-
  159  slack_token(Token),
  160  format(atom(GetURL),'https://slack.com/api/rtm.start?token=~w',[Token]),
  161  http_open(GetURL, In, []),
  162  json_read_dict(In,Term),
  163  dict_pairs(Term,_,Pairs),
  164  must(maplist(slack_receive(rtm),Pairs)),
  165  URL=Term.url,
  166  % listing(tmpd:slack_info/3),
  167  close(In).
  168
  169:- dynamic(slack_websocket/3).  170
  171slack_get_websocket(WS):- slack_websocket(WS,_,_),!.
  172slack_get_websocket(WS):-
  173   slack_get_websocket_url(URL),!,
  174   slack_open_websocket(URL,WS),!.
  175
  176slack_open_websocket(URL,WS):-
  177   ignore(slack_websocket(OLD_WS,_,_)),
  178   http_open_websocket(URL, WS, []),
  179   stream_pair(WS,I,O),
  180   asserta(slack_websocket(WS,I,O)),
  181   (nonvar(OLD_WS)->slack_remove_websocket(OLD_WS);true).
  182
  183slack_remove_websocket(OLD_WS):-
  184   ignore(retract(slack_websocket(OLD_WS,_,_))),
  185   ignore(catch(ws_close(OLD_WS,1000,''),_,true)).
  186
  187% ===============================================
  188% Property Names
  189% ===============================================
  190skip_propname(K):- var(K),!.
  191skip_propname(_-_):-!,fail.
  192skip_propname(Type):-string(Type),!,string_to_atom(Type,K),!,skip_propname(K).
  193skip_propname(rtm).
  194skip_propname(rtm_e).
  195skip_propname(data).
  196skip_propname(var).
  197
  198slack_propname(Type,var):-var(Type),!.
  199slack_propname(Type,K):-string(Type),!,string_to_atom(Type,K).
  200slack_propname(Key-Type,NewType):-!,slack_propname(Key,Type,NewType).
  201slack_propname(Key.Type,NewType):-!,slack_propname(Key,Type,NewType).
  202slack_propname(Key,Key).
  203
  204slack_propname(Key,Type,NewType):- skip_propname(Type),!,slack_propname(Key,NewType).
  205slack_propname(Type,Key,NewType):- skip_propname(Type),!,slack_propname(Key,NewType).
  206slack_propname(_Type,Key,NewType):-slack_propname(Key,NewType).
  207
  208
  209slack_start_listener:-
  210 call_cleanup((
  211  repeat,
  212  once(slack_get_websocket(WS)),
  213  flush_output_safe,
  214  once(ws_receive(WS,Data,[format(json)])),
  215  flush_output_safe,
  216  (Data==
  217    end_of_file->!;
  218  (once(slack_receive_now(rtm_e,Data)),fail))),
  219  slack_remove_websocket(WS)).
  220
  221
  222
  223undict(ID,IDO):- is_dict(ID),ID.IDK=IDV,IDK=id,IDO=IDV.
  224undict(ID,ID).
  225
  226
  227% ignored?
  228slack_event(reconnect_url,Dict):- 
  229  must((Dict.url=URL,
  230   dbgM(reconnect(URL)),!,
  231   dbgM(slack_open_websocket(URL,_)))),
  232  nop(slack_open_websocket(URL,_)).
  233
  234% typify the data objects
  235slack_event(rtm_e,O):- is_dict(O),O.Key=Type,Key=type,!,slack_receive(Type,O),!.
  236
  237% simplify the data objects
  238slack_event(Type,O):- is_dict(O),O.Key=Data,Key=data,!,slack_receive(Type,Data),!.
  239
  240% Notice newly created IMs
  241slack_event(im_open,Dict):-
  242  Dict.channel=IDI,
  243  Dict.user=User,
  244  undict(IDI,ID),
  245  string_to_atom(ID,IDA),
  246  asserta(tmpd:slack_info(ims, instance, IDA)),
  247  asserta(tmpd:slack_info(IDA, id, ID)),
  248  asserta(tmpd:slack_info(IDA, user, User)).
  249
  250slack_event(_,end_of_file):- throw(slack_event(rtm_e,end_of_file)).
  251
  252
  253% slack_event(Type,Data):-add_slack_info(now,Type,Data).
  254
  255slack_unused(user_typing).
  256slack_unused(reconnect_url).
  257
  258%slack_receive_now(Type,Data):- dbgM(srn(Type,Data)),fail.
  259slack_receive_now(Type,Data):-
  260  nb_setval(websocket_in,Data),
  261  slack_receive(Type,Data),!.
  262
  263slack_receive( Var-Type, Data) :- Var==(var),!,slack_receive(Type,Data).
  264
  265%slack_receive(Type,Data):- dbgM(slack_receive(Type,Data)),fail.
  266slack_receive(Type,Data):- string(Data),(string_to_dict(Data,Dict)->true;string_to_atom(Data,Dict)),!,slack_receive(Type,Dict).
  267slack_receive(Type,Data):- slack_propname(Type,NewType)-> Type\==NewType,!,slack_receive(NewType,Data).
  268slack_receive(Type,Dict):- type_to_url(K,Type)-> K \== Type,!,slack_receive(K,Dict).
  269slack_receive(Type,Data):- slack_event(Type,Data),!.
  270slack_receive(Type,Data):- slack_inform(Type,Data),!.
  271slack_receive(Type,Data):- slack_unused(Type), dbgM(unused(slack_receive(Type,Data))),!.
  272slack_receive(Type,Data):- (nb_current(websocket_in,Data2)->true;Data2=[]),dbgM(unknown(slack_receive(Type,Data):-Data2)).
  273
  274
  275
  276% :- dynamic(tmpd:slack_info/3).
  277
  278slack_inform(Type,Data):-is_dict(Data),Data.Key=ID,Key=id,!,string_to_atom(ID,Atom), add_slack_info(Type,Atom,Data).
  279slack_inform(Type,Data):-is_dict(Data),dict_pairs(Data,_Tag,Pairs),!,slack_inform(Type,Pairs).
  280
  281slack_inform(rtm,Data):- is_list(Data),!, maplist(slack_receive(rtm),Data).
  282slack_inform(Type,Key-[A|Data]):-is_dict(A),is_list(Data),!,maplist(slack_receive(Type-Key),[A|Data]).
  283slack_inform(Type,Key-Data):- atomic(Data),add_slack_info(Type,Key,Data).
  284slack_inform(Type,Key-Data):- is_dict(Data),dict_pairs(Data,Tag,Pairs),maplist(slack_receive(Type-Key-Tag),Pairs).
  285
  286
  287
  288add_slack_info(Type,ID,Data):- is_dict(Data),dict_pairs(Data,_Tag,Pairs),!, 
  289   add_slack_info1(Type,instance,ID),
  290   maplist(add_slack_info1(Type,ID),Pairs).
  291
  292add_slack_info(Type,ID,Data):-add_slack_info1(Type,ID,Data).
  293
  294add_slack_info1(Type,Profile,Data):- is_dict(Data),dict_pairs(Data,_Tag,Pairs),!,add_slack_info1(Profile,Type,Pairs).
  295add_slack_info1(Type,ID,K-V):- Type==var, !,add_slack_info1(ID,K,V).
  296add_slack_info1(Type,ID,K-V):- Type==profile, !,add_slack_info1(ID,K,V).
  297add_slack_info1(Type,ID,Data):- is_list(Data),!,maplist(add_slack_info1(Type,ID),Data).
  298add_slack_info1(Type,ID,Data):- dbgM(add_slack_info1(Type,ID,Data)),fail.
  299add_slack_info1(Type,ID,K-V):- atom(Type),!,add_slack_info1(ID,K,V).
  300add_slack_info1(Type,ID,Data):-assert(tmpd:slack_info(Type,ID,Data)).
  301
  302get_slack_info(Object, Prop, Value):- tmpd:slack_info(Object, Prop, Value).
  303
  304name_to_id(Name,ID):-text_to_string(Name,NameS),get_slack_info(ID,name,NameS),ID\==var,!.
  305name_to_id(Name,ID):-text_to_string(Name,NameS),get_slack_info(ID,real_name,NameS),ID\==var,!.
  306name_to_id(Name,ID):-text_to_string(Name,NameS),get_slack_info(_,instance,ID), get_slack_info(ID,_,NameS),ID\==var,!.
  307
  308same_ids(ID,IDS):-text_to_string(ID,IDA),text_to_string(IDS,IDB),IDA==IDB.
  309
  310slack_ensure_im2(To,IM):- name_to_id(To,ID), get_slack_info(IM,user,IDS),same_ids(ID,IDS),get_slack_info(ims,instance,IM),!.
  311slack_ensure_im(To,IM):- slack_ensure_im2(To,IM),!.
  312slack_ensure_im(To,IM):- name_to_id(To,ID), slack_send({type:'im_open',user:ID}),!,must(slack_ensure_im2(To,IM)),!.
  313
  314
  315slack_id_time(ID,TS):-flag(slack_id,OID,OID+1),ID is OID+1,get_time(Time),number_string(Time,TS).
  316
  317
  318slack_self(Self):- get_slack_info(Self, real_name, "prolog_bot"),!.
  319                                  
  320%  {"id":2,"type":"ping","time":1484999912}
  321slack_ping :- slack_id_time(ID,_),get_time(Time),TimeRnd is round(Time),slack_send({"id":ID,"type":"ping", "time":TimeRnd}).
  322
  323% {"id":3,"type":"message","channel":"D3U47CE4W","text":"hi there"}
  324slack_chat :- slack_chat(logicmoo,"hi there").
  325slack_chat2:- slack_chat(dmiles,"hi dmiles").
  326
  327
  328slack_chat(To,Msg):-  slack_ensure_im(To,IM),
  329	  slack_send({
  330            type: "message", 
  331            username:"@prologmud_connection",
  332	    channel: IM,
  333            text: Msg
  334	   }),!.
  335
  336slack_post(Cmd,Params):- slack_token(Token),
  337	  make_url_params(Params,URLParams),
  338	  format(string(S),'https://slack.com/api/~w?token=~w&~w',[Cmd,Token,URLParams]),
  339	  dbgM('~N SLACK-POST ~q ~n',[S]),!,
  340	  http_open(S,Out,[]),!,
  341	  json_read_dict(Out,Dict),
  342	  dict_append_curls(Dict,Params,NewDict),
  343	  slack_receive(Cmd,NewDict).
  344
  345dict_append_curls(Dict,Params,NewDict):-any_to_curls(Params,Curly),
  346	dict_append_curls3(Dict,Curly,NewDict).
  347
  348dict_append_curls3(Dict,{},Dict):-!.
  349dict_append_curls3(Dict,{Curly},NewDict):-!,dict_append_curls3(Dict,Curly,NewDict).
  350dict_append_curls3(Dict,(A,B),NewDict):-!,dict_append_curls3(Dict,A,NewDictM),dict_append_curls3(NewDictM,B,NewDict).
  351dict_append_curls3(Dict,KS:V,NewDict):- string_to_atom(KS,K), put_dict(K,Dict,V,NewDict).
  352
  353
  354string_to_dict:-
  355 string_to_dict("{\"type\":\"dnd_updated_user\",\"user\":\"U3T3R279S\",\"dnd_status\":{\"dnd_enabled\":false,\"next_dnd_start_ts\":1,\"next_dnd_end_ts\":1},\"event_ts\":\"1485012634.280271\"}",Dict),
  356  dbgM(Dict).
  357
  358string_to_dict(String,Dict):-
  359   open_string(String,Stream),
  360   catch(json_read_dict(Stream,Dict),_,fail),!.
  361
  362
  363
  364type_to_url("message",'chat.postMessage').
  365type_to_url("im_open",'im.open').
  366
  367make_url_params({In},Out):-!,make_url_params(In,Out).
  368make_url_params((A,B),Out):-!,make_url_params(A,AA),make_url_params(B,BB),format(atom(Out),'~w&~w',[AA,BB]).
  369make_url_params([A|B],Out):-!,make_url_params(A,AA),make_url_params(B,BB),format(atom(Out),'~w&~w',[AA,BB]).
  370make_url_params([A],Out):-!,make_url_params(A,Out).
  371make_url_params(KV,Out):-get_kv_local(KV,K,A),www_form_encode(A,AA),format(atom(Out),'~w=~w',[K,AA]).
  372
  373get_kv_local(K:V,K,V):- must(nonvar(K);throw(get_kv_local(K:V,K,V))).
  374get_kv_local(K-V,K,V).
  375get_kv_local(K=V,K,V).
  376
  377slack_send(DataI):- any_to_curls(DataI,Data),slack_send00(Data).
  378
  379slack_send00({"type":Type,Params}):-type_to_url(Type,Cmd),!,slack_post(Cmd,Params).
  380% @TODO comment the above and fix this next block
  381slack_send00(Data):-slack_get_websocket(WebSocket),
  382   slack_websocket(WebSocket, _WsInput, WsOutput),
  383   flush_output(WsOutput),
  384   slack_send(WsOutput,Data),
  385   flush_output(WsOutput).
  386
  387dict_to_curly(Dict,{type:Type,Data}):- del_dict(type,Dict,Type,DictOut),dict_pairs(DictOut,_,Pairs),any_to_curls(Pairs,Data).
  388dict_to_curly(Dict,{type:Type,Data}):- dict_pairs(Dict,Type,Pairs),nonvar(Type),any_to_curls(Pairs,Data).
  389dict_to_curly(Dict,{Data}):- dict_pairs(Dict,_,Pairs),any_to_curls(Pairs,Data).
  390
  391any_to_curls(Dict,Out):- is_dict(Dict),!,dict_to_curly(Dict,Data),any_to_curls(Data,Out).
  392any_to_curls(Var,"var"):- \+ must(\+ var(Var)),!.
  393any_to_curls({DataI},{Data}):-!,any_to_curls(DataI,Data).
  394any_to_curls((A,B),(AA,BB)):-!,any_to_curls(A,AA),any_to_curls(B,BB).
  395any_to_curls([A|B],(AA,BB)):-!,any_to_curls(A,AA),any_to_curls(B,BB).
  396any_to_curls([A],AA):-!,any_to_curls(A,AA).
  397any_to_curls(KV,AA:BB):-get_kv_local(KV,A,B),!,any_to_curls(A,AA),any_to_curls(B,BB).
  398any_to_curls(A,AA):- catch(text_to_string(A,AA),_,fail),!.
  399any_to_curls(A,A).
  400
  401slack_send(WsOutput,Data):- format(WsOutput,'~q',[Data]),dbgM(slack_sent(Data)).
  402
  403
  404% start slack listener in a thread
  405:- if(( \+ (is_thread_running(slack_start_listener)))).  406:- thread_create(slack_start_listener,_,[alias(slack_start_listener)]).  407:- endif.  408
  409% if the above fails .. run in debug mode
  410:- if(( \+ (is_thread_running(slack_start_listener)))).  411:- slack_start_listener.  412:- endif.  413
  414
  415% if the above fails .. run in debug mode
  416:- if(( \+ (is_thread_running(slack_start_listener)))).  417:- rtrace(slack_start_listener).  418:- endif.