Search code examples
node.jsazurebotframeworkmicrosoft-teams

Microsoft Bot - Node SDK: How to post to a public channel *without replying to a prev. activity*


Context & theory

According to the docs and examples, you must save the reference to a conversation in order to restore it on demand (for instance when the server receives an HTTP request) and send a reactive message to a public channel.

So I'm doing:

  1. Any user mentions the bot on a channel (at #CorpChannel for example)
  2. Bot stores (in particular I'm using Azure Cosmos db) the reference of the conversation(storage.write(storeItems))
  3. [later] Bot receives an HTTP request that means: "send 'hi there' to #CorpChannel"
  4. Bot restores the conversation reference and uses it for creating a TurnContext in order to call sendActivity()

Problem

The activity 'hi there' is replying to the original mention to my bot instead of starting a new thread/conversation over that channel. I want to start a new fresh conversation at #CorpChannel

Visually:

Jane Doe: --------------
| @MyBot        09:00AM |
------------------------

Jhon Doe: --------------
| what ever     10:00AM |
------------------------

HTTP request: "send 'hi there' to #CorpChannel"

Jhon Doe: --------------
| whatever      10:00AM |
------------------------

Jane Doe: --------------
| @MyBot        09:00AM |
------------------------
   |> MyBot: -----------
   |  Hi there  11:00AM |
    --------------------

What I've tried

This is the code where I'm sending the activity on demand

server.post("/api/notify", async (req, res) => {
  const channel = req.body.channel;
  const message = req.body.message;
  const conversation = await bot.loadChannelConversation(channel);

  if (!conversation) { /* ... */ }

  await adapter.continueConversation(conversation, async (context) => {
      await context.sendActivity(message);
  });

  return res.send({ notified: { channel, message } });
});

this is the code where I'm going to db

// (storage) is in the scope
const loadChannelConversation = async (key) => {
  try {
    const storeItems = await storage.read(['channels']);
    const channels = storeItems['channels'] || {};
    return channels[key] || null;
  } catch (err) {
    console.error(err);
    return undefined;
  }
};

how can I post a new message instead of replying to the original thread?

==== EDIT ====

I've tried also to use createConversation() method from the SDK, but as it says in the documentation:

The Bot Connector service supports the creating of group conversations; however, this method and most channels only support initiating a direct message (non-group) conversation.

It starts a new conversation with the original user that posted the first message in private


Solution

  • A conversation ID in a Teams channel thread looks like this: 19:[email protected];messageid=12345

    All you need to do to start a new thread is to send a message to that conversation ID with the "messageid" portion removed.

    You can remove the messageid portion like this:

    function getRootConversationId(turnContext) {
        const threadId = turnContext.activity.conversation.id;
        const messageIdIndex = threadId.indexOf(";messageid=");
        return messageIdIndex > 0 ? threadId.slice(0, messageIdIndex) : threadId;
    }
    

    If you want to send a message through the turn context to retain its middleware pipeline, you can modify the incoming activity's conversation ID directly. Since this would affect the turn context for the rest of the turn, it might be a good idea to change the conversation ID back afterwards.

    const conversationId = getRootConversationId(turnContext);
    
    // Send through the turn context
    
    const conversation = turnContext.activity.conversation;
    const threadId = conversation.id;
    conversation.id = conversationId;
    
    await turnContext.sendActivity("New thread (through the turn context)");
    
    conversation.id = threadId;  // restore conversation ID
    

    You can also send a message through a connector client directly if you don't want to worry about the turn context.

    const conversationId = getRootConversationId(turnContext);
    
    // Send through a connector client
    
    const client = turnContext.turnState.get(turnContext.adapter.ConnectorClientKey);
    
    await client.conversations.sendToConversation(
        conversationId,
        MessageFactory.text("New thread (through a connector client)"));