chat_sdk-teams 0.6.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5077208fda83c5941a590c7daa94b0d821399dcd49d92f3b5368498cd7e30f0b
4
- data.tar.gz: 6271ccca3f36db8cadb455bfa47ead36eccf5ab6b6363edc39342d223f39979d
3
+ metadata.gz: 2c1d10cccac3513f5852697594070029b0f106a6f1519110a54b865e26d58384
4
+ data.tar.gz: d707ebb3bf03165482bccf057873f0da94907259dc0f6d7d0b7292e5d080c66f
5
5
  SHA512:
6
- metadata.gz: 7a219d54aa4725525307540f3fec911ce6c956f3421faf2ba88ac2ab672972a46509b85128d044b1f8d48152ffe52973a3e241c5bc3a047c110edfef3f607ebd
7
- data.tar.gz: 19555fa3ae535a42cf93ebd79e3e7695221648880d19d42d2a1562550b29085db991f630a12f4b60424074a85941fb4c485bf4e6f67f39ffbb4025223289840e
6
+ metadata.gz: 51fb098edc33d6a63f33a713b9d5b157012fc2a24c5247c07127d9345d08bd083c24c83b3b4bbb998703bfbfd0d0288253c6a1cd53f85f89572277a23d1e03b9
7
+ data.tar.gz: 0edf7931855e4332b57cc8c70e32a4ae7ec4f081759e49cb2c5c399f014f5ac1baaf053ee4ffc3987dc4845c6eb8eec3953cdf13ab11130ea48a4026221cc402
@@ -4,11 +4,12 @@ module ChatSDK
4
4
  module Teams
5
5
  class Adapter < ChatSDK::Adapter::Base
6
6
  capabilities :edit_messages, :delete_messages, :threads, :direct_messages,
7
- :file_uploads, :streaming_edit, :typing_indicator
7
+ :file_uploads, :streaming_edit, :typing_indicator, :message_history
8
8
 
9
9
  attr_reader :client
10
10
 
11
- def initialize(app_id: nil, app_password: nil, tenant_id: nil)
11
+ def initialize(app_id: nil, app_password: nil, tenant_id: nil,
12
+ graph_client_id: nil, graph_client_secret: nil, graph_tenant_id: nil)
12
13
  @app_id = app_id || ENV["TEAMS_APP_ID"]
13
14
  @app_password = app_password || ENV["TEAMS_APP_PASSWORD"]
14
15
  @tenant_id = tenant_id || ENV["TEAMS_TENANT_ID"]
@@ -16,6 +17,11 @@ module ChatSDK
16
17
  raise ChatSDK::ConfigurationError, "Teams app_id required" unless @app_id
17
18
  raise ChatSDK::ConfigurationError, "Teams app_password required" unless @app_password
18
19
 
20
+ @graph_client_id = graph_client_id || ENV["TEAMS_GRAPH_CLIENT_ID"]
21
+ @graph_client_secret = graph_client_secret || ENV["TEAMS_GRAPH_CLIENT_SECRET"]
22
+ @graph_tenant_id = graph_tenant_id || ENV["TEAMS_GRAPH_TENANT_ID"]
23
+ @graph_client = GraphClient.new(@graph_client_id, @graph_client_secret, @graph_tenant_id) if @graph_client_id
24
+
19
25
  @client = BotFrameworkClient.new(app_id: @app_id, app_password: @app_password)
20
26
  @jwt_verifier = JwtVerifier.new(app_id: @app_id)
21
27
  @renderer = AdaptiveCardRenderer.new
@@ -177,6 +183,20 @@ module ChatSDK
177
183
  end
178
184
  end
179
185
 
186
+ def fetch_messages(channel_id:, thread_id: nil, cursor: nil, limit: 50)
187
+ require_capability!(:message_history)
188
+ unless @graph_client
189
+ raise ChatSDK::ConfigurationError,
190
+ "Teams message history requires Graph API credentials " \
191
+ "(TEAMS_GRAPH_CLIENT_ID, TEAMS_GRAPH_CLIENT_SECRET, TEAMS_GRAPH_TENANT_ID)"
192
+ end
193
+
194
+ result = @graph_client.fetch_messages(chat_id: channel_id, limit: limit)
195
+ messages = (result["value"] || []).map { |m| parse_graph_message(m, channel_id) }
196
+ next_link = result["@odata.nextLink"]
197
+ [messages, next_link]
198
+ end
199
+
180
200
  # Store a service URL for a conversation (useful when sending proactive messages)
181
201
  def register_service_url(conversation_id, service_url)
182
202
  @service_urls[conversation_id] = service_url
@@ -211,6 +231,28 @@ module ChatSDK
211
231
  end
212
232
  url
213
233
  end
234
+
235
+ def parse_graph_message(data, channel_id)
236
+ from = data["from"] || {}
237
+ user = from["user"] || from["application"] || {}
238
+ is_bot = !from["application"].nil?
239
+
240
+ ChatSDK::Message.new(
241
+ id: data["id"],
242
+ text: data.dig("body", "content") || "",
243
+ author: ChatSDK::Author.new(
244
+ id: user["id"] || "unknown",
245
+ name: user["displayName"] || "unknown",
246
+ platform: :teams,
247
+ bot: is_bot
248
+ ),
249
+ thread_id: data["replyToId"] || data["id"],
250
+ channel_id: channel_id,
251
+ platform: :teams,
252
+ timestamp: data["createdDateTime"],
253
+ raw: data
254
+ )
255
+ end
214
256
  end
215
257
  end
216
258
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Teams
5
+ class FormatConverter < ChatSDK::Format::Converter
6
+ # Teams HTML → Markdown
7
+ def to_markdown(platform_text)
8
+ text = platform_text.to_s
9
+ return "" if text.empty?
10
+
11
+ # Protect <pre> blocks first — extract them before processing inline HTML
12
+ pre_blocks = []
13
+ text = text.gsub(%r{<pre>(.*?)</pre>}mi) do
14
+ pre_blocks << Regexp.last_match(1)
15
+ "\x00PRE#{pre_blocks.size - 1}\x00"
16
+ end
17
+
18
+ # Protect <code> spans
19
+ code_spans = []
20
+ text = text.gsub(%r{<code>(.*?)</code>}mi) do
21
+ code_spans << Regexp.last_match(1)
22
+ "\x00CODE#{code_spans.size - 1}\x00"
23
+ end
24
+
25
+ # Convert block-level elements
26
+ text = convert_lists_to_markdown(text)
27
+
28
+ # Convert inline formatting
29
+ text = text.gsub(%r{<(?:b|strong)>(.*?)</(?:b|strong)>}mi, '**\1**')
30
+ text = text.gsub(%r{<(?:i|em)>(.*?)</(?:i|em)>}mi, '*\1*')
31
+ text = text.gsub(%r{<(?:s|strike|del)>(.*?)</(?:s|strike|del)>}mi, '~~\1~~')
32
+ text = text.gsub(%r{<a\s+href="([^"]*)"[^>]*>(.*?)</a>}mi, '[\2](\1)')
33
+ text = text.gsub(%r{<at>(.*?)</at>}mi, '@\1')
34
+ text = text.gsub(%r{<br\s*/?>}i, "\n")
35
+
36
+ # Decode HTML entities
37
+ text = decode_html_entities(text)
38
+
39
+ # Strip any remaining HTML tags
40
+ text = text.gsub(/<[^>]+>/, "")
41
+
42
+ # Restore code spans
43
+ code_spans.each_with_index do |content, i|
44
+ text = text.sub("\x00CODE#{i}\x00", "`#{content}`")
45
+ end
46
+
47
+ # Restore pre blocks
48
+ pre_blocks.each_with_index do |content, i|
49
+ text = text.sub("\x00PRE#{i}\x00", "```\n#{content}\n```")
50
+ end
51
+
52
+ text
53
+ end
54
+
55
+ # Markdown → Teams HTML
56
+ def from_markdown(markdown)
57
+ text = markdown.to_s
58
+ return "" if text.empty?
59
+
60
+ # Protect fenced code blocks first
61
+ code_blocks = []
62
+ text = text.gsub(/```\n?(.*?)\n?```/m) do
63
+ code_blocks << Regexp.last_match(1)
64
+ "\x00CODEBLOCK#{code_blocks.size - 1}\x00"
65
+ end
66
+
67
+ # Protect inline code spans
68
+ code_spans = []
69
+ text = text.gsub(/`([^`]+)`/) do
70
+ code_spans << Regexp.last_match(1)
71
+ "\x00CODESPAN#{code_spans.size - 1}\x00"
72
+ end
73
+
74
+ # Convert markdown formatting to HTML
75
+ text = text.gsub(/\*\*(.+?)\*\*/m, '<b>\1</b>')
76
+ text = text.gsub(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/m, '<i>\1</i>')
77
+ text = text.gsub(/~~(.+?)~~/m, '<s>\1</s>')
78
+ text = text.gsub(/\[([^\]]+)\]\(([^)]+)\)/, '<a href="\2">\1</a>')
79
+ text = text.gsub("\n", "<br>")
80
+
81
+ # Restore inline code spans
82
+ code_spans.each_with_index do |content, i|
83
+ text = text.sub("\x00CODESPAN#{i}\x00", "<code>#{content}</code>")
84
+ end
85
+
86
+ # Restore code blocks
87
+ code_blocks.each_with_index do |content, i|
88
+ text = text.sub("\x00CODEBLOCK#{i}\x00", "<pre>#{content}</pre>")
89
+ end
90
+
91
+ text
92
+ end
93
+
94
+ private
95
+
96
+ def convert_lists_to_markdown(html)
97
+ # Convert unordered lists
98
+ html = html.gsub(%r{<ul>(.*?)</ul>}mi) do
99
+ items = Regexp.last_match(1)
100
+ items.gsub(%r{<li>(.*?)</li>}mi) { "- #{Regexp.last_match(1).strip}\n" }.strip
101
+ end
102
+
103
+ # Convert ordered lists
104
+ html.gsub(%r{<ol>(.*?)</ol>}mi) do
105
+ items = Regexp.last_match(1)
106
+ index = 0
107
+ items.gsub(%r{<li>(.*?)</li>}mi) do
108
+ index += 1
109
+ "#{index}. #{Regexp.last_match(1).strip}\n"
110
+ end.strip
111
+ end
112
+ end
113
+
114
+ def decode_html_entities(text)
115
+ text
116
+ .gsub("&amp;", "&")
117
+ .gsub("&lt;", "<")
118
+ .gsub("&gt;", ">")
119
+ .gsub("&quot;", '"')
120
+ .gsub("&#39;", "'")
121
+ .gsub("&nbsp;", " ")
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Teams
5
+ class GraphClient
6
+ TOKEN_URL_TEMPLATE = "https://login.microsoftonline.com/%s/oauth2/v2.0/token"
7
+ GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0"
8
+ SCOPE = "https://graph.microsoft.com/.default"
9
+
10
+ def initialize(client_id, client_secret, tenant_id)
11
+ @client_id = client_id
12
+ @client_secret = client_secret
13
+ @tenant_id = tenant_id
14
+ @access_token = nil
15
+ @token_expires_at = nil
16
+ end
17
+
18
+ def fetch_messages(chat_id:, limit: 50)
19
+ token = fetch_token
20
+ url = "#{GRAPH_BASE_URL}/chats/#{chat_id}/messages?$top=#{limit}&$orderby=createdDateTime desc"
21
+
22
+ response = api_connection.get(url) do |req|
23
+ req.headers["Authorization"] = "Bearer #{token}"
24
+ end
25
+
26
+ unless response.success?
27
+ raise ChatSDK::PlatformError.new(
28
+ "Graph API error: #{response.status}",
29
+ status: response.status,
30
+ body: response.body,
31
+ adapter_name: :teams
32
+ )
33
+ end
34
+
35
+ response.body
36
+ end
37
+
38
+ def fetch_token
39
+ return @access_token if @access_token && @token_expires_at && Time.now < @token_expires_at
40
+
41
+ token_url = format(TOKEN_URL_TEMPLATE, @tenant_id)
42
+
43
+ response = token_connection.post(token_url, {
44
+ grant_type: "client_credentials",
45
+ client_id: @client_id,
46
+ client_secret: @client_secret,
47
+ scope: SCOPE
48
+ })
49
+
50
+ unless response.success?
51
+ raise ChatSDK::PlatformError.new(
52
+ "Failed to acquire Graph API token: #{response.status}",
53
+ status: response.status,
54
+ body: response.body,
55
+ adapter_name: :teams
56
+ )
57
+ end
58
+
59
+ data = JSON.parse(response.body)
60
+ @access_token = data["access_token"]
61
+ @token_expires_at = Time.now + (data["expires_in"].to_i - 60)
62
+ @access_token
63
+ end
64
+
65
+ private
66
+
67
+ def token_connection
68
+ @token_connection ||= Faraday.new do |f|
69
+ f.request :url_encoded
70
+ f.adapter :net_http
71
+ end
72
+ end
73
+
74
+ def api_connection
75
+ @api_connection ||= Faraday.new do |f|
76
+ f.request :json
77
+ f.response :json
78
+ f.adapter :net_http
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chat_sdk-teams
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Quentin Rousseau
@@ -63,6 +63,8 @@ files:
63
63
  - lib/chat_sdk/teams/adapter.rb
64
64
  - lib/chat_sdk/teams/adaptive_card_renderer.rb
65
65
  - lib/chat_sdk/teams/bot_framework_client.rb
66
+ - lib/chat_sdk/teams/format_converter.rb
67
+ - lib/chat_sdk/teams/graph_client.rb
66
68
  - lib/chat_sdk/teams/jwt_verifier.rb
67
69
  homepage: https://github.com/rootlyhq/chat-sdk
68
70
  licenses: