expiring-buttons

# Making expiring buttons with when_any

D++ Coroutines are currently only supported by D++ on g++ 13, clang/LLVM 14, and MSVC 19.37 or above. Additionally, your program has to support C++20.

In the last example we've explored how to await events using coroutines, we ran into the problem of the coroutine waiting forever if the button was never clicked. Wouldn't it be nice if we could add an "or" to our algorithm, for example wait for the button to be clicked or for a timer to expire? I'm glad you asked! D++ offers when_any which allows exactly that. It is a templated class that can take any number of awaitable objects and can be `co_await`-ed itself, will resume when the first awaitable completes and return a result object that allows to retrieve which awaitable completed as well as its result, in a similar way as std::variant.

    #include<dpp/dpp.h>

    intmain(){
    dpp::clusterbot{"token"};

    bot.on_log(dpp::utility::cout_logger());

    bot.on_slashcommand([](constdpp::slashcommand_t&event)->dpp::task<void>{
    if(event.command.get_command_name()=="test"){
    //MakeamessageandaddabuttonwithitscustomIDsettothecommandinteraction'sIDsowecanidentifyit
    dpp::messagem{"Test"};
    std::stringid{event.command.id.str()};
    m.add_component(
    dpp::component{}.add_component(
    dpp::component{}
    .set_type(dpp::cot_button)
    .set_label("Clickme!")
    .set_id(id)
    )
    );
    co_awaitevent.co_reply(m);

    autoresult=co_awaitdpp::when_any{//Whichevercompletesfirst...
    event.owner->on_button_click.when([&id](constdpp::button_click_t&b){//Buttonclicked
    returnb.custom_id==id;
    }),
    event.owner->co_sleep(5)//Orsleep5seconds
    };
    //Note!!Duetoabuging++11andg++12,idmustbecapturedasareferenceaboveorthecompilerwilldestroyittwice.Thisisfixeding++13
    if(result.index()==0){//Awaitable#0completedfirst,thatisthebuttonclickevent
    //Acknowledgetheclickandedittheoriginalresponse,removingthebutton
    constdpp::button_click_t&click_event=result.get<0>();
    click_event.reply();
    event.edit_original_response(dpp::message{"Youclickedthebuttonwiththeid"+click_event.custom_id});
    }else{//Hereindex()is1,thetimerexpired
    event.edit_original_response(dpp::message{"Ihaven'tgotallday!"});
    }
    }
    });

    bot.on_ready([&bot](constdpp::ready_t&event){
    if(dpp::run_once<struct register_bot_commands>()){
    dpp::slashcommandcommand{"test","Testawaitingforanevent",bot.me.id};

    bot.global_command_create(command);
    }
    });

    bot.start(dpp::st_wait);

    return0;
    }

Any awaitable can be used with when_any, even dpp::task, dpp::coroutine, dpp::async. When the when_any object is destroyed, any of its awaitables with a cancel() method (for example dpp::task) will have it called. With this you can easily make commands that ask for input in several steps, or maybe a timed text game, the possibilities are endless! Note that if the first awaitable completes with an exception, result.get will throw it.

when_any will try to construct awaitable objects from the parameter you pass it, which it will own. In practice this means you can only pass it temporary objects (rvalues) as most of the coroutine-related objects in D++ are move-only.