teems 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/LICENSE +21 -0
  4. data/README.md +136 -0
  5. data/bin/teems +7 -0
  6. data/lib/teems/api/calendar.rb +94 -0
  7. data/lib/teems/api/channels.rb +26 -0
  8. data/lib/teems/api/chats.rb +29 -0
  9. data/lib/teems/api/client.rb +40 -0
  10. data/lib/teems/api/files.rb +12 -0
  11. data/lib/teems/api/messages.rb +58 -0
  12. data/lib/teems/api/users.rb +88 -0
  13. data/lib/teems/api/users_mailbox.rb +16 -0
  14. data/lib/teems/api/users_presence.rb +43 -0
  15. data/lib/teems/cli.rb +133 -0
  16. data/lib/teems/commands/activity.rb +222 -0
  17. data/lib/teems/commands/auth.rb +268 -0
  18. data/lib/teems/commands/base.rb +146 -0
  19. data/lib/teems/commands/cal.rb +891 -0
  20. data/lib/teems/commands/channels.rb +115 -0
  21. data/lib/teems/commands/chats.rb +159 -0
  22. data/lib/teems/commands/help.rb +107 -0
  23. data/lib/teems/commands/messages.rb +281 -0
  24. data/lib/teems/commands/ooo.rb +385 -0
  25. data/lib/teems/commands/org.rb +232 -0
  26. data/lib/teems/commands/status.rb +224 -0
  27. data/lib/teems/commands/sync.rb +390 -0
  28. data/lib/teems/commands/who.rb +377 -0
  29. data/lib/teems/formatters/calendar_formatter.rb +227 -0
  30. data/lib/teems/formatters/format_utils.rb +56 -0
  31. data/lib/teems/formatters/markdown_formatter.rb +113 -0
  32. data/lib/teems/formatters/message_formatter.rb +67 -0
  33. data/lib/teems/formatters/output.rb +105 -0
  34. data/lib/teems/models/account.rb +59 -0
  35. data/lib/teems/models/channel.rb +31 -0
  36. data/lib/teems/models/chat.rb +111 -0
  37. data/lib/teems/models/duration.rb +46 -0
  38. data/lib/teems/models/event.rb +124 -0
  39. data/lib/teems/models/message.rb +125 -0
  40. data/lib/teems/models/parsing.rb +56 -0
  41. data/lib/teems/models/user.rb +25 -0
  42. data/lib/teems/models/user_profile.rb +45 -0
  43. data/lib/teems/runner.rb +81 -0
  44. data/lib/teems/services/api_client.rb +217 -0
  45. data/lib/teems/services/cache_store.rb +32 -0
  46. data/lib/teems/services/configuration.rb +56 -0
  47. data/lib/teems/services/file_downloader.rb +39 -0
  48. data/lib/teems/services/headless_extract.rb +192 -0
  49. data/lib/teems/services/safari_oauth.rb +285 -0
  50. data/lib/teems/services/sync_dir_naming.rb +42 -0
  51. data/lib/teems/services/sync_engine.rb +194 -0
  52. data/lib/teems/services/sync_store.rb +193 -0
  53. data/lib/teems/services/teams_url_parser.rb +78 -0
  54. data/lib/teems/services/token_exchange_scripts.rb +56 -0
  55. data/lib/teems/services/token_extractor.rb +401 -0
  56. data/lib/teems/services/token_extractor_scripts.rb +116 -0
  57. data/lib/teems/services/token_refresher.rb +169 -0
  58. data/lib/teems/services/token_store.rb +116 -0
  59. data/lib/teems/support/error_logger.rb +35 -0
  60. data/lib/teems/support/help_formatter.rb +80 -0
  61. data/lib/teems/support/timezone.rb +44 -0
  62. data/lib/teems/support/xdg_paths.rb +62 -0
  63. data/lib/teems/version.rb +5 -0
  64. data/lib/teems.rb +117 -0
  65. data/support/token_helper.swift +485 -0
  66. metadata +110 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5255a4b8e6b5e3e23bcbd8f5f341a8945022c380ab6c2f76fa18c2b74ecc1ba4
4
+ data.tar.gz: c31fd7ed82087b636fafabb34f2acc10b8697fe918061fcecce7056b0e54fbb7
5
+ SHA512:
6
+ metadata.gz: 1128d0e53047f9cef7fca4111dabf95306cd98e06c83c539816838b3b7f21a75f84e45a0fdf111484bec7a17d91e56ad2c3c75254a2b40314a5c81d1d759cc3a
7
+ data.tar.gz: 506950f2200002e07101641e65282eae9018716f31a49fe1048ca83f87a7b759c403b2ffa1051183217d8fa9155ae9d98f06bd997d142439f2a5dd63145c728c
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-04-10
4
+
5
+ ### Added
6
+ - `teems auth login` - Headless, Safari OAuth, and Safari-based token extraction
7
+ - `teems auth status` - Show authentication status
8
+ - `teems auth logout` - Clear stored tokens
9
+ - `teems channels` - List joined teams and channels
10
+ - `teems chats` - List recent chats
11
+ - `teems messages` - Read messages from channels and chats
12
+ - `teems cal` - List calendar events, view details, accept/decline/tentative
13
+ - `teems cal create` - Create calendar events with attendees, rooms, and all-day support
14
+ - `teems cal delete` - Delete calendar events
15
+ - `teems activity` - Show activity feed (mentions, reactions, calendar)
16
+ - `teems who` - Look up user profiles
17
+ - `teems org` - Show org chart
18
+ - `teems ooo` - Manage out-of-office (auto-reply, status, presence, calendar event)
19
+ - `teems status` - View and manage presence status
20
+ - `teems sync` - Sync chat history locally
21
+ - Automatic token refresh via OIDC
22
+ - Configurable API endpoints for commercial and GCC environments
23
+ - JSON output support with `--json` flag
24
+ - Pure Ruby implementation with no runtime dependencies
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eric Boehs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # teems
2
+
3
+ A command-line interface for Microsoft Teams. Read messages, list channels and chats from the terminal.
4
+
5
+ Pure Ruby, no dependencies.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ gem install teems
11
+ ```
12
+
13
+ Or build from source:
14
+
15
+ ```bash
16
+ git clone https://github.com/ericboehs/teems
17
+ cd teems
18
+ gem build teems.gemspec
19
+ gem install teems-*.gem
20
+ ```
21
+
22
+ ## Requirements
23
+
24
+ - Ruby 3.2+
25
+ - macOS (for Safari token extraction)
26
+ - Microsoft Teams account
27
+
28
+ ## Authentication
29
+
30
+ teems requires authentication tokens from Teams. The easiest way is Safari automation:
31
+
32
+ ```bash
33
+ teems auth login
34
+ ```
35
+
36
+ This opens Safari to teams.microsoft.com, waits for you to log in, then extracts the tokens.
37
+
38
+ Alternatively, extract tokens manually:
39
+
40
+ ```bash
41
+ teems auth manual
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ### List Teams and Channels
47
+
48
+ ```bash
49
+ teems channels
50
+ ```
51
+
52
+ ### List Chats
53
+
54
+ ```bash
55
+ teems chats
56
+ teems chats -n 50 # Show 50 chats
57
+ ```
58
+
59
+ ### Read Messages
60
+
61
+ ```bash
62
+ # Read from a chat
63
+ teems messages <chat-id>
64
+
65
+ # Read from a channel (requires team ID)
66
+ teems messages <channel-id> -t <team-id>
67
+
68
+ # Show more messages
69
+ teems messages <chat-id> -n 50
70
+ ```
71
+
72
+ ### Check Authentication Status
73
+
74
+ ```bash
75
+ teems auth status
76
+ ```
77
+
78
+ ### Clear Authentication
79
+
80
+ ```bash
81
+ teems auth logout
82
+ ```
83
+
84
+ ## Global Options
85
+
86
+ | Option | Description |
87
+ |--------|-------------|
88
+ | `-n, --limit N` | Number of items to show (default: 20) |
89
+ | `-v, --verbose` | Show debug output |
90
+ | `-q, --quiet` | Suppress output |
91
+ | `--json` | Output as JSON |
92
+ | `-h, --help` | Show help |
93
+
94
+ ## Configuration
95
+
96
+ Configuration is stored in XDG-compliant directories:
97
+
98
+ - Config: `~/.config/teems/`
99
+ - Cache: `~/.cache/teems/`
100
+
101
+ ### Custom Endpoints
102
+
103
+ By default, teems connects to commercial Microsoft Teams endpoints. To use a different environment (e.g., GCC, GCC High), add an `endpoints` section to `~/.config/teems/config.json`:
104
+
105
+ ```json
106
+ {
107
+ "endpoints": {
108
+ "msgservice": "https://ng.msg.gcc.teams.microsoft.com",
109
+ "presence": "https://presence.gcc.teams.microsoft.com"
110
+ }
111
+ }
112
+ ```
113
+
114
+ Available endpoint keys: `graph`, `teams`, `msgservice`, `presence`.
115
+
116
+ ## Token Expiration
117
+
118
+ Teams tokens expire after ~24 hours. When you see authentication errors, run:
119
+
120
+ ```bash
121
+ teems auth login
122
+ ```
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ git clone https://github.com/ericboehs/teems
128
+ cd teems
129
+ rake check # Syntax check
130
+ rake test # Run tests
131
+ rake console # Interactive console
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT License. See [LICENSE](LICENSE).
data/bin/teems ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'teems'
6
+
7
+ exit Teems::CLI.new(ARGV).run
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teems
4
+ module Api
5
+ # API wrapper for Microsoft Graph Calendar endpoints
6
+ class Calendar < Client
7
+ CALENDAR_VIEW_SELECT = %w[
8
+ id subject start end location isAllDay organizer attendees
9
+ bodyPreview onlineMeeting showAs importance isCancelled
10
+ responseStatus sensitivity
11
+ ].join(',').freeze
12
+
13
+ EVENT_DETAIL_SELECT = %w[
14
+ id subject start end location isAllDay organizer attendees
15
+ body onlineMeeting showAs importance isCancelled
16
+ responseStatus sensitivity
17
+ ].join(',').freeze
18
+
19
+ # List events in a date range using CalendarView
20
+ def list_events(time_range:, top: 50)
21
+ params = calendar_view_params(time_range[:start_dt], time_range[:end_dt], top)
22
+ headers = timezone_header(time_range[:timezone])
23
+ paginate_events('/v1.0/me/calendarView', params: params, headers: headers)
24
+ end
25
+
26
+ # Get a single event by ID with full details
27
+ def get_event(event_id:, timezone:)
28
+ encoded_id = URI.encode_www_form_component(event_id)
29
+ params = { '$select' => EVENT_DETAIL_SELECT }
30
+ headers = timezone_header(timezone)
31
+
32
+ response = get("/v1.0/me/events/#{encoded_id}", params: params, headers: headers)
33
+ Models::Event.from_api(response)
34
+ end
35
+
36
+ # Create a new calendar event
37
+ def create_event(body)
38
+ response = post('/v1.0/me/events', body: body)
39
+ Models::Event.from_api(response)
40
+ end
41
+
42
+ # Delete an event
43
+ def delete_event(event_id:)
44
+ encoded_id = URI.encode_www_form_component(event_id)
45
+ delete("/v1.0/me/events/#{encoded_id}")
46
+ end
47
+
48
+ # RSVP to an event (accept, decline, or tentatively accept)
49
+ def rsvp_event(event_id:, action:, **opts)
50
+ encoded_id = URI.encode_www_form_component(event_id)
51
+ api_action = action == 'tentative' ? 'tentativelyAccept' : action
52
+ post("/v1.0/me/events/#{encoded_id}/#{api_action}", body: rsvp_body(opts))
53
+ end
54
+
55
+ private
56
+
57
+ def rsvp_body(opts)
58
+ comment = opts[:comment]
59
+ body = { sendResponse: opts.fetch(:notify, :send) == :send }
60
+ body[:comment] = comment if comment
61
+ body
62
+ end
63
+
64
+ def calendar_view_params(start_dt, end_dt, top)
65
+ { 'startDateTime' => start_dt, 'endDateTime' => end_dt,
66
+ '$select' => CALENDAR_VIEW_SELECT, '$orderby' => 'start/dateTime', '$top' => top }
67
+ end
68
+
69
+ def paginate_events(path, params:, headers:)
70
+ events = []
71
+ response = get(path, params: params, headers: headers)
72
+ collect_paginated_events(events, response, headers)
73
+ end
74
+
75
+ def collect_paginated_events(events, response, headers)
76
+ loop do
77
+ events.concat(parse_events(response))
78
+ next_link = response['@odata.nextLink']
79
+ break events unless next_link
80
+
81
+ response = get(next_link, headers: headers)
82
+ end
83
+ end
84
+
85
+ def timezone_header(timezone)
86
+ { 'Prefer' => "outlook.timezone=\"#{timezone}\"" }
87
+ end
88
+
89
+ def parse_events(response)
90
+ (response['value'] || []).map { |data| Models::Event.from_api(data) }
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teems
4
+ module Api
5
+ # API wrapper for Teams and channels endpoints using Microsoft Graph API
6
+ class Channels < Client
7
+ # Get list of teams the user has joined
8
+ def list_teams
9
+ get('/v1.0/me/joinedTeams')
10
+ end
11
+
12
+ # Get channels for a specific team
13
+ def list_channels(team_id:)
14
+ encoded_team = URI.encode_www_form_component(team_id)
15
+ get("/v1.0/teams/#{encoded_team}/channels")
16
+ end
17
+
18
+ # Get details about a specific channel
19
+ def get_channel(team_id:, channel_id:)
20
+ encoded_team = URI.encode_www_form_component(team_id)
21
+ encoded_channel = URI.encode_www_form_component(channel_id)
22
+ get("/v1.0/teams/#{encoded_team}/channels/#{encoded_channel}")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teems
4
+ module Api
5
+ # API wrapper for chat endpoints
6
+ # Uses Teams ng.msg service for chat operations
7
+ class Chats < Client
8
+ ENDPOINT = :msgservice
9
+
10
+ # Get list of conversations (chats, meetings)
11
+ def list(limit: 50)
12
+ get('/v1/users/ME/conversations',
13
+ params: { pageSize: limit, view: 'msnp24Equivalent' })
14
+ end
15
+
16
+ # Get a specific chat
17
+ def get_chat(chat_id:)
18
+ encoded_id = URI.encode_www_form_component(chat_id)
19
+ get("/v1/users/ME/conversations/#{encoded_id}")
20
+ end
21
+
22
+ # Get members of a chat
23
+ def members(chat_id:)
24
+ encoded_id = URI.encode_www_form_component(chat_id)
25
+ get("/v1/threads/#{encoded_id}/members")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teems
4
+ module Api
5
+ # Base API client for Teams endpoints
6
+ class Client
7
+ ENDPOINT = :graph
8
+
9
+ def initialize(api_client, account)
10
+ @api = api_client
11
+ @account = account
12
+ end
13
+
14
+ protected
15
+
16
+ def endpoint = self.class::ENDPOINT
17
+
18
+ def get(path, params: {}, headers: {})
19
+ @api.get(endpoint, path, account: @account, params: params, headers: headers)
20
+ end
21
+
22
+ def post(path, body: nil)
23
+ @api.post(endpoint, path, account: @account, body: body)
24
+ end
25
+
26
+ def patch(path, body: nil)
27
+ @api.patch(endpoint, path, account: @account, body: body)
28
+ end
29
+
30
+ def post_to(target_endpoint, request)
31
+ @api.post(target_endpoint, request[:path], account: @account, body: request[:body])
32
+ end
33
+
34
+ def delete(path)
35
+ base_url = Services::ConnectionPool::DEFAULT_ENDPOINTS[endpoint]
36
+ @api.delete("#{base_url}#{path}", endpoint_key: endpoint, account: @account)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teems
4
+ module Api
5
+ # API wrapper for SharePoint file operations via Microsoft Graph
6
+ class Files < Client
7
+ def drive_item(site_id:, list_id:, item_id:)
8
+ get("/v1.0/sites/#{site_id}/lists/#{list_id}/items/#{item_id}/driveItem")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teems
4
+ module Api
5
+ # API wrapper for messages endpoints
6
+ # Uses Teams ng.msg service for reading messages
7
+ # Requires skypeToken from authsvc exchange (not the JWT from localStorage)
8
+ class Messages < Client
9
+ ENDPOINT = :msgservice
10
+
11
+ # Get messages from a channel using ng.msg API
12
+ def channel_messages(channel_id:, limit: 50)
13
+ # The ng.msg API uses /v1/users/ME/conversations/{threadId}/messages
14
+ encoded_id = URI.encode_www_form_component(channel_id)
15
+ get("/v1/users/ME/conversations/#{encoded_id}/messages",
16
+ params: { pageSize: limit, view: 'msnp24Equivalent|supportsMessageProperties' })
17
+ end
18
+
19
+ # Get messages from a chat using ng.msg API
20
+ def chat_messages(chat_id:, limit: 50)
21
+ encoded_id = URI.encode_www_form_component(chat_id)
22
+ get("/v1/users/ME/conversations/#{encoded_id}/messages",
23
+ params: { pageSize: limit, view: 'msnp24Equivalent|supportsMessageProperties' })
24
+ end
25
+
26
+ # Get a page of messages for sync, with pagination support.
27
+ # Returns the full response hash including _metadata for pagination.
28
+ #
29
+ # When backward_link is provided, follows it directly to get older messages.
30
+ # Otherwise builds a request with startTime for time-range filtering.
31
+ #
32
+ # The ng.msg API returns newest-first with _metadata.backwardLink for older pages.
33
+ def chat_messages_page(chat_id:, limit: 200, **pagination)
34
+ backward_link = pagination[:backward_link]
35
+ return get(backward_link, params: {}) if backward_link
36
+
37
+ encoded_id = URI.encode_www_form_component(chat_id)
38
+ params = messages_page_params(limit, pagination[:start_time])
39
+ get("/v1/users/ME/conversations/#{encoded_id}/messages", params: params)
40
+ end
41
+
42
+ # Get replies to a message
43
+ def replies(thread_id:, message_id:, limit: 50)
44
+ encoded_id = URI.encode_www_form_component(thread_id)
45
+ get("/v1/users/ME/conversations/#{encoded_id}/messages/#{message_id}/replies",
46
+ params: { pageSize: limit })
47
+ end
48
+
49
+ private
50
+
51
+ def messages_page_params(limit, start_time)
52
+ { pageSize: limit, view: 'msnp24Equivalent|supportsMessageProperties' }.tap do |params|
53
+ params[:startTime] = (start_time.to_f * 1000).to_i if start_time
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'users_presence'
4
+ require_relative 'users_mailbox'
5
+
6
+ module Teems
7
+ module Api
8
+ # API wrapper for Microsoft Graph user endpoints
9
+ class Users < Client
10
+ include UsersPresence
11
+ include UsersMailbox
12
+
13
+ USER_SELECT = %w[
14
+ id displayName mail userPrincipalName jobTitle
15
+ department officeLocation businessPhones mobilePhone
16
+ ].join(',').freeze
17
+
18
+ def me
19
+ response = get('/v1.0/me', params: { '$select' => USER_SELECT })
20
+ Models::UserProfile.from_api(response)
21
+ end
22
+
23
+ def get_user(user_id)
24
+ encoded_id = URI.encode_www_form_component(user_id)
25
+ response = get("/v1.0/users/#{encoded_id}", params: { '$select' => USER_SELECT })
26
+ Models::UserProfile.from_api(response)
27
+ end
28
+
29
+ def search(query)
30
+ sanitized = query.gsub(/["\\]/, '')
31
+ headers = { 'ConsistencyLevel' => 'eventual' }
32
+ response = get('/v1.0/users', params: search_params(sanitized), headers: headers)
33
+ (response['value'] || []).map { |data| Models::UserProfile.from_api(data) }
34
+ end
35
+
36
+ def search_params(sanitized)
37
+ { '$search' => "\"displayName:#{sanitized}\" OR \"mail:#{sanitized}\"",
38
+ '$select' => USER_SELECT, '$count' => 'true', '$top' => 10 }
39
+ end
40
+
41
+ def manager(user_id)
42
+ encoded_id = URI.encode_www_form_component(user_id)
43
+ response = get("/v1.0/users/#{encoded_id}/manager", params: { '$select' => USER_SELECT })
44
+ Models::UserProfile.from_api(response)
45
+ end
46
+
47
+ def manager_me
48
+ response = get('/v1.0/me/manager', params: { '$select' => USER_SELECT })
49
+ Models::UserProfile.from_api(response)
50
+ end
51
+
52
+ def direct_reports(user_id)
53
+ encoded_id = URI.encode_www_form_component(user_id)
54
+ response = get("/v1.0/users/#{encoded_id}/directReports", params: { '$select' => USER_SELECT })
55
+ (response['value'] || []).map { |data| Models::UserProfile.from_api(data) }
56
+ end
57
+
58
+ def direct_reports_me
59
+ response = get('/v1.0/me/directReports', params: { '$select' => USER_SELECT })
60
+ (response['value'] || []).map { |data| Models::UserProfile.from_api(data) }
61
+ end
62
+
63
+ def presence(user_id)
64
+ encoded_id = URI.encode_www_form_component(user_id)
65
+ get("/v1.0/users/#{encoded_id}/presence")
66
+ end
67
+
68
+ def teams_presence(mri)
69
+ post_to(:presence, path: '/v1/presence/getpresence/', body: [{ mri: mri }])
70
+ end
71
+
72
+ def schedule(email, time_range:)
73
+ response = post('/v1.0/me/calendar/getSchedule', body: schedule_body(email, time_range))
74
+ response.dig('value', 0)
75
+ end
76
+
77
+ private
78
+
79
+ def schedule_body(email, time_range)
80
+ tz = time_range[:timezone]
81
+ { schedules: [email],
82
+ startTime: { dateTime: time_range[:start_time], timeZone: tz },
83
+ endTime: { dateTime: time_range[:end_time], timeZone: tz },
84
+ availabilityViewInterval: 15 }
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teems
4
+ module Api
5
+ # Mailbox settings API methods for Users class (automatic replies / OOO)
6
+ module UsersMailbox
7
+ def auto_replies
8
+ get('/v1.0/me/mailboxSettings/automaticRepliesSetting')
9
+ end
10
+
11
+ def update_auto_replies(settings)
12
+ patch('/v1.0/me/mailboxSettings', body: { automaticRepliesSetting: settings })
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Teems
6
+ module Api
7
+ # Presence-related API methods for Users class
8
+ module UsersPresence
9
+ def my_presence
10
+ get('/v1.0/me/presence')
11
+ end
12
+
13
+ def set_presence(availability:, activity:, duration:)
14
+ post('/v1.0/me/presence/setPresence', body: {
15
+ sessionId: SecureRandom.uuid,
16
+ availability: availability,
17
+ activity: activity,
18
+ expirationDuration: duration
19
+ })
20
+ end
21
+
22
+ def set_status_message(message:, expiry: nil)
23
+ post('/v1.0/me/presence/setStatusMessage', body: build_status_message_body(message, expiry))
24
+ end
25
+
26
+ def clear_status_message
27
+ set_status_message(message: '')
28
+ end
29
+
30
+ def clear_presence
31
+ post('/v1.0/me/presence/clearPresence', body: { sessionId: SecureRandom.uuid })
32
+ end
33
+
34
+ private
35
+
36
+ def build_status_message_body(message, expiry)
37
+ body = { statusMessage: { message: { content: message, contentType: 'text' } } }
38
+ body[:statusMessage][:expiryDateTime] = { dateTime: expiry, timeZone: 'UTC' } if expiry
39
+ body
40
+ end
41
+ end
42
+ end
43
+ end