Class: Teams::App
- Inherits:
-
Object
- Object
- Teams::App
- Defined in:
- lib/teams/app.rb
Constant Summary collapse
- DEFAULT_MESSAGING_ENDPOINT =
"/api/messages"- DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS_ENV_VAR =
"DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS"- TRUE_ENV_VALUES =
%w[1 true yes on].freeze
- FALSE_ENV_VALUES =
%w[0 false no off].freeze
Instance Attribute Summary collapse
-
#api ⇒ Object
readonly
Returns the value of attribute api.
-
#default_connection_name ⇒ Object
readonly
Returns the value of attribute default_connection_name.
-
#graph ⇒ Object
readonly
Returns the value of attribute graph.
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#messaging_endpoint ⇒ Object
readonly
Returns the value of attribute messaging_endpoint.
-
#storage ⇒ Object
readonly
Returns the value of attribute storage.
Instance Method Summary collapse
- #client_id ⇒ Object
- #emit_error(error, activity: nil) ⇒ Object
- #emit_sign_in(context, token_response) ⇒ Object
-
#initialize(client_id: ENV["CLIENT_ID"], client_secret: ENV["CLIENT_SECRET"], tenant_id: ENV["TENANT_ID"], service_url: ENV.fetch("SERVICE_URL", "https://smba.trafficmanager.net/teams"), cloud: CloudEnvironments.public, logger: Logger.new($stdout), storage: Storage::MemoryStore.new, api: nil, token_manager: nil, dangerously_allow_unauthenticated_requests: nil, skip_auth: nil, messaging_endpoint: DEFAULT_MESSAGING_ENDPOINT, default_connection_name: "graph") ⇒ App
constructor
A new instance of App.
- #initialize! ⇒ Object
- #on(name, &block) ⇒ Object
-
#on_card_search(&block) ⇒ Object
application/search invokes from Adaptive Card dynamic typeahead Input.ChoiceSet queries; the handler's return (Api::SearchResponse or hash) becomes the invoke response body.
-
#on_conversation_update(&block) ⇒ Object
conversationUpdate activities plus their channel/team lifecycle sub-events (on_channel_created, on_team_renamed, ...), routed by channelData.eventType with the Python method names.
-
#on_dialog_open(dialog_id = nil, &block) ⇒ Object
Registers a dialog (task module) open handler for task/fetch invokes.
-
#on_dialog_submit(action = nil, &block) ⇒ Object
Registers a dialog (task module) submit handler for task/submit invokes, optionally filtered by the "action" value in the submit data.
- #on_edit_message(&block) ⇒ Object
-
#on_error(&block) ⇒ Object
Called with (error, activity) when a default OAuth handler hits an unexpected failure or the Teams client reports a sign-in failure.
-
#on_function(name, &block) ⇒ Object
Registers a remote function callable from tabs via POST /api/functions/name.
- #on_meeting_end(&block) ⇒ Object
-
#on_meeting_start(&block) ⇒ Object
Meeting start/end events (Teams posts them to bots installed in the meeting chat when the meeting begins and ends).
- #on_message(pattern = nil, &block) ⇒ Object
-
#on_message_submit(&block) ⇒ Object
message/submitAction invokes; on_message_submit_feedback filters to feedback-loop submissions (thumbs up/down from add_feedback).
- #on_message_submit_feedback(&block) ⇒ Object
- #on_message_update(&block) ⇒ Object
-
#on_sign_in(&block) ⇒ Object
Called with (ctx, token_response) whenever a sign-in completes through the default token-exchange or verify-state handlers.
- #on_signin_failure(&block) ⇒ Object
-
#on_signin_token_exchange(&block) ⇒ Object
Sign-in invokes: signin/tokenExchange arrives for silent SSO token exchange, signin/verifyState after interactive OAuth card sign-in, signin/failure when the Teams client reports a failed SSO attempt.
- #on_signin_verify_state(&block) ⇒ Object
- #on_suggested_action_submit(&block) ⇒ Object
- #on_undelete_message(&block) ⇒ Object
-
#post(conversation_id, activity_or_text, service_url: nil) ⇒ Object
The TypeScript, Python, and .NET SDKs call this operation
send. - #process_function(name, data, env: {}) ⇒ Object
- #process_inbound(payload, env: {}) ⇒ Object
-
#reply(conversation_id, activity_id_or_activity, activity_or_text = nil, service_url: nil) ⇒ Object
Proactive threaded replies use a ";messageid=" conversation ID like the TypeScript and Python SDKs; the service decides whether threading applies.
- #send_activity(conversation_reference, activity_or_text) ⇒ Object
- #to_rack ⇒ Object
-
#update(conversation_id, activity_id, activity_or_text, service_url: nil) ⇒ Object
Sugar over post: the SDKs update by sending an activity that already carries an id, and post does exactly that.
- #use(&block) ⇒ Object
Constructor Details
#initialize(client_id: ENV["CLIENT_ID"], client_secret: ENV["CLIENT_SECRET"], tenant_id: ENV["TENANT_ID"], service_url: ENV.fetch("SERVICE_URL", "https://smba.trafficmanager.net/teams"), cloud: CloudEnvironments.public, logger: Logger.new($stdout), storage: Storage::MemoryStore.new, api: nil, token_manager: nil, dangerously_allow_unauthenticated_requests: nil, skip_auth: nil, messaging_endpoint: DEFAULT_MESSAGING_ENDPOINT, default_connection_name: "graph") ⇒ App
Returns a new instance of App.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/teams/app.rb', line 18 def initialize( client_id: ENV["CLIENT_ID"], client_secret: ENV["CLIENT_SECRET"], tenant_id: ENV["TENANT_ID"], service_url: ENV.fetch("SERVICE_URL", "https://smba.trafficmanager.net/teams"), cloud: CloudEnvironments.public, logger: Logger.new($stdout), storage: Storage::MemoryStore.new, api: nil, token_manager: nil, dangerously_allow_unauthenticated_requests: nil, skip_auth: nil, messaging_endpoint: DEFAULT_MESSAGING_ENDPOINT, default_connection_name: "graph" ) @default_connection_name = default_connection_name @sign_in_handlers = [] @error_handlers = [] @functions = {} @logger = logger @storage = storage @cloud = cloud @dangerously_allow_unauthenticated_requests = resolve_unauthenticated_requests_option( dangerously_allow_unauthenticated_requests, skip_auth ) @messaging_endpoint = normalize_messaging_endpoint(messaging_endpoint) @router = Router.new @token_manager = token_manager || Auth::TokenManager.from_env(client_id:, client_secret:, tenant_id:, cloud:) @api = api || Api::Client.new( service_url:, http: Common::HttpClient.new(token: -> { @token_manager.bot_token }), logger:, oauth_url: cloud.token_service_url ) @jwt_validator = Auth::JwtValidator.new( client_id: @token_manager.client_id, tenant_id: @token_manager.credentials&.tenant_id, cloud: ) if @token_manager.client_id # Graph base URL derives from the cloud's graph scope host (sovereign # clouds), like the TypeScript SDK; the app-identity client requests # app-only tokens through the client-credentials flow. graph_root = cloud.graph_scope.to_s[%r{\Ahttps?://[^/]+}] @graph = Graph::Client.new( token: -> { @token_manager.token_for(cloud.graph_scope, @token_manager.credentials&.tenant_id || cloud.login_tenant) }, base_url_root: graph_root ) register_default_oauth_handlers warn_missing_credentials end |
Instance Attribute Details
#api ⇒ Object (readonly)
Returns the value of attribute api.
12 13 14 |
# File 'lib/teams/app.rb', line 12 def api @api end |
#default_connection_name ⇒ Object (readonly)
Returns the value of attribute default_connection_name.
12 13 14 |
# File 'lib/teams/app.rb', line 12 def default_connection_name @default_connection_name end |
#graph ⇒ Object (readonly)
Returns the value of attribute graph.
12 13 14 |
# File 'lib/teams/app.rb', line 12 def graph @graph end |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
12 13 14 |
# File 'lib/teams/app.rb', line 12 def logger @logger end |
#messaging_endpoint ⇒ Object (readonly)
Returns the value of attribute messaging_endpoint.
12 13 14 |
# File 'lib/teams/app.rb', line 12 def messaging_endpoint @messaging_endpoint end |
#storage ⇒ Object (readonly)
Returns the value of attribute storage.
12 13 14 |
# File 'lib/teams/app.rb', line 12 def storage @storage end |
Instance Method Details
#client_id ⇒ Object
14 15 16 |
# File 'lib/teams/app.rb', line 14 def client_id @token_manager.client_id end |
#emit_error(error, activity: nil) ⇒ Object
175 176 177 |
# File 'lib/teams/app.rb', line 175 def emit_error(error, activity: nil) @error_handlers.each { |handler| handler.call(error, activity) } end |
#emit_sign_in(context, token_response) ⇒ Object
171 172 173 |
# File 'lib/teams/app.rb', line 171 def emit_sign_in(context, token_response) @sign_in_handlers.each { |handler| handler.call(context, token_response) } end |
#initialize! ⇒ Object
76 77 78 79 |
# File 'lib/teams/app.rb', line 76 def initialize! @token_manager.bot_token true end |
#on(name, &block) ⇒ Object
86 87 88 89 |
# File 'lib/teams/app.rb', line 86 def on(name, &block) @router.on(name, &block) self end |
#on_card_search(&block) ⇒ Object
application/search invokes from Adaptive Card dynamic typeahead Input.ChoiceSet queries; the handler's return (Api::SearchResponse or hash) becomes the invoke response body.
152 153 154 155 |
# File 'lib/teams/app.rb', line 152 def on_card_search(&block) @router.on_card_search(&block) self end |
#on_conversation_update(&block) ⇒ Object
conversationUpdate activities plus their channel/team lifecycle sub-events (on_channel_created, on_team_renamed, ...), routed by channelData.eventType with the Python method names.
226 227 228 229 |
# File 'lib/teams/app.rb', line 226 def on_conversation_update(&block) @router.on_conversation_update(&block) self end |
#on_dialog_open(dialog_id = nil, &block) ⇒ Object
Registers a dialog (task module) open handler for task/fetch invokes. With a dialog_id, only invokes whose card action data carries that "dialog_id" value match. The handler's return value (a Api::TaskModuleResponse or hash) becomes the invoke response body.
105 106 107 108 |
# File 'lib/teams/app.rb', line 105 def on_dialog_open(dialog_id = nil, &block) @router.on_dialog_open(dialog_id, &block) self end |
#on_dialog_submit(action = nil, &block) ⇒ Object
Registers a dialog (task module) submit handler for task/submit invokes, optionally filtered by the "action" value in the submit data.
112 113 114 115 |
# File 'lib/teams/app.rb', line 112 def on_dialog_submit(action = nil, &block) @router.on_dialog_submit(action, &block) self end |
#on_edit_message(&block) ⇒ Object
254 255 256 257 |
# File 'lib/teams/app.rb', line 254 def (&block) @router.(&block) self end |
#on_error(&block) ⇒ Object
Called with (error, activity) when a default OAuth handler hits an unexpected failure or the Teams client reports a sign-in failure.
166 167 168 169 |
# File 'lib/teams/app.rb', line 166 def on_error(&block) @error_handlers << block self end |
#on_function(name, &block) ⇒ Object
Registers a remote function callable from tabs via POST /api/functions/name. Requests carry an Entra token for the tab user, validated against the app's client id and tenant; the handler receives a FunctionContext and its return value becomes the JSON response body.
184 185 186 187 188 189 |
# File 'lib/teams/app.rb', line 184 def on_function(name, &block) raise ArgumentError, "handler block is required" unless block @functions[name.to_s] = block self end |
#on_meeting_end(&block) ⇒ Object
218 219 220 221 |
# File 'lib/teams/app.rb', line 218 def on_meeting_end(&block) @router.on_meeting_end(&block) self end |
#on_meeting_start(&block) ⇒ Object
Meeting start/end events (Teams posts them to bots installed in the meeting chat when the meeting begins and ends).
213 214 215 216 |
# File 'lib/teams/app.rb', line 213 def on_meeting_start(&block) @router.on_meeting_start(&block) self end |
#on_message(pattern = nil, &block) ⇒ Object
91 92 93 94 |
# File 'lib/teams/app.rb', line 91 def (pattern = nil, &block) @router.(pattern, &block) self end |
#on_message_submit(&block) ⇒ Object
message/submitAction invokes; on_message_submit_feedback filters to feedback-loop submissions (thumbs up/down from add_feedback).
139 140 141 142 |
# File 'lib/teams/app.rb', line 139 def (&block) @router.(&block) self end |
#on_message_submit_feedback(&block) ⇒ Object
144 145 146 147 |
# File 'lib/teams/app.rb', line 144 def (&block) @router.(&block) self end |
#on_message_update(&block) ⇒ Object
249 250 251 252 |
# File 'lib/teams/app.rb', line 249 def (&block) @router.(&block) self end |
#on_sign_in(&block) ⇒ Object
Called with (ctx, token_response) whenever a sign-in completes through the default token-exchange or verify-state handlers.
159 160 161 162 |
# File 'lib/teams/app.rb', line 159 def on_sign_in(&block) @sign_in_handlers << block self end |
#on_signin_failure(&block) ⇒ Object
132 133 134 135 |
# File 'lib/teams/app.rb', line 132 def on_signin_failure(&block) @router.on_signin_failure(&block) self end |
#on_signin_token_exchange(&block) ⇒ Object
Sign-in invokes: signin/tokenExchange arrives for silent SSO token exchange, signin/verifyState after interactive OAuth card sign-in, signin/failure when the Teams client reports a failed SSO attempt. Default handlers registered at construction complete these flows and fire on_sign_in / on_error; handlers registered here run after them.
122 123 124 125 |
# File 'lib/teams/app.rb', line 122 def on_signin_token_exchange(&block) @router.on_signin_token_exchange(&block) self end |
#on_signin_verify_state(&block) ⇒ Object
127 128 129 130 |
# File 'lib/teams/app.rb', line 127 def on_signin_verify_state(&block) @router.on_signin_verify_state(&block) self end |
#on_suggested_action_submit(&block) ⇒ Object
96 97 98 99 |
# File 'lib/teams/app.rb', line 96 def on_suggested_action_submit(&block) @router.on("suggested-action.submit", &block) self end |
#on_undelete_message(&block) ⇒ Object
259 260 261 262 |
# File 'lib/teams/app.rb', line 259 def (&block) @router.(&block) self end |
#post(conversation_id, activity_or_text, service_url: nil) ⇒ Object
The TypeScript, Python, and .NET SDKs call this operation send.
Ruby already defines Object#send for dynamic dispatch, so the public
Ruby API uses post to avoid shadowing a core language method.
289 290 291 292 293 294 295 296 |
# File 'lib/teams/app.rb', line 289 def post(conversation_id, activity_or_text, service_url: nil) assert_string!(conversation_id, "conversation_id") send_activity( proactive_reference(conversation_id, service_url:), activity_or_text ) end |
#process_function(name, data, env: {}) ⇒ Object
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/teams/app.rb', line 191 def process_function(name, data, env: {}) handler = @functions[name.to_s] unless handler return Response.new(status: 404, body: { "detail" => "function #{name.inspect} is not registered" }) end context, error = build_function_context(name.to_s, data, env) unless context logger&.warn("Rejected function call #{name.inspect}: #{error}") return Response.new(status: 401, body: { "detail" => error }) end result = handler.call(context) body = result.respond_to?(:to_h) ? result.to_h : result # Function responses always carry valid JSON: callers fetch and parse # them, so a nil handler return becomes an empty object rather than an # empty body with a JSON content type. Response.new(status: 200, body: body.nil? ? {} : body) end |
#process_inbound(payload, env: {}) ⇒ Object
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/teams/app.rb', line 264 def process_inbound(payload, env: {}) activity = Activity.new(payload) validate_inbound!(env, activity) conversation_reference = Api::ConversationReference.from_activity(activity) context = ActivityContext.new( app: self, activity:, conversation_reference:, stream: HttpStream.new(app: self, conversation_reference:) ) result = run_handlers(context) context.stream.close return result if result.is_a?(Response) body = activity.invoke? ? invoke_response_body(result) : nil Response.new(status: 200, body:) rescue StreamCancelledError Response.new(status: 200) end |
#reply(conversation_id, activity_id_or_activity, activity_or_text = nil, service_url: nil) ⇒ Object
Proactive threaded replies use a ";messageid=" conversation ID like the TypeScript and Python SDKs; the service decides whether threading applies.
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'lib/teams/app.rb', line 300 def reply(conversation_id, activity_id_or_activity, activity_or_text = nil, service_url: nil) assert_string!(conversation_id, "conversation_id") if activity_or_text assert_string!(activity_id_or_activity, "activity_id") post( Teams.to_threaded_conversation_id(conversation_id, activity_id_or_activity), activity_or_text, service_url: ) else post(conversation_id, activity_id_or_activity, service_url:) end end |
#send_activity(conversation_reference, activity_or_text) ⇒ Object
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/teams/app.rb', line 325 def send_activity(conversation_reference, activity_or_text) activity = activity_for_reference(conversation_reference, activity_or_text) targeted = targeted_activity?(activity) if targeted && conversation_reference.conversation.conversation_type == "personal" raise ArgumentError, "Targeted messages are not supported in 1:1 (personal) chats." end id = activity_id(activity) conversation_id = conversation_reference.conversation_id service_url = conversation_reference.service_url response = if id && targeted api.conversations.update_targeted_activity(conversation_id, id, activity, service_url:) elsif id api.conversations.update_activity(conversation_id, id, activity, service_url:) elsif targeted api.conversations.create_targeted_activity(conversation_id, activity, service_url:) else api.conversations.create_activity(conversation_id, activity, service_url:) end Api::SentActivity.merge(activity, response) end |
#update(conversation_id, activity_id, activity_or_text, service_url: nil) ⇒ Object
Sugar over post: the SDKs update by sending an activity that already carries an id, and post does exactly that. See AGENTS.md.
318 319 320 321 322 323 |
# File 'lib/teams/app.rb', line 318 def update(conversation_id, activity_id, activity_or_text, service_url: nil) assert_string!(conversation_id, "conversation_id") assert_string!(activity_id, "activity_id") post(conversation_id, activity_with_id(activity_id, activity_or_text), service_url:) end |
#use(&block) ⇒ Object
81 82 83 84 |
# File 'lib/teams/app.rb', line 81 def use(&block) @router.use(&block) self end |