<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Gustav Ehrenborg</title>
    <description>The latest articles on Forem by Gustav Ehrenborg (@gutsav).</description>
    <link>https://forem.com/gutsav</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F208625%2Fd760d189-4584-4468-85d9-bb3acd22c222.jpeg</url>
      <title>Forem: Gustav Ehrenborg</title>
      <link>https://forem.com/gutsav</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/gutsav"/>
    <language>en</language>
    <item>
      <title>Home Assistant dev.to views sensor</title>
      <dc:creator>Gustav Ehrenborg</dc:creator>
      <pubDate>Tue, 31 May 2022 19:21:37 +0000</pubDate>
      <link>https://forem.com/gutsav/home-assistant-devto-views-sensor-9pl</link>
      <guid>https://forem.com/gutsav/home-assistant-devto-views-sensor-9pl</guid>
      <description>&lt;p&gt;&lt;a href="https://www.home-assistant.io/" rel="noopener noreferrer"&gt;Home Assistant (HA)&lt;/a&gt; is a popular home automation software. Is has tons of built-in features, for example something called &lt;a href="https://www.home-assistant.io/integrations/sensor.rest/" rel="noopener noreferrer"&gt;RESTful Sensor&lt;/a&gt;. It let's you configure an API call and how to process the data to show the data you want on your Home Assistant dashboard.&lt;/p&gt;

&lt;p&gt;This guide will show you how to set it up to show your dev.to total views count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://developers.forem.com/api/" rel="noopener noreferrer"&gt;Dev.to has an API&lt;/a&gt; that can be used to fetch user and article information. However, total post view count is not available in the API, but by adding the view count of each article together will produce this number. &lt;/p&gt;

&lt;p&gt;Go to &lt;a href="https://dev.to/settings/account"&gt;https://dev.to/settings/account&lt;/a&gt; and generate a new API key. It is used for authorization, since the view count is not publicly available. &lt;/p&gt;

&lt;p&gt;The API endpoint that we're gonna use is documented &lt;a href="https://developers.forem.com/api#operation/getUserPublishedArticles" rel="noopener noreferrer"&gt;here&lt;/a&gt;. It returns the published articles, including the attribute &lt;code&gt;page_views_count&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl 'https://dev.to/api/articles/me/published' \
-H 'api_key: &amp;lt;your API key&amp;gt;' \
-H 'per_page: 100'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;will return&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"page_views_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;103&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"page_views_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3245&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure to add the &lt;code&gt;per_page: 100&lt;/code&gt; (or more, up to 1000), since 30 is the default. Once you go over 30 posts, all views will not be counted. &lt;/p&gt;

&lt;h2&gt;
  
  
  Configure the sensor in HA
&lt;/h2&gt;

&lt;p&gt;In the secrets.yml, add &lt;code&gt;dev_to_api_key: &amp;lt;your API key&amp;gt;&lt;/code&gt;. Separating the secrets from the configuration.yml is a good practice. &lt;code&gt;!secret dev_to_api_key&lt;/code&gt; will be substituted for the secret value. &lt;/p&gt;

&lt;p&gt;In configuration.yml, add the following sensor code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;sensor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rest&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dev to views&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://dev.to/api/articles/me/published&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!secret&lt;/span&gt; &lt;span class="s"&gt;dev_to_api_key&lt;/span&gt;
      &lt;span class="na"&gt;per_page&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
    &lt;span class="na"&gt;scan_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;
    &lt;span class="na"&gt;value_template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;value_json&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sum(attribute='page_views_count')&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;unit_of_measurement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;views&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sensor attributes are pretty self-explanatory. Scan interval takes in the amount of seconds between each call (3600 seconds are one hour). The value template needs some explanation though. &lt;/p&gt;

&lt;p&gt;The data received looks like in the snippet above. It is automatically parsed as json and put in the value_json variable. That is piped through the sum method that sums the specific attribute. &lt;/p&gt;

&lt;p&gt;Stop and start HA to reload the configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add to the dashboard in HA
&lt;/h2&gt;

&lt;p&gt;Add a new entity card and select your new Dev.to entity and a suitable icon. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnxlae7jbs13o6be32jp6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnxlae7jbs13o6be32jp6.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will be the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmk35t51hmb55to2zw2j8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmk35t51hmb55to2zw2j8.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Good luck!&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>automation</category>
      <category>api</category>
    </item>
    <item>
      <title>Using dotnet secrets</title>
      <dc:creator>Gustav Ehrenborg</dc:creator>
      <pubDate>Wed, 20 Apr 2022 18:05:01 +0000</pubDate>
      <link>https://forem.com/gutsav/using-dotnet-secrets-43bg</link>
      <guid>https://forem.com/gutsav/using-dotnet-secrets-43bg</guid>
      <description>&lt;p&gt;Instead of putting secret environment variables in the appsetting.json file, the dotnet CLI has functionality to add secrets to a project, without keeping them in the project folder.&lt;/p&gt;

&lt;p&gt;This will decrease the risk of leaking the secrets, for example by adding them to a commit by mistake. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&amp;amp;tabs=windows"&gt;This link leads to Microsoft's own documentation.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheatsheet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet user-secrets &lt;span class="nt"&gt;--help&lt;/span&gt; &lt;span class="c"&gt;# See available commands&lt;/span&gt;

dotnet user-secrets init &lt;span class="c"&gt;# Initiate secrets&lt;/span&gt;

dotnet user-secrets &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"MY_API_KEY"&lt;/span&gt; &lt;span class="s2"&gt;"xyz"&lt;/span&gt; &lt;span class="c"&gt;# Set a secret&lt;/span&gt;

dotnet user-secrets &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"SOME_API.MY_API_KEY"&lt;/span&gt; &lt;span class="s2"&gt;"xyz"&lt;/span&gt; &lt;span class="c"&gt;# Set a nested secret&lt;/span&gt;

&lt;span class="nb"&gt;cat &lt;/span&gt;secrets.json | dotnet user-secrets &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="c"&gt;# Set all secrets in a json file&lt;/span&gt;

dotnet user-secrets list &lt;span class="c"&gt;# List the secrets&lt;/span&gt;

dotnet user-secrets clear &lt;span class="c"&gt;# Delete all secrets in the project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Initiating
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;dotnet user-secrets init&lt;/code&gt; command enables storage of secrets. It adds a line with a GUID in the .csproj file. This GUID is used to identify which secrets belong to which project. The command only needs to be run once for every project, if you are sharing your code with other people, they don't need to rerun it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;UserSecretsId&amp;gt;&lt;/span&gt;71ad533a-ed09-4780-9037-a5aafb01958b&lt;span class="nt"&gt;&amp;lt;/UserSecretsId&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command also creates a folder in &lt;code&gt;~/.microsoft/usersecrets&lt;/code&gt; (on a Mac), with a file contaning the secrets in json format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.microsoft/usersecrets
|
└───71ad533a-ed09-4780-9037-a5aafb01958b
│   └──secrets.json
|
└───&amp;lt;some-other-GUID&amp;gt;
    └──secrets.json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The secrets are saved in plain text in this json file. &lt;/p&gt;

&lt;h3&gt;
  
  
  Setting secrets
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet user-secrets &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"MY_API_KEY"&lt;/span&gt; &lt;span class="s2"&gt;"xyz"&lt;/span&gt;
dotnet user-secrets &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"SOME_API.MY_API_KEY"&lt;/span&gt; &lt;span class="s2"&gt;"xyz"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commands above are self explanatory and the resulting secrets.json file will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"MY_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xyz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SOME_API.MY_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xyz"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are given secrets in the same format as above, these can be imported all at once with the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;secrets.json | dotnet user-secrets &lt;span class="nb"&gt;set&lt;/span&gt;
&lt;span class="go"&gt;Successfully saved 2 secrets to the secret store.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>dotnet</category>
      <category>tooling</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Using dotnet tools</title>
      <dc:creator>Gustav Ehrenborg</dc:creator>
      <pubDate>Tue, 12 Apr 2022 17:03:44 +0000</pubDate>
      <link>https://forem.com/gutsav/using-dotnet-tools-46ln</link>
      <guid>https://forem.com/gutsav/using-dotnet-tools-46ln</guid>
      <description>&lt;p&gt;Dotnet tools are applications that can assist you with various tasks and functionality. There are tools for checking test coverage, deploying to certain cloud services, code formatting, etc... Nuget has a &lt;a href="https://www.nuget.org/packages?packagetype=dotnettool"&gt;list here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The following article will help you get started with the tools using the &lt;a href="https://www.nuget.org/packages/Swashbuckle.AspNetCore.Cli/"&gt;Swagger CLI&lt;/a&gt;, called &lt;code&gt;Swashbuckle.AspNetCore.Cli&lt;/code&gt;, as an example.&lt;/p&gt;

&lt;h1&gt;
  
  
  The cheatsheet
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet new tool-manifest # Create a tool manifest file

dotnet tool list # List installed tools

dotnet tool install ToolName # Install a tool

dotnet tool restore # Install the tools in the manifest file

dotnet tool update ToolName # Update a tool

dotnet tool uninstall ToolName # Uninstall a tool

dotnet ToolName # Run a tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  The walkthrough
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Local and global tools
&lt;/h2&gt;

&lt;p&gt;Tools can be installed locally or globally. Locally installed tools only work for the specific project, but can leverage the tool manifest file which make the tool setup more portable. &lt;/p&gt;

&lt;p&gt;Globally installed tools are available everywhere. The commands for global tools are the same, but the &lt;code&gt;--global&lt;/code&gt; flag must be passed on. &lt;/p&gt;

&lt;h2&gt;
  
  
  The tool manifest file
&lt;/h2&gt;

&lt;p&gt;The command &lt;code&gt;dotnet new tool-manifest&lt;/code&gt; will create a file &lt;code&gt;.config/dotnet-tools.json&lt;/code&gt;. It is a file that contains a list of the installed tools and which version. If you are familiar with the package.json file in npm, this file has the same purpose. The empty file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isRoot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When tools are installed, the &lt;code&gt;tools&lt;/code&gt; object will be populated with data about the tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install a tool
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dotnet tool install Swashbuckle.AspNetCore.Cli&lt;/code&gt; will install the Swagger CLI and add it to the manifest file. If you want a specific version, add &lt;code&gt;--version x.y.z&lt;/code&gt;. If you have multiple Nuget sources added, the installation might fail since the installer will look in the wrong source first. Add &lt;code&gt;--ignore-failed-sources&lt;/code&gt; to circumvent this. &lt;/p&gt;

&lt;p&gt;This is the manifest file with the tool installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"swashbuckle.aspnetcore.cli"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5.5.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"commands"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"swagger"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run a tool
&lt;/h2&gt;

&lt;p&gt;From the manifest file, we can see that the Swagger CLI comes with the command &lt;code&gt;swagger&lt;/code&gt;. This means it can be executed with &lt;code&gt;dotnet swagger&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Swagger CLI generates the Swagger documentation. It only has one command, &lt;code&gt;tofile&lt;/code&gt; and takes two required arguments, the path to the compiled .dll file and which version of the API that should be exported.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dotnet swagger tofile bin/debug/net.../something.dll 1&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  List tools
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;dotnet tool list&lt;/code&gt; to see installed tools. This command is especially useful when listing global tools, since no manifest file exists for these.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;➜ dotnet tool list
Package Id                   Version   Commands   Manifest
-------------------------------------------------------------------------------------
swashbuckle.aspnetcore.cli   5.5.1     swagger    ...tools/.config/dotnet-tools.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Restore tools
&lt;/h2&gt;

&lt;p&gt;Since the manifest file keeps track of the tools, they can easily be restored on a new machine by running &lt;code&gt;dotnet tool restore&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Uninstall tools
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dotnet tool uninstall swashbuckle.aspnetcore.cli&lt;/code&gt; will uninstall the Swagger CLI. &lt;/p&gt;

&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;p&gt;There you have it! Explore the &lt;a href="https://www.nuget.org/packages?packagetype=dotnettool"&gt;available tools&lt;/a&gt;, there's probably a tool out there which will enhance your development experience. &lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Multiple ssh keys</title>
      <dc:creator>Gustav Ehrenborg</dc:creator>
      <pubDate>Sat, 05 Feb 2022 14:18:41 +0000</pubDate>
      <link>https://forem.com/gutsav/multiple-ssh-keys-4gjk</link>
      <guid>https://forem.com/gutsav/multiple-ssh-keys-4gjk</guid>
      <description>&lt;p&gt;Yesterday, I was gonna add my ssh key to Azure DevOps, to be able to contribute to a repository. I was presented with the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxonwk7dus14sexeq81gk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxonwk7dus14sexeq81gk.png" alt="Error from DevOps"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Apparently, &lt;a href="https://developercommunity.visualstudio.com/t/cant-use-ed25519-ssh-key/462263" rel="noopener noreferrer"&gt;DevOps does only support rsa keys&lt;/a&gt;, and not ed25519 keys, even though ed25519 is considered to be more &lt;a href="https://docs.gitlab.com/ee/ssh/index.html#ed25519-ssh-keys" rel="noopener noreferrer"&gt;secure&lt;/a&gt;. The ed25519 is also the default algorithm in &lt;a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent" rel="noopener noreferrer"&gt;GitHub's guide to setting up keys&lt;/a&gt; which many people follow. &lt;/p&gt;

&lt;p&gt;Generating a new rsa key is the solution to this. However, having multiple ssh keys on the same machine presented new problems. I want to use my ed25519 key as much as possible, for example on repositories on GitHub, but how do I tell my computer to use the rsa key for the repository on Azure DevOps? &lt;/p&gt;

&lt;p&gt;Apparently, there is a file, &lt;code&gt;~/.ssh/config&lt;/code&gt;, that solves these kind of troubles. You probably have a couple of lines in it, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host *
  AddKeysToAgent yes
  IdentityFile ~/.ssh/id_ed25519
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These lines tells the ssh client to automatically add keys to the running agent and use the key in &lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt;, for all hosts.&lt;/p&gt;

&lt;p&gt;By adding these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host ssh.dev.azure.com
  IdentityFile ~/.ssh/id_rsa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We tell the ssh client to use the key &lt;code&gt;~/.ssh/id_rsa&lt;/code&gt; when connecting to &lt;code&gt;ssh.dev.azure.com&lt;/code&gt;, which is the host of the repositories.&lt;/p&gt;

&lt;p&gt;Simple as that!&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus 1
&lt;/h2&gt;

&lt;p&gt;If you tired of writing the entire &lt;code&gt;ssh username@some-host.com:xxxx&lt;/code&gt; when using ssh, aliases can be configured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host my-alias
    HostName some-host.com
    User username
    Port xxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;ssh my-alias&lt;/code&gt; will be enough to connect. &lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus 2
&lt;/h2&gt;

&lt;p&gt;If you ever find yourself on a network were port 22 is blocked and you want to use ssh to connect GitHub, you can use ssh over port 443 instead. Just add this configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host github.com
  Hostname ssh.github.com
  Port 443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>ssh</category>
      <category>azure</category>
      <category>git</category>
    </item>
    <item>
      <title>Firebase Function to populate Algolia</title>
      <dc:creator>Gustav Ehrenborg</dc:creator>
      <pubDate>Thu, 03 Feb 2022 15:25:46 +0000</pubDate>
      <link>https://forem.com/gutsav/firebase-function-to-populate-algolia-47g7</link>
      <guid>https://forem.com/gutsav/firebase-function-to-populate-algolia-47g7</guid>
      <description>&lt;p&gt;This is a guide for setting up a Firebase Function that will receive data and update an Algolia index. Algolia is a widely used search engine, and populating and updating its data using webhooks and serverless functions is a common approach to integrate with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup!
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Firebase setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm install -g firebase-tools&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;firebase login&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;firebase init functions&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This will result in a file, &lt;code&gt;functions/src/index.js&lt;/code&gt; in which all code will be written. &lt;/p&gt;

&lt;h3&gt;
  
  
  Algolia setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create an account, create an index&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings&lt;/strong&gt; and then &lt;strong&gt;API keys&lt;/strong&gt; to get the application ID and admin API key. You will also need the name of the index. &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Add environment variables
&lt;/h3&gt;

&lt;p&gt;Exchange the placeholders in the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;firebase functions:config:set algolia.application_id="THE APPLICATION ID" algolia.api_key="THE API KEY" algolia.index_name=”THE INDEX NAME”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;firebase functions:config:get&lt;/code&gt; to display the variables. &lt;/p&gt;

&lt;p&gt;To run the function locally, you'll need to save the variables to a specfic file, using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;firebase functions:config:get &amp;gt; .runtimeconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference the variables in code for easy access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALGOLIA_APPLICATION_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;algolia&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALGOLIA_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;algolia&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALGOLIA_INDEX_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;algolia&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install dependencies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm install algoliasearch –save&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...and import it with&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const algoliasearch = require('algoliasearch');&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Code!
&lt;/h2&gt;

&lt;p&gt;Add the following function. The &lt;code&gt;functions.https.onRequest&lt;/code&gt; will expose it and bind it to a specific endpoint. Multiple such function can exist in the same file, but we will only have one. Express is the underlying framework, and the request and response objects work similar to when using Express directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publishedEntry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getAlgoliaIndex&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DELETED_ENTRY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deleteObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BOOK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createAlgoliaBookRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;saveObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data sent in contained in the &lt;code&gt;request.body&lt;/code&gt;. The &lt;code&gt;getAlgoliaIndex()&lt;/code&gt; method is presented below. The if-statement idenfies the action, or the type, and performs the right action. &lt;/p&gt;

&lt;p&gt;Using the Algolia SDK, the following function will return a SearchIndex. It's on this object that inserts, updates, deletions, etc, of objects are made. The &lt;a href="https://www.algolia.com/doc/api-client/methods/indexing/"&gt;Algolia documentation&lt;/a&gt; has all the available methods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getAlgoliaIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;algoliasearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ALGOLIA_APPLICATION_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ALGOLIA_API_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ALGOLIA_INDEX_NAME&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;createAlgoliaBookRecord()&lt;/code&gt; filters out just the attributes we're interested in having in our index. A book might have a price, a number of pages, etc, that we do not want to search in. The same result can be achieved by configuring the searchableAttributes in Algolia, however, the more data that is added to the index, the slower it will become. &lt;/p&gt;

&lt;p&gt;Our book model has an ObjectID, that we pass along to Algolia. All Algolia records must have an ObjectId, and if it's not supplied, Algolia will create one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createAlgoliaBookRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isbn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;objectID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isbn&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy!
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm run deploy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deploy script will run the linter, build the code, upload the code and environment variables and deploy it. The URL of the function(s) will be presented. &lt;/p&gt;

&lt;h3&gt;
  
  
  Try it out
&lt;/h3&gt;

&lt;p&gt;POST the following data to the function URL...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PUBLISHED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BOOK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"61fbdf833dbc31f5935dea1b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Some book name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Some Author"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isbn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and watch how it is added to Algolia.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--DaopDBNp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p0aauf032u6exi9ia2l8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--DaopDBNp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p0aauf032u6exi9ia2l8.png" alt="A record in Algolia" width="880" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To test the deletion, only the following data is needed in the POST:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DELETED_ENTRY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"61fbdf833dbc31f5935dea1b"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations! You now have a Firebase Function that is updating an Algolia index. &lt;/p&gt;

</description>
      <category>algolia</category>
      <category>firebase</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Webhook your weight with Withings API</title>
      <dc:creator>Gustav Ehrenborg</dc:creator>
      <pubDate>Mon, 24 Jan 2022 14:18:01 +0000</pubDate>
      <link>https://forem.com/gutsav/webhook-your-weight-with-withings-api-1okk</link>
      <guid>https://forem.com/gutsav/webhook-your-weight-with-withings-api-1okk</guid>
      <description>&lt;p&gt;This is a short walkthrough on how to authenticate with and use the Withings API. Its purpose is to explain how to get going with the code in &lt;a href="https://github.com/GuTsaV/trumpet"&gt;this GitHub repo&lt;/a&gt;. The goal of the code is to be lightweight and have few dependencies, making it possible to run on weaker computers, often used in home automation. &lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You'll need a Withings product. I use a scale. The webhook is the same for all products.&lt;/li&gt;
&lt;li&gt;You'll also need a domain name pointing towards your server and an open port, or use ngrok. This will be your callback url.&lt;/li&gt;
&lt;li&gt;I write cURL commands below, if you prefer to use Postman, it has an Import button to convert from cURL. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Withings account setup
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://account.withings.com/partner/dashboard_oauth2"&gt;Click here to create a partner account&lt;/a&gt;. You’ll receive a client id and a consumer secret, hereafter called client secret. &lt;/p&gt;

&lt;h2&gt;
  
  
  Withings authentication
&lt;/h2&gt;

&lt;p&gt;The authentication flow contains quite many steps, and we'll cheat by running some requests manually, instead of implementing the whole process in code. &lt;a href="https://developer.withings.com/api-reference/#tag/oauth2"&gt;This links leads to the authentication documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Using your client id, client secret and callback url, complete the following URL and enter it in a &lt;strong&gt;browser&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://account.withings.com/oauth2_user/authorize2?response_type=code&amp;amp;client_id=&amp;lt;your client id&amp;gt;&amp;amp;state=whatever&amp;amp;scope=user.info,user.metrics&amp;amp;redirect_uri=&amp;lt;your callback url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;em&gt;Allow this app&lt;/em&gt;, which will redirect to your callback url with a query param called &lt;strong&gt;code&lt;/strong&gt;. This is an authorization code and it is valid for 30 seconds. &lt;/p&gt;

&lt;p&gt;Then, fill out your values in this cURL command and run it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl --data "action=requesttoken&amp;amp;grant_type=authorization_code&amp;amp;client_id=&amp;lt;your client id&amp;gt;&amp;amp;client_secret=&amp;lt;client secret&amp;gt;&amp;amp;code=&amp;lt;the authorization code from above&amp;gt;&amp;amp;redirect_uri=&amp;lt;your callback url&amp;gt;" 'https://wbsapi.withings.net/v2/oauth2'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will receive a JSON response, containing a user id, an access token and a refresh token. The access token is needed to fetch data, and it's valid for three hours. The refresh token is used to get a new access token, and every time the access token is renewed, the refresh token is also renewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The webhook server
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Persisting the user id and refresh tokens
&lt;/h3&gt;

&lt;p&gt;As mentioned above, the refresh token is also renewed, thus demanding a way both to persist and update it. A database would be the best option, but this would require more dependencies. Instead, we'll persist the user id and refresh tokens in a file on disk. &lt;/p&gt;

&lt;p&gt;Two really simple methods is all that is needed, one for reading and one for writing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getRefreshToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`./&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;persistRefreshToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`./&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last step is also to create a file for your user id and current refresh token. Use this command: &lt;code&gt;echo '&amp;lt;refresh token&amp;gt;' &amp;gt;&amp;gt; &amp;lt;userid&amp;gt;&lt;/code&gt;, for example: &lt;code&gt;echo 'xxyyzz112233' &amp;gt;&amp;gt; 1234&lt;/code&gt;. &lt;/p&gt;

&lt;h3&gt;
  
  
  Getting an access token (and a refresh token)
&lt;/h3&gt;

&lt;p&gt;Axios is used to make the API request. An axios instance for communicating with the Withings API is made, setting the defaults of the API. Note that the data is not sent as JSON, it is form-data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withingsAxios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://wbsapi.withings.net&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/GuTsaV/trumpet/blob/master/index.js#L25"&gt;&lt;code&gt;refreshAccessToken&lt;/code&gt;&lt;/a&gt; function fetches the current refresh token from file, uses it to obtain both an access and a new refresh token, and then persists the new refresh token. &lt;a href="https://developer.withings.com/api-reference/#operation/oauth2-refreshaccesstoken"&gt;The Withings endpoint used is described more here.&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refreshAccessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentRefreshToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;getRefreshToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;requesttoken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;refresh_token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WITHINGS_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WITHINGS_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentRefreshToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;withingsAxios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v2/oauth2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newRefreshAccessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;persistRefreshToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newRefreshAccessToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fetching a measurement
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/GuTsaV/trumpet/blob/master/index.js#L46"&gt;&lt;code&gt;getMeasures&lt;/code&gt;&lt;/a&gt; function fetches mesurements from your Withings appliances. An access token must be supplied, as well as a start and an end date. The dates should be Unix timestamps. However, we don't have to worry about those, because the callback will include timestamps. &lt;/p&gt;

&lt;p&gt;Again, the axios instance is used, the data is form-data. The access token is included in the header. &lt;a href="https://developer.withings.com/api-reference/#operation/measure-getmeas"&gt;This links to the documentation.&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getMeasures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getmeas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;meastype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;enddate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;withingsAxios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/measure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;measuregrps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Webhook server using Express
&lt;/h3&gt;

&lt;p&gt;A simple Express endpoint is used to receive the webhook call. The call does not include any measurements, only timestamps, but your code will get a new access token and fetch the measurement data using the functions above, using the timestamps that were included in the webhook.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/withingsWebhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startdate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;endDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enddate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;refreshAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;measures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;getMeasures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weightInGrams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;measures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;measures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;weightInGrams&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Subscribing to the Withings webhook
&lt;/h3&gt;

&lt;p&gt;The last step is to tell Withings to post to your webhook. We will do this manually as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl --header "Authorization: Bearer &amp;lt;your access token&amp;gt;" --data "action=subscribe&amp;amp;callbackurl=&amp;lt;your callback url&amp;gt;&amp;amp;appli=1&amp;amp;comment=comment" 'https://wbsapi.withings.net/notify'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finalizing this will have your webhook endpoint be called when a new measurement is made. There is, unfortunately, about 30 seconds delay from Withings. &lt;/p&gt;

&lt;h2&gt;
  
  
  The optional last steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Add it to cron to start at system reboot&lt;/li&gt;
&lt;li&gt;Publish your weight on mqtt&lt;/li&gt;
&lt;li&gt;Have Home Assistant receive it&lt;/li&gt;
&lt;li&gt;...and configure Google Home to say it out load!&lt;/li&gt;
&lt;li&gt;Add error handling, and tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For full code, head over here: &lt;a href="https://github.com/GuTsaV/trumpet"&gt;https://github.com/GuTsaV/trumpet&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/g0XYX7SInqM"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

</description>
      <category>withings</category>
      <category>javascript</category>
    </item>
    <item>
      <title>My thoughts on the Contentful Certification</title>
      <dc:creator>Gustav Ehrenborg</dc:creator>
      <pubDate>Fri, 24 Dec 2021 09:48:27 +0000</pubDate>
      <link>https://forem.com/gutsav/my-thoughts-on-the-contentful-certification-4359</link>
      <guid>https://forem.com/gutsav/my-thoughts-on-the-contentful-certification-4359</guid>
      <description>&lt;p&gt;I recently passed the Contentful certification, and became a "certified professional". I want to share my thoughts about the exam and recommendations on what to study to pass the exam. Please remember that the following reflects the conditions when I took the exam (winter 2021) and things might have changed. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Contentful Training Center
&lt;/h2&gt;

&lt;p&gt;Contentful has its own training center, &lt;a href="https://training.contentful.com/"&gt;training.contentful.com&lt;/a&gt;. It gives you access to courses, consisting of text, interactive images, videos and knowledge checks. The content is good, short pieces of text describes features and concepts, and videos show how to work with them. There is some overlap in the different courses, but its easy skip through. However, it is quite difficult to get an overview of the available material in the training center. There are activities, paths and courses, and its difficult to find the way back to where you were. &lt;/p&gt;

&lt;p&gt;I went throught an activity called Contentful Essentials. It has paths for developers, content modelers and content authors. Since I wanted to be prepared for the exam, I took all three paths. The courses in the paths are good, but sometimes too basic. Being a developer, I especially liked the developer courses, since they showcased specific Contentful features. There are excellent videos on how to setup approval flows, configure webhooks, etc. &lt;/p&gt;

&lt;h2&gt;
  
  
  Contentful Developer Documentation
&lt;/h2&gt;

&lt;p&gt;There is also technical documentation available at &lt;a href="https://www.contentful.com/developers/docs/"&gt;contentful.com/developers/docs/&lt;/a&gt;. The documentation includes API references, videos showing features and more. Since the exam is said to be not only for developers, but also for project managers and content architects, I did not study the developer documentation as much as I would have wished. In retrospect, the exam contained quite many technical questions that are covered by these docs. &lt;/p&gt;

&lt;h2&gt;
  
  
  The exam
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;50 questions&lt;/li&gt;
&lt;li&gt;75 minutes&lt;/li&gt;
&lt;li&gt;70 % score needed to pass&lt;/li&gt;
&lt;li&gt;"closed-book", no notes or google allowed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You are presented with one question at a time and you can't go back to change your answers! Your score is presented to you directly after submitting the last question. &lt;/p&gt;

&lt;p&gt;The exam was more difficult than I anticipated. I thought the questions were gonna be as difficult as the knowledge checks in the training courses, or should I say as &lt;em&gt;easy&lt;/em&gt; as the knowledge checks... I was wrong. &lt;/p&gt;

&lt;p&gt;The exam does contain some easy questions, however, there are a lot of questions on details, for example on the capabilities of the Contentful CLI and the SLAs.&lt;/p&gt;

&lt;p&gt;To make things even more difficult, there are a lot of questions where you should select all the correct answers out of the four alternatives, and the alternatives are slightly rephrased from what you have seen before. &lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Don't be fooled by the easy knowledge checks. Prepare for technical questions by spending more time reading the developer documentation, but make sure to go through some training courses, especially if you are lacking content model experience. &lt;/p&gt;

</description>
      <category>contentful</category>
      <category>certification</category>
      <category>content</category>
    </item>
  </channel>
</rss>
