Showing iframe when youtube channel is live [duplicate] - javascript

I can't find any informations to check if a YouTube channel is actually streaming or not.
With Twitch you just need the channel name, and with the API you can check if there is a live or not.
I don't want to use OAuth, normally a public API key is enough. Like checking the videos of a channel I want to know if the channel is streaming.

You can do this by using search.list and specifying the channel ID, setting the type to video, and setting eventType to live.
For example, when I searched for:
https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=UCXswCcAMb5bvEUIDEzXFGYg&type=video&eventType=live&key=[API_KEY]
I got the following:
{
"kind": "youtube#searchListResponse",
"etag": "\"sGDdEsjSJ_SnACpEvVQ6MtTzkrI/gE5P_aKHWIIc6YSpRcOE57lf9oE\"",
"pageInfo": {
"totalResults": 1,
"resultsPerPage": 5
},
"items": [
{
"kind": "youtube#searchResult",
"etag": "\"sGDdEsjSJ_SnACpEvVQ6MtTzkrI/H-6Tm7-JewZC0-CW4ALwOiq9wjs\"",
"id": {
"kind": "youtube#video",
"videoId": "W4HL6h-ZSws"
},
"snippet": {
"publishedAt": "2015-09-08T11:46:23.000Z",
"channelId": "UCXswCcAMb5bvEUIDEzXFGYg",
"title": "Borussia Dortmund vs St. Pauli 1-0 Live Stream",
"description": "Borussia Dortmund vs St. Pauli Live Stream Friendly Match.",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/W4HL6h-ZSws/default.jpg"
},
"medium": {
"url": "https://i.ytimg.com/vi/W4HL6h-ZSws/mqdefault.jpg"
},
"high": {
"url": "https://i.ytimg.com/vi/W4HL6h-ZSws/hqdefault.jpg"
}
},
"channelTitle": "",
"liveBroadcastContent": "live"
}
}
]
}

The search-method (https://www.googleapis.com/youtube/v3/search) is awfully expensive to use though. It costs 100 quota units (https://developers.google.com/youtube/v3/determine_quota_cost) out of the 10,000 you have by default.
This means you only get 100 requests per day which is terrible.
You could request an increase in the quota but that seems like brute forcing the the problem.
Is there really no other simpler method?

I know this is old, but I figured it out myself with PHP.
$API_KEY = 'your api3 key';
$ChannelID = 'the users channel id';
$channelInfo = 'https://www.googleapis.com/youtube/v3/search?part=snippet&channelId='.$ChannelID.'&type=video&eventType=live&key='.$API_KEY;
$extractInfo = file_get_contents($channelInfo);
$extractInfo = str_replace('},]',"}]",$extractInfo);
$showInfo = json_decode($extractInfo, true);
if($showInfo['pageInfo']['totalResults'] === 0){
echo 'Users channel is Offline';
} else {
echo 'Users channel is LIVE!';
}

Guys I found better way to do this. Yes, it requires you to make GET requests to a YouTube page and parse HTML, but it will work with newer versions + works with consent + works with captcha (most likely, 90%)
All you need to do is make a request to https://youtube.com/channel/[CHANNELID]/live and check the href attribute of the <link rel="canonical" /> tag.
For example,
<link rel="canonical" href="https://www.youtube.com/channel/UC4cueEAH9Oq94E1ynBiVJhw">
means there is no livestream, while
<link rel="canonical" href="https://www.youtube.com/watch?v=SR9w_ofpqkU">
means there is a stream, and you can even fetch its data by videoid.
Since canonical URL is very important for SEO and redirect does not work in GET or HEAD requests anymore, I recommend using my method.
Also here is the simple script I use:
import { parse } from 'node-html-parser'
import fetch from 'node-fetch'
const channelID = process.argv[2] // process.argv is array of arguments passed in console
const response = await fetch(`https://youtube.com/channel/${channelID}/live`)
const text = await response.text()
const html = parse(text)
const canonicalURLTag = html.querySelector('link[rel=canonical]')
const canonicalURL = canonicalURLTag.getAttribute('href')
const isStreaming = canonicalURL.includes('/watch?v=')
console.log(isStreaming)
Then run npm init -y && npm i node-html-parser node-fetch to create project in working directory and install dependencies
Then run node isStreaming.js UC4cueEAH9Oq94E1ynBiVJhw and it will print true/false (400-600 ms per one execution)
It does require you to depend on node-html-parser and node-fetch, but you can make requests with the built-in HTTP library (which sucks) and rewrite this to use regex. (Do not parse HTML with regex.)

I was also struggling with API limits. The most reliable and cheapest way I've found was simply a HEAD request to https://www.youtube.com/channel/CHANNEL_ID/live. If the channel is live then it will auto load the stream. If not then it will load the channels videos feed. You can simply check the Content-Length header size to determine which. If live the size is almost 2x when NOT live.
And depending on your region you might need to accept the cookies consent page. Just send your request with cookies={ "CONSENT": "YES+cb.20210420-15-p1.en-GB+FX+634" }.

if you point streamlink at a https://www.youtube.com/channel/CHANNEL_ID/live link, it will tell you if it is live or not
e.g. lofi beats is usually live,
$ streamlink "https://www.youtube.com/channel/UCSJ4gkVC6NrvII8umztf0Ow/live"
[cli][info] Found matching plugin youtube for URL https://www.youtube.com/channel/UCSJ4gkVC6NrvII8umztf0Ow/live
Available streams: 144p (worst), 240p, 360p, 480p, 720p, 1080p (best)
whereas MKBHD is not
$ streamlink "https://www.youtube.com/c/mkbhd/live"
[cli][info] Found matching plugin youtube for URL https://www.youtube.com/c/mkbhd/live
error: Could not find a video on this page

The easisest way that I have found to this has been scraping the site. This can be done by finding this:
<link rel="canonical" href="linkToActualYTLiveVideoPage">
as in Vitya's answer.
This is my simple Python code using bs4:
import requests
from bs4 import BeautifulSoup
def is_liveYT():
channel_url = "https://www.youtube.com/c/LofiGirl/live"
page = requests.get(channel_url, cookies={'CONSENT': 'YES+42'})
soup = BeautifulSoup(page.content, "html.parser")
live = soup.find("link", {"rel": "canonical"})
if live:
print("Streaming")
else:
print("Not Streaming")
if __name__ == "__main__":
is_liveYT()
It is pretty weird, honestly, that YouTube doesn't have a simple way to do this through the API, although this is probably easier.

I found the answer by #VityaSchel to be quite useful, but it doesn't distinguish between channels which have a live broadcast scheduled, and those which are broadcasting live now.
To distinguish between scheduled and live, I have extended his code to access the YouTube Data API to find the live streaming details:
import { parse } from 'node-html-parser'
import fetch from 'node-fetch'
const youtubeAPIkey = 'YOUR_YOUTUBE_API_KEY'
const youtubeURLbase = 'https://www.googleapis.com/youtube/v3/videos?key=' + youtubeAPIkey + '&part=liveStreamingDetails,snippet&id='
const c = {cid: process.argv[2]} // process.argv is array of arguments passed in console
const response = await fetch(`https://youtube.com/channel/${c.cid}/live`)
const text = await response.text()
const html = parse(text)
const canonicalURLTag = html.querySelector('link[rel=canonical]')
const canonicalURL = canonicalURLTag.getAttribute('href')
c.live = false
c.configured = canonicalURL.includes('/watch?v=')
if (!c.configured) process.exit()
c.vid = canonicalURL.match(/(?<==).*/)[0]
const data = await fetch(youtubeURLbase + c.vid).then(response => response.json())
if (data.error) {
console.error(data)
process.exit(1)
}
const i = data.items.pop() // pop() grabs the last item
c.title = i.snippet.title
c.thumbnail = i.snippet.thumbnails.standard.url
c.scheduledStartTime = i.liveStreamingDetails.scheduledStartTime
c.live = i.liveStreamingDetails.hasOwnProperty('actualStartTime')
if (c.live) {
c.actualStartTime = i.liveStreamingDetails.actualStartTime
}
console.log(c)
Sample output from the above:
% node index.js UCNlfGuzOAKM1sycPuM_QTHg
{
cid: 'UCNlfGuzOAKM1sycPuM_QTHg',
live: true,
configured: true,
vid: '8yRgYiNH39E',
title: '🔴 Deep Focus 24/7 - Ambient Music For Studying, Concentration, Work And Meditation',
thumbnail: 'https://i.ytimg.com/vi/8yRgYiNH39E/sddefault_live.jpg',
scheduledStartTime: '2022-05-23T01:25:00Z',
actualStartTime: '2022-05-23T01:30:22Z'
}

Every YouTube channel as a permanent livestream, even if the channel is currently not actively livestreaming. In the liveStream resource, you can find a boolean named isDefaultStream.
But where can we get this video (livestream) id? Go to https://www.youtube.com/user/CHANNEL_ID/live, right click on the stream and copy the video URL.
You can now make a GET request to
https://youtube.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=[VIDEO_ID]&key=[API_KEY] (this request has a quota cost of 1 unit, see here)
This will be the result if the stream is currently active/online.
{
"kind": "",
"etag": "",
"items": [
{
"kind": "",
"etag": "",
"id": "",
"liveStreamingDetails": {
"actualStartTime": "",
"scheduledStartTime": "",
"concurrentViewers": "",
"activeLiveChatId": ""
}
}
],
"pageInfo": {
"totalResults": 1,
"resultsPerPage": 1
}
}
If the stream is currently offline, the property concurrentViewers will not exist. In other words, the only difference between an online and offline livestream is that concurrentViewers is present or not present. With this information, you can check, if the channel is currently streaming or not (at least for his default stream).

I found youtube API to be very restrictive given the cost of search operation. Web scraping with aiohttp and beautifulsoup was not an option since the better indicators required javascript support. Hence I turned to selenium. I looked for the css selector
#info-text
and then search for the string Started streaming or with watching now in it.
You can run a small API on heroku with flask as well.

Related

Pushing and pulling JSON data with a filter from API

I am working on a REACT JS project in an attempt to create a small Todo List app.
I have my data in a JSON file, currently hosted on jsonbin.io, in a format that looks like this...
{
"moduleAccess": {
"tasks": [
{
"email": "campbell#yahoo.com",
"id": 0,
"task_name": "Call mom",
"due_date": 44875,
"completed": true
},
{
"email": "palsner593#gmail.com",
"id": 1,
"task_name": "Buy eggs",
"due_date": 44880,
"completed": false
},
{
"email": "rob#gmail.com",
"id": 2,
"task_name": "Go to dog park",
"due_date": 44879,
"completed": false
}
]
}
}
Currently, I fetch the data using jsonbin.io's API. The data is brought into a variable called Tasks. If a user updates a specific to-do item, deletes a to-do item, or creates a new one, all those changes are put back into the Tasks variable. I can then send push those tasks to the server.
What I explained above works fine; however, the caveat is that I would like to allow multiple users to log in and then pull only the Todo items that pertain to their respective email.
Say, campbell#yahoo.com is logged in to my app. In this case, in my fetch pull request, I can specify that I would only like records with campbell#yahoo.com
async function loadData() {
const newPath = '$..tasks[?(#.email==' + campbell#yahoo.com + ')]';
console.log(newPath);
const url = 'https://api.jsonbin.io/v3/b/*binid*?meta=false'
const response = await
fetch(url, {
method: "GET",
headers: {
"X-Master-Key": key,
"X-JSON-Path": newPath
}
});
const data = await response.json();
setTasks([...data]); //or whatever
console.log(tasks);
}
This concept works as well. However, when pushing my task data back to a server after a user has made changes, I encounter an issue. The API I am using does not seem to allow parameters for specifying the JSON path upon PUSH. JSON-PATH is only allowed for a pull request. So when I push data to the server, it seems as if all JSON data will be overwritten, regardless of the user.
Does anybody have an alternative way to push/pull user-specific data? I am sorry if the detail I have provided is unnecessary. Not sure what the easiest way to approach this problem is for a react app.
Any help is appreciated. Thanks!
I did a little research in jsonbin.io API and came up with a solution that might work.
So I'm not really sure that this will work, but still.
When creating a new bin, you can add it to some collection using X-Collection-Id. So you might be able to make next flow:
When user register, create a separate bin for tasks for this user
Add user with bin id to some users collection where you will have all your users
When user auth, get his bin id using filters that you used in your code and store it for future use somewhere in your app.
After this you will be able to fetch users tasks by that bin id and modify it, cause now it will be a separate bin for each user and you can simply override all of its content.
Hope this works.

How can I make a script in python to reload an app in qlik cloud

At this moment I'm trying to reload a Qlik app through a python script, but I had a few problems, I'm gonna explain the things I already tested, but if someone knows how can I solve that, I will appreciate it a lot
The first thing I tried, was the "reloads API" from qlik, to do this you have to send a request with the app id, and with the parameter "Partial" true or false, but even more important, you have to send with this request a JSON web token, so I was searching how to obtain the jwt of qlik and I found this page: "https://qlik.dev/tutorials/create-signed-tokens-for-jwt-authorization", I created all as the page said, and finally I make this code in javascript to test, but this doesn't work:
const fs = require('fs');
const uid = require('uid-safe');
const jwt = require('jsonwebtoken');
const https = require('https')
const payload = {
jti: uid.sync(32), // 32 bytes random string
sub: '(id of my user that appears in assignment users)',
subType: 'user',
name: '(Name of my user)',
email: '(email of my user)',
email_verified: true,
};
const privateKey = fs.readFileSync("path/certificate.pem");
// I don't know the meaning of that 'kid and issuer have to match with the IDP config'
// audience has to be qlik.API/jwt-login-session
const signingOptions = {
keyid: I put = 'my-custom-jwt',
algorithm: I put = 'RS256',
issuer: '(hostname)',
audience: I put = 'qlik.api/login/jwt-session',
};
const myToken = jwt.sign(payload, privateKey, signingOptions);
const qlikUrl = "(hostname)"
const data = JSON.stringify({"appId": "(appId)", "partial": true})
const options = {
hostname: qlikUrl,
port: 443,
path: '/api/v1/reloads',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer '+ myToken
}
}
https.request(options)
req.write(data)
The second thing I did was, try to use the "qsAPI" library of python to connect to my qlik and reload an app specifying their id by a method of this library, but I don't understand what I have to put here, and in the documentation, doesn't say nothing about what's the meaning of the parameters, except ('hostname'). The problem is in the method to do the connection because I have to do this:
qrs = qsAPI.QRS(proxy='hostname', user=('yor_domain', 'username', 'password'))
I say that because if I go to the python IDE, this show which parameters I have to put, and these are different, now I have to put this:
qrs = qsAPI.QRS(proxy='hostname', user=('userDirectory', 'userId', 'password'))
I don't know where I can find the user directory because I don't know
what it is.
The user id (I'm guessing it's the user id that appears in "mapping
users").
The "password", is no problem
But still have an opportunity, because I can connect to Qlik through python if I find where I can download the certificate authentication of a user, but I don't know where is it in qlik cloud.
Can someone help me, please
I havent tried with Python (only with JS/TS) but the approach should be the same.
Couple of things:
imo instead of web token you can try with API Key (for a start). Managing API keys -> Generating an API key from the hub.
the second point is that qsAPI seems to be for Qlik Sense Enterprise on Windows and not for SaaS.
the only Python lib dedicated for Qlik SaaS (that im aware of) is qsaas. Havent used it myself ... just found it on GH
and a bit of a warning regarding the partial: true. Please make sure that you really have to use partial reload. Partial reloads have a specific use case and be careful when using them ... just saying :)
I finally have the solution of that as Stefan Stoichev said before publishing this post, the qsApi seems to be a python library for Qlik Sense Enterprise on Windows, and the correct library for the Qlik Cloud is qsaas, but I'm going to explain every step because I don't want that any person of this world suffer this.
First of all, you have to create an API KEY, in Qlik Cloud, IMPORTANT, you have to save the api_key code that appears in a green text box when you created successfully your API KEY, save this as your dear friend because you will need this code in the future
Subsequently, you have to create a new python code as this:
from qsaas.qsaas import Tenant
import JSON
api_key = <API_KEY>
q = Tenant(api_key=api_key, tenant=<hostname>,
tenant_id=<tenant_id>)
q.post('reloads', json.dumps({'appId': 'dbf3e4ce-c6b3-4190-876c-c443a8691fa6'})))
Don't worry my dear friend if you don't know where is it, the 'hostname' and the 'tenant_id' are in qlik cloud, here is a little tutorial for you:
First login to your qlik
Then click on your profile photo
Click in about, and there you have these two data information

Cannot map variable from data stream to users identified response while developing voice app

I am currently developing a voice app with Google Actions where users are able to ask for information about items in a list that is provided through a file stream with Axios as shown in the following LINK. The data looks like this:
[
{
"Name": "Beam",
"Level": "2",
"Zone": "A",
"Location": "Beam is located on Level 2 in zone A",
"Responsible": "Contractor"
},
{
"Name": "Column",
"Level": "3",
"Zone": "A",
"Location": "Column is located on Level 3 in zone A",
"Responsible": "Kiewit"
},
{
"Name": "Window",
"Level": "2",
"Zone": "B",
"Location": "Window is located on Level 2 in zone B",
"Responsible": "Tech"
}
]
Here, it shows three items being a BEam, a Column, and a Window so the objective is that users ask about one of the items and the voice app will provide the other information such as Level, ZOne, Location, or Responsible to the user.
To complete this, I am using the web interface of Google Actions and using inline cloud functions as webhooks in Google Actions that looks like this:
const { conversation } = require('#assistant/conversation');
const functions = require('firebase-functions');
require('firebase-functions/lib/logger/compat'); // console.log compact
const axios = require('axios');
const app = conversation({debug: true});
app.handle('getItem', async conv => {
const data = await getItem();
const itemParam = conv.intent.params.Item.resolved;
// console.log(itemParam);
// conv.add(`This test to see if we are accessing the webhook for ${itemParam}`);
data.map(item => {
if (item.Name === itemParam);
conv.add(`These are the datails for ${itemParam}. It is located in zone ${item.Zone}, at level ${item.Level}`);
// conv.add(`This test to see if we are accessing the webhook for ${item.Name}`);
});
});
async function getItem() {
const res = await axios.get('https://sheetdb.io/api/v1/n3ol4hwmfsmqd');
console.log(res.data);
return res.data; // To use in your Action's response
}
exports.ActionsOnGoogleFulfillment = functions.https.onRequest(app);
When I check the console logs, I can see that I am retrieving the data in a single event as provided in the LINK. Also, the recognition of the item name is working in the app by defining a type within the app to be recognized based on type categories. And this information is being stored in ItemParam.
However, the main issue I have right now is to link both things together. I was trying to use a map function to match the itemParam and the Item.Name from the data stream. However, this is not working at al. The function I was trying to do is:
data.map(item => {
if (item.Name === itemParam);
conv.add(`These are the datails for ${itemParam}. It is located in zone ${item.Zone}, at level ${item.Level}`);
What I am trying to do here is when the function detects that the user ItemParam is matched to the Item in the stream, use the information from that stream item and add a phrase to the conversation that includes the ItemParam, and the other information about that same item.
Besides, the way this function is right now, also shoots this error:
cf-GPfYHj4HKDWGvHKWArq34w-name
Error: Error adding simple response: **Two simple responses already defined**
at addSimple (/workspace/node_modules/#assistant/conversation/dist/conversation/prompt/prompt.js:34:15)
at Prompt.add (/workspace/node_modules/#assistant/conversation/dist/conversation/prompt/prompt.js:108:17)
at ConversationV3.add (/workspace/node_modules/#assistant/conversation/dist/conversation/conv.js:102:21)
at data.map.item (/workspace/index.js:16:13)
at Array.map (<anonymous>) at app.handle (/workspace/index.js:14:8) at process._tickCallback (internal/process/next_tick.js:68:7)
I am honestly not that familiar with Javascript and I might be doing silly mistakes but I really cannot figure out this.
Any help will be much appreciated. Thank you
The error you are seeing is:
Error: Error adding simple response: Two simple responses already defined
Your action's response can only include two simple responses. Each response is rendered as a separate text bubble on a phone, for instance.
So it seems like the item.Name === itemParam is true multiple times and you end up creating too many responses.
Why does this happen? It comes from how your conditional is written:
data.map(item => {
if (item.Name === itemParam);
conv.add(`These are the datails for ${itemParam}. It is located in zone ${item.Zone}, at level ${item.Level}`);
});
You have correctly identified that the ; semicolon character denotes the end of a statement. However, this does not apply to if statements. Because the way it's written, you have this conditional and then conclude it before you actually run conv.add. This means that conv.add escapes your check and runs for every item. If you were to log the conv response, you'd see a bunch of text.
To fix it, keep in mind that a conditional needs to wrap the surrounding code. This is done with curly braces { & }.
data.map(item => {
if (item.Name === itemParam) {
conv.add(`These are the datails for ${itemParam}. It is located in zone ${item.Zone}, at level ${item.Level}`);
}
});
You can even see this in the map method, where the mapping logic surrounds your if-statement with curly braces. This shows that one is contained entirely within the other.
Thanks, Nick, I fixed my function based on your feedback and now I understand a little better about the map function. Another issue that I figured out along the way was that upper and lower case does matter to match the map function so I also had to modify the type to lowercase and add .toLowerCase() methods to variables.
Now my code is working with two variables Item and Item_ID so if the user asks about a generic item, it can get detailed by adding the ID of the item to the query question.
Now my code looks like this:
// From here, there are all the required libraries to be loaded
const { conversation } = require('#assistant/conversation'); // This the app coversation
const functions = require('firebase-functions'); //These are the firebase functions
require('firebase-functions/lib/logger/compat'); // console.log compact
const axios = require('axios'); // This is axios to retrieve the data stream
// To here, there all the required libraries to be loaded
const app = conversation({debug: true}); // This instantiate the conversation
/* This function retrieve the data from the file stream */
async function getItem() {
const res = await axios.get('https://sheetdb.io/api/v1/n3ol4hwmfsmqd');
return res.data; // To use in your Action's response
}
/* This is the fuction to match user's responses and data stream*/
app.handle('getItem', async conv => { //getItem is the weekhook name used in Google Actions, conv is the conversation
const data = await getItem(); // Here the data stream is retrieved and send to the data variable
// console.log(data);
const itemParam = conv.intent.params.Item.resolved; // This is the user's response, in other words, what item the user's want to know from the data.
const itemIDParam = conv.intent.params.Item_ID.resolved.replace(/\s/g, ''); //This is the user's response for item ID
const itemFromUser = itemParam + " " + itemIDParam;
console.log(itemParam);
console.log(itemIDParam);
console.log(itemFromUser);
// conv.add(`This test to see if we are accessing the webhook for ${itemParam}`); // This is to know if I was getting the correct item from the user. Currently this is working
// console.log(data);
data.map(item => { //Then, I am trying to map the data stream to recognize the data headers and identify items
// console.log(data);
// console.log(item);
if (item.Name.toLowerCase() === itemFromUser.toLowerCase()){
console.log(item);
conv.add(`These are the details for ${itemFromUser}. It is located in zone ${item.Zone}, at level ${item.Level}.`);
// console.log(conv);
// console.log(data);
}
else {
conv.add(`I am sorry. I could not find any information about that object. Please try with another construction object.`);
}
});
});
exports.ActionsOnGoogleFulfillment = functions.https.onRequest(app);
Now I can handle most of the questions except when something is not in the data stream which makes the app to show me this error:
"error": "Error adding simple response: Two simple responses already defined"
This is the same error as I was getting before and I am not sure how to fix it yet. I tried to implement an else statement for that condition as follows:
else {
conv.add(`I am sorry. I could not find any information about that object. Please try with another construction object.`);
}
But I am still getting same error.
I am still working on this.

Updating an activity in BotFramework v4 on Teams platform

I have a bot developed using the Bot Framework v4 using NodeJS and deployed on multiple channels in Teams. Is there a way we can update a message sent by the bot? I tried implementing the updateActivity() function in the BotFrameworkAdapter. However, it does not update the activity.
I have this card sent from the bot to a Teams channel. When someone clicks on the button, is there a way I can update the card or the message (disabling the button)?
The key to this is making sure that when you use updateActivity(), you use the right activity ID that is created by the Teams Channel. You also need to make sure that the updated activity gets all of the Teams data set to it.
In onTurn, capture outgoing activities so that you can easily save all of the necessary Teams Channel data:
public onTurn = async (turnContext: TurnContext) => {
turnContext.onSendActivities(async (ctx, activities, nextSend) => {
activities.forEach(async (activity) => {
if (activity.channelData.saveMe) {
this.savedActivity = activity;
}
});
return await nextSend();
});
Note: There might be another way to do this. I just found this to be the easiest, since you need to save all of the channelData, conversation info, and activity.id, at a minimum
How you store that activity to be used later is up to you. If you store it in the constructor, it will either be re-instantiated on every message (C# SDK) or any user has the ability to change it (JS SDK). You might consider writing custom storage.
Activities keep all channelData. By specifying a saveMe flag, we ensure we save the right activity
Instantiate some key variables:
const teamsChannel = '19:8d60061c3d10xxxxxxxxxxxxxxxx#thread.skype';
const serviceUrl = 'https://smba.trafficmanager.net/amer/';
Note: the easiest way to get these variables is to send a message from Teams to the bot while putting a breakpoint on the incoming activity
serviceUrl likely varies by geo region
Send the first activity and store the ID:
// This ensures that your bot can send to Teams
turnContext.activity.conversation.id = teamsChannel;
turnContext.activity.serviceUrl = serviceUrl;
MicrosoftAppCredentials.trustServiceUrl(serviceUrl);
// Add the saveMe flag
yourActivity.channelData = { saveMe: true };
const response = await turnContext.sendActivity(yourActivity);
this.activityToUpdateId = response.id;
How you store that ID to be used later is up to you. If you store it in the constructor, it will either be re-instantiated on every message (C# SDK) or any user has the ability to change it (JS SDK). You might consider writing custom storage.
Update your saved activity:
// New data
const card2 = CardFactory.adaptiveCard(adaptiveCard2);
// Set the saved activity.id and new activity data (an adaptiveCard, in this example)
this.savedActivity.id = this.activityToUpdateId;
this.savedActivity.attachments = [card2];
Send the update:
await turnContext.updateActivity(this.savedActivity);
Note: you can update the activity with anything. I swapped out entirely different Adaptive Cards
Before:
After:
I've tried this using the middleware but keep getting: "The bot is not part of the conversation roster". Question: My bot is updating a message that a user wrote, so do I need special permissions?
let ActivityID = context.activity.conversation.id.split("=")[1];
let updatedActivity: Partial<Activity> = {
"id": ActivityID,
"channelId": context.activity.channelId,
"channelData": context.activity.channelData,
"conversation":
{
"name": "",
"id": context.activity.conversation.id,
"isGroup": context.activity.conversation.isGroup,
"conversationType": context.activity.conversation.conversationType,
"tenantId": context.activity.conversation.tenantId
},
"type": "message",
"text": "",
"summary": "",
"attachments": [ attachment ]
} await context.updateActivity(updatedActivity);

Get count of unique values of properties from JSON API response

I have a JSON API served by a Ruby on Rails backend. One of the endpoints returns an array of objects structured like this
{
"title_slug": "16-gaijin-games-bittrip-beat-linux-tar-gz",
"platform": "Linux",
"format": ".tar.gz",
"title": "BIT.TRIP BEAT",
"bundle": "Humble Bundle for Android 3",
"unique_games": 9
},
{
"title_slug": "17-gaijin-games-bittrip-beat-linux-deb",
"platform": "Linux",
"format": ".deb",
"title": "BIT.TRIP BEAT",
"bundle": "Humble Bundle for Android 3",
"unique_games": 9
},
Because there are different types of downloads for a single title the "Title" is not unique across several objects. I would like a count of only unique titles.
I was thinking of doing it in Ruby on Rails in the model and just sending it in the JSON response but that does not work because it needs the whole array to count them, obviously. I am using angular on the front end so I am thinking it needs to be done in the controller. I also filter the response in a table and want updated numbers of the unique titles being displayed.
Here's a screenshot of the page this is going on to get better perspective. http://i.imgur.com/Iu1Xajf.png
Thank you very much,
Thomas Le
BTW, this is a site I am developing that is not going to be a public website. It is a database site that holds all the data on the bundles I have bought from IndieGala and HumbleBundle. I am not going to make these links available to the public. I am making it more functional than the bare minimum because it is an open source project that I have on GitHub that people can use themselves locally.
Just in case people were wondering why I have Humble Bundle stuff listed on the image.
http://jsfiddle.net/hy7rasp4/
Aggregate your data in an array indexed by the unique key, Then you get access to information on duplicates and count.
var i,
title,
uniqueResults= {};
for (i in results) {
title= results[i].title;
if (!uniqueResults[title]) {
uniqueResults[title]= [];
}
uniqueResults[title].push(results[i]);
}
Maybe it would be better to restructure your data at the same time, so you can also get those items easily later as well as a quick lookup for the number of titles, e.g. in JavaScript
// assuming arrayOfObjects
var objectOfTitles = {},
i;
for (i = 0; i < arrayOfObjects.length; ++i) {
if (!objectOfTitles.hasOwnProperty(arrayOfObjects[i].title)) {
objectOfTitles[arrayOfObjects[i].title] = [];
}
objectOfTitles[arrayOfObjects[i].title].push(arrayOfObjects[i]);
}
var numberOfTitles = Object.keys(objectOfTitles).length;
// then say you choose a title you want, and you can do
// objectOfTitles[chosenTitle] to get entries with just that title

Categories