diff --git a/src/lib/slacko.ml b/src/lib/slacko.ml index 6261b26..a901bab 100644 --- a/src/lib/slacko.ml +++ b/src/lib/slacko.ml @@ -195,6 +195,8 @@ type channel = ChannelId of string | ChannelName of string type conversation = string +type im = string + type user = UserId of string | UserName of string type bot = BotId of string @@ -202,7 +204,7 @@ type bot = BotId of string type group = GroupId of string | GroupName of string (* TODO: Sure about user? *) -type chat = Channel of channel | Im of conversation | User of user | Group of group +type chat = Channel of channel | Im of im | User of user | Group of group type sort_criterion = Score | Timestamp @@ -226,13 +228,17 @@ let channel_of_yojson = function | `String x -> Ok (ChannelId x) | _ -> Error "Couldn't parse channel type" +let conversation_of_yojson = function + | `String x -> Ok x + | _ -> Error "Couldn't parse conversation type" + let group_of_yojson = function | `String x -> Ok (GroupId x) | _ -> Error "Couldn't parse group type" -let conversation_of_yojson = function +let im_of_yojson = function | `String x -> Ok x - | _ -> Error "Couldn't parse conversation type" + | _ -> Error "Couldn't parse im type" type topic_obj = { value: string; @@ -248,6 +254,7 @@ type channel_obj = { creator: user; is_archived: bool; is_general: bool; + name_normalized: string; is_member: bool; members: user list; topic: topic_obj; @@ -259,6 +266,26 @@ type channel_obj = { num_members: int option [@default None]; } [@@deriving of_yojson { strict = false }] +type conversation_obj = { + id: conversation; + name: string; + is_channel: bool; + created: Timestamp.t; + creator: user; + is_archived: bool; + is_general: bool; + name_normalized: string; + is_member: bool; + topic: topic_obj; + purpose: topic_obj; + last_read: Timestamp.t option [@default None]; + latest: string option [@default None]; + unread_count: int option [@default None]; + unread_count_display: int option [@default None]; + num_members: int option [@default None]; +} [@@deriving of_yojson { strict = false }] + + type user_obj = { id: user; name: string; @@ -336,7 +363,7 @@ type file_obj = { (*public_url_shared: ???;*) channels: channel list; groups: group list; - ims: conversation list; + ims: im list; initial_comment: Yojson.Safe.t option [@default None]; num_stars: int option [@default None]; } [@@deriving of_yojson { strict = false }] @@ -485,6 +512,8 @@ type im_obj = { user: user; created: Timestamp.t; is_user_deleted: bool; + is_open: bool option [@default None]; + last_read: Timestamp.t option [@default None]; unread_count: int option [@default None]; unread_count_display: int option [@default None]; } [@@deriving of_yojson { strict = false }] @@ -719,9 +748,9 @@ let maybe fn = function | None -> None (* nonpublic types for conversion in list types *) -type channels_list_obj = { - channels: channel_obj list -} [@@deriving of_yojson] +type conversations_list_obj = { + channels: conversation_obj list +} [@@deriving of_yojson { strict = false }] type users_list_obj = { members: user_obj list @@ -735,15 +764,15 @@ type im_list_obj = { ims: im_obj list; } [@@deriving of_yojson] -let channels_list ?exclude_archived session = - api_request "channels.list" +let conversations_list ?exclude_archived session = + api_request "conversations.list" |> optionally_add "exclude_archived" @@ maybe string_of_bool @@ exclude_archived |> query session >|= function | `Json_response d -> - (match d |> channels_list_obj_of_yojson with + (match d |> conversations_list_obj_of_yojson with | Ok x -> `Success x.channels - | Error x -> `ParseFailure x) + | Error e -> `ParseFailure e) | #parsed_auth_error as res -> res | _ -> `Unknown_error @@ -782,11 +811,9 @@ let lookupk session (listfn : 'a listfn) filterfn k = let id_of_channel session = function | ChannelId id -> Lwt.return @@ `Found id | ChannelName name -> - let base = String.sub name 1 @@ String.length name - 1 in - lookupk session channels_list (fun (x:channel_obj) -> x.name = base) @@ function + lookupk session conversations_list (fun (x:conversation_obj) -> x.name = name || x.name_normalized = name) @@ function | [] -> `Channel_not_found - | [{id = ChannelId s; _}] -> `Found s - | [_] -> failwith "Bad result from channel id lookup." + | [{id = s; _}] -> `Found s | _::_::_ -> failwith "Too many results from channel id lookup." (* like id_of_channel but does not resolve names to ids *) @@ -883,8 +910,8 @@ let user_of_string s = let group_of_string s = if s.[0] = 'G' then GroupId s else GroupName s -(* TODO Create a conversation if conversation does not exist? *) -let conversation_of_string s = +(* TODO Create a im if im does not exist? *) +let im_of_string s = if s.[0] = 'D' then s else failwith "Not an IM channel" let translate_parsing_error = function diff --git a/src/lib/slacko.mli b/src/lib/slacko.mli index 901b321..ce31c3c 100644 --- a/src/lib/slacko.mli +++ b/src/lib/slacko.mli @@ -279,9 +279,12 @@ type message channel id. *) type channel -(** A type of an IM conversation *) +(** A channel-like container for a conversation used by the Conversations API. *) type conversation +(** A type of an IM conversation *) +type im + (** An user, represented by either a user name or a user id. *) type user @@ -292,7 +295,7 @@ type bot type group (** A place one can post messages to. *) -type chat = Channel of channel | Im of conversation | User of user | Group of group +type chat = Channel of channel | Im of im | User of user | Group of group (** What criterion to use in search. *) type sort_criterion = Score | Timestamp @@ -357,6 +360,7 @@ type channel_obj = { creator: user; is_archived: bool; is_general: bool; + name_normalized: string; is_member: bool; members: user list; topic: topic_obj; @@ -368,6 +372,26 @@ type channel_obj = { num_members: int option; } +(** Object representing information about a Slack channel. *) +type conversation_obj = { + id: conversation; + name: string; + is_channel: bool; + created: timestamp; + creator: user; + is_archived: bool; + is_general: bool; + name_normalized: string; + is_member: bool; + topic: topic_obj; + purpose: topic_obj; + last_read: timestamp option; + latest: string option; + unread_count: int option; + unread_count_display: int option; + num_members: int option; +} + (** Object representing a message attachment field. *) type field_obj = { title: string option; @@ -468,13 +492,15 @@ type groups_rename_obj = { created: timestamp } -(** Information about a direct conversation with a person. *) +(** Information about a direct im with a person. *) type im_obj = { id: string; is_im: bool; user: user; created: timestamp; is_user_deleted: bool; + is_open: bool option; + last_read: timestamp option; unread_count: int option; unread_count_display: int option; } @@ -483,7 +509,7 @@ type im_channel_obj = { id: string; } -(** Information about an direct conversation channel. *) +(** Information about an direct im channel. *) type im_open_obj = { no_op: bool option; already_open: bool option; @@ -555,7 +581,7 @@ type file_obj = { (*public_url_shared: ???;*) channels: channel list; groups: group list; - ims: conversation list; + ims: im list; initial_comment: Yojson.Safe.t option; num_stars: int option; } @@ -690,9 +716,9 @@ val bot_of_string : string -> bot id by means of an additional request. *) val channel_of_string: string -> channel -(** Create a conversation type out of a given string. The string is usually - starting with a capital 'D' and represents an IM conversation channel. *) -val conversation_of_string: string -> conversation +(** Create a im type out of a given string. The string is usually + starting with a capital 'D' and represents an IM im channel. *) +val im_of_string: string -> im (** {2 Slack API calls} *) @@ -737,7 +763,7 @@ val channels_kick: session -> channel -> user -> [ `Success | parsed_auth_error val channels_leave: session -> channel -> [ `Success of channel_leave_obj | parsed_auth_error | channel_error | archive_error | leave_general_error | `User_is_restricted | bot_error ] Lwt.t (** Lists all channels in a Slack team. *) -val channels_list: ?exclude_archived:bool -> session -> [ `Success of channel_obj list | parsed_auth_error ] Lwt.t +val conversations_list: ?exclude_archived:bool -> session -> [ `Success of conversation_obj list | parsed_auth_error ] Lwt.t (** Sets the read cursor in a channel. *) val channels_mark: session -> channel -> timestamp -> [ `Success | parsed_auth_error | channel_error | archive_error | not_in_channel_error ] Lwt.t @@ -823,16 +849,16 @@ val groups_set_topic: session -> group -> topic -> topic_result Lwt.t val groups_unarchive: session -> group -> [ `Success | parsed_auth_error | channel_error | `Not_archived | `User_is_restricted | bot_error ] Lwt.t (** Close a direct message channel. *) -val im_close: session -> conversation -> [ `Success of chat_close_obj | parsed_auth_error | channel_error | `User_does_not_own_channel ] Lwt.t +val im_close: session -> im -> [ `Success of chat_close_obj | parsed_auth_error | channel_error | `User_does_not_own_channel ] Lwt.t (** Fetches history of messages and events from direct message channel. *) -val im_history: session -> ?latest:timestamp -> ?oldest:timestamp -> ?count:int -> ?inclusive:bool -> conversation -> history_result Lwt.t +val im_history: session -> ?latest:timestamp -> ?oldest:timestamp -> ?count:int -> ?inclusive:bool -> im -> history_result Lwt.t (** Lists direct message channels for the calling user. *) val im_list: session -> [ `Success of im_obj list | parsed_auth_error ] Lwt.t (** Sets the read cursor in a direct message channel. *) -val im_mark: session -> conversation -> timestamp -> [ `Success | parsed_auth_error | channel_error | not_in_channel_error ] Lwt.t +val im_mark: session -> im -> timestamp -> [ `Success | parsed_auth_error | channel_error | not_in_channel_error ] Lwt.t (** Opens a direct message channel. *) val im_open: session -> user -> [ `Success of im_open_obj | parsed_auth_error | user_error | user_visibility_error ] Lwt.t diff --git a/test/abbrtypes.ml b/test/abbrtypes.ml index 8c90f11..42c46ef 100644 --- a/test/abbrtypes.ml +++ b/test/abbrtypes.ml @@ -72,6 +72,52 @@ let abbr_channel_obj (chan : Slacko.channel_obj) = { type abbr_channel_obj_list = abbr_channel_obj list [@@deriving show, yojson] +type abbr_conversation_obj = { + (* id: conversation; *) + name: string; + is_channel: bool; + created: Timestamp.t; + (* creator: user; *) + is_archived: bool; + is_general: bool; + is_member: bool; + (* members: user list; *) + topic: abbr_topic_obj; + purpose: abbr_topic_obj; + last_read: Timestamp.t option [@default None]; + (* latest: json option [@default None]; *) + unread_count: int option [@default None]; + unread_count_display: int option [@default None]; + num_members: int option [@default None]; +} [@@deriving show, yojson { strict = false }] + +let abbr_conversation_obj (conversation : Slacko.conversation_obj) = { + name = conversation.Slacko.name; + is_channel = conversation.Slacko.is_channel; + created = conversation.Slacko.created; + is_archived = conversation.Slacko.is_archived; + is_general = conversation.Slacko.is_general; + is_member = conversation.Slacko.is_member; + topic = abbr_topic_obj conversation.Slacko.topic; + purpose = abbr_topic_obj conversation.Slacko.purpose; + last_read = conversation.Slacko.last_read; + unread_count = conversation.Slacko.unread_count; + unread_count_display = conversation.Slacko.unread_count_display; + num_members = conversation.Slacko.num_members; +} + +type abbr_conversation_obj_list = abbr_conversation_obj list +[@@deriving show] + +type abbr_conversation_list_obj = { + channels: abbr_conversation_obj list +} [@@deriving show, yojson { strict = false }] + +let abbr_conversation_obj_list_of_yojson json = + match abbr_conversation_list_obj_of_yojson json with + | Ok obj -> Ok obj.channels + | (Error _) as err -> err + type abbr_message_obj = { type': string [@key "type"]; ts: Timestamp.t; diff --git a/test/conversations.json b/test/conversations.json new file mode 100644 index 0000000..262b375 --- /dev/null +++ b/test/conversations.json @@ -0,0 +1,119 @@ +{ + "channels": [ + { + "id": "C3XTJPLFL", + "name": "archivable_channel", + "name_normalized": "archivable_channel", + "is_channel": true, + "created": 1485679967, + "creator": "U3UMJU868", + "is_archived": false, + "is_general": false, + "is_member": true, + "topic": { + "value": "", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "", + "creator": "", + "last_set": 0 + }, + "previous_names": [], + "num_members": 1 + }, + { + "id": "C3XTHDCTC", + "name": "archived_channel", + "name_normalized": "archived_channel", + "is_channel": true, + "created": 1485678562, + "creator": "U3UMJU868", + "is_archived": true, + "is_general": false, + "is_member": false, + "topic": { + "value": "", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "", + "creator": "", + "last_set": 0 + }, + "previous_names": [], + "num_members": 0 + }, + { + "id": "C3UK9TS3C", + "name": "general", + "name_normalized": "general", + "is_channel": true, + "created": 1484993283, + "creator": "U3UMJU868", + "is_archived": false, + "is_general": true, + "is_member": true, + "topic": { + "value": "Company-wide announcements and work-based matters", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "This channel is for team-wide communication and announcements. All team members are in this channel.", + "creator": "", + "last_set": 0 + }, + "previous_names": [], + "num_members": 1 + }, + { + "id": "C3TTWNCTA", + "name": "random", + "name_normalized": "random", + "is_channel": true, + "created": 1484993283, + "creator": "U3UMJU868", + "is_archived": false, + "is_general": false, + "is_member": true, + "topic": { + "value": "Non-work banter and water cooler conversation", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "A place for non-work-related flimflam, faffing, hodge-podge or jibber-jabber you'd prefer to keep out of more focused work-related channels.", + "creator": "", + "last_set": 0 + }, + "previous_names": [], + "num_members": 1 + }, + { + "id": "C3V9V3E9L", + "name": "slackobot", + "name_normalized": "slackobot", + "is_channel": true, + "created": 1484993366, + "creator": "U3UMJU868", + "is_archived": false, + "is_general": false, + "is_member": true, + "topic": { + "value": "", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "Testing slackobot.", + "creator": "U3UMJU868", + "last_set": 1484993366 + }, + "previous_names": [], + "num_members": 1 + } + ] +} diff --git a/test/fake_slack.ml b/test/fake_slack.ml index 6d44235..d48d952 100644 --- a/test/fake_slack.ml +++ b/test/fake_slack.ml @@ -19,6 +19,7 @@ let im_slackbot = "D3UMJU8VA" let channels_json = Yojson.Safe.from_file "channels.json" +let conversations_json = Yojson.Safe.from_file "conversations.json" let new_channel_json = Yojson.Safe.from_file "new_channel.json" let authed_json = Yojson.Safe.from_file "authed.json" let random_history_json = Yojson.Safe.from_file "random_history.json" @@ -101,8 +102,8 @@ let channels_archive req _body = let channels_create req _body = match get_arg "name" req with - | "#general" | "#random" -> reply_err "name_taken" [] - | "#new_channel" | _ -> reply_ok ["channel", new_channel_json] + | "general" | "random" -> reply_err "name_taken" [] + | "new_channel" | _ -> reply_ok ["channel", new_channel_json] let channels_history req _body = (* TODO: Check various filtering params. *) @@ -141,6 +142,9 @@ let users_list _req _body = (* TODO: Check presence param. *) reply_ok (json_fields users_json) +let conversations_list _req _body = + reply_ok (json_fields conversations_json) + (* Dispatcher, etc. *) let server ?(port=7357) ~stop () = @@ -158,6 +162,7 @@ let server ?(port=7357) ~stop () = | "/api/im.history" -> check_auth im_history | "/api/im.list" -> check_auth im_list | "/api/users.list" -> check_auth users_list + | "/api/conversations.list" -> check_auth conversations_list | _ -> bad_path in handler req body diff --git a/test/new_channel.json b/test/new_channel.json index f5d5bb9..e518025 100644 --- a/test/new_channel.json +++ b/test/new_channel.json @@ -12,6 +12,7 @@ "U3UMJU868" ], "name": "new_channel", + "name_normalized": "new_channel", "previous_names": [], "purpose": { "creator": "", diff --git a/test/test_slacko.ml b/test/test_slacko.ml index c93a20b..58d0854 100644 --- a/test/test_slacko.ml +++ b/test/test_slacko.ml @@ -94,7 +94,7 @@ let test_channels_archive_bad_auth _tctx = let test_channels_archive_existing _tctx = let session = Slacko.start_session ?base_url token in - let new_channel = Slacko.channel_of_string "#archivable_channel" in + let new_channel = Slacko.channel_of_string "archivable_channel" in Slacko.channels_archive session new_channel >|= fun resp -> assert_equal `Success resp @@ -106,13 +106,13 @@ let test_channels_archive_missing _tctx = let test_channels_archive_archived _tctx = let session = Slacko.start_session ?base_url token in - let archived_channel = Slacko.channel_of_string "#archived_channel" in + let archived_channel = Slacko.channel_of_string "archived_channel" in Slacko.channels_archive session archived_channel >|= fun resp -> assert_equal `Already_archived resp let test_channels_archive_general _tctx = let session = Slacko.start_session ?base_url token in - let general = Slacko.channel_of_string "#general" in + let general = Slacko.channel_of_string "general" in Slacko.channels_archive session general >|= fun resp -> assert_equal `Cant_archive_general resp @@ -133,7 +133,7 @@ let test_channels_create_bad_auth _tctx = let test_channels_create_new _tctx = let session = Slacko.start_session ?base_url token in - Slacko.channels_create session "#new_channel" >|= get_success >|= + Slacko.channels_create session "new_channel" >|= get_success >|= abbr_channel_obj >|= fun channel -> assert_equal ~printer:show_abbr_channel_obj (abbr_json abbr_channel_obj_of_yojson Fake_slack.new_channel_json) @@ -141,7 +141,7 @@ let test_channels_create_new _tctx = let test_channels_create_existing _tctx = let session = Slacko.start_session ?base_url token in - Slacko.channels_create session "#general" >|= fun resp -> + Slacko.channels_create session "general" >|= fun resp -> assert_equal `Name_taken resp let channels_create_tests = fake_slack_tests "channels_create" [ @@ -160,7 +160,7 @@ let test_channels_history_bad_auth _tctx = let test_channels_history_no_params _tctx = let session = Slacko.start_session ?base_url token in - let random = Slacko.channel_of_string "#random" in + let random = Slacko.channel_of_string "random" in Slacko.channels_history session random >|= get_success >|= fun history -> assert_equal ~printer:show_abbr_history_obj (abbr_json abbr_history_obj_of_yojson Fake_slack.random_history_json) @@ -177,24 +177,26 @@ let channels_history_tests = fake_slack_tests "channels_history" [ (* channels_kick *) (* channels_leave *) -(* channels_list *) +(* conversations_list *) -let test_channels_list_bad_auth _tctx = +let test_conversations_list_bad_auth _tctx = let session = Slacko.start_session ?base_url badtoken in - Slacko.channels_list session >|= fun resp -> + Slacko.conversations_list session >|= fun resp -> assert_equal `Invalid_auth resp -let test_channels_list _tctx = +let test_conversations_list _tctx = let session = Slacko.start_session ?base_url token in - Slacko.channels_list session >|= get_success >|= - List.map abbr_channel_obj >|= fun channels -> - assert_equal ~printer:show_abbr_channel_obj_list - (abbr_json abbr_channel_obj_list_of_yojson Fake_slack.channels_json) - channels - -let channels_list_tests = fake_slack_tests "channels_list" [ - "test_bad_auth", test_channels_list_bad_auth; - "test", test_channels_list; + Slacko.conversations_list session + >|= get_success + >|= List.map abbr_conversation_obj + >|= fun conversations -> + assert_equal ~printer:show_abbr_conversation_obj_list + (abbr_json abbr_conversation_obj_list_of_yojson Fake_slack.conversations_json) + conversations + +let conversations_list_tests = fake_slack_tests "conversations_list" [ + "test_bad_auth", test_conversations_list_bad_auth; + "test", test_conversations_list; ] (* channels_mark *) @@ -292,13 +294,13 @@ let groups_list_tests = fake_slack_tests "groups_list" [ let test_im_history_bad_auth _tctx = let session = Slacko.start_session ?base_url badtoken in - let slackbot = Slacko.conversation_of_string Fake_slack.im_slackbot in + let slackbot = Slacko.im_of_string Fake_slack.im_slackbot in Slacko.im_history session slackbot >|= fun resp -> assert_equal `Invalid_auth resp let test_im_history_no_params _tctx = let session = Slacko.start_session ?base_url token in - let slackbot = Slacko.conversation_of_string Fake_slack.im_slackbot in + let slackbot = Slacko.im_of_string Fake_slack.im_slackbot in Slacko.im_history session slackbot >|= get_success >|= fun history -> assert_equal ~printer:show_abbr_history_obj (abbr_json abbr_history_obj_of_yojson Fake_slack.slackbot_history_json) @@ -377,7 +379,7 @@ let suite = "tests" >::: [ (* channels_join_tests; *) (* channels_kick_tests; *) (* channels_leave_tests; *) - channels_list_tests; + conversations_list_tests; (* channels_mark_tests; *) (* channels_rename_tests; *) (* channels_set_purpose_tests; *)