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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE +21 -0
- data/README.md +136 -0
- data/bin/teems +7 -0
- data/lib/teems/api/calendar.rb +94 -0
- data/lib/teems/api/channels.rb +26 -0
- data/lib/teems/api/chats.rb +29 -0
- data/lib/teems/api/client.rb +40 -0
- data/lib/teems/api/files.rb +12 -0
- data/lib/teems/api/messages.rb +58 -0
- data/lib/teems/api/users.rb +88 -0
- data/lib/teems/api/users_mailbox.rb +16 -0
- data/lib/teems/api/users_presence.rb +43 -0
- data/lib/teems/cli.rb +133 -0
- data/lib/teems/commands/activity.rb +222 -0
- data/lib/teems/commands/auth.rb +268 -0
- data/lib/teems/commands/base.rb +146 -0
- data/lib/teems/commands/cal.rb +891 -0
- data/lib/teems/commands/channels.rb +115 -0
- data/lib/teems/commands/chats.rb +159 -0
- data/lib/teems/commands/help.rb +107 -0
- data/lib/teems/commands/messages.rb +281 -0
- data/lib/teems/commands/ooo.rb +385 -0
- data/lib/teems/commands/org.rb +232 -0
- data/lib/teems/commands/status.rb +224 -0
- data/lib/teems/commands/sync.rb +390 -0
- data/lib/teems/commands/who.rb +377 -0
- data/lib/teems/formatters/calendar_formatter.rb +227 -0
- data/lib/teems/formatters/format_utils.rb +56 -0
- data/lib/teems/formatters/markdown_formatter.rb +113 -0
- data/lib/teems/formatters/message_formatter.rb +67 -0
- data/lib/teems/formatters/output.rb +105 -0
- data/lib/teems/models/account.rb +59 -0
- data/lib/teems/models/channel.rb +31 -0
- data/lib/teems/models/chat.rb +111 -0
- data/lib/teems/models/duration.rb +46 -0
- data/lib/teems/models/event.rb +124 -0
- data/lib/teems/models/message.rb +125 -0
- data/lib/teems/models/parsing.rb +56 -0
- data/lib/teems/models/user.rb +25 -0
- data/lib/teems/models/user_profile.rb +45 -0
- data/lib/teems/runner.rb +81 -0
- data/lib/teems/services/api_client.rb +217 -0
- data/lib/teems/services/cache_store.rb +32 -0
- data/lib/teems/services/configuration.rb +56 -0
- data/lib/teems/services/file_downloader.rb +39 -0
- data/lib/teems/services/headless_extract.rb +192 -0
- data/lib/teems/services/safari_oauth.rb +285 -0
- data/lib/teems/services/sync_dir_naming.rb +42 -0
- data/lib/teems/services/sync_engine.rb +194 -0
- data/lib/teems/services/sync_store.rb +193 -0
- data/lib/teems/services/teams_url_parser.rb +78 -0
- data/lib/teems/services/token_exchange_scripts.rb +56 -0
- data/lib/teems/services/token_extractor.rb +401 -0
- data/lib/teems/services/token_extractor_scripts.rb +116 -0
- data/lib/teems/services/token_refresher.rb +169 -0
- data/lib/teems/services/token_store.rb +116 -0
- data/lib/teems/support/error_logger.rb +35 -0
- data/lib/teems/support/help_formatter.rb +80 -0
- data/lib/teems/support/timezone.rb +44 -0
- data/lib/teems/support/xdg_paths.rb +62 -0
- data/lib/teems/version.rb +5 -0
- data/lib/teems.rb +117 -0
- data/support/token_helper.swift +485 -0
- 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,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
|