I'm having some difficulty creating "next page" and "previous page" buttons using discord-buttons.
What my command so far does is grab role info from an array formatted like:
{ role: 'hot rod red', color: '#fc0000' , price: 25000, roleid: '733373020491481219', id: 'red' }
Then create embeds for each of those roles. The code I have so far sends all of the roles at once with no issues, but I am trying to get them to send one at a time, and when the "next page" button is pressed, the message edits, and it goes to the next role embed in that array, all the way to the end. Since discord buttons are fairly new, there's not that much information on them yet, so any help's appreciated, thanks! (Please let me know I need to provide more code)
Buttons are basically, a kind of alternative to using reactions. For page-like systems, it works almost identically. You send a message with reactions/buttons, wait for reactions, and then edit the message.
There are a few differences:
Buttons are immediately applied to the message and do not need to be added on afterward.
Buttons need to be responded to within 3 seconds of being pressed to avoid the user getting "This interaction failed" on their screen.
When interacted with, the buttons on the message cannot be interacted with again by the same user until acknowledged by the bot or the interaction times out.
Buttons cannot be removed by users
This means that buttons are very useful for actions such as editing the message it is on, but not as much for something like toggling a role.
Each message can have 5 ActionRows, and each ActionRow can have 5 Buttons (a total of 25)
Buttons have a label, style, and a custom id (or URL, if it is a 'URL-styled' button).
Since you are using discord-buttons, you can do something like this to send the message with buttons:
const { MessageActionRow, MessageButton } = require("discord-buttons");
// create a row!
const row = new MessageActionRow(),
backButton = new MessageButton()
.setLabel("Back")
// https://discord.com/developers/docs/interactions/message-components#buttons-button-styles
.setStyle("blurple"),
// identifies the button, so you can know which button was pressed
.setID("back"),
nextButton = new MessageButton()
.setLabel("Next")
.setStyle("blurple")
.setID("next");
row.addComponent(backButton).addComponent(nextButton);
const pages = [
"Hello",
"World",
"Foo",
"Bar"
];
let index = 0;
const message = await channel.send(pages[index], {
component: row
});
Then, you can wait for the interactions using either the methods or events that discord-buttons provide:
discord-buttons extends functionalities of regular discord.js classes, so similar options can be used.
function handleInteractions() {
// originalMessage is the message sent by the user to activate this 'command'
const filter = (button) => button.clicker.user.id === originalMessage.author.id;
const clickedButton = (await message.awaitButtons(filter, {
max: 1, // how many to collect
time: 60000 // how long to wait in milliseconds
})).first();
// check if a button was actually clicked
if (clickedButton) {
// IMPORTANT: Respond to the interaction to prevent false errors on the user's side.
// https://discord-buttons.js.org/events/clickbutton#functions
// Acknowledges the interaction, doesn't send or edit anything
// You may find it easier to use a different method to acknowledge and edit/send a new message, but in this example, the message will be edited normally.
await clickedButton.defer();
if (clickedButton.id === "back") {
index--;
} else {
index++;
}
// constrain the pages
if (index < 0) index = pages.length - 1;
if (index >= pages.length) index = 0;
// edit your message!
message.edit(pages[index]);
// re-listen to button clicks
handleInteractions();
} else {
// It may be useful to delete the message or remove the buttons from the
// message once you are no longer listening to the events.
message.edit({components: []});
}
}
handleInteractions();
Related
Sorry for the poorly worded title, I'll try to explain as best as I can. I am creating a role shop command using the new discord-buttons module, and came across a problem, to my understanding I would have to create a button for each individual role, in order for someone to buy it. After searching through documentation, I'm still a bit stumped. Here's some example code I put together to show what I'm trying to do:
let embedRed = new Discord.MessageEmbed()
.setTitle('Red Role')
.setColor('#c46413')
.addField('**Price**', '10,000', true)
.addField('**Color Hex:**', '#ffffff',true)
let embedBlue = new Discord.MessageEmbed()
.setTitle('Blue')
.setColor('#c2a289')
.addField('**Price**', '10,000', true)
.addField('**Color Hex:**', '#ffffff',true)
///Buttons
let buttonBuyRed = new MessageButton()
.setStyle('green')
.setLabel('Buy Red Role')
.setID('role_buy1')
let buttonBuyBlue = new MessageButton()
.setStyle('green')
.setLabel('Buy Blue Role')
.setID('role_buy2')
//embeded messages being sent
message.channel.send({ buttons: [buttonBuyRed], embed: embedRed});
message.channel.send({ buttons: [buttonBuyRed], embed: embedBlue});
//What happens if buttons are pressed
client.on('clickButton', async (role_buy1) => {
if (button.id === 'roley_buy1') {
button.channel.send(`${button.clicker.user.tag} bought red role`);
db.push(message.author.id, `${message.guild.roles.cache.get('role id here')}`) //role being pushed to user's inventory
}
});
client.on('clickButton', async (role_buy2) => {
if (button.id === 'role_buy2') {
button.channel.send(`${button.clicker.user.tag} bought blue role`);
db.push(message.author.id, `${message.guild.roles.cache.get('role id here')}`) //role being pushed to user's inventory
}
});
Since I have about 25 different roles that I want users to be able to purchase, it's quite a hassle to create a button for each role, I am looking for a way to just use one single "buy_role" button that works for all available roles.
If I didn't explain something clearly, please let me know, any help is appreciated!
So i came to a conclusion, this code works, but if your guild has a lot of roles, it would throw an error "Invalid form body"
const rolesInGuild = message.guild.roles.cache.array(); //creating array from collection of roles in a guild
const buttons = []; // an empty array for our buttons
for (const role of rolesInGuild) { // creating a loop inorder to create a button for every roles in rolesInGuild Array
const button = new MessageButton()
.setStyle('red') // default: blurple
.setLabel(`${role.name}`) // default: NO_LABEL_PROVIDED
.setID(`${role.id}`);
buttons.push(button); // button id is the same as role id so its unique!
}
console.log(rolesInGuild);
console.log(buttons);
await message.channel.send('test', { buttons: buttons }); // sending our buttons
bot.on('clickButton', async(button) => {
for (const btn of buttons) {
if (btn.custom_id == button.id) {
const role = button.guild.roles.cache.get(btn.custom_id);
const member = message.guild.members.cache.get(button.clicker.user.id);
member.roles.add(role);
}
}
});
you could add specific roles to the array rolesInGuild in this format
[{ name: 'rolename', id: 'roleid' }] instead of every roles in the guild ( I wasn't sure what your goal was)
you have ${message.guild...}, that’s the wrong if you have an error, so try this:
client.on('clickButton', async (button) => {
if (button.id === 'roley_buy1') {
button.channel.send(`${button.clicker.user.tag} bought red role`);
db.push(message.author.id, `${button.guild.roles.cache.get('role id here')}`)
//role being pushed to user's inventory
button.clicker.roles.add('your role id');
// or you can find the role using
const role = button.guild.roles.cache.find(role => role.name == 'rolename');
button.clicker.roles.add(role);
}
});```
Is there a way to call an external API Endpoint on Google Forms every time the form is filled out?
First:
you'll need to set up your App script project and you'll do that by:
Visit script.google.com to open the script editor. (You'll need to be signed in to your Google account.) If this is the first time you've been to script.google.com, you'll be redirected to a page that introduces Apps Script. Click Start Scripting to proceed to the script editor.
A welcome screen will ask what kind of script you want to create. Click Blank Project or Close.
Delete any code in the script editor and paste in the code below.
This video and the doc will help
Second
you'll need to create an installable trigger, you can add it to the form directly or to the spreadsheet that has the responses
function setUpTrigger(){
ScriptApp.newTrigger('sendPostRequest') /* this has the name of the function that will have the post request */
.forForm('formkey') // you'll find it in the url
.onFormSubmit()
.create();
}
Check the doc
Third
create the sendPostRequest function and add the UrlFetchApp to it
function sendPostRequest(e){
// Make a POST request with form data.
var resumeBlob = Utilities.newBlob('Hire me!', 'text/plain', 'resume.txt');
var formData = {
'name': 'Bob Smith',
'email': 'bob#example.com',
'resume': resumeBlob
};
// Because payload is a JavaScript object, it is interpreted as
// as form data. (No need to specify contentType; it automatically
// defaults to either 'application/x-www-form-urlencoded'
// or 'multipart/form-data')
var options = {
'method' : 'post',
'payload' : formData
};
UrlFetchApp.fetch('https://httpbin.org/post', options);
}
Check the doc
Try something like this in your app script:
var POST_URL = "enter your webhook URL";
function onSubmit(e) {
var form = FormApp.getActiveForm();
var allResponses = form.getResponses();
var latestResponse = allResponses[allResponses.length - 1];
var response = latestResponse.getItemResponses();
var payload = {};
for (var i = 0; i < response.length; i++) {
var question = response[i].getItem().getTitle();
var answer = response[i].getResponse();
payload[question] = answer;
}
var options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify(payload)
};
UrlFetchApp.fetch(POST_URL, options);
};
Be sure to replace the POST_URL variable with your webhook, you can use requestcatcher.com to test this out.
Add a trigger to the script by clicking "Triggers" in the side menu
Open the menu (top-right dots)
Click in Script Editor
Paste the above code (changing the POST_URL)
Click in the clock icon (left-side menu), which means Triggers.
On the right-bottom corner, click in the blue Add trigger button (a form will show as the image below).
It should show onSubmit under Choose which function to run.
Make sure Select event type is set as On form submit.
Click Save button.
After that, submit your form and watch for the request to come in.
This is pretty straightforward with Google Scripts.
Just create a new project bound to your spreadsheet and create 2 elements:
A function that will contain all relevant data to make the call (see docs for making a HTTP request from Google Apps Script)
A trigger linked to the spreadsheet. You can set it to run each time an edit occurs or form is submitted
Voilà, your sheet will call whatever endpoint you wish on submission. You can even parse the spreadsheet to return that data to your endpoint
I want to load all the items on start without showing any message, but once after loaded. I want to capture any new row in subscriber and show it to the desktop notification.
The problem is, I'm not sure how to check if all the previous items are loaded and if the row is new item or is it from previous existing item.
this.items = this.af.database.list('notifications/'+this.uid+'/');
this.items.subscribe(list => {
list.forEach(row => {
// doing something here...
});
// once all the rows are finished loading, then any new row, show desktop notification message
});
I have user lodash for the minimal code.
// this varible holds the initial loaded keys
let loadedKeys = [];
this.items = this.af.database.list('notifications/'+this.uid+'/');
this.items.subscribe((list)=>{
// we skip it while initial load
if(!_.isEmpty(loadedKeys)){
// different is the new keys
let newKeys = _.difference(_.map(list, "$key"), loadedKeys);
if(!_.isEmpty(newKeys)){
// ... notification code here
}
}
loadedKeys = _.map(list, "$key");
});
The behave you are looking for is the default Subject approach in RxJS.
Check this reactiveX url to follow the marble diagram of Publish Subject (the equivalent for Subject in RxJS).
So you have two easy options:
1) manually index witch rows you want to display like #bash replied
2) create a Rx.Subject() and assign only the newest's rows to it. Then you subscribe to this subject in your app workflow.
The advantage of method 2 is when a new .subscribe occur, it will not retrieve previous data.
Edit: I wrote this codepen as a guide to implement your custom RxJS Subject. Hope it helps.
Assuming your rows have something unique to match with previous rows you can do the following:
// A Row item has a unique identifier id
interface Row {
id: number;
}
this.rows: Row[];
this.items$ = this.af.database.list(`notifications/${this.uid}/`).pipe(
tap(list => {
// if rows is not array, first time...
if(!Array.isArray(this.rows)) {
// first time nothing to do
return;
}
// returns true if some item from list is not found in this.rows
const foundNewRow = list.some(item => {
return ! this.rows.find(row => row.id === item.id);
});
if (foundNewRow) {
// call method to show desktop message here
}
}
);
I used a pipe and a tap operator (that you will have to import). If you subscribe to this.items$ the tap operator will do the work:
this.items$.subscribe((items => this.rows = items));
If you do not want to set this.rows when normally subscribing than you can also do this in the tap operator. But that would assume you only use it for checking difference between existing and new items.
I'm in the process of learning meteor. I followed the tutorial to create microscope. If some one submits a post meteor will re render the template for all users. This could be very annoying if there are hundreds of posts then the user will come back to the top of the page and loose track of where he was. I want to implement something similar to what facebook has. When a new post is submitted template isn't rendered rather, a button or link will appear. Clicking it will cause the template to re-render and show the new posts.
I was thinking of using observeChanges on the collection to detect any changes and it does stop the page from showing new posts but only way to show them is to reload the page.
Meteor.publish('posts', function(options) {
var self = this, postHandle = null;
var initializing = true;
postHandle = Posts.find({}, options).observeChanges({
added: function(id, post) {
if (initializing){
self.added('posts', id, post);
}
},
changed: function(id, fields) {
self.changed('posts', id, fields);
}
});
self.ready();
initializing = false;
self.onStop(function() { postHandle.stop(); });
});
Is this the right path to take? If yes, how do I alert the user of new posts? Else, what would be a better way to implement this?
Thank you
This is a tricky question but also valuable as it pertains to a design pattern that is applicable in many instances. One of the key aspects is wanting to know that there is new data but not wanting to show it (yet) to the user. We can also assume that when the user does want to see the data, they probably don't want to wait for it to be loaded into the client (just like Facebook). This means that the client still needs to cache the data as it arrives, just not display it immediately.
Therefore, you probably don't want to restrict the data displayed in the publication - because this won't send the data to the client. Rather, you want to send all the (relevant) data to the client and cache it there until it is ready.
The easiest way involves having a timestamp in your data to work from. You can then couple this with a Reactive Variable to only add new documents to your displayed set when that Reactive Variable changes. Something like this (code will probably be in different files):
// Within the template where you want to show your data
Template.myTemplate.onCreated(function() {
var self = this;
var options = null; // Define non-time options
// Subscribe to the data so everything is loaded into the client
// Include relevant options to limit data but exclude timestamps
self.subscribe("posts", options);
// Create and initialise a reactive variable with the current date
self.loadedTime = new ReactiveVar(new Date());
// Create a reactive variable to see when new data is available
// Create an autorun for whenever the subscription changes ready() state
// Ignore the first run as ready() should be false
// Subsequent false values indicate new data is arriving
self.newData = new ReactiveVar(false);
self.autorun(function(computation) {
if(!computation.firstRun) {
if(!self.subscriptionsReady()) {
self.newData.set(true);
}
}
});
});
// Fetch the relevant data from that subscribed (cached) within the client
// Assume this will be within the template helper
// Use the value (get()) of the Reactive Variable
Template.myTemplate.helpers({
displayedPosts = function() {
return Posts.find({timestamp: {$lt: Template.instance().loadedTime.get()}});
},
// Second helper to determine whether or not new data is available
// Can be used in the template to notify the user
newData = function() {
return Template.instance().newData.get();
});
// Update the Reactive Variable to the current time
// Assume this takes place within the template helper
// Assume you have button (or similar) with a "reload" class
Template.myTemplate.events({
'click .reLoad' = function(event, template) {
template.loadedTime.set(new Date());
}
});
I think this is the simplest pattern to cover all of the points you raise. It gets more complicated if you don't have a timestamp, you have multiple subscriptions (then need to use the subscription handles) etc. Hope this helps!
As Duncan said in his answer, ReactiveVar is the way to go. I've actually implemented a simple facebook feed page with meteor where I display the public posts from a certain page. I use infinite scroll to keep adding posts to the bottom of the page and store them in a ReactiveVar. Check the sources on github here and the live demo here. Hope it helps!
I'm trying to create simple notification system for my site admin, and I need to send only real-time messages to every admin user. But when I use firebase it loads old data on every page, and user see all messages from database. If I set limit(1) user will see last notification on every page reloading:
var eventsList = new Firebase('https://*****-messages.firebaseio.com/');
eventsList.on('child_added', function(message) {
var message = message.val();
$.notification(message.message);
});
How I can load only new messages, without old notification history?
This is by design, in a real-time system there is no concept of the "latest" data because it's always changing. However, if you want to only display items added to the list after the page has loaded, you can do the following:
var newItems = false;
var eventsList = new Firebase('https://*****-messages.firebaseio.com/');
eventsList.on('child_added', function(message) {
if (!newItems) return;
var message = message.val();
$.notification(message.message);
});
eventsList.once('value', function(messages) {
newItems = true;
});
I would comment on the above, but due to reputation I cannot, so hoping this is adequate and this is to address the last comment by Gruff McGruff.
Once fires after because you want to break out of the loop of grabbing all child items. Once that loops is broken, you'll set the newItems variable to true, and then it will be able to get all new children after that.
If you fired it before, it would defeat the purpose and grab all child items regardless because you'll set the newItems variable immediately.
Also, I've used this approach and it works well.