I'm currently working on a Discord bot that tracks Steam prices and sends them to chat. I made this code for it:
setInterval(() => {
const currentDate = new Date();
var yourchannel = client.channels.cache.get('[CHANNEL ID]');
fetch('https://steamcommunity.com/market/priceoverview/?appid=730&market_hash_name=Operation%20Breakout%20Weapon%20Case¤cy=6', )
.then(res => res.text())
.then(text => yourchannel.send(`Breakout case price on ${currentDate.toLocaleDateString(`pl-PL`)} is ${text}`))
}, 1000 * 60 * 60 * 24);
});
I want my bot to send message "Breakout case price on [date] is [price]." For example "Breakout case price on 10.02.2021 is 5.94zł", but instead it sends this:
Breakout case price on 10.02.2021 is {"success":true,"lowest_price":"5,92zł","volume":"13,807","median_price":"6,01zł"}
It's because you send the whole object returned from fetch. You only need to send a property of that object (like json.lowest_price). You'll also need to make sure that you parse the body text as JSON. You'll need to use res.json() instead of res.text().
if (message.content === 'lowest_price') {
fetch(
'https://steamcommunity.com/market/priceoverview/?appid=730&market_hash_name=Operation%20Breakout%20Weapon%20Case¤cy=6',
)
.then((res) => res.json())
.then((json) =>
message.channel.send(
`Breakout case price on ${new Date().toLocaleDateString('pl-PL')} is ${
json.lowest_price
}`,
),
)
.catch((error) => {
console.log(error);
message.channel.send('Oops, there was an error fetching the price');
});
}
Check out the basics of objects on MDN.
Related
I created a command to get the price data of a given cryptocurrency. When I run my command, the embed has "undefined" for each price value.
Here is my code:
module.exports = {
name: 'crypto',
description: 'gets crypto data',
async execute (message, args) {
let cc = args.slice(0).join(' ');
const noArgs = new Discord.MessageEmbed()
.setTitle('Missing arguments')
.setColor((Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'))
.setDescription('You are missing some args (ex: -crypto bitcoin || -covid dogecoin)')
.setTimestamp(new Date().getTime())
if(!cc) return message.channel.send(noArgs);
await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${cc}&vs_currencies=usd%2Ceur%2Cgbp`)
.then (response => response.json)
.then(data => {
let usdprice = data.usd
let europrice = data.eur
let gbpprice = data.gbp
const embed = new Discord.MessageEmbed()
.setTitle(`**Current price of ${cc}:**`)
.setDescription('This data might be inaccurate.')
.setColor((Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'))
.setTimestamp(new Date().getTime())
.addField('**USD:**', usdprice, true)
.addField('**EURO:**', europrice, true)
.addField('**GBP:**', gbpprice, true)
message.channel.send(embed)
})
}
}
There is also no error in the console when I run the command.
You can use async/await if you've already got an async execute function. this way you can get rid of the then()s.
One of the problem is what #Mellet mentioned, you need to call response.json().
The other one is that the fetch data looks like this (if the coin id is "bitcoin"):
{
"bitcoin": {
"usd": 44833,
"eur": 36948,
"gbp": 32383
}
}
It means that it returns an object which contains another object with the coin id as the key. So if cc's value is bitcoin, you can get the USD price from data.bitcoin.usd. You can't and don't want to hardcode the coin id though, so you will need to add the key as a variable: data[coinId].usd.
I also added a helper function to check if the returned data is empty, so you can send an error message:
const isEmptyObject = (obj) => Object.keys(obj).length === 0;
module.exports = {
name: 'crypto',
description: 'gets crypto data',
async execute(message, args) {
let cc = args.slice(0).join(' ');
if (!cc) {
const noArgs = new Discord.MessageEmbed()
.setTitle('Missing arguments')
.setColor('RANDOM')
.setDescription(
'You are missing some args (ex: -crypto bitcoin || -covid dogecoin)',
)
.setTimestamp(new Date().getTime());
return message.channel.send(noArgs);
}
const response = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${cc}&vs_currencies=usd%2Ceur%2Cgbp`
);
const data = await response.json();
if (isEmptyObject(data)) {
return message.channel.send(
`No returned data from the API. Are you sure "${cc}" is a valid id?`,
);
}
let usdprice = data[cc].usd;
let europrice = data[cc].eur;
let gbpprice = data[cc].gbp;
const embed = new Discord.MessageEmbed()
.setTitle(`**Current price of ${cc}:**`)
.setDescription('This data might be inaccurate.')
.setColor('RANDOM')
.setTimestamp(new Date().getTime())
.addField('**USD:**', usdprice, true)
.addField('**EURO:**', europrice, true)
.addField('**GBP:**', gbpprice, true);
message.channel.send(embed);
},
};
PS: You can set the colour to 'RANDOM', so you don't need to use extra functions.
.then (response => response.json)
Should be
.then (response => response.json())
Trying to delete the original user command the bot reply after the bot is reacted to. For some reason, it only works every other time and when it does work it relays a DiscordAPIError: Unknown Message error
const isValidCommand = (message, cmdName) => message.content.toLowerCase().startsWith(PREFIX + cmdName)
client.on('message', function(message) {
if (message.author.bot) return;
if (isValidCommand(message, "banker"))
message.reply("Bankers are on the way to get your cash. Please be patient as Bankers are busy individuals as well. If there is a major delay you are welcome to use !HeadBanker. Bankers Please React With Money Bag When Sent/Sending!").then(r_msg =>
r_msg.react('💰'))
if (isValidCommand(message, "banker"))
message.channel.send("<#&717082379470110885> Banker Cash Needed")
.then(msg => {
msg.delete({
timeout: 0100
})
})
if (isValidCommand(message, "headbanker"))
message.reply("I see you have pinged Head Banker. If its between 14:00TCT and 5:00TCT you should get a response within a minute or two max. If no response in five minutes you have permission to ping again.").then(r_msg =>
r_msg.react('💰'))
if (isValidCommand(message, "headbanker"))
message.channel.send("<#&716843712092569640> Cash Needed")
.then(msg => {
msg.delete({
timeout: 0100
})
.catch(console.log);
})
client.on('messageReactionAdd', (reaction, user) => {
let limit = 2;
if (reaction.emoji.name == '💰' && reaction.count >= limit) reaction.message.delete()
.catch(console.log);
if (reaction.emoji.name == '💰' && reaction.count >= limit) message.delete()
.catch(console.log);
});
})
I think that's caused by the fact that you've declared your messageReactionAdd listener inside your message listener: that means that every time you get a new message your client will add a new handler, and so it will try to delete the same message multiple time, resulting in an "unknown message" error from the API.
Instead of adding a listener for the reactions on your client, you should try creating a reaction collector using either Message.awaitReactions() or Message.createReactionCollector(). Here's an example:
const isValidCommand = (message, cmdName) => message.content.toLowerCase().startsWith(PREFIX + cmdName)
client.on('message', function(message) {
if (message.author.bot) return;
if (isValidCommand(message, "banker")) { // Use single if statements where possible
message.reply("Bankers are on the way to get your cash. Please be patient as Bankers are busy individuals as well. If there is a major delay you are welcome to use !HeadBanker. Bankers Please React With Money Bag When Sent/Sending!").then(r_msg =>
r_msg.react('💰'))
message.channel.send("<#&717082379470110885> Banker Cash Needed")
.then(msg => {
msg.delete({ timeout: 0100 })
})
}
if (isValidCommand(message, "headbanker")) {
message.reply("I see you have pinged Head Banker. If its between 14:00TCT and 5:00TCT you should get a response within a minute or two max. If no response in five minutes you have permission to ping again.").then(r_msg =>
r_msg.react('💰'))
message.channel.send("<#&716843712092569640> Cash Needed")
.then(msg => {
msg.delete({ timeout: 0100 })
.catch(console.log);
})
}
message.awaitReactions(r => r.emoji.name == '💰', { max: 2 }).then(collected => {
if (collected.size >= 2)
message.delete()
})
})
Also, it's good practice not to repeat if statements, you should merge them wherever you can.
I want to delete all the messages posted by a particular user. So far I have:
async function clear() {
let botMessages;
botMessages = await message.channel.fetch(708292930925756447);
message.channel.bulkDelete(botMessages).then(() => {
message.channel.send("Cleared bot messages").then(msg => msg.delete({timeout: 3000}))
});
}
clear();
There seems to be an issue with passing botMessages to bulkDelete(), it wants an array or collection but apparantly botMessages isn't an array or collection.
How would I give botMessages to bulkDelete, or am I going about this totally wrong?
message.channel.fetch() fetches the channel the message is sent to, not the messages in that channel.
You need to fetch a certain amount of messages and filter it so you're only getting messages sent by your bot then pass them to bulkDelete()
message.channel.messages.fetch({
limit: 100 // Change `100` to however many messages you want to fetch
}).then((messages) => {
const botMessages = [];
messages.filter(m => m.author.id === BOT_ID_HERE).forEach(msg => botMessages.push(msg))
message.channel.bulkDelete(botMessages).then(() => {
message.channel.send("Cleared bot messages").then(msg => msg.delete({
timeout: 3000
}))
});
})
I came across a problem in a book which I can't seem to figure out. Unfortunately, I don't have a live link for it, so if anyone could help me with my approach to this theoretically, I'd really appreciate it.
The process:
I get from a fetch call an array of string codes (["abcde", "fghij", "klmno", "pqrst"]).
I want to make a call to a link with each string code.
example:
fetch('http://my-url/abcde').then(res => res.json()).then(res => res).catch(error => new Error(`Error: ${error}`)); // result: 12345
fetch('http://my-url/fghij').then(res => res.json()).then(res => res).catch(error => new Error(`Error: ${error}`)); // result: 67891
...etc
Each of the calls is going to give me a number code, as shown.
I need to get the highest number of the 5 and get its afferent string code and make another call with that.
"abcde" => 1234
"fghij" => 5314
"klmno" => 3465
"pqrst" => 7234 <--- winner
fetch('http://my-url/pqrst').then(res => res.json()).then(res => res).catch(error => new Error(`Error: ${error}`));
What I tried:
let codesArr = []; // array of string codes
let promiseArr = []; // array of fetch using each string code in `codesArr`, meant to be used in Promise.all()
let codesObj = {}; // object with string code and its afferent number code gotten from the Promise.all()
fetch('http://my-url/some-code')
.then(res => res.json())
.then(res => codesArr = res) // now `codesArr` is ["abcde", "fghij", "klmno", "pqrst"]
.catch(error => new Error(`Error: ${error}`);
for(let i = 0; i < codesArr.length; i++) {
promiseArr.push(
fetch(`http://my-url/${codesArr[i]}`)
.then(res => res.text())
.then(res => {
codesObj[codesArr[i]] = res;
// This is to get an object from which I can later get the highest number and its string code. Like this:
// codesObj = {
// "abcde": 12345,
// "fghij": 67891
// }
})
.catch(error => new Error(`Error: ${error}`));
// I am trying to make an array with fetch, so that I can use it later in Promise.all()
}
Promise.all(promiseArray) // I wanted this to go through all the fetches inside the `promiseArr` and return all of the results at once.
.then(res => {
for(let i = 0; i < res.length; i++) {
console.log(res[i]);
// this should output the code number for each call (`12345`, `67891`...etc)
// this is where I get lost
}
})
One of the problems with my approach so far seems to be that it makes too many requests and I get 429 error. I sometimes get the number codes all right, but not too often.
Like you already found out the 429 means that you send too many requests:
429 Too Many Requests
The user has sent too many requests in a given amount of time ("rate
limiting").
The response representations SHOULD include details explaining the
condition, and MAY include a Retry-After header indicating how long to
wait before making a new request.
For example:
HTTP/1.1 429 Too Many Requests
Content-Type: text/html
Retry-After: 3600
<html>
<head>
<title>Too Many Requests</title>
</head>
<body>
<h1>Too Many Requests</h1>
<p>I only allow 50 requests per hour to this Web site per
logged in user. Try again soon.</p>
</body>
</html>
Note that this specification does not define how the origin server
identifies the user, nor how it counts requests. For example, an
origin server that is limiting request rates can do so based upon
counts of requests on a per-resource basis, across the entire server,
or even among a set of servers. Likewise, it might identify the user
by its authentication credentials, or a stateful cookie.
Responses with the 429 status code MUST NOT be stored by a cache.
To handle this issue you should reduce the amount of requests made in a set amount of time. You should iterate your codes with a delay, spacing out the request by a few seconds. If not specified in the API documentation or the 429 response, you have to use trial and error approach to find a delay that works. In the example below I've spaced them out 2 seconds (2000 milliseconds).
The can be done by using the setTimeout() to execute some piece of code later, combine this with a Promise to create a sleep function. When iterating the initially returned array, make sure to await sleep(2000) to create a 2 second delay between each iteration.
An example could be:
const fetch = createFetchMock({
"/some-code": ["abcde", "fghij", "klmno", "pqrst"],
"/abcde": 12345,
"/fghij": 67891,
"/klmno": 23456,
"/pqrst": 78912,
});
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
(async function () {
try {
const url = "https://my-url/some-code";
console.log("fetching url", url);
const response = await fetch(url);
const codes = await response.json();
console.log("got", codes);
const codesObj = {};
for (const code of codes) {
await sleep(2000);
const url = `https://my-url/${code}`;
console.log("fetching url", url);
const response = await fetch(url);
const value = await response.json();
console.log("got", value);
codesObj[code] = value;
}
console.log("codesObj =", codesObj);
} catch (error) {
console.error(error);
}
})();
// fetch mocker factory
function createFetchMock(dataByPath = {}) {
const empty = new Blob([], {type: "text/plain"});
const status = {
ok: { status: 200, statusText: "OK" },
notFound: { status: 404, statusText: "Not Found" },
};
const blobByPath = Object.create(null);
for (const path in dataByPath) {
const json = JSON.stringify(dataByPath[path]);
blobByPath[path] = new Blob([json], { type: "application/json" });
}
return function (url) {
const path = new URL(url).pathname;
const response = (path in blobByPath)
? new Response(blobByPath[path], status.ok)
: new Response(empty, status.notFound);
return Promise.resolve(response);
};
}
In this case... You should run and wait each fetch run finish before run new fetch by using async/await
runFetch = async (codesArr) => {
for(let i = 0; i < codesArr.length; i++){
const rawResponse = await fetch(`http://my-url/${codesArr[i]}`);
const codeResponse = rawResponse.json();
console.log(rawResponse);
codesObj[codesArr[i]] = codeResponse;
}
}
hope that help you.
I use rxjs to handle a websocket connection
var socket = Rx.Observable.webSocket('wss://echo.websocket.org')
socket.resultSelector = (e) => e.data
I want to periodically (5s) sent a ping message and wait 3s to receive a pong response and subscribe to the a stream if no response has been receive.
I try that without success. I admit I'm a bit lost will all the operator available to handle timeout, deboune or throttle.
// periodically send a ping message
const ping$ = Rx.Observable.interval(2000)
.timeInterval()
.do(() => socket.next('ping'))
const pong$ = socket
.filter(m => /^ping$/.test(`${m}`))
.mergeMap(
ping$.throttle(2000).map(() => Observable.throw('pong timeout'))
)
pong$.subscribe(
(msg) => console.log(`end ${msg}`),
(err) => console.log(`err ${err}`),
() => console.log(`complete`)
)
But unfortunately, no ping are send.
I've also try to achieved that using without success.
const ping$ = Rx.Observable.interval(2000)
.timeInterval()
.do(() => socket.next('ping'))
const pong$ = socket
.filter(m => /^ping$/.test(`${m}`))
const heartbeat$ = ping$
.debounceTime(5000)
.mergeMap(() => Rx.Observable.timer(5000).takeUntil(pong$))
heartbeat$.subscribe(
(msg) => console.log(`end ${msg}`),
(err) => console.log(`err ${err}`),
() => console.log(`complete`)
)
Any help appreciated.
You can use race() operator to always connect only to the Observable that emits first:
function sendMockPing() {
// random 0 - 5s delay
return Observable.of('pong').delay(Math.random() * 10000 / 2);
}
Observable.timer(0, 5000)
.map(i => 'ping')
.concatMap(val => {
return Observable.race(
Observable.of('timeout').delay(3000),
sendMockPing()
);
})
//.filter(response => response === 'timeout') // remove all successful responses
.subscribe(val => console.log(val));
See live demo: https://jsbin.com/lavinah/6/edit?js,console
This randomly simulates response taking 0 - 5s. When the response takes more than 3s than Observable.of('timeout').delay(3000) completes first and the timeout string is passed to its observer by concatMap().
I finally found a solution based on mergeMapand takeUntil
My initial mistake was to use ping$ as an input for my heartBeat$ where I should use $pong
// define the pong$
const pong$ = socket
.filter(m => /^ping$/.test(`${m}`))
.share()
//use share() because pong$ is used twice
const heartbeat$ = pong$
.startWith('pong') // to bootstrap the stream
.debounceTime(5000) // wait for 5s after the last received pong$ value
.do(() => this.socket.next('ping')) // send a ping
.mergeMap(() => Observable.timer(3000).takeUntil(pong$))
// we merge the current stream with another one that will
// not produce value while a pong is received before the end of the
// timer
heartbeat$.subscribe(
(msg) => console.log(`handle pong timeout`),
)
Below heartbeat$ function return an observable which you can continuously listen to
1) the latency value of each round trip (time of socket.receive - socket.send) in every 5000ms
or
2) -1 if the round trip goes beyond the threshold (e.g. 3000ms)
You will keep receiving latency value or -1 even though -1 has been emitted which gives you the flexibility to decide what to do ^.^
heartbeat$(pingInterval: number, pongTimeout: number) {
let start = 0;
const timer$ = timer(0, pingInterval).pipe(share());
const unsub = timer$.subscribe(() => {
start = Date.now();
this.ws.next('ping');
});
const ping$ = this.ws$.pipe(
switchMap(ws =>
ws.pipe(
filter(m => /^ping$/.test(`${m}`)),
map(() => Date.now() - start),
),
),
share(),
);
const dead$ = timer$.pipe(
switchMap(() =>
of(-1).pipe(
delay(pongTimeout),
takeUntil(ping$),
),
),
);
return merge(ping$, dead$).pipe(finalize(() => unsub.unsubscribe()));
}
heartbeat$(5000, 3000).subscribe(
(latency) => console.log(latency) // 82 83 82 -1 101 82 -1 -1 333 ...etc
)