Getting 401 Unauthorized on WhatsApp Cloud API via Mekari Qontak

need help from anyone familiar with whatsapp cloud api. I’m using the Mekari Qontak API https://github.com/mekari-engineering/qontak-api-js and follow the documentation provided in the repo.

Somehow my request here got response 401

curl --request POST 
  --url https://api.mekari.com/qontak/chat/v1/broadcasts/whatsapp/direct 
  --header 'Accept: application/json' 
  --header 'Authorization: hmac username=“{{Username}}“, algorithm="hmac-sha256", headers="date request-line", signature="{{Signature}}"' 
  --header 'Content-Type: application/json' 
  --data '{
  "to_name": "Test User",
  "to_number": "6281234567890",
  "message_template_id": "60cccaa0-ccd9-4efd-bdfb-875859c4b50a",
  "channel_integration_id": "56b60c3c-0123-46af-958b-32f3ad12ee37",
  "language": {
    "code": "id"
  },
  "parameters": {
    "buttons": [
      {
        "index": "0",
        "type": "url",
        "value": "123456"
      }
    ],
    "body": [
      {
        "key": "1",
        "value_text": "123454321",
        "value": "otp"
      }
    ]
  }
}'

Does anyone know what might cause this 401 error?

Are there specific steps needed for authentication, such as generating an HMAC signature?

Has anyone successfully sent messages using this API and can share a working example?

  • API key and credentials are correct.
  • The request headers match the API documentation.
  • The username, algorithm, headers, and signature are properly set in the Authorization header.
  • The message_template_id and channel_integration_id are valid.

How to pass error handler from app.js to test.js

Here is the app.js created by executing “express --view=hbs /tmp/foo

var createError = require('http-errors');
var express = require('express');
...
var app = express();
...

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

Here is the test.js for implementing error handler:

const express = require("express");
const router = express.Router();

// How to do error handler here?

How to pass error handler from app.js to test.js?

// error handler
app.use(function(err, req, res, next) {

  // How to pass it to the test.js for error handling?
  
});

PHP-Javascript complex interactions in WordPress

I’ve been stuck on a complex problem for several days and I haven’t found anything to unblock it.

  • I’ve created a wordpress plugin, called keywordize, that allows me to modify keywords from about 400 texts sorted by years. I use Mistral AI for that.
  • I call the plugin within a wordpress page with a shortcode [keywordize]
  • Here is the keywordize.php file :
function keywordize_enqueue_scripts()
{
    wp_enqueue_style('keywordize-style', plugin_dir_url(__FILE__) . 'style.css');
    wp_enqueue_script('keywordize-script', plugin_dir_url(__FILE__) . 'script.js', array('jquery'), null, true);

    // Get data from DB 
    global $wpdb;
    $r_sujets = "SELECT * FROM Sujet ORDER BY AnneeS DESC;";
    $q_sujets = $wpdb -> get_results($r_sujets, ARRAY_A);
    $php_vars = array(
        'sujets' => $q_sujets,
        'pluginDir' => plugin_dir_url(__FILE__),
    );
    wp_localize_script('keywordize-script', 'php_vars', $php_vars );    
}
add_action('wp_enqueue_scripts', 'keywordize_enqueue_scripts');

function keywordize_shortcode()
{
    ob_start(); ?>
    <div id="keywordize">
        <div id="timeout-wrapper">
            Timeout :  <input id="timeout" type="text"/>
        </div>
        <div id="years-select-wrapper">
            Sélection : 
            <select id="years-select">
                <option value=""></option>
            </select>
        </div>
        <div id="submit-wrapper">
            <button id="submit">Submit</button>
        </div>
        <div id="years-select-wrapper"></div>
        <div id="subjects-by-year-wrapper"></div>
        <div id="submit-subjects-by-year"></div>
    </div>
    <div id="update"><?php
        include_once('update.php');
    ?></div>    
<?php
    return ob_get_clean();
}
add_shortcode('keywordize', 'keywordize_shortcode');
  • This php file loads a js file script.js. This file takes the result of my php request, calls Mistral AI on each text to get keywords from the text, then add lines to the page:
jQuery(document).ready(function ($) {
    // get variables from keywordize.php
    const sujets = php_vars.sujets;
    const pluginDir = php_vars.pluginDir;
    console.log("sujets", sujets);

    const tempAnnees = sujets.map(sujet => sujet.AnneeS);
    const annees = [...new Set(tempAnnees)];
    console.log("annees", annees);

    $("#timeout").val(1000);

    annees.forEach(function (annee) {
        $("#years-select").append('<option value="' + annee + '">' + annee + '</option>');
    });

    $("#submit").on('click', function () {
        if ($(this).text() == "Reset") {
            $("#timeout").val(1000);
            $("#years-select").val("");
            $(this).text("Submit");
        } else {
            $(this).text("Reset");
            const annee = $("#years-select").val();
            const timeout = $("#timeout").val();
            $("#subjects-by-year-wrapper").text("");

            const sujetsParAnnee = sujets.filter(function (sujet) {
                return sujet.AnneeS == annee;
            });
            console.log("sujetsParAnnee", sujetsParAnnee);
            $("#subjects-by-year-wrapper").append("<br/><h3>" + sujetsParAnnee.length + " sujets pour l'année " + annee + "</h3>");

            getKeywords(sujetsParAnnee, timeout, function () {
                console.log("FINISHED");
            });
        }

    });

    function getOptions(resume) {
        return options = {
            method: 'POST',
            headers: {
                'Authorization': 'Bearer API_KEY',
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            },
            body: JSON.stringify({
                "model": "mistral-large-2407",
                "messages": [
                    {
                        "role": "user",
                        "content": "Donne moi 4 mots-clés issus de ce texte : " + resume
                    }
                ]
            }),
        }
    }

    function modifyKeywords(submitSubject, idS, keywordsArray) {
        console.log("let's replace keywords of subject " + idS);
    }

    async function getKeywords(sujets, timeout, callback) {
        const url = "https://api.mistral.ai/v1/chat/completions";
        const nbSujets = sujets.length;
        const delay = (ms = timeout) => new Promise(r => setTimeout(r, ms));
        let i = 1;
        for (let sujet of sujets) {

            // Construction des lignes d'accueil de la réponse
            const subjectLineWrapper = document.createElement("div");

            const formWrapper = document.createElement("div");
            formWrapper.setAttribute("class", "form");
            const form = document.createElement("form");
            form.setAttribute("action", "javascript:;");

            const subjectWrapper = document.createElement("div");
            subjectWrapper.setAttribute("class", "subject");
            const subject = document.createElement("div");

            try {
                const resume = sujet.TitreS + "." + sujet.DescriptionS;
                const response = await fetch(url, getOptions(resume));
                if (!response.ok) {
                    throw new Error('Response status: ' + response.status);
                }
                const json = await response.json();
                const keywords = json.choices[0].message.content;
                // console.log(keywords);

                // REGEX sur réponse IA keywords
                const keywordsArray = [];
                const regex = /^[0-9]+. (.*(?:n(?![0-9]+.).*)*)/gm;
                // const regex = new RegExp('^[0-9]+\. (.*(?:\n(?![0-9]+\.).*)*)', 'gm')
                let match;
                while ((match = regex.exec(keywords)) !== null) {
                    keywordsArray.push(match[1].replaceAll("*", ""));
                }
                // console.log("keyword array = ", keywordsArray);

                // Mise en forme et ajout texte des lignes de réponse

                if (i % 2 == 0) subjectLineWrapper.setAttribute("class", "subject-line subject-line-gray");
                else subjectLineWrapper.setAttribute("class", "subject-line subject-line-white");

                const button = document.createElement("input");
                button.setAttribute("type", "submit");
                button.setAttribute("value", "Replace keywords");
                form.appendChild(button);
                formWrapper.appendChild(form);
                form.addEventListener('submit', function (event) {
                    modifyKeywords(event.submitter.value, sujet.IdS, keywordsArray);
                });

                subject.innerHTML += "(" + i + "/" + nbSujets + ") "
                    + sujet.Theme1 + " / "
                    + sujet.Theme2 + " / "
                    + sujet.Theme3 + " / "
                    + sujet.Theme4 + " / "
                    + " => " + keywordsArray;

                subjectLineWrapper.appendChild(subjectWrapper);
                subjectLineWrapper.appendChild(formWrapper);

            } catch (error) {
                subject.setAttribute("class", "subject-red");
                subject.innerHTML += "(" + i + "/" + nbSujets + ") " + sujet.TitreS + " > " + error.message;
                subjectLineWrapper.setAttribute("class", "subject-line subject-line-red");
                subjectLineWrapper.appendChild(subjectWrapper);
                // console.error(error.message);
            }
            subjectWrapper.appendChild(subject);
            document.getElementById("subjects-by-year-wrapper").appendChild(subjectLineWrapper);
            await delay();
            i++;
        };
        callback();
    }
});
  • Everything works perfectly, and I get this result:

Page result after JS treatment

  • My problem is now this one : how to treat each line, so update each subject (I want it one by one to avoid errors), by clicking each button one by one ? I put here the research I’ve made so far :
function modifyKeywords(submitSubject, idS, keywordsArray) {
        console.log("let's replace keywords of subject " + idS);
        $.ajax({
            url: pluginDir + "update.php",
            type: "POST",
            data: { 
                "submitSubject": submitSubject,
                "idS": idS,
                "keywordsArray": keywordsArray,
            },
            error: function (xhr, status, error) {
                console.log("Error: " + error);
            },
            success: function (result, status, xhr) {
                console.log("Success: " + result);
            }
        });
    }
  • So with AJAX, I send some variables to another php file, called update.php :
$submitSubject = $_POST['submitSubject'];
$idS = $_POST['idS'];
$keywordsArray = $_POST['keywordsArray'];
if (isset($idS))
    echo "idS = " . $idS . "<br/>";
  • But I’m stuck here because when I click a button, I obtain only a console output, which is my AJAX success.
Success: idS = 585<br/>
  • Or if I modify update.php with $r_subject = "SELECT * FROM Sujet WHERE IdS = $idS;";echo "request = " . $r_subject . "<br>";:
Success: request = SELECT * FROM Sujet WHERE IdS = 586;<br>
  • Then I have another error when I execute the request, but it will be another question soon on SO 🙂

So what I want to be able to do is to interact with update.php when I click a button. How to display something in my php file update.php without refreshing ? How my php file knows that some modifications have been made by javascript on itself ? I’m lost 🙂

Many thanks

appsflyer-cordova-plugin not opening app installed from XCode

  • XCode Version: 16.0
  • IOS Version: IOS18
  • cordova-appsflyer Version: 6.15.3
  • cordova-ios version: 7.1.0

  • We have an existing app that is published but is limited to countries outside our area.

  • I am trying to integrate Appsflyer to the app and test it on a build installed from XCode on my local phone IOS18.

  • Associated domains has also been added to XCode

  • After clicking the Appsflyer link, it opens a browser and tells me that the app is not available in our country.
    App Not Available

  • The SDK integration based on the Appsflyer Wizard is successful
    enter image description here

select box cell renderer does not update when deleting a react table row

Could you please take a look into the tiny sandbox project?

https://codesandbox.io/p/sandbox/festive-nova-s58h2v

I have a simple react table, each row contains a “raw” data and the same data as a select box value.
when I delete a row, the table is rerendered correctly, the “raw” data is displayed correctly,
but the selected value in the select boxes stay unchanged.
Why?

Thanks in advance!

Is there a better way to make a menu bar similar to my complex solution? [closed]

I am trying to make a menu that looks nice… but to implement it I added a lot of long things, states, and code. It took hundreds of lines to implement it.

So I am asking – are there any alternatives? Or some way(s) I can implement what I have now easier/more efficiently?

This feature: when you click on a choice in the menu you go ‘deeper’, but the deeper window comes from the right, and the previous one goes out to the left.

I am using React and Tailwind CSS.

To see my code here is a link to CodeSandbox.

my react code looks bad, and every time a implement new feature it gets worse

I am trying to make a menu that looks nice little bit, but to implement it I did a lot of things, used many states, a lot of code, and hundreds of lines to implement it,

so I am asking about any alternative or can I implement it easier?

“The feature is when you click on a choice in the menu you go deeper but the deeper window comes from the right, and the previous one goes out to the left”

I use:

  • react
  • HTML
  • tailwind CSS

To see my code here is a link to CodeSandBox.

Vue.js 3 – pass contents of a component’s slot to another slot outside it

I have a component Foo with the following contents:

<Bar>
  <template #option="slotProps" v-if="$slots.option">
    <slot name="option" v-bind="slotProps"></slot>
  </template>
</Bar>

<div>
  <slot :selected="selectedOption"></slot>
</div>

Is there a way to pass the contents of

<slot name="option" v-bind="slotProps"></slot>

to

<slot :selected="selectedOption"></slot>

by default, while still having the ability to override it if needed?

I tried this but it didn’t work

    <slot :selected="selectedOption">
      <template v-if="$slots.option">
        <slot name="option" :selected="selectedOption"></slot>
      </template>
    </slot>

ag-chart How to adjust corner radius of series for stacked bar chart

I try to achieve stacked bar chart with round border as
goal design

here is what I try with the sandbox

    theme: {
      overrides: {
        bar: {
          series: {
            fillOpacity: 0.8,
            cornerRadius: 8,
            itemStyler: (s) => {
              if (s.yKey == "iphone") {
                return {
                  cornerRadius: 999,
                  fillOpacity: 0.1,
                  fill: "#cf5200",
                };
              }
            },
          },
        },
      },
    },

the cornerRadius inside itemStyler is not doing anything

https://codesandbox.io/p/devbox/stacked-bars-forked-6fqld7?workspaceId=ws_33rxSAV11fcwYzWEHLCn6X

Can I get the Cypress API to run `.feature` files as well as `spec` files?

I’ve got a mixture of Cucumber .feature files and normal Cypress .spec.js files.

Normally, they run together fine as I have included:

specPattern: ["cypress/e2e/**/*.spec.js", "cypress/e2e/*.feature"],

in my cypress.config.js file.

However, I want to use Cypress’s Module API, eg:

const mySpecs = [
    `homePage.spec.js`,
    `aboutPage.spec.js`
];

cypress.run({
    browser: 'chrome',
    spec: mySpecs,
});

The above works, but not if I also have a .feature file.

const mySpecs = [
    `homePage.spec.js`,
    `aboutPage.spec.js`,
    `contactPage.feature`
];

I’ve tied modifying the above, eg:

cypress.run({
    browser: 'chrome',
    spec: mySpecs,
    config: {
        specPattern: ["cypress/e2e/**/*.spec.js", "cypress/e2e/*.feature"],
    }
});

and

cypress.run({
    browser: 'chrome',
    specPattern: ["cypress/e2e/**/*.spec.js", "cypress/e2e/*.feature"],
    spec: mySpecs
});

Does anyone know if this is possible?

How to update a ref with a signal in Solid.js to control table scroll?

I’m working with Solid.js and have a table with rows and cells. I’m using signals to manage the selected row and cell indices, but I need to update a ref (reference to the selected cell) based on those signals in order to manipulate the table’s scroll when the content overflows.

The issue is that I don’t want to use querySelector, as I don’t consider it a good practice, and I also want the reference to update according to the state of the signals.

Here is a simplified version of my implementation:

import { batch, Component, createEffect, createSelector, createSignal, For, JSX } from "solid-js";

export const DataGrid: Component = () => {
  const rows = Array.from({ length: 10 });
  const cells = Array.from({ length: 10 });

  const [selectedRowIndex, setSelectedRowIndex] = createSignal(0);
  const [selectedCellIndex, setSelectedCellIndex] = createSignal(0);

  const isSelectedRow = createSelector(selectedRowIndex);
  const isSelectedCell = createSelector(selectedCellIndex);

  const keysToPrevent = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"];

  const onKeyDown: JSX.EventHandler<HTMLDivElement, KeyboardEvent> = (event) => {
    const { key, ctrlKey } = event;

    if (keysToPrevent.includes(key)) {
      event.preventDefault();
    }

    const rowLength = rows.length;
    const cellLength = cells.length;

    switch (key) {
      case "ArrowUp":
        if (ctrlKey) {
          setSelectedRowIndex(0);
        } else if (selectedRowIndex() > 0) {
          setSelectedRowIndex((v) => v - 1);
        }
        break;

      case "ArrowDown":
        if (ctrlKey) {
          setSelectedRowIndex(rowLength - 1);
        } else if (selectedRowIndex() < rowLength - 1) {
          setSelectedRowIndex((v) => v + 1);
        }
        break;

      case "ArrowLeft":
        if (ctrlKey) {
          setSelectedCellIndex(0);
        } else if (selectedCellIndex() > 0) {
          setSelectedCellIndex((v) => v - 1);
        }
        break;

      case "ArrowRight":
        if (ctrlKey) {
          setSelectedCellIndex(cellLength - 1);
        } else if (selectedCellIndex() < cellLength - 1) {
          setSelectedCellIndex((v) => v + 1);
        }
        break;
    }
  };

  const onMouseDown: JSX.EventHandler<HTMLDivElement, MouseEvent> = (event) => {
    const rowIndex = parseInt(event.currentTarget.dataset.rowIndex!);
    const colIndex = parseInt(event.currentTarget.dataset.colIndex!);

    batch(() => {
      setSelectedRowIndex(rowIndex);
      setSelectedCellIndex(colIndex);
    });
  };

  let tableRef!: HTMLDivElement;
  let selectedCellRef!: HTMLDivElement;

  createEffect(() => {
    selectedRowIndex();
    selectedCellIndex();

    /* I need to update scrollTop and scrollLeft of the table
       when the selected cell changes. */
  });

  return (
    <div class="table" tabIndex={0} onKeyDown={onKeyDown} ref={tableRef}>
      <For each={rows}>
        {(_, rowIndex) => (
          <div class="row" classList={{ selected: isSelectedRow(rowIndex()) }}>
            <For each={cells}>
              {(_, cellIndex) => (
                <div
                  class="cell"
                  classList={{ selected: isSelectedRow(rowIndex()) && isSelectedCell(cellIndex()) }}
                  data-row-index={rowIndex()}
                  data-col-index={cellIndex()}
                  onMouseDown={onMouseDown}
                >
                  row {rowIndex()} - cell {cellIndex()}
                </div>
              )}
            </For>
          </div>
        )}
      </For>
    </div>
  );
};
*,
*::after,
*::before {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

:root {
  --cell-height: 27px;
  --cell-border-color: #caddf7;
}

html {
  font-size: 12px;
  font-family: "Tahoma";
}

a,
input,
button,
select,
textarea {
  font-size: inherit;
  font-family: inherit;
}

body {
  padding: 20px;
}

.table {
  width: 400px;
  height: 400px;
  outline: none;
  overflow: auto;
  border: 1px solid #8ea6c3;
  user-select: none;
}

.row {
  display: flex;
  width: fit-content;
}

.cell {
  width: 100px;
  flex-shrink: 0;
  height: var(--cell-height);
  line-height: var(--cell-height);
  padding-inline: 4px;
  border-right: 1px solid var(--cell-border-color);
  border-bottom: 1px solid var(--cell-border-color);

  &.selected {
    box-shadow: inset 0 0 0 1px blue;
  }
}