Class: Teams::ActivityContext

Inherits:
Object
  • Object
show all
Defined in:
lib/teams/activity_context.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app:, activity:, conversation_reference:, extra: {}, stream: nil) ⇒ ActivityContext

Returns a new instance of ActivityContext.



10
11
12
13
14
15
16
# File 'lib/teams/activity_context.rb', line 10

def initialize(app:, activity:, conversation_reference:, extra: {}, stream: nil)
  @app = app
  @activity = activity
  @conversation_reference = conversation_reference
  @extra = extra
  @stream = stream
end

Instance Attribute Details

#activityObject (readonly)

Returns the value of attribute activity.



8
9
10
# File 'lib/teams/activity_context.rb', line 8

def activity
  @activity
end

#appObject (readonly)

Returns the value of attribute app.



8
9
10
# File 'lib/teams/activity_context.rb', line 8

def app
  @app
end

#conversation_referenceObject (readonly)

Returns the value of attribute conversation_reference.



8
9
10
# File 'lib/teams/activity_context.rb', line 8

def conversation_reference
  @conversation_reference
end

#extraObject (readonly)

Returns the value of attribute extra.



8
9
10
# File 'lib/teams/activity_context.rb', line 8

def extra
  @extra
end

#streamObject (readonly)

Returns the value of attribute stream.



8
9
10
# File 'lib/teams/activity_context.rb', line 8

def stream
  @stream
end

Instance Method Details

#apiObject



34
35
36
# File 'lib/teams/activity_context.rb', line 34

def api
  app.api
end

#app_graphObject

Microsoft Graph client with the app's own identity (app-only tokens).



71
72
73
# File 'lib/teams/activity_context.rb', line 71

def app_graph
  app.graph
end

#logObject



26
27
28
# File 'lib/teams/activity_context.rb', line 26

def log
  app.logger
end

#post(activity_or_text) ⇒ 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.



41
42
43
# File 'lib/teams/activity_context.rb', line 41

def post(activity_or_text)
  app.send_activity(conversation_reference, apply_targeted_defaults(activity_or_text))
end

#quote(message_id, activity_or_text) ⇒ Object

Quoted replies are plain sends carrying the quote placeholder and entity, matching the TypeScript and Python SDKs; no replyToId is set.



56
57
58
# File 'lib/teams/activity_context.rb', line 56

def quote(message_id, activity_or_text)
  post(quoted_activity(message_id, activity_or_text))
end

#refObject



18
19
20
# File 'lib/teams/activity_context.rb', line 18

def ref
  conversation_reference
end

#referenceObject



22
23
24
# File 'lib/teams/activity_context.rb', line 22

def reference
  conversation_reference
end

#reply(activity_or_text) ⇒ Object



45
46
47
48
49
50
51
52
# File 'lib/teams/activity_context.rb', line 45

def reply(activity_or_text)
  return post(activity_or_text) unless activity.id
  # Replies to targeted inbound messages are targeted sends: no quoted
  # reply and no replyToId, matching the TypeScript and Python SDKs.
  return post(activity_or_text) if targeted_reply?(activity_or_text)

  quote(activity.id, activity_or_text)
end

#sign_in(connection_name: nil, oauth_card_text: "Please Sign In...", sign_in_button_text: "Sign In") ⇒ Object

Starts the user sign-in flow: returns the token if the user is already signed in, otherwise sends an OAuth card and returns nil. In group conversations the card goes to a 1:1 conversation with the user (group OAuth is not supported by Teams), like the TypeScript/Python SDKs.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/teams/activity_context.rb', line 98

def (connection_name: nil, oauth_card_text: "Please Sign In...", sign_in_button_text: "Sign In")
  connection_name ||= app.default_connection_name

  begin
    response = api.users.get_token(
      user_id: activity.from.id,
      connection_name:,
      channel_id: activity.channel_id
    )
    return response.token if response.token && !response.token.to_s.empty?
  rescue HttpError
    # No token yet; continue with the OAuth card flow.
  end

  conversation_id = conversation_reference.conversation_id
  if activity.conversation.is_group
    one_on_one = api.conversations.create(
      members: [activity.from.to_h],
      tenant_id: activity.conversation.tenant_id
    )
    conversation_id = one_on_one.id
    # Deliberately posts the plain text notice into the group (matching
    # TypeScript and Python): group chats don't support SSO, so the card
    # itself goes to the 1:1 below while the group sees only the notice.
    post(oauth_card_text)
  end

  state = Base64.strict_encode64(JSON.generate(
    "connectionName" => connection_name,
    "conversation" => conversation_reference.to_h,
    "msAppId" => app.client_id
  ))
  resource = api.bots..get_resource(state:)

  card = {
    "text" => oauth_card_text,
    "connectionName" => connection_name,
    "tokenExchangeResource" => resource.token_exchange_resource&.to_h,
    "tokenPostResource" => resource.token_post_resource&.to_h,
    "buttons" => [
      { "type" => "signin", "title" => , "value" => resource. }
    ]
  }.compact

  payload = {
    "type" => "message",
    "recipient" => activity.from.to_h,
    "attachments" => [
      { "contentType" => "application/vnd.microsoft.card.oauth", "content" => card }
    ]
  }

  app.send_activity((conversation_id), payload)
  nil
end

#sign_out(connection_name: nil) ⇒ Object

Clears the user's token for the connection. Failures are logged, not raised, matching the TypeScript/Python SDKs.



156
157
158
159
160
161
162
163
164
165
166
# File 'lib/teams/activity_context.rb', line 156

def sign_out(connection_name: nil)
  api.users.sign_out(
    user_id: activity.from.id,
    connection_name: connection_name || app.default_connection_name,
    channel_id: activity.channel_id
  )
  nil
rescue HttpError => error
  log&.error("Failed to sign out user: #{error.message}")
  nil
end

#storageObject



30
31
32
# File 'lib/teams/activity_context.rb', line 30

def storage
  app.storage
end

#typing(text = nil) ⇒ Object



66
67
68
# File 'lib/teams/activity_context.rb', line 66

def typing(text = nil)
  post(Api::TypingActivity.new(text))
end

#update(activity_id, activity_or_text) ⇒ Object

Sugar over post: the SDKs update by sending an activity that already carries an id, and post does exactly that. See AGENTS.md.



62
63
64
# File 'lib/teams/activity_context.rb', line 62

def update(activity_id, activity_or_text)
  post(activity_with_id(activity_id, activity_or_text))
end

#user_graph(connection_name: nil) ⇒ Object

Microsoft Graph client with the signed-in user's token. Raises when the user is not signed in, like the Python SDK's user_graph.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/teams/activity_context.rb', line 77

def user_graph(connection_name: nil)
  connection = connection_name || app.default_connection_name
  @user_graph ||= {}
  @user_graph[connection] ||= begin
    response = api.users.get_token(
      user_id: activity.from.id,
      connection_name: connection,
      channel_id: activity.channel_id
    )
    raise Error, "User must be signed in to access the Graph client" if response.token.to_s.empty?

    Graph::Client.new(token: response.token, base_url_root: app.graph.base_url_root)
  rescue HttpError
    raise Error, "User must be signed in to access the Graph client"
  end
end