Class: Teams::App
- Inherits:
-
Object
- Object
- Teams::App
- Defined in:
- lib/teams/app.rb
Constant Summary collapse
- DEFAULT_MESSAGING_ENDPOINT =
"/api/messages"
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, skip_auth: false, messaging_endpoint: DEFAULT_MESSAGING_ENDPOINT, default_connection_name: "graph") ⇒ App
constructor
A new instance of App.
- #initialize! ⇒ Object
- #on(name, &block) ⇒ Object
-
#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, skip_auth: false, messaging_endpoint: DEFAULT_MESSAGING_ENDPOINT, default_connection_name: "graph") ⇒ App
Returns a new instance of App.
15 16 17 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 |
# File 'lib/teams/app.rb', line 15 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, skip_auth: false, 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 @skip_auth = 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.
9 10 11 |
# File 'lib/teams/app.rb', line 9 def api @api end |
#default_connection_name ⇒ Object (readonly)
Returns the value of attribute default_connection_name.
9 10 11 |
# File 'lib/teams/app.rb', line 9 def default_connection_name @default_connection_name end |
#graph ⇒ Object (readonly)
Returns the value of attribute graph.
9 10 11 |
# File 'lib/teams/app.rb', line 9 def graph @graph end |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
9 10 11 |
# File 'lib/teams/app.rb', line 9 def logger @logger end |
#messaging_endpoint ⇒ Object (readonly)
Returns the value of attribute messaging_endpoint.
9 10 11 |
# File 'lib/teams/app.rb', line 9 def messaging_endpoint @messaging_endpoint end |
#storage ⇒ Object (readonly)
Returns the value of attribute storage.
9 10 11 |
# File 'lib/teams/app.rb', line 9 def storage @storage end |
Instance Method Details
#client_id ⇒ Object
11 12 13 |
# File 'lib/teams/app.rb', line 11 def client_id @token_manager.client_id end |
#emit_error(error, activity: nil) ⇒ Object
161 162 163 |
# File 'lib/teams/app.rb', line 161 def emit_error(error, activity: nil) @error_handlers.each { |handler| handler.call(error, activity) } end |
#emit_sign_in(context, token_response) ⇒ Object
157 158 159 |
# File 'lib/teams/app.rb', line 157 def emit_sign_in(context, token_response) @sign_in_handlers.each { |handler| handler.call(context, token_response) } end |
#initialize! ⇒ Object
70 71 72 73 |
# File 'lib/teams/app.rb', line 70 def initialize! @token_manager.bot_token true end |
#on(name, &block) ⇒ Object
80 81 82 83 |
# File 'lib/teams/app.rb', line 80 def on(name, &block) @router.on(name, &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.
99 100 101 102 |
# File 'lib/teams/app.rb', line 99 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.
106 107 108 109 |
# File 'lib/teams/app.rb', line 106 def on_dialog_submit(action = nil, &block) @router.on_dialog_submit(action, &block) self end |
#on_edit_message(&block) ⇒ Object
225 226 227 228 |
# File 'lib/teams/app.rb', line 225 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.
152 153 154 155 |
# File 'lib/teams/app.rb', line 152 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.
170 171 172 173 174 175 |
# File 'lib/teams/app.rb', line 170 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
204 205 206 207 |
# File 'lib/teams/app.rb', line 204 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).
199 200 201 202 |
# File 'lib/teams/app.rb', line 199 def on_meeting_start(&block) @router.on_meeting_start(&block) self end |
#on_message(pattern = nil, &block) ⇒ Object
85 86 87 88 |
# File 'lib/teams/app.rb', line 85 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).
133 134 135 136 |
# File 'lib/teams/app.rb', line 133 def (&block) @router.(&block) self end |
#on_message_submit_feedback(&block) ⇒ Object
138 139 140 141 |
# File 'lib/teams/app.rb', line 138 def (&block) @router.(&block) self end |
#on_message_update(&block) ⇒ Object
220 221 222 223 |
# File 'lib/teams/app.rb', line 220 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.
145 146 147 148 |
# File 'lib/teams/app.rb', line 145 def on_sign_in(&block) @sign_in_handlers << block self end |
#on_signin_failure(&block) ⇒ Object
126 127 128 129 |
# File 'lib/teams/app.rb', line 126 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.
116 117 118 119 |
# File 'lib/teams/app.rb', line 116 def on_signin_token_exchange(&block) @router.on_signin_token_exchange(&block) self end |
#on_signin_verify_state(&block) ⇒ Object
121 122 123 124 |
# File 'lib/teams/app.rb', line 121 def on_signin_verify_state(&block) @router.on_signin_verify_state(&block) self end |
#on_suggested_action_submit(&block) ⇒ Object
90 91 92 93 |
# File 'lib/teams/app.rb', line 90 def on_suggested_action_submit(&block) @router.on("suggested-action.submit", &block) self end |
#on_undelete_message(&block) ⇒ Object
230 231 232 233 |
# File 'lib/teams/app.rb', line 230 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.
260 261 262 263 264 265 266 267 |
# File 'lib/teams/app.rb', line 260 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
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/teams/app.rb', line 177 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
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/teams/app.rb', line 235 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.
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/teams/app.rb', line 271 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
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/teams/app.rb', line 296 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.
289 290 291 292 293 294 |
# File 'lib/teams/app.rb', line 289 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
75 76 77 78 |
# File 'lib/teams/app.rb', line 75 def use(&block) @router.use(&block) self end |