teams_rb 2.0.1
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 +47 -0
- data/LICENSE +21 -0
- data/README.md +500 -0
- data/docs/README.md +40 -0
- data/docs/essentials/README.md +13 -0
- data/docs/essentials/api-client.md +62 -0
- data/docs/essentials/app-authentication.md +27 -0
- data/docs/essentials/app-basics.md +50 -0
- data/docs/essentials/graph.md +41 -0
- data/docs/essentials/on-activity.md +72 -0
- data/docs/essentials/on-event.md +29 -0
- data/docs/essentials/proactive-messaging.md +44 -0
- data/docs/essentials/sending-messages.md +76 -0
- data/docs/essentials/sovereign-cloud.md +26 -0
- data/docs/getting-started/README.md +7 -0
- data/docs/getting-started/code-basics.md +61 -0
- data/docs/getting-started/quickstart.md +54 -0
- data/docs/getting-started/running-in-teams.md +44 -0
- data/docs/in-depth-guides/README.md +14 -0
- data/docs/in-depth-guides/adaptive-cards.md +62 -0
- data/docs/in-depth-guides/dialogs.md +77 -0
- data/docs/in-depth-guides/feedback.md +29 -0
- data/docs/in-depth-guides/meeting-events.md +27 -0
- data/docs/in-depth-guides/message-extensions.md +77 -0
- data/docs/in-depth-guides/message-reactions.md +29 -0
- data/docs/in-depth-guides/observability.md +28 -0
- data/docs/in-depth-guides/streaming.md +52 -0
- data/docs/in-depth-guides/tabs.md +59 -0
- data/docs/in-depth-guides/user-authentication.md +60 -0
- data/lib/teams/activity.rb +160 -0
- data/lib/teams/activity_context.rb +278 -0
- data/lib/teams/api/account.rb +90 -0
- data/lib/teams/api/activity_value.rb +49 -0
- data/lib/teams/api/bot_sign_in_client.rb +54 -0
- data/lib/teams/api/channel_data.rb +86 -0
- data/lib/teams/api/channel_info.rb +19 -0
- data/lib/teams/api/citation_appearance.rb +83 -0
- data/lib/teams/api/client.rb +28 -0
- data/lib/teams/api/conversation_account.rb +40 -0
- data/lib/teams/api/conversation_client.rb +156 -0
- data/lib/teams/api/conversation_reference.rb +83 -0
- data/lib/teams/api/conversation_resource.rb +19 -0
- data/lib/teams/api/meeting_client.rb +57 -0
- data/lib/teams/api/meeting_info.rb +26 -0
- data/lib/teams/api/meeting_notification_response.rb +29 -0
- data/lib/teams/api/meeting_participant.rb +33 -0
- data/lib/teams/api/message_activity.rb +201 -0
- data/lib/teams/api/message_extension.rb +110 -0
- data/lib/teams/api/model.rb +49 -0
- data/lib/teams/api/notification_info.rb +28 -0
- data/lib/teams/api/paged_members_result.rb +16 -0
- data/lib/teams/api/quoted_reply_entity.rb +65 -0
- data/lib/teams/api/reaction_client.rb +34 -0
- data/lib/teams/api/sent_activity.rb +48 -0
- data/lib/teams/api/task_module.rb +83 -0
- data/lib/teams/api/team_client.rb +45 -0
- data/lib/teams/api/team_details.rb +36 -0
- data/lib/teams/api/team_info.rb +44 -0
- data/lib/teams/api/tenant_info.rb +11 -0
- data/lib/teams/api/token.rb +81 -0
- data/lib/teams/api/typing_activity.rb +19 -0
- data/lib/teams/api/user_client.rb +83 -0
- data/lib/teams/app.rb +602 -0
- data/lib/teams/auth/client_secret_credentials.rb +7 -0
- data/lib/teams/auth/jwt_validator.rb +148 -0
- data/lib/teams/auth/token.rb +39 -0
- data/lib/teams/auth/token_manager.rb +68 -0
- data/lib/teams/cards/generated.rb +1764 -0
- data/lib/teams/cards/generated_base.rb +105 -0
- data/lib/teams/cards.rb +81 -0
- data/lib/teams/cloud_environment.rb +41 -0
- data/lib/teams/common/hashes.rb +20 -0
- data/lib/teams/common/http_client.rb +111 -0
- data/lib/teams/common/retry.rb +31 -0
- data/lib/teams/errors.rb +50 -0
- data/lib/teams/function_context.rb +82 -0
- data/lib/teams/graph/client.rb +73 -0
- data/lib/teams/http_stream.rb +460 -0
- data/lib/teams/rack_app.rb +62 -0
- data/lib/teams/response.rb +9 -0
- data/lib/teams/router.rb +171 -0
- data/lib/teams/storage/memory_store.rb +27 -0
- data/lib/teams/version.rb +5 -0
- data/lib/teams.rb +70 -0
- metadata +217 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
class ConversationAccount < Model
|
|
6
|
+
def id
|
|
7
|
+
read("id")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def tenant_id
|
|
11
|
+
read("tenantId", "tenant_id")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def conversation_type
|
|
15
|
+
read("conversationType", "conversation_type")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def name
|
|
19
|
+
read("name")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def is_group
|
|
23
|
+
read("isGroup", "is_group")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
body = raw.dup
|
|
28
|
+
body["id"] = id if id
|
|
29
|
+
body["tenantId"] = tenant_id if tenant_id
|
|
30
|
+
body["conversationType"] = conversation_type if conversation_type
|
|
31
|
+
body["name"] = name if name
|
|
32
|
+
body["isGroup"] = is_group unless is_group.nil?
|
|
33
|
+
body.delete("tenant_id")
|
|
34
|
+
body.delete("conversation_type")
|
|
35
|
+
body.delete("is_group")
|
|
36
|
+
body
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Teams
|
|
6
|
+
module Api
|
|
7
|
+
class ConversationClient
|
|
8
|
+
TARGETED_PARAMS = { "isTargetedActivity" => "true" }.freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :service_url, :http
|
|
11
|
+
|
|
12
|
+
def initialize(service_url:, http:, logger: nil)
|
|
13
|
+
@service_url = service_url.sub(%r{/+\z}, "")
|
|
14
|
+
@http = http
|
|
15
|
+
@logger = logger
|
|
16
|
+
@reactions = ReactionClient.new(service_url: @service_url, http:)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Creates a conversation (or returns the pre-existing one for the same
|
|
20
|
+
# members). isGroup/bot/topicName are omitted: the SDKs deprecate them
|
|
21
|
+
# for removal, and Python never had them.
|
|
22
|
+
def create(members: nil, tenant_id: nil, activity: nil, channel_data: nil, service_url: nil)
|
|
23
|
+
body = {}
|
|
24
|
+
body["members"] = members.map { |member| account_to_h(member) } if members
|
|
25
|
+
body["tenantId"] = tenant_id if tenant_id
|
|
26
|
+
body["activity"] = activity_to_h(activity) if activity
|
|
27
|
+
body["channelData"] = Common::Hashes.deep_stringify_keys(channel_data) if channel_data
|
|
28
|
+
|
|
29
|
+
url = absolute("/v3/conversations", service_url:)
|
|
30
|
+
@logger&.debug("Teams API POST #{url}")
|
|
31
|
+
ConversationResource.new(http.post(url, json: body))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create_activity(conversation_id, activity, service_url: nil)
|
|
35
|
+
path = "/v3/conversations/#{escape(conversation_id)}/activities"
|
|
36
|
+
url = absolute(path, service_url:)
|
|
37
|
+
@logger&.debug("Teams API POST #{url}")
|
|
38
|
+
http.post(url, json: activity_to_h(activity))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reply_to_activity(conversation_id, activity_id, activity, service_url: nil)
|
|
42
|
+
body = activity_to_h(activity)
|
|
43
|
+
body = body.merge("replyToId" => activity_id) if body.is_a?(Hash)
|
|
44
|
+
path = "/v3/conversations/#{escape(conversation_id)}/activities/#{escape(activity_id)}"
|
|
45
|
+
url = absolute(path, service_url:)
|
|
46
|
+
@logger&.debug("Teams API POST #{url}")
|
|
47
|
+
http.post(url, json: body)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def update_activity(conversation_id, activity_id, activity, service_url: nil)
|
|
51
|
+
path = "/v3/conversations/#{escape(conversation_id)}/activities/#{escape(activity_id)}"
|
|
52
|
+
url = absolute(path, service_url:)
|
|
53
|
+
@logger&.debug("Teams API PUT #{url}")
|
|
54
|
+
http.put(url, json: activity_to_h(activity))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def delete_activity(conversation_id, activity_id, service_url: nil)
|
|
58
|
+
path = "/v3/conversations/#{escape(conversation_id)}/activities/#{escape(activity_id)}"
|
|
59
|
+
url = absolute(path, service_url:)
|
|
60
|
+
@logger&.debug("Teams API DELETE #{url}")
|
|
61
|
+
http.delete(url)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def create_targeted_activity(conversation_id, activity, service_url: nil)
|
|
65
|
+
path = "/v3/conversations/#{escape(conversation_id)}/activities"
|
|
66
|
+
url = absolute(path, service_url:)
|
|
67
|
+
@logger&.debug("Teams API POST #{url} (targeted)")
|
|
68
|
+
http.post(url, json: activity_to_h(activity), params: TARGETED_PARAMS)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def update_targeted_activity(conversation_id, activity_id, activity, service_url: nil)
|
|
72
|
+
path = "/v3/conversations/#{escape(conversation_id)}/activities/#{escape(activity_id)}"
|
|
73
|
+
url = absolute(path, service_url:)
|
|
74
|
+
@logger&.debug("Teams API PUT #{url} (targeted)")
|
|
75
|
+
http.put(url, json: activity_to_h(activity), params: TARGETED_PARAMS)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def delete_targeted_activity(conversation_id, activity_id, service_url: nil)
|
|
79
|
+
path = "/v3/conversations/#{escape(conversation_id)}/activities/#{escape(activity_id)}"
|
|
80
|
+
url = absolute(path, service_url:)
|
|
81
|
+
@logger&.debug("Teams API DELETE #{url} (targeted)")
|
|
82
|
+
http.delete(url, params: TARGETED_PARAMS)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The backend returns objectId instead of aadObjectId on some member
|
|
86
|
+
# endpoints; Api::Account reads both, so the accounts here are already
|
|
87
|
+
# normalized like the other SDKs' TeamsChannelAccount.
|
|
88
|
+
def get_members(conversation_id, service_url: nil)
|
|
89
|
+
path = "/v3/conversations/#{escape(conversation_id)}/members"
|
|
90
|
+
url = absolute(path, service_url:)
|
|
91
|
+
@logger&.debug("Teams API GET #{url}")
|
|
92
|
+
Array(http.get(url)).map { |member| Account.new(member) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def get_member_by_id(conversation_id, member_id, service_url: nil)
|
|
96
|
+
path = "/v3/conversations/#{escape(conversation_id)}/members/#{escape(member_id)}"
|
|
97
|
+
url = absolute(path, service_url:)
|
|
98
|
+
@logger&.debug("Teams API GET #{url}")
|
|
99
|
+
Account.new(http.get(url))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def get_paged_members(conversation_id, page_size: nil, continuation_token: nil, service_url: nil)
|
|
103
|
+
params = {}
|
|
104
|
+
params["pageSize"] = page_size if page_size
|
|
105
|
+
params["continuationToken"] = continuation_token if continuation_token
|
|
106
|
+
|
|
107
|
+
path = "/v3/conversations/#{escape(conversation_id)}/pagedMembers"
|
|
108
|
+
url = absolute(path, service_url:)
|
|
109
|
+
@logger&.debug("Teams API GET #{url}")
|
|
110
|
+
PagedMembersResult.new(http.get(url, params: params.empty? ? nil : params))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def get_activity_members(conversation_id, activity_id, service_url: nil)
|
|
114
|
+
path = "/v3/conversations/#{escape(conversation_id)}/activities/#{escape(activity_id)}/members"
|
|
115
|
+
url = absolute(path, service_url:)
|
|
116
|
+
@logger&.debug("Teams API GET #{url}")
|
|
117
|
+
Array(http.get(url)).map { |member| Account.new(member) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def add_reaction(conversation_id, activity_id, reaction_type)
|
|
121
|
+
reactions.add(conversation_id, activity_id, reaction_type)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def delete_reaction(conversation_id, activity_id, reaction_type)
|
|
125
|
+
reactions.delete(conversation_id, activity_id, reaction_type)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
attr_reader :reactions
|
|
131
|
+
|
|
132
|
+
def account_to_h(account)
|
|
133
|
+
account.is_a?(Hash) ? Common::Hashes.deep_stringify_keys(account) : account.to_h
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def activity_to_h(activity)
|
|
137
|
+
case activity
|
|
138
|
+
when String
|
|
139
|
+
MessageActivity.new(activity).to_h
|
|
140
|
+
when Hash
|
|
141
|
+
Common::Hashes.deep_stringify_keys(activity)
|
|
142
|
+
else
|
|
143
|
+
activity.respond_to?(:to_h) ? activity.to_h : activity
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def absolute(path, service_url: nil)
|
|
148
|
+
"#{(service_url || self.service_url).sub(%r{/+\z}, "")}#{path}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def escape(value)
|
|
152
|
+
URI.encode_www_form_component(value.to_s)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Teams
|
|
6
|
+
module Api
|
|
7
|
+
class ConversationReference
|
|
8
|
+
attr_reader :activity_id, :user, :locale, :bot, :conversation, :channel_id, :service_url
|
|
9
|
+
|
|
10
|
+
def self.from_activity(activity)
|
|
11
|
+
new(
|
|
12
|
+
activity_id: activity.id,
|
|
13
|
+
user: activity.from,
|
|
14
|
+
locale: activity.locale,
|
|
15
|
+
bot: activity.recipient,
|
|
16
|
+
conversation: activity.conversation,
|
|
17
|
+
channel_id: activity.channel_id,
|
|
18
|
+
service_url: activity.service_url
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.from_h(raw)
|
|
23
|
+
raw = raw.to_h if raw.respond_to?(:to_h)
|
|
24
|
+
|
|
25
|
+
new(
|
|
26
|
+
activity_id: raw["activityId"] || raw["activity_id"] || raw[:activityId] || raw[:activity_id],
|
|
27
|
+
user: raw["user"] || raw[:user],
|
|
28
|
+
locale: raw["locale"] || raw[:locale],
|
|
29
|
+
bot: raw.fetch("bot") { raw.fetch(:bot) },
|
|
30
|
+
conversation: raw.fetch("conversation") { raw.fetch(:conversation) },
|
|
31
|
+
channel_id: raw["channelId"] || raw["channel_id"] || raw[:channelId] || raw[:channel_id],
|
|
32
|
+
service_url: raw["serviceUrl"] || raw["service_url"] || raw[:serviceUrl] || raw[:service_url]
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(activity_id: nil, user: nil, locale: nil, bot:, conversation:, channel_id:, service_url:)
|
|
37
|
+
@activity_id = activity_id
|
|
38
|
+
@user = wrap_account(user)
|
|
39
|
+
@locale = locale
|
|
40
|
+
@bot = wrap_account(bot)
|
|
41
|
+
@conversation = wrap_conversation(conversation)
|
|
42
|
+
@channel_id = channel_id
|
|
43
|
+
@service_url = service_url
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def conversation_id
|
|
47
|
+
conversation.id
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_h
|
|
51
|
+
body = {
|
|
52
|
+
"bot" => bot.to_h,
|
|
53
|
+
"conversation" => conversation.to_h,
|
|
54
|
+
"channelId" => channel_id,
|
|
55
|
+
"serviceUrl" => service_url
|
|
56
|
+
}
|
|
57
|
+
body["activityId"] = activity_id if activity_id
|
|
58
|
+
body["user"] = user.to_h if user
|
|
59
|
+
body["locale"] = locale if locale
|
|
60
|
+
body
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_json(*args)
|
|
64
|
+
JSON.generate(to_h, *args)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def wrap_account(value)
|
|
70
|
+
return value if value.is_a?(Account)
|
|
71
|
+
return nil if value.nil?
|
|
72
|
+
|
|
73
|
+
Account.new(value)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def wrap_conversation(value)
|
|
77
|
+
return value if value.is_a?(ConversationAccount)
|
|
78
|
+
|
|
79
|
+
ConversationAccount.new(value)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
class ConversationResource < Model
|
|
6
|
+
def id
|
|
7
|
+
read("id")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def activity_id
|
|
11
|
+
read("activityId", "activity_id")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def service_url
|
|
15
|
+
read("serviceUrl", "service_url")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Teams
|
|
6
|
+
module Api
|
|
7
|
+
class MeetingClient
|
|
8
|
+
attr_reader :service_url, :http
|
|
9
|
+
|
|
10
|
+
def initialize(service_url:, http:, logger: nil)
|
|
11
|
+
@service_url = service_url.sub(%r{/+\z}, "")
|
|
12
|
+
@http = http
|
|
13
|
+
@logger = logger
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get_by_id(meeting_id, service_url: nil)
|
|
17
|
+
path = "/v1/meetings/#{escape(meeting_id)}"
|
|
18
|
+
url = absolute(path, service_url:)
|
|
19
|
+
@logger&.debug("Teams API GET #{url}")
|
|
20
|
+
MeetingInfo.new(http.get(url))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# participant_id is the user's AAD object id; tenant_id is required by
|
|
24
|
+
# the service.
|
|
25
|
+
def get_participant(meeting_id, participant_id, tenant_id, service_url: nil)
|
|
26
|
+
path = "/v1/meetings/#{escape(meeting_id)}/participants/#{escape(participant_id)}"
|
|
27
|
+
url = absolute(path, service_url:)
|
|
28
|
+
@logger&.debug("Teams API GET #{url}")
|
|
29
|
+
MeetingParticipant.new(http.get(url, params: { "tenantId" => tenant_id }))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Sends a targeted in-meeting notification. Returns nil when every
|
|
33
|
+
# recipient succeeded (202); returns a MeetingNotificationResponse with
|
|
34
|
+
# per-recipient failures on partial success (207).
|
|
35
|
+
def send_notification(meeting_id, params, service_url: nil)
|
|
36
|
+
body = Common::Hashes.deep_stringify_keys(params.respond_to?(:to_h) ? params.to_h : params)
|
|
37
|
+
body["type"] ||= "targetedMeetingNotification"
|
|
38
|
+
|
|
39
|
+
path = "/v1/meetings/#{escape(meeting_id)}/notification"
|
|
40
|
+
url = absolute(path, service_url:)
|
|
41
|
+
@logger&.debug("Teams API POST #{url}")
|
|
42
|
+
response = http.post(url, json: body)
|
|
43
|
+
response ? MeetingNotificationResponse.new(response) : nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def absolute(path, service_url: nil)
|
|
49
|
+
"#{(service_url || self.service_url).sub(%r{/+\z}, "")}#{path}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def escape(value)
|
|
53
|
+
URI.encode_www_form_component(value.to_s)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
class MeetingInfo < Model
|
|
6
|
+
def id
|
|
7
|
+
read("id")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def details
|
|
11
|
+
value = read("details")
|
|
12
|
+
value.is_a?(Hash) ? ActivityValue.new(value) : value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def conversation
|
|
16
|
+
value = read("conversation")
|
|
17
|
+
value ? ConversationAccount.new(value) : nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def organizer
|
|
21
|
+
value = read("organizer")
|
|
22
|
+
value ? Account.new(value) : nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
class MeetingNotificationRecipientFailure < Model
|
|
6
|
+
def recipient_mri
|
|
7
|
+
read("recipientMri", "recipient_mri")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def error_code
|
|
11
|
+
read("errorCode", "error_code")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def failure_reason
|
|
15
|
+
read("failureReason", "failure_reason")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returned on partial success (HTTP 207) of a meeting notification; the
|
|
20
|
+
# client returns nil instead when every recipient succeeded (HTTP 202).
|
|
21
|
+
class MeetingNotificationResponse < Model
|
|
22
|
+
def recipients_failure_info
|
|
23
|
+
Array(read("recipientsFailureInfo", "recipients_failure_info")).map do |failure|
|
|
24
|
+
MeetingNotificationRecipientFailure.new(failure)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
# A participant's meeting-specific details: role and presence status.
|
|
6
|
+
class Meeting < Model
|
|
7
|
+
def role
|
|
8
|
+
read("role")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def in_meeting
|
|
12
|
+
read("inMeeting", "in_meeting")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class MeetingParticipant < Model
|
|
17
|
+
def user
|
|
18
|
+
value = read("user")
|
|
19
|
+
value ? Account.new(value) : nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def meeting
|
|
23
|
+
value = read("meeting")
|
|
24
|
+
value ? Meeting.new(value) : nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def conversation
|
|
28
|
+
value = read("conversation")
|
|
29
|
+
value ? ConversationAccount.new(value) : nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
class MessageActivity
|
|
6
|
+
TEXT_FORMATS = %w[plain markdown xml extendedmarkdown].freeze
|
|
7
|
+
FEEDBACK_MODES = %w[default custom].freeze
|
|
8
|
+
AI_MESSAGE_ENTITY_TYPE = "https://schema.org/Message"
|
|
9
|
+
|
|
10
|
+
attr_reader :id, :text, :attachments, :text_format, :summary, :input_hint, :entities, :channel_data, :recipient
|
|
11
|
+
|
|
12
|
+
def initialize(text = nil, id: nil, attachments: [], text_format: nil, summary: nil, input_hint: nil)
|
|
13
|
+
@id = id
|
|
14
|
+
@text = text
|
|
15
|
+
@attachments = attachments
|
|
16
|
+
@text_format = normalize_text_format(text_format)
|
|
17
|
+
@summary = summary
|
|
18
|
+
@input_hint = input_hint
|
|
19
|
+
@entities = []
|
|
20
|
+
@channel_data = {}
|
|
21
|
+
@recipient = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add_text(value)
|
|
25
|
+
@text = "#{text}#{value}"
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add_quote(message_id, text = nil)
|
|
30
|
+
@entities << QuotedReplyEntity.new("quotedReply" => { "messageId" => message_id })
|
|
31
|
+
add_text(%(<quoted messageId="#{message_id}"/>))
|
|
32
|
+
add_text(" #{text}") if text
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def prepend_quote(message_id)
|
|
37
|
+
@entities << QuotedReplyEntity.new("quotedReply" => { "messageId" => message_id })
|
|
38
|
+
placeholder = %(<quoted messageId="#{message_id}"/>)
|
|
39
|
+
@text = text.to_s.strip.empty? ? placeholder : "#{placeholder} #{text}"
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get_quoted_messages
|
|
44
|
+
entities.select { |entity| quoted_reply_entity?(entity) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add_card(card)
|
|
48
|
+
@attachments << {
|
|
49
|
+
"contentType" => "application/vnd.microsoft.card.adaptive",
|
|
50
|
+
"content" => card.respond_to?(:to_h) ? card.to_h : card
|
|
51
|
+
}
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def with_text_format(text_format)
|
|
56
|
+
@text_format = normalize_text_format(text_format)
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def with_id(id)
|
|
61
|
+
@id = id
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def with_recipient(account, is_targeted: nil)
|
|
66
|
+
body = account.respond_to?(:to_h) ? account.to_h : account
|
|
67
|
+
body = body.dup
|
|
68
|
+
body["isTargeted"] = is_targeted unless is_targeted.nil?
|
|
69
|
+
@recipient = body
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Adds a targetedMessageInfo entity for prompt preview, once per message.
|
|
74
|
+
# Any quotedReply entities and the matching quote placeholder are removed
|
|
75
|
+
# to avoid collision with the prompt preview.
|
|
76
|
+
def add_targeted_message_info(message_id)
|
|
77
|
+
@entities.reject! { |entity| quoted_reply_entity?(entity) }
|
|
78
|
+
@text = text.gsub(%(<quoted messageId="#{message_id}"/>), "").strip if text
|
|
79
|
+
|
|
80
|
+
unless @entities.any? { |entity| entity_type(entity) == "targetedMessageInfo" }
|
|
81
|
+
@entities << { "type" => "targetedMessageInfo", "messageId" => message_id }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def add_ai_generated
|
|
88
|
+
entity = ensure_single_root_level_message_entity
|
|
89
|
+
additional_types = Array(entity["additionalType"])
|
|
90
|
+
return self if additional_types.include?("AIGeneratedContent")
|
|
91
|
+
|
|
92
|
+
entity["additionalType"] = additional_types + ["AIGeneratedContent"]
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def add_mention(account, text: nil, add_text: true)
|
|
97
|
+
account_hash = account.respond_to?(:to_h) ? account.to_h : account
|
|
98
|
+
mention_text = text || account_hash["name"]
|
|
99
|
+
|
|
100
|
+
self.add_text("<at>#{mention_text}</at>") if add_text
|
|
101
|
+
|
|
102
|
+
@entities << {
|
|
103
|
+
"type" => "mention",
|
|
104
|
+
"mentioned" => account_hash,
|
|
105
|
+
"text" => "<at>#{mention_text}</at>"
|
|
106
|
+
}
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Adds a content sensitivity label. The label is carried as usageInfo on
|
|
111
|
+
# the root schema.org/Message entity, the wire shape TypeScript, Python,
|
|
112
|
+
# and the Teams documentation use.
|
|
113
|
+
def add_sensitivity_label(name, description: nil, pattern: nil)
|
|
114
|
+
usage_info = {
|
|
115
|
+
"type" => AI_MESSAGE_ENTITY_TYPE,
|
|
116
|
+
"@type" => "CreativeWork",
|
|
117
|
+
"name" => name
|
|
118
|
+
}
|
|
119
|
+
usage_info["description"] = description if description
|
|
120
|
+
usage_info["pattern"] = pattern if pattern
|
|
121
|
+
|
|
122
|
+
ensure_single_root_level_message_entity["usageInfo"] = usage_info
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def add_citation(position, appearance)
|
|
127
|
+
entity = ensure_single_root_level_message_entity
|
|
128
|
+
entity["citation"] ||= []
|
|
129
|
+
entity["citation"] << {
|
|
130
|
+
"@type" => "Claim",
|
|
131
|
+
"position" => position,
|
|
132
|
+
"appearance" => citation_appearance(appearance).to_h
|
|
133
|
+
}
|
|
134
|
+
self
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def add_feedback(mode = "default")
|
|
138
|
+
@channel_data["feedbackLoop"] = { "type" => normalize_feedback_mode(mode) }
|
|
139
|
+
@channel_data.delete("feedbackLoopEnabled")
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def to_h
|
|
144
|
+
body = { "type" => "message" }
|
|
145
|
+
body["id"] = id if id
|
|
146
|
+
body["text"] = text if text
|
|
147
|
+
body["textFormat"] = text_format if text_format
|
|
148
|
+
body["summary"] = summary if summary
|
|
149
|
+
body["inputHint"] = input_hint if input_hint
|
|
150
|
+
body["attachments"] = attachments unless attachments.empty?
|
|
151
|
+
body["entities"] = @entities.map { |entity| entity.respond_to?(:to_h) ? entity.to_h : entity } unless @entities.empty?
|
|
152
|
+
body["channelData"] = channel_data unless channel_data.empty?
|
|
153
|
+
body["recipient"] = recipient if recipient
|
|
154
|
+
body
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def quoted_reply_entity?(entity)
|
|
160
|
+
entity_type(entity) == "quotedReply"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def ensure_single_root_level_message_entity
|
|
164
|
+
entity = @entities.find { |item| entity_type(item) == AI_MESSAGE_ENTITY_TYPE }
|
|
165
|
+
return entity if entity
|
|
166
|
+
|
|
167
|
+
entity = {
|
|
168
|
+
"type" => AI_MESSAGE_ENTITY_TYPE,
|
|
169
|
+
"@type" => "Message",
|
|
170
|
+
"@context" => "https://schema.org"
|
|
171
|
+
}
|
|
172
|
+
@entities << entity
|
|
173
|
+
entity
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def citation_appearance(appearance)
|
|
177
|
+
appearance.is_a?(CitationAppearance) ? appearance : CitationAppearance.new(appearance || {})
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def entity_type(entity)
|
|
181
|
+
entity.respond_to?(:type) ? entity.type : entity["type"]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def normalize_text_format(value)
|
|
185
|
+
return nil if value.nil?
|
|
186
|
+
|
|
187
|
+
normalized = value.to_s
|
|
188
|
+
return normalized if TEXT_FORMATS.include?(normalized)
|
|
189
|
+
|
|
190
|
+
raise ArgumentError, "text_format must be one of: #{TEXT_FORMATS.join(", ")}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def normalize_feedback_mode(value)
|
|
194
|
+
normalized = value.to_s
|
|
195
|
+
return normalized if FEEDBACK_MODES.include?(normalized)
|
|
196
|
+
|
|
197
|
+
raise ArgumentError, "feedback mode must be one of: #{FEEDBACK_MODES.join(", ")}"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|