<?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>DEV Community: Artur Piszek</title>
    <description>The latest articles on DEV Community by Artur Piszek (@artpi).</description>
    <link>https://dev.to/artpi</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%2F790654%2F50ecd695-6769-4e73-bd44-df1ecec09be2.jpeg</url>
      <title>DEV Community: Artur Piszek</title>
      <link>https://dev.to/artpi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/artpi"/>
    <language>en</language>
    <item>
      <title>Live demo of your WP Plugin with GH releases and WP Playground</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Sat, 04 Jan 2025 16:37:44 +0000</pubDate>
      <link>https://dev.to/artpi/live-demo-of-your-wp-plugin-with-gh-releases-and-wp-playground-1jm1</link>
      <guid>https://dev.to/artpi/live-demo-of-your-wp-plugin-with-gh-releases-and-wp-playground-1jm1</guid>
      <description>&lt;p&gt;I have been relying on Github actions to publish my plugins to the .org repositiory for some now. It feels magical – you publish a release and it builds, zips and uploads the plugin wherever it needs to go.&lt;/p&gt;

&lt;p&gt;With WP Playground, you can take that process to another level – the moment you hit publish, new version of your plugin will be immediately available for testing right in browser, without installing and exposing existing sites. This means anybody can try out your plugin before downloading, without installing it with zero downsides, in seconds.&lt;/p&gt;

&lt;h1&gt;
  
  
  What is WordPress Playground?
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://wordpress.org/playground/" rel="noopener noreferrer"&gt;Playground is a way to run an entire instance of WordPress in the browser&lt;/a&gt;, with no external backend. It is destroyed on reload, but perfect for trying out changes before installing on your site&lt;/p&gt;

&lt;h2&gt;
  
  
  Action to build plugin on release
&lt;/h2&gt;

&lt;p&gt;I’m assuming you are using Github as a sane person. This Github action will run your plugin build process, zip it, and upload it as a release asset. Remember to change your zip file name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Build and Upload Plugin ZIP
on:
  release:
    types: [published]

jobs:
  build-and-zip:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout Repository
      uses: actions/checkout@v4

    - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
        node-version: 20 # Use Node.js v20

    - name: Install Node.js Dependencies
      run: npm install

    - name: Set up PHP and Composer
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.2' # Adjust PHP version as needed
        extensions: mbstring, zip

    - name: Install Composer Dependencies (Production Only)
      run: composer install --no-dev --optimize-autoloader

    - name: Build Plugin
      run: npm run build

    - name: Create Plugin ZIP
      run: npm run plugin-zip

    - name: Upload Release Asset
      uses: actions/upload-release-asset@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ github.event.release.upload_url }}
        asset_path: ${{github.workspace}}/wp-personal-os.zip
        asset_name: wp-personal-os.zip
        asset_content_type: application/zip

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

&lt;/div&gt;



&lt;p&gt;You will also have to make sure your &lt;code&gt;package.json&lt;/code&gt; has npm scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    "scripts": {
        "build": "wp-scripts build",
        "plugin-zip": "wp-scripts plugin-zip",
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to give your Github actions write access:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2025/01/Zrzut-ekranu-2025-01-4-o-17.21.59.png?ssl=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.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%2Fvmbet07dplgh6b43iat0.png" width="620" height="305"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Link to latest plugin zip
&lt;/h2&gt;

&lt;p&gt;There is undocumented URL for the “release asset of the latest release” on Github:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/USER/PROJECT/releases/latest/download/ASSET.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace user, project and asset accordingly&lt;/p&gt;

&lt;h3&gt;
  
  
  Create your first release
&lt;/h3&gt;

&lt;p&gt;Create your first Github release for the action to run&lt;/p&gt;

&lt;h3&gt;
  
  
  Your Playground Blueprint
&lt;/h3&gt;

&lt;p&gt;With this link, you can now create a playground blueprint that will install the plugin to a temporary WordPress site that runs in the browser. You can also add additional steps that will create sample data for the best demo experience. Here is the very basic blueprint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "steps": [
    {
      "step": "installPlugin",
      "pluginData": {
        "resource": "url",
        "url": "https://github.com/USER/PROJECT/releases/latest/download/ASSET.zip"
      }
    }
  ],
  "login": true
}

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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://playground.wordpress.net/builder/builder.html" rel="noopener noreferrer"&gt;I highly recommend playground builder to work on these blueprints and debug them.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/artpi/personalOS/?tab=readme-ov-file#-open-personalos-playground" rel="noopener noreferrer"&gt;Once you have your blueprint, you can create a special link and drop it in the README of your plugin, like here&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Link will load playground&lt;/li&gt;
&lt;li&gt;It will install latest release of your plugin&lt;/li&gt;
&lt;li&gt;redirect the user to the proper page so they can see what your plugin is all about&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2025/01/Zrzut-ekranu-2025-01-4-o-17.33.47.png?ssl=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.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%2Fwabwa3bb2nxxtbkbvem7.png" width="620" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://piszek.com/2025/01/04/wp-plugin-live-demo/" rel="noopener noreferrer"&gt;Live demo of your WP Plugin with GH releases and WP Playground&lt;/a&gt; appeared first on &lt;a href="https://piszek.com" rel="noopener noreferrer"&gt;Artur Piszek&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>wordpress</category>
      <category>github</category>
    </item>
    <item>
      <title>WordPress Data Views: Basic setup</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Sun, 22 Dec 2024 18:30:35 +0000</pubDate>
      <link>https://dev.to/artpi/wordpress-data-views-basic-setup-4dnb</link>
      <guid>https://dev.to/artpi/wordpress-data-views-basic-setup-4dnb</guid>
      <description>&lt;p&gt;WordPress has some sweet, sweet new APIs. One of them I am super excited about is Data Views. Data Views is the API to introduce modern table views into WordPress and I was excited to try it out for my &lt;a href="https://github.com/artpi/personalOS/" rel="noopener noreferrer"&gt;Personal OS plugin&lt;/a&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Example: Custom taxonomy
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2024/12/Zrzut-ekranu-2024-12-22-o-20.42.45.png?ssl=1" rel="noopener noreferrer"&gt;&lt;img src="https://media2.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%2Fanbgnft4d4au6p48fudr.png" width="620" height="203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my example, I wanted to create a “Bucketlist” page in my WP-Admin to render a custom taxonomy that would hold my &lt;a href="https://piszek.com/bucketlist/" rel="noopener noreferrer"&gt;Bucketlist&lt;/a&gt;. We will need 3 elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We will need to hook into PHP to create a new WP-Admin page and render React there&lt;/li&gt;
&lt;li&gt;React code to display Data Views&lt;/li&gt;
&lt;li&gt;A build process to compile this React code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  React Gutenberg code
&lt;/h4&gt;

&lt;p&gt;I will start with the actual Gutenberg React code. I put my code together using &lt;a href="https://developer.wordpress.org/news/2024/08/using-data-views-to-display-and-interact-with-data-in-plugins/" rel="noopener noreferrer"&gt;this fantastic tutorial&lt;/a&gt;and the code from &lt;a href="https://wordpress.github.io/gutenberg/?path=/docs/dataviews-dataviews--docs" rel="noopener noreferrer"&gt;WordPress Storybook&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Icon, __experimentalHStack as HStack } from '@wordpress/components';
import domReady from '@wordpress/dom-ready';
import { useState, useMemo, createRoot } from '@wordpress/element';
// Per https://github.com/WordPress/gutenberg/tree/trunk/packages/dataviews :
// Important note If you're trying to use the DataViews component in a WordPress plugin or theme and you're building your scripts using the @wordpress/scripts package, you need to import the components from @wordpress/dataviews/wp instead of @wordpress/dataviews.
import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews/wp';
import { __ } from '@wordpress/i18n';
import { useEntityRecords } from '@wordpress/core-data';
import './style.scss';
import { trash, flag } from '@wordpress/icons';

function NotebookAdmin() {
    const [view, setView] = useState( {
        type: 'table',
        search: '',
        page: 1,
        perPage: 100,
        fields: ['name', 'description', 'flags', 'count'],
        layout: {},
        filters: [],
        sort: {
            order: 'asc',
            orderby: 'name',
        },
    } );

    // Our setup in this custom taxonomy.
    const fields = [
        {
            label: __( 'Name', 'your-textdomain' ),
            id: 'name',
            enableHiding: false,
            enableGlobalSearch: true,
            type: 'string',
        },
        {
            label: __( 'Description', 'your-textdomain' ),
            id: 'description',
            enableSorting: false,
            enableGlobalSearch: true,
            type: 'string',
        },
        {
            label: __( 'Flags', 'your-textdomain' ),
            id: 'flags',
            header: (
                &amp;lt;HStack spacing={ 1 } justify="start"&amp;gt;
                    &amp;lt;Icon icon={ flag } /&amp;gt;
                    &amp;lt;span&amp;gt;{ __( 'Flags', 'your-textdomain' ) }&amp;lt;/span&amp;gt;
                &amp;lt;/HStack&amp;gt;
            ),
            type: 'array',
            render: ( { item } ) =&amp;gt; {
                return item.meta?.flag?.join( ', ' ) || '';
            },
            enableSorting: false,
        },
        {
            label: __( 'Count', 'your-textdomain' ),
            id: 'count',
            enableSorting: true,
            enableGlobalSearch: false,
            type: 'number',
        },
    ];

    // We will use the entity records hook to fetch all the items from the "notebook" custom taxonomy
    const { records } = useEntityRecords( 'taxonomy', 'notebook', {
        per_page: -1,
        page: 1,
        hide_empty: false,
    } );

    // filterSortAndPaginate works in memory. We theoretically could pass the parameters to backend to filter sort and paginate there.
    const { data: shownData, paginationInfo } = useMemo( () =&amp;gt; {
        return filterSortAndPaginate( records, view, fields );
    }, [view, records] );

    return (
        &amp;lt;DataViews
            getItemId={ ( item ) =&amp;gt; item.id.toString() }
            paginationInfo={ paginationInfo }
            data={ shownData }
            view={ view }
            fields={ fields }
            onChangeView={ setView }
            actions={ [
                {
                    id: 'delete',
                    label: __( 'Delete', 'your-textdomain' ),
                    icon: trash,
                    callback: async ( items ) =&amp;gt; {
                        // Implement delete functionality
                        console.log( 'Delete items:', items );
                    },
                },
            ] }
            defaultLayouts={ {
                table: {
                    // Define default table layout settings
                    spacing: 'normal',
                    showHeader: true,
                },
            } }
        /&amp;gt;
    );
}

domReady( () =&amp;gt; {
    const root = createRoot( document.getElementById( 'bucketlist-root' ) );
    root.render( &amp;lt;NotebookAdmin /&amp;gt; );
} );

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

&lt;/div&gt;



&lt;p&gt;In your plugin you have to import the components from &lt;strong&gt;@wordpress/dataviews/wp&lt;/strong&gt; instead of @wordpress/dataviews. I expect this is a recent change and it will be further evolve. You also need to add&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“@wordpress/dependency-extraction-webpack-plugin”: “^6.14.0”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To your devpependencies (check at the end of this post)&lt;/p&gt;

&lt;h1&gt;
  
  
  If you you import directly from @wordpress/dataviews you will get errors like these (click to expand)
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;column-header-menu.js:105 Uncaught TypeError: Cannot read properties of undefined (reading 'Group')
    at HeaderMenu (column-header-menu.js:105:44)
    at renderWithHooks (react-dom.js?ver=18.3.1:15496:20)
    at updateForwardRef (react-dom.js?ver=18.3.1:19255:22)
    at beginWork (react-dom.js?ver=18.3.1:21685:18)
    at HTMLUnknownElement.callCallback (react-dom.js?ver=18.3.1:4151:16)
    at Object.invokeGuardedCallbackDev (react-dom.js?ver=18.3.1:4200:18)
    at invokeGuardedCallback (react-dom.js?ver=18.3.1:4264:33)
    at beginWork$1 (react-dom.js?ver=18.3.1:27500:9)
    at performUnitOfWork (react-dom.js?ver=18.3.1:26606:14)
    at workLoopSync (react-dom.js?ver=18.3.1:26515:7)
react-dom.js?ver=18.3.1:18714 The above error occurred in the &amp;lt;AddFilterMenu&amp;gt; component:

    at AddFilterMenu (http://localhost:8901/wp-content/plugins/personalos/modules/bucketlist/js/build/admin.js?ver=5a41136…:352:3)
    at div
    at FiltersToggle (http://localhost:8901/wp-content/plugins/personalos/modules/bucketlist/js/build/admin.js?ver=5a41136…:742:3)
    at div
    at http://localhost:8901/wp-includes/js/dist/components.js?ver=490baf0…:14024:47
    at UnforwardedView (http://localhost:8901/wp-includes/js/dist/components.js?ver=490baf0…:14903:3)
    at UnconnectedHStack (http://localhost:8901/wp-includes/js/dist/components.js?ver=490baf0…:32221:23)
    at div
    at http://localhost:8901/wp-includes/js/dist/components.js?ver=490baf0…:14024:47
    at UnforwardedView (http://localhost:8901/wp-includes/js/dist/components.js?ver=490baf0…:14903:3)
    at UnconnectedHStack (http://localhost:8901/wp-includes/js/dist/components.js?ver=490baf0…:32221:23)
    at div
    at DataViews (http://localhost:8901/wp-content/plugins/personalos/modules/bucketlist/js/build/admin.js?ver=5a41136…:2415:3)
    at BucketlistAdmin (http://localhost:8901/wp-content/plugins/personalos/modules/bucketlist/js/build/admin.js?ver=5a41136…:14023:87)

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
3
react-dom.js?ver=18.3.1:18714 The above error occurred in the &amp;lt;ForwardRef(HeaderMenu)&amp;gt; component:

    at HeaderMenu (http://localhost:8901/wp-content/plugins/personalos/modules/bucketlist/js/build/admin.js?ver=5a41136…:3894:3)
    at th
    at tr
    at thead
    at table
    at ViewTable (http://localhost:8901/wp-content/plugins/personalos/modules/bucketlist/js/build/admin.js?ver=5a41136…:4363:3)
    at DataViewsLayout (http://localhost:8901/wp-content/plugins/personalos/modules/bucketlist/js/build/admin.js?ver=5a41136…:1526:69)
    at div
    at DataViews (http://localhost:8901/wp-content/plugins/personalos/modules/bucketlist/js/build/admin.js?ver=5a41136…:2415:3)
    at BucketlistAdmin (http://localhost:8901/wp-content/plugins/personalos/modules/bucketlist/js/build/admin.js?ver=5a41136…:14023:87)

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
react-dom.js?ver=18.3.1:26972 Uncaught TypeError: Cannot read properties of undefined (reading 'Item')
    at add-filter.js:32:31
    at Array.map (&amp;lt;anonymous&amp;gt;)
    at AddFilterMenu (add-filter.js:31:1)
    at renderWithHooks (react-dom.js?ver=18.3.1:15496:20)
    at mountIndeterminateComponent (react-dom.js?ver=18.3.1:20113:15)
    at beginWork (react-dom.js?ver=18.3.1:21636:18)
    at beginWork$1 (react-dom.js?ver=18.3.1:27475:16)
    at performUnitOfWork (react-dom.js?ver=18.3.1:26606:14)
    at workLoopSync (react-dom.js?ver=18.3.1:26515:7)
    at renderRootSync (react-dom.js?ver=18.3.1:26483:9)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  PHP end
&lt;/h4&gt;

&lt;p&gt;In PHP, you have to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set up a WP-Admin page&lt;/li&gt;
&lt;li&gt;enqueue your script&lt;/li&gt;
&lt;li&gt;enqueue your style
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );

public function add_admin_menu(): void {
    add_menu_page(
        'Bucketlist',
        'Bucketlist',
        'read',
        'bucketlist',
        array( $this, 'render_admin_page' ),
        'dashicons-list-view'
    );
}

public function render_admin_page(): void {
    ?&amp;gt;
    &amp;lt;div class="wrap"&amp;gt;
        &amp;lt;h1&amp;gt;&amp;lt;?php echo esc_html( get_admin_page_title() ); ?&amp;gt;&amp;lt;/h1&amp;gt;
        &amp;lt;div id="bucketlist-root"&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;?php
}

public function enqueue_admin_scripts( string $hook ): void {
    if ( 'toplevel_page_bucketlist' !== $hook ) {
        return;
    }

    $asset_file = plugin_dir_path( __FILE__ ) . 'js/build/admin.asset.php';
    $asset = require $asset_file;

    wp_enqueue_script(
        'bucketlist-admin',
        plugin_dir_url( __FILE__ ) . 'js/build/admin.js',
        $asset['dependencies'],
        $asset['version'],
        false
    );

    wp_enqueue_style(
        'bucketlist-admin',
        plugin_dir_url( __FILE__ ) . 'js/build/style-admin.css',
        array(),
        $asset['version']
    );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Build code
&lt;/h4&gt;

&lt;p&gt;This is my &lt;code&gt;package.json&lt;/code&gt;. Remember to add &lt;code&gt;”@wordpress/dependency-extraction-webpack-plugin”: “^6.14.0″&lt;/code&gt; in this specific version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Otherwise, &lt;code&gt;wp-scripts&lt;/code&gt; will treat dataviews as WordPress dependency and it won’t render in your app!&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;{
    "name": "bucketlist",
    "version": "1.0.0",
    "description": "Bucketlist module for PersonalOS",
    "scripts": {
        "build": "wp-scripts build js/src/admin.js --output-path=js/build",
        "start": "wp-scripts start js/src/admin.js --output-path=js/build"
    },
    "dependencies": {
        "@wordpress/components": "^25.0.0",
        "@wordpress/data": "^9.0.0",
        "@wordpress/dataviews": "^4.10.0",
        "@wordpress/element": "^5.0.0"
    },
    "devDependencies": {
        "@wordpress/scripts": "^30.7.0",
        "@wordpress/dependency-extraction-webpack-plugin": "^6.14.0"
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://developer.wordpress.org/news/2024/08/using-data-views-to-display-and-interact-with-data-in-plugins/" rel="noopener noreferrer"&gt;For style tweaks and the rest you can refer to this fantastic walkthrough&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://piszek.com/2024/12/22/wordpress-data-views-basic-setup/" rel="noopener noreferrer"&gt;WordPress Data Views: Basic setup&lt;/a&gt; appeared first on &lt;a href="https://piszek.com" rel="noopener noreferrer"&gt;Artur Piszek&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>wordpress</category>
      <category>gutenberg</category>
    </item>
    <item>
      <title>Building sync engines in WordPress</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Fri, 21 Jun 2024 13:50:11 +0000</pubDate>
      <link>https://dev.to/artpi/building-sync-engines-in-wordpress-fge</link>
      <guid>https://dev.to/artpi/building-sync-engines-in-wordpress-fge</guid>
      <description>&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forgpki9t7w846xqnmn7o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forgpki9t7w846xqnmn7o.png" alt="Image description" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s say you want to synchronize some data in your WordPress with an external service. How do you do that? For my little &lt;a href="https://github.com/artpi/personalOS/"&gt;“Second Brain” WordPress plugin&lt;/a&gt;, I have implemented 2 sync services so far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Readwise&lt;/li&gt;
&lt;li&gt;Evernote&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both of them synchronize data with a custom post type called “&lt;code&gt;notes&lt;/code&gt;“. Here is what I learned&lt;/p&gt;

&lt;h2&gt;
  
  
  WP-Cron
&lt;/h2&gt;

&lt;p&gt;All sync software has some kind of background service that looks for changes and syncs the detected ones. We do not have operating system access in WordPress to fire an actual process, but we have &lt;strong&gt;wp-cron&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;WP-Cron is a job management system designed for periodic or scheduled maintenance actions that need to be performed: like publishing scheduled posts, checking plugin updates etc.&lt;/p&gt;

&lt;p&gt;So here is what we are going to do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We are going to create an &lt;code&gt;hourly&lt;/code&gt; cron job for our sync.&lt;/li&gt;
&lt;li&gt;Each job run will work through a batch – a limited number of elements (I gravitated towards 100 of updates)&lt;/li&gt;
&lt;li&gt;Whenever this job finishes a run, it will check if more updates are waiting to sync. If so,

&lt;ul&gt;
&lt;li&gt;It will cancel the next run (in one hour)&lt;/li&gt;
&lt;li&gt;Schedule itself in 1 minute&lt;/li&gt;
&lt;li&gt;Unschedule any “regular” – hourly sync events so that at any time, there is only one sync event waiting&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We are doing all these because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We don’t want any of these runs to be too compute and memory-intensive. WordPress is running on a variety of hosts with different limitations, and we don’t want our run to be killed&lt;/li&gt;
&lt;li&gt;We don’t know how many updates in total we are expecting. I have 8000+ notes in my Evernote account, so the initial sync with Evernote will take quite some time. That’s why we redo the job every minute if there are pending updates&lt;/li&gt;
&lt;li&gt;We don’t want to poll any external service too often because rate limits are a pain to manage&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  New cron job
&lt;/h3&gt;

&lt;p&gt;I have a little abstraction layer of modules in my code, but essentially, each service has its own module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class External_Service_Module extends POS_Module {
    public $id = 'external_service';
    public $name = 'External Service';

    public function get_sync_hook_name() {
        return 'pos_sync_' . $this-&amp;gt;id;
    }

    public function register_sync( $interval = 'hourly' ) {
        $hook_name = $this-&amp;gt;get_sync_hook_name();
        add_action( $hook_name, array( $this, 'sync' ) );
        if ( ! wp_next_scheduled( $hook_name ) ) {
            wp_schedule_event( time(), $interval, $hook_name );
        }
    }

    public function sync() {
        $this-&amp;gt;log( 'EMPTY SYNC' );
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So here is a simplified &lt;code&gt;sync&lt;/code&gt; method for my Evernote module. &lt;a href="https://github.com/artpi/PersonalOS/blob/f332bccfc26f1dfdb2136e4e94b695e61cd544ab/modules/evernote/class-evernote-module.php"&gt;You can see the full code here.&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Evernote_Module extends External_Service_Module {
    public $id = 'evernote';
    public $name = 'Evernote';
    public $description = 'Syncs with evernote service';
    public $advanced_client = null;

    public function __construct( \POS_Module $notes_module ) {

        $this-&amp;gt;register_sync( 'hourly' );
    }

    /**
     * Sync with Evernote. This is triggered by the cron job.
     *
     * @see register_sync
     */
    public function sync() {
        $this-&amp;gt;log( 'Syncing Evernote triggering ' );
        $usn = get_option( $this-&amp;gt;get_setting_option_name( 'usn' ), 0 );

        $sync_chunk = $this-&amp;gt;advanced_client-&amp;gt;getNoteStore()-&amp;gt;getFilteredSyncChunk( $usn, 100, $sync_filter );
        if ( ! $sync_chunk ) {
            $this-&amp;gt;log( 'Evernote: Sync failed', E_USER_WARNING );
            return;
        }
        if ( ! empty( $sync_chunk-&amp;gt;chunkHighUSN ) &amp;amp;&amp;amp; $sync_chunk-&amp;gt;chunkHighUSN &amp;gt; $usn ) {
            // We want to unschedule any regular sync events until we have initial sync complete.
            wp_unschedule_hook( $this-&amp;gt;get_sync_hook_name() );
            // We will schedule ONE TIME sync event for the next page.
            update_option( $this-&amp;gt;get_setting_option_name( 'usn' ), $sync_chunk-&amp;gt;chunkHighUSN );
            wp_schedule_single_event( time() + 60, $this-&amp;gt;get_sync_hook_name() );
            $this-&amp;gt;log( "Scheduling next page chunk with cursor {$sync_chunk-&amp;gt;chunkHighUSN}" );
        } else {
            $this-&amp;gt;log( 'Evernote: Full sync completed' );
        }
        // ACTUALLY PROCESS ITEMS HERE
    }

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

&lt;/div&gt;



&lt;p&gt;I have a very similar sync code for Readwise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

class Readwise extends External_Service_Module {
    public $id = 'readwise';
    public $name = 'Readwise';
    public $description = 'Syncs with readwise service';

    public function __construct( $notes_module ) {
        $this-&amp;gt;register_sync( 'hourly' );
    }

    public function sync() {
        $this-&amp;gt;log( '[DEBUG] Syncing readwise triggering ' );

        $query_args = array();
        $page_cursor = get_option( $this-&amp;gt;get_setting_option_name( 'page_cursor' ), null );
        if ( $page_cursor ) {
            $query_args['pageCursor'] = $page_cursor;
        } else {
            $last_sync = get_option( $this-&amp;gt;get_setting_option_name( 'last_sync' ) );
            if ( $last_sync ) {
                $query_args['updatedAfter'] = $last_sync;
            }
        }

        $request = wp_remote_get(
            'https://readwise.io/api/v2/export/?' . http_build_query( $query_args ),
            array(
                'headers' =&amp;gt; array(
                    'Authorization' =&amp;gt; 'Token ' . $token,
                ),
            )
        );
        if ( is_wp_error( $request ) ) {
            $this-&amp;gt;log( '[ERROR] Fetching readwise ' . $request-&amp;gt;get_error_message(), E_USER_WARNING );
            return false; // Bail early
        }

        $body = wp_remote_retrieve_body( $request );
        $data = json_decode( $body );
        $this-&amp;gt;log( "[DEBUG] Readwise Syncing {$data-&amp;gt;count} highlights" );

        //phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
        if ( ! empty( $data-&amp;gt;nextPageCursor ) ) {
            // We want to unschedule any regular sync events until we have initial sync complete.
            wp_unschedule_hook( $this-&amp;gt;get_sync_hook_name() );
            // We will schedule ONE TIME sync event for the next page.
            //phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
            update_option( $this-&amp;gt;get_setting_option_name( 'page_cursor' ), $data-&amp;gt;nextPageCursor );
            wp_schedule_single_event( time() + 60, $this-&amp;gt;get_sync_hook_name() );
            $this-&amp;gt;log( "Scheduling next page sync with cursor {$data-&amp;gt;nextPageCursor}" );
        } else {
            $this-&amp;gt;log( '[DEBUG] Full sync completed' );
            update_option( $this-&amp;gt;get_setting_option_name( 'last_sync' ), gmdate( 'c' ) );
            delete_option( $this-&amp;gt;get_setting_option_name( 'page_cursor' ) );
        }

        foreach ( $data-&amp;gt;results as $book ) {
            $this-&amp;gt;sync_book( $book );
        }
    }

}

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Two-way sync
&lt;/h2&gt;

&lt;p&gt;In the case of Evernote, we can have a two-way sync. When you update something in Evernote, the above job will trickle the updates to WordPress. If you update something in WordPress, it should reflect in Evernote.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;main trap is an update loop&lt;/strong&gt;. We want to avoid a loop where you update something in WordPress, Evernote picks up the change and updates there, so WordPress gets the notification, updates in Evernote…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--L7UO5uqO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://piszek.com/wp-content/uploads/2024/06/recursion-winnie.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L7UO5uqO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://piszek.com/wp-content/uploads/2024/06/recursion-winnie.gif" width="488" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So here is what we are going to do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We are going to hook into &lt;code&gt;save_post_&lt;/code&gt; hook. This means we don’t have to &lt;strong&gt;really&lt;/strong&gt; do sync. We assume WordPress is online, which means that we can push updates &lt;strong&gt;synchronously&lt;/strong&gt; when they happen, not after some time, hopefully reducing conflicts.&lt;/li&gt;
&lt;li&gt;We are going to update the edited post with the returned Evernote content &lt;strong&gt;immediately&lt;/strong&gt; on save. That way:

&lt;ul&gt;
&lt;li&gt;Even if Evernote changes the data on their end, we are sure that after post saving the data is similar on both ends&lt;/li&gt;
&lt;li&gt;We can calculate the bodyHash and other indicators to know if data got out of sync&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Since we are going to update WordPress end &lt;strong&gt;again&lt;/strong&gt; after pushing data to Evernote, we have to remember to unhook &lt;code&gt;save_post_&lt;/code&gt; hook to prevent recursion.&lt;/li&gt;
&lt;li&gt;This is not relevant to WordPress, but because Evernote has a limited syntax, we have to strip out and convert some data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the simplified code. &lt;a href="https://github.com/artpi/PersonalOS/blob/f332bccfc26f1dfdb2136e4e94b695e61cd544ab/modules/evernote/class-evernote-module.php"&gt;You can read the whole thing here.&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
* This is hooked into the save_post action of the notes module.
* Every time a post is updated, this will check if it is in the synced notebooks and sync it to evernote.
* It will then receive the returned content and update the post, so some content may be lost if it is not handled by evernote
*
* @param int $post_id
* @param \WP_Post $post
* @param bool $update
*/
public function sync_note_to_evernote( int $post_id, \WP_Post $post, bool $update ) {
    $guid = get_post_meta( $post-&amp;gt;ID, 'evernote_guid', true );

    if ( $guid ) {
        $note = $this-&amp;gt;advanced_client-&amp;gt;getNoteStore()-&amp;gt;getNote( $guid, false, false, false, false );
        if ( $note ) {
            $note-&amp;gt;title = $post-&amp;gt;post_title;
            $note-&amp;gt;content = self::html2enml( $post-&amp;gt;post_content );
            $result = $this-&amp;gt;advanced_client-&amp;gt;getNoteStore()-&amp;gt;updateNote( $note );
        }
        return;
    }

    // edam note
    $note = new \EDAM\Types\Note();
    $note-&amp;gt;title = $post-&amp;gt;post_title;
    $note-&amp;gt;content = self::html2enml( $post-&amp;gt;post_content );

    $result = $this-&amp;gt;advanced_client-&amp;gt;getNoteStore()-&amp;gt;createNote( $note );
    if ( $result ) {
        $this-&amp;gt;update_note_from_evernote( $result, $post );
    }

}

/**
* This is called when a note is updated from evernote.
* It will update the post with the new data.
* It is triggered by both directions of the sync:
* - When a note is updated in evernote, it will be updated in WordPress
* - When a note is updated in WordPress, it will be updated in evernote and then the return will be passed here.
*
* @param \EDAM\Types\Note $note
* @param \WP_Post $post
* @param bool $sync_resources - If true, it will also upload note resources. We want this in most cases, EXCEPT when we are sending the data from WordPress and know the response will not have new resources for us.
*/
public function update_note_from_evernote( \EDAM\Types\Note $note, \WP_Post $post, $sync_resources = false ): int {
    remove_action( 'save_post_' . $this-&amp;gt;notes_module-&amp;gt;id, array( $this, 'sync_note_to_evernote' ), 10 );
    // updates updates 
    add_action( 'save_post_' . $this-&amp;gt;notes_module-&amp;gt;id, array( $this, 'sync_note_to_evernote' ), 10, 3 );
    return $post-&amp;gt;ID;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gutenberg blocks as sync items
&lt;/h2&gt;

&lt;p&gt;Evernote’s core primitive is a note. But in the case of Readwise sync, I was stuck between 2 primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Individual highlight&lt;/li&gt;
&lt;li&gt;A book/article/podcast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I decided to store the books/articles as post type (of &lt;code&gt;note&lt;/code&gt;), but keep highlights’ individual existence as blocks of the &lt;strong&gt;Readwise&lt;/strong&gt; type. Each block is tracked individually.&lt;/p&gt;

&lt;p&gt;Each instance of the block is appended in PHP – &lt;a href="https://piszek.com/2024/03/19/get_comment_delimited_block_content/"&gt;I published how to do it in a separate tiny tutorial.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7h28h4QO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://piszek.com/wp-content/uploads/2024/06/Zrzut-ekranu-2024-06-21-o-13.29.01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7h28h4QO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://piszek.com/wp-content/uploads/2024/06/Zrzut-ekranu-2024-06-21-o-13.29.01.png" width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see how the block is implemented in this pull request:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/artpi/PersonalOS/pull/1"&gt;https://github.com/artpi/PersonalOS/pull/1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Subscribe to my newsletter to get updates my Personal OS plugin and my other musings:&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://piszek.com/2024/06/21/wordpress-sync/"&gt;Building sync engines in WordPress&lt;/a&gt; appeared first on &lt;a href="https://piszek.com"&gt;Artur Piszek&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>evernote</category>
      <category>programming</category>
      <category>wordpress</category>
      <category>readwise</category>
    </item>
    <item>
      <title>Prompt hacking is Oxygen</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Thu, 05 Oct 2023 14:34:11 +0000</pubDate>
      <link>https://dev.to/artpi/prompt-hacking-is-oxygen-2n8e</link>
      <guid>https://dev.to/artpi/prompt-hacking-is-oxygen-2n8e</guid>
      <description>&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%2Fs17360ejs0u95wdogedq.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%2Fs17360ejs0u95wdogedq.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If Communication is Oxygen, and Prompt hacking is essentially communication, then…&lt;/p&gt;

&lt;h2&gt;
  
  
  1: Communication is Oxygen
&lt;/h2&gt;

&lt;p&gt;At Automattic, we like to say that communication is the oxygen of a &lt;strong&gt;distributed&lt;/strong&gt; company. Why is that exactly?&lt;/p&gt;

&lt;p&gt;For remote work to work, you have to provide sufficient context so your coworkers are on the same page. They cannot grab you across the desk to clarify what you meant, and any follow-up question can take up to a full day to be answered.&lt;/p&gt;

&lt;p&gt;We try to share context early, and preemptively. We prepare information for each other to be easily surfaced and reached. &lt;strong&gt;We stuff the context window&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2: Prompt hacking is Async Communication.
&lt;/h2&gt;

&lt;p&gt;With the release of ChatGPT, prompt hacking became a &lt;strong&gt;thing&lt;/strong&gt; that captured the imagination of journalists around the world: “&lt;em&gt;If only I describe my task in this weird way, AI will do it.&lt;/em&gt;“&lt;/p&gt;

&lt;p&gt;Now, with the initial hype cycle stabilizing a bit, they are deeming it dead and a bygone fad.&lt;/p&gt;

&lt;p&gt;For me, it was never it’s own &lt;strong&gt;thing&lt;/strong&gt;. When I try to write a prompt, it is no different than describing a Github issue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Describe the task to be done in sufficient detail&lt;/li&gt;
&lt;li&gt;Try to be mindful of the receiver’s context (window)&lt;/li&gt;
&lt;li&gt;Share as much information as possible to avoid follow-up questions&lt;/li&gt;
&lt;li&gt;Describe the shape of the result&lt;/li&gt;
&lt;li&gt;Sidenote: I use &lt;em&gt;please&lt;/em&gt; a lot because I don’t see the prompt as something different than any other “delegation”.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am trying to set up the other party for success and make it easier for them to deliver the result I would like to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  3: Prompt hacking is oxygen
&lt;/h2&gt;

&lt;p&gt;This post started as an interesting title, but it got me thinking about the implications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Async communication is essentially zero-shot prompting. How can we use our async work expertise to elevate our prompts?&lt;/li&gt;
&lt;li&gt;What other learnings from Remote work setup can make us better at working with AI models?&lt;/li&gt;
&lt;li&gt;Can learnings from prompt hacking make us better at managing?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’re still early.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://piszek.com/2023/10/05/prompt-hacking-is-oxygen/" rel="noopener noreferrer"&gt;Prompt hacking is Oxygen&lt;/a&gt; appeared first on &lt;a href="https://piszek.com" rel="noopener noreferrer"&gt;Artur Piszek&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>remote</category>
      <category>gpt</category>
    </item>
    <item>
      <title>The AIKEA Effect</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Sun, 03 Sep 2023 14:54:06 +0000</pubDate>
      <link>https://dev.to/artpi/the-aikea-effect-1mo5</link>
      <guid>https://dev.to/artpi/the-aikea-effect-1mo5</guid>
      <description>&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%2F1kendeggbosfqrou31ti.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%2F1kendeggbosfqrou31ti.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just like the IKEA Effect, but for the AI products. Try not to overenginner when working with LLMs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The IKEA Effect
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;The IKEA effect is a cognitive bias in which consumers place a disproportionately high value on products they partially created. The name refers to Swedish manufacturer and furniture retailer IKEA, which sells many items of furniture that require assembly.&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;Wikipedia&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is substantial evidence that people value things (including software) after they put in at least &lt;strong&gt;some&lt;/strong&gt; effort, regardless of the “objective” end state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Low-hanging fruit from the Tree of Knowledge
&lt;/h3&gt;

&lt;p&gt;The current models (GPT-3.5, GPT-4, LLAMA2) are very good at working “out of the box”. So much so, that we really cannot work fast enough at integrating them. There is a broad class of “ &lt;strong&gt;synthesize these documents in 2-3 paragraphs&lt;/strong&gt; ” that provides immediate business value. Subclasses of this solutions include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Q&amp;amp;A mechanism for dataset X, where you enable an AI model to query your dataset and formulate an answer to your question

&lt;ul&gt;
&lt;li&gt;Think of it as Bing Chat for your proprietary dataset&lt;/li&gt;
&lt;li&gt;You could also use AI for a vector search throughout your dataset, but existing search algorithms also work.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Synthesize a number of random document chunks in a format Y

&lt;ul&gt;
&lt;li&gt;A dashboard that will surface key points&lt;/li&gt;
&lt;li&gt;Scan huge documents for relevant passages&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Turn unstructured data into a structured format to (finally) connect one system to the other.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While tackling this low-hanging fruit has obvious business value, it is also technically not very challenging. You plug a model, craft a prompt, and you’re done. And when it works – you feel a little bit guilty.&lt;/p&gt;

&lt;h3&gt;
  
  
  The typical AI Developer
&lt;/h3&gt;

&lt;p&gt;With the obvious business opportunities, what should companies do? Obviously, they put their &lt;strong&gt;best people&lt;/strong&gt; on it.&lt;/p&gt;

&lt;p&gt;But their best people are used to solving complicated and challenging problems. It’s in their blood to &lt;a href="https://piszek.com/2020/01/11/hard-things-are-easier/" rel="noopener noreferrer"&gt;go after hard things&lt;/a&gt;. This is how they became “one of the best people”.&lt;/p&gt;

&lt;p&gt;They do what they are best at: try to solve a complicated engineering problem where one might not exist. Where just shoving your problem into prompt would suffice, they immediately launch into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Talking about training and fine-tuning models&lt;/li&gt;
&lt;li&gt;Start building infrastructure where you could call OpenAI&lt;/li&gt;
&lt;li&gt;Using Embeddings where existing search algorithms are enough&lt;/li&gt;
&lt;li&gt;Build complicated pipelines using the fresh findings from AI papers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engineering also serves a bit as a reassurance that we understand the whole stack – at least I’ll understand the parts I built.&lt;/p&gt;

&lt;h4&gt;
  
  
  The moat of OpenAI Wrappers
&lt;/h4&gt;

&lt;p&gt;The early crop of “AI Writing Assistants” (jasper.ai, copy.ai, etc.) is not doing so well now that bigger players are catching up to them. This is often used as an illustration that it’s not worth grabbing this low-hanging fruit because this is not a viable long-term strategy.&lt;/p&gt;

&lt;p&gt;But the fact that these companies were able to gather users, funding, and revenue just by being a wrapper over somebody else’s API is impressive.&lt;/p&gt;

&lt;p&gt;Yes, in the age of Open Source LLMs the tech will probably not be a moat. But distribution, marketing, and speed of execution definitely can.&lt;/p&gt;

&lt;h4&gt;
  
  
  The multiple dividends on speed in the AI world
&lt;/h4&gt;

&lt;p&gt;Not only do we not know the full extent of LLM capabilities, but we also don’t know what customers are comfortable with, what interfaces they prefer, and what improvements wait just around the corner.&lt;/p&gt;

&lt;p&gt;Speed is always a force multiplier, but in the AI world, a shorter feedback loop is a much bigger advantage than your perfect architecture.&lt;/p&gt;

&lt;p&gt;This is where your “best of the best” AI team can really shine. The low-hanging fruit from the tree of knowledge will keep coming for some time before we get kicked out of the garden.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://piszek.com/2023/08/28/aikea-effect/" rel="noopener noreferrer"&gt;The AIKEA Effect&lt;/a&gt; appeared first on &lt;a href="https://piszek.com" rel="noopener noreferrer"&gt;Artur Piszek&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>Adding ChatGPT to your Slack in multiplayer mode</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Tue, 07 Mar 2023 11:14:41 +0000</pubDate>
      <link>https://dev.to/artpi/adding-chatgpt-to-your-slack-in-multiplayer-mode-5ajk</link>
      <guid>https://dev.to/artpi/adding-chatgpt-to-your-slack-in-multiplayer-mode-5ajk</guid>
      <description>&lt;p&gt;I just deployed ChatGPT as a Slackbot at Automattic, and let me tell you: &lt;strong&gt;it’s so much better and cheaper&lt;/strong&gt; than the ChatGPT App. Here is how it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can ping &lt;code&gt;@gpt&lt;/code&gt; in any public or private channel,&lt;/li&gt;
&lt;li&gt;It will respond in a Slack thread.&lt;/li&gt;
&lt;li&gt;The state is kept in that slack thread, and it counts as a continuous conversation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I had the (obvious in retrospect) idea of keeping conversations as separate threads, it came together in about 3 hours of coding. The result is surprisingly awesome, as having AI help integrated with the tool we are already using (Slack) reduces cognitive load and incentivizes use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2023/03/Zrzut-ekranu-2023-03-7-o-10.55.47.png?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9GY64Q1z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2023/03/Zrzut-ekranu-2023-03-7-o-10.55.47.png%3Fresize%3D620%252C458%26ssl%3D1" alt="" width="620" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  It’s more convenient.
&lt;/h3&gt;

&lt;p&gt;ChatGPT is a useful tool, but the mental load of deciding that you want to consult the AI overlord and the need to retype (or copy-paste) your message is sometimes too much of a cognitive load.&lt;/p&gt;

&lt;p&gt;With GPT Slackbot, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask it for advice under a message somebody else started&lt;/li&gt;
&lt;li&gt;Consult it quickly where you already are without switching tabs&lt;/li&gt;
&lt;li&gt;Return to past conversations easily.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My coworkers found it immediately helpful:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I enjoyed using this in a Slack thread more than the ChatGPT interface. And Slack is searchable, which is way better! Thank you for doing this !&lt;/p&gt;

&lt;p&gt;Also, I like that @gpt replies to older threads, without the need to retype the context of the initial chat, which I found incredibly useful!&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;My coworkers&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  It’s cheaper
&lt;/h3&gt;

&lt;p&gt;ChatGPT is priced at about &lt;strong&gt;$1 for 75 000&lt;/strong&gt; words, and you are billed per use without a subscription (like ChatGPT Plus). With the pay-per-use pricing, any member of your organization can play without committing to a subscription.&lt;/p&gt;

&lt;h3&gt;
  
  
  It’s more whimsical
&lt;/h3&gt;

&lt;p&gt;But where it really shines is the interaction of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiplayer mode, as anybody can jump in on the thread&lt;/li&gt;
&lt;li&gt;Public use in the public channels, as people can easily learn from each other&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It reminds me of the &lt;a href="https://www.midjourney.com/"&gt;Midjourney discord bot&lt;/a&gt;, where you learn from other people’s prompts while waiting for your own, turning the wait time into a collaboration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2023/03/yelling.png?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aQPk81Gu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2023/03/yelling.png%3Fresize%3D620%252C755%26ssl%3D1" alt="" width="620" height="755"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing your own ChatGPT Slackbot
&lt;/h2&gt;

&lt;p&gt;Consider yourself warned: You are going to see some &lt;strong&gt;PHP&lt;/strong&gt;. This uses the recently released &lt;a href="https://openai.com/blog/introducing-chatgpt-and-whisper-apis"&gt;ChatGPT API.&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  SlackBot setup
&lt;/h3&gt;

&lt;p&gt;First, you need to create a Slackbot App that will be able to be pinged and has appropriate permissions to respond:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new App on Slack &lt;a href="https://api.slack.com/apps"&gt;here&lt;/a&gt;. Make sure you select the Bot app – I chose “From Scratch”&lt;/li&gt;
&lt;li&gt;Add Events API subscription for the &lt;code&gt;app_mentions:read&lt;/code&gt; event&lt;/li&gt;
&lt;li&gt;Add scopes to read public and private channels and one to post in channels ( &lt;code&gt;channels:history&lt;/code&gt;, &lt;code&gt;groups:history&lt;/code&gt;, &lt;code&gt;chat:write&lt;/code&gt; )&lt;/li&gt;
&lt;li&gt;Once you add all the scopes, install the app to your workspace. You need admin permissions to do that or help of a workspace admin.&lt;/li&gt;
&lt;li&gt;Copy the &lt;code&gt;xoxb-....&lt;/code&gt; token and save it in your project as &lt;code&gt;SLACK_TOKEN&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here are all the scopes our app has:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2023/03/Zrzut-ekranu-2023-03-7-o-08.28.55.png?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qtt-Wae5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2023/03/Zrzut-ekranu-2023-03-7-o-08.28.55.png%3Fresize%3D620%252C281%26ssl%3D1" alt="" width="620" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2023/03/Zrzut-ekranu-2023-03-7-o-08.28.33.png?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--V110odoN--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2023/03/Zrzut-ekranu-2023-03-7-o-08.28.33.png%3Fresize%3D620%252C524%26ssl%3D1" alt="" width="620" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Webhook
&lt;/h3&gt;

&lt;p&gt;Once you have the Slack part figured out, you can build code to handle it. This is a very simplified code assuming that you already implemented &lt;a href="https://api.slack.com/authentication/verifying-requests-from-slack"&gt;webhook verification&lt;/a&gt;. &lt;em&gt;(You can use it without verification, but it’s a bad idea).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This webhook will be triggered each time you ping &lt;code&gt;@gpt&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;
$payload = json_decode( file_get_contents( 'php://input') );

// Respond to url_verification challenge
if ( 'url_verification' === $payload-&amp;gt;type &amp;amp;&amp;amp; isset( $payload-&amp;gt;challenge ) ) {
    print wp_kses( $payload-&amp;gt;challenge, array() );
    return;
}

if (
    ! isset( $payload-&amp;gt;type, $payload-&amp;gt;event ) ||
    $payload-&amp;gt;type !== 'event_callback' ||
    $payload-&amp;gt;event-&amp;gt;type !== 'app_mention'
) {
    return;
}

$parent_thread_id = '';
if ( isset( $payload-&amp;gt;event-&amp;gt;thread_ts ) ) {
    //This is a response in thread, so we need to pass the original top message.
    $parent_thread_id = sanitize_text_field( $payload-&amp;gt;event-&amp;gt;thread_ts );
} else {
    $parent_thread_id = sanitize_text_field( $payload-&amp;gt;event-&amp;gt;ts );
}
$channel = sanitize_text_field( $payload-&amp;gt;event-&amp;gt;channel );

queue_async_job(
    (object) array(
        'channel' =&amp;gt; $channel,
        'thread' =&amp;gt; $parent_thread_id,
    ),
    'slack_gpt_respond'
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, all that it does after sanitizing some data is to trigger a job in a job system. This is because Slack expects your webhook to respond within 3 seconds.&lt;/p&gt;

&lt;p&gt;The “Respond to url_verification challenge” code block makes sure you can submit this webhook in the slack interface. Now you are ready to paste the URL where this code runs as Slack event handler.&lt;/p&gt;

&lt;h3&gt;
  
  
  The job
&lt;/h3&gt;

&lt;p&gt;This job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retrieves the backscroll of the slack thread from the Slack API&lt;/li&gt;
&lt;li&gt;Changes the format of the messages to one used by ChatGPT API&lt;/li&gt;
&lt;li&gt;Sends the entire list to ChatGPT API&lt;/li&gt;
&lt;li&gt;Posts the response in the Slack thread.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That way, I can use Slack as the state, which requires no additional database and reduces complexity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// This is the actual job triggered by your job system
function slack_gpt_respond( $job ) {
    $data = $job-&amp;gt;data;
    if ( isset( $data-&amp;gt;thread, $data-&amp;gt;channel ) ) {
        $chatgt_response = get_chatgpt_response( $data-&amp;gt;thread, $data-&amp;gt;channel );
        slack_gpt_respond_in_thread( $data-&amp;gt;thread, $data-&amp;gt;channel, $chatgt_response );
    }
}

// We use WordPress hooks as the job system
add_action( 'wpj_slack_gpt_respond', 'slack_gpt_respond' );

function slack_gpt_respond_in_thread( $ts, $channel, $response ) {
    $data = [
        'channel' =&amp;gt; $channel,
        'thread_ts' =&amp;gt; $ts,
        'text' =&amp;gt; $response,
    ];

    $res = wp_remote_post(
        'https://slack.com/api/chat.postMessage',
        array(
                'headers' =&amp;gt; array(
                    'Content-type' =&amp;gt; 'application/json; charset=utf-8',
                    'Authorization' =&amp;gt; 'Bearer ' . SLACK_TOKEN,
                ),
            'body' =&amp;gt; json_encode( $data ),
        )
    );

}

function slack_gpt_retrieve_backscroll( $thread, $channel ) {
    $response = wp_remote_get(
        "https://slack.com/api/conversations.replies?channel={$channel}&amp;amp;ts={$thread}",
        array(
                'headers' =&amp;gt; array(
                    'Content-type' =&amp;gt; 'application/json; charset=utf-8',
                    'Authorization' =&amp;gt; 'Bearer ' . SLACK_TOKEN,
                ),
        )
    );
    $body = wp_remote_retrieve_body( $response );
    $data = json_decode( $body );

    return array_map( 'slack_message_to_gpt_message', $data-&amp;gt;messages );
}

function slack_message_to_gpt_message( $message ) {
    $text = preg_replace( '#\s?&amp;lt;@[0-9A-Z]+&amp;gt;\s?#is', '', $message-&amp;gt;text );
    $role = isset( $message-&amp;gt;bot_id ) ? 'assistant' : 'user';
    return [
        'role' =&amp;gt; $role,
        'content' =&amp;gt; $text,
    ];
}

function get_chatgpt_response( $ts, $channel ) {
    $backscroll = slack_gpt_retrieve_backscroll( $ts, $channel );

    $api_call = wp_remote_post(
            'https://api.openai.com/v1/chat/completions',
            array(
                'headers' =&amp;gt; array(
                    'Content-Type' =&amp;gt; 'application/json',
                    'Authorization' =&amp;gt; 'Bearer ' . OPENAI_TOKEN,
                ),
                'body' =&amp;gt; json_encode( [
                    'model' =&amp;gt; 'gpt-3.5-turbo', // ChatGPT
                    'messages' =&amp;gt; $backscroll,
                ] ),
                'method' =&amp;gt; 'POST',
                'data_format' =&amp;gt; 'body',
                'timeout' =&amp;gt; 120,
            )
        );
    if ( is_wp_error( $api_call ) ) {
        return;
    }

    $result = json_decode( $api_call['body'] );
    if ( is_wp_error( $results ) ) {
        return;
    }

    return $results-&amp;gt;choices[0]-&amp;gt;message-&amp;gt;content;
}

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

&lt;/div&gt;



&lt;p&gt;The post &lt;a href="https://piszek.com/2023/03/07/slack-gpt/"&gt;Adding ChatGPT to your Slack in multiplayer mode&lt;/a&gt; appeared first on &lt;a href="https://piszek.com"&gt;Artur Piszek&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>slack</category>
      <category>chatgpt</category>
      <category>ai</category>
      <category>gpt3</category>
    </item>
    <item>
      <title>Transcribing your iOS Voice Memos to Markdown with Whisper</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Sun, 23 Oct 2022 14:53:27 +0000</pubDate>
      <link>https://dev.to/artpi/transcribing-your-ios-voice-memos-to-markdown-with-whisper-4c55</link>
      <guid>https://dev.to/artpi/transcribing-your-ios-voice-memos-to-markdown-with-whisper-4c55</guid>
      <description>&lt;p&gt;Open AI has recently introduced an &lt;a href="https://github.com/openai/whisper"&gt;Open-Source library to transcribe voice recordings&lt;/a&gt;, and it immediatelly caught my eye. I like &lt;a href="https://piszek.com/2019/11/14/y-u-bots/"&gt;automating things&lt;/a&gt;, and transcribing memos is a great example of a high leverage automation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recording voice or video is much faster and easier than writing text&lt;/li&gt;
&lt;li&gt;Text is much easier to parse and consume than video or audio.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When I record a voice memo for myself and a bot transcribes it, I have something easy to produce, but also searchable and easily read.&lt;/p&gt;

&lt;p&gt;As my current note-taking app is Logseq, it auto-imports those markdown files to my notes database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--O-GakNdA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0she7vvbgsz9zwq8isfn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--O-GakNdA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0she7vvbgsz9zwq8isfn.png" alt="Image description" width="880" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing Whisper
&lt;/h3&gt;

&lt;p&gt;If you have Python, pip3 and ffmpeg ready, you should be able to run a command like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip3 install git+https://github.com/openai/whisper.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can run it on any audio or video file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;whisper /path/to/file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More instruction in the &lt;a href="https://github.com/openai/whisper"&gt;GH repo.&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS Voice Memos
&lt;/h3&gt;

&lt;p&gt;When iCloud sync is enabled, the voice memos from your phone sync to your laptop via icloud. This will be perfect!&lt;/p&gt;

&lt;p&gt;The recordings are stored in a directory like this: (replace artpi with your username)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/Users/artpi/Library/Application Support/com.apple.voicememos/Recordings/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unusually accessible for an Apple product, but a win for us! Let’s put it all together. The following code will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read all .m4a files from &lt;code&gt;/Users/artpi/Library/Application Support/com.apple.voicememos/Recordings/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Transcribe&lt;/li&gt;
&lt;li&gt;Save each as &lt;code&gt;/Users/artpi/GIT/logseq/pages&lt;/code&gt;/RECORDING_NAME.md file&lt;/li&gt;
&lt;li&gt;When re-run, it will skip the files already saved.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;?php
//https://piszek.com/?p=5874

function sync_apple_voice_memos( $dir, $save_dir, $whisper_dir ) {
    foreach ( glob( dir ) as $filename ) {
        $name = basename( $filename, ".m4a" );
        $file_to_save = $save_dir . '/Voice Memo - ' . $name . '.md' ;
        // Only proceed if file not already transcribed
        if ( ! file_exists( $file_to_save ) ) {
            $lines = [];
            exec( "{$whisper_dir} \"{$filename}\"", $lines );

            $lines = array_slice( $lines, 2 ); // Some default headers.
            $lines = array_map( function( $line ){
                return preg_replace( '#\[[0-9.:]+ --&amp;gt; [0-9.:]+\]\s+(.*?)$#is', '- \\1', $line );
            }, $lines );

            if ( $lines ) {
                file_put_contents( $file_to_save, implode( "\n", $lines ) );
            }
        }
    }
}

sync_apple_voice_memos(
    '/Users/artpi/Library/Application Support/com.apple.voicememos/Recordings/*.m4a',
    '/Users/artpi/GIT/logseq/pages',
    '/opt/homebrew/bin/whisper'
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  More Logseq imports
&lt;/h3&gt;

&lt;p&gt;Here are some other importer ideas for Logseq:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I wrote a bunch of scripts to import everything to my &lt;a href="https://twitter.com/logseq?ref_src=twsrc%5Etfw"&gt;@logseq&lt;/a&gt;, and it is glorious.&lt;br&gt;&lt;br&gt;
I have an overview of every day, including today.&lt;br&gt;&lt;br&gt;
It prevents me from opening a bunch of apps to check notifications and puts me in a more proactive frame of mind.  &lt;/p&gt;

&lt;p&gt;Here is what syncs hourly:&lt;/p&gt;

&lt;p&gt;— Minimum Viable Polish (&lt;a class="mentioned-user" href="https://dev.to/artpi"&gt;@artpi&lt;/a&gt;) &lt;a href="https://twitter.com/artpi/status/1554566274151170048?ref_src=twsrc%5Etfw"&gt;August 2, 2022&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The post &lt;a href="https://piszek.com/2022/10/23/voice-memos-whisper/"&gt;Transcribing your iOS Voice Memos to Markdown with Whisper&lt;/a&gt; appeared first on &lt;a href="https://piszek.com"&gt;Artur Piszek&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>notes</category>
      <category>markdown</category>
    </item>
    <item>
      <title>Running Tumblr on WooCommerce</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Tue, 13 Sep 2022 11:20:58 +0000</pubDate>
      <link>https://dev.to/artpi/running-tumblr-on-woocommerce-cnl</link>
      <guid>https://dev.to/artpi/running-tumblr-on-woocommerce-cnl</guid>
      <description>&lt;p&gt;Tumblr has recently released features for creators to earn a living with their art. Whenever you turn those features on, we provision an entire WordPress site, complete with WooCommerce (and WooCommerce Payments) plugins to facilitate billing. This post explains why (in case it’s not obvious).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2022/08/Zrzut-ekranu-2022-08-30-o-11.26.08.png?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RTjA_PBg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2022/08/Zrzut-ekranu-2022-08-30-o-11.26.08.png%3Fresize%3D620%252C140%26ssl%3D1" alt="" width="620" height="140"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Your new WooCommerce is hiding behind this toggle&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Read more about &lt;a href="https://postplus.tumblr.com/home"&gt;Post+&lt;/a&gt; and &lt;a href="https://help.tumblr.com/hc/en-us/articles/4417356885527"&gt;Tipping&lt;/a&gt; on Tumblr.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When I am not writing blog posts on this site, I work at WordPress.com (actually, the company is called &lt;a href="https://piszek.com/category/automattic/"&gt;Automattic&lt;/a&gt;) on &lt;a href="https://wordpress.com/blog/2019/11/12/recurring-payments/"&gt;helping creators earn a living&lt;/a&gt; without sacrificing artistic freedom. When we bought Tumblr, we wanted to extend that option to Tumblr creators as well. Only, Tumblr isn’t running WordPress.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Or is it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At Automattic, we also work on WooCommerce – the most extensible eCommerce platform on the web. Why not extend it to power Tumblr features as well? Actually, there are lots of reasons not to do it, but we did it anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;Whenever you enable Tumblr monetization features, we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new WordPress site on WordPress.com infrastructure to serve as a payments backend&lt;/li&gt;
&lt;li&gt;Install WooCommerce, and WooCommerce-Subscriptions plugins to power the payments&lt;/li&gt;
&lt;li&gt;Create a Stripe account for you via WooCommerce-Payments&lt;/li&gt;
&lt;li&gt;Create products on that site with the price point you set on Tumblr&lt;/li&gt;
&lt;li&gt;Link that backend site to your Tumblr blog&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From now on, your Tumblr blog will use that site as its billing API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2022/08/image.png?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oEt6dLGc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2022/08/image.png%3Fresize%3D620%252C314%26ssl%3D1" alt="" width="620" height="314"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A design I originally drew many months ago, still accurate.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why?
&lt;/h2&gt;

&lt;p&gt;At Automattic, we like to eat our dogfood. And eat the dog food bowl. And eat the table under which the dog food is served. It is really surprising how the dog has managed to survive.&lt;/p&gt;

&lt;p&gt;The above design is definitely inefficient from data perspective: &lt;strong&gt;Yes&lt;/strong&gt; , we do have some duplication. &lt;strong&gt;Yes&lt;/strong&gt; , we create 38 new SQL tables each time somebody decides to try out tipping on Tumblr.&lt;/p&gt;

&lt;p&gt;But it is &lt;strong&gt;very&lt;/strong&gt; efficient from an organizational perspective. We already have composable pieces for each requirement. Even if any one piece is not fitting exactly right, we would have to &lt;strong&gt;make it so&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  WordPress.com (WPCOM)
&lt;/h3&gt;

&lt;p&gt;WordPress.com is a giant multisite (It’s one installation of WordPress running all the blogs – &lt;a href="https://wordpress.org/support/article/create-a-network/"&gt;read more about multisites&lt;/a&gt;). Our systems teams made it run on bare metal, effectively turning WordPress into “serverless”. New WordPress sites have a marginal cost for us. The ones with WooCommerce are slightly more problematic, but that is something we have to solve anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  WooCommerce
&lt;/h3&gt;

&lt;p&gt;WooCommerce is an eCommerce platform based on WordPress distributed as a plugin. It is designed with extensibility in mind to suit any use case, including our billing system (dubbed “Tumblrpay”).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2022/08/Zrzut-ekranu-2022-08-30-o-11.53.01.png?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zHSe9U0g--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2022/08/Zrzut-ekranu-2022-08-30-o-11.53.01.png%3Fresize%3D398%252C479%26ssl%3D1" alt="" width="398" height="479"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Checkout is the only interface of WooCommerce that we expose&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hundreds of open source contributors and our colleagues are making sure WooCommerce is reliable, secure, and a perfect solution for any payment-related use case. &lt;a href="https://woocommerce.com/products/"&gt;It powers a thriving ecosystem of extensions&lt;/a&gt; that make it do exactly what you want, although some use cases are easier than others.&lt;/p&gt;

&lt;p&gt;By far, the biggest challenge of our work was making it run on the giant multisite of WordPress.com. We had to improve security, reliability, stability, and data handling. We are streamlining database structure and plugin APIs and fixing bugs that are only uncovered at this scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  WooCommerce Payments
&lt;/h3&gt;

&lt;p&gt;WooCommerce Payments is an Automattic-owned payment gateway based on Stripe. It leverages Automattic’s infrastructure and experience from running WooCommerce at scale to provide the most convenient merchant experience of any WooCommerce payment method.&lt;/p&gt;

&lt;p&gt;Our teams are monitoring fraud, paying attention to the payment flow and a set of processes to ensure security, stability, and maintenance on the payment side.&lt;/p&gt;

&lt;p&gt;But the main reason we made it handle Tumblr web payments and renewals is because it’s &lt;strong&gt;fully vertically integrated&lt;/strong&gt; with the rest of our business and existing workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternative designs
&lt;/h2&gt;

&lt;p&gt;I am pretty confident that this is not how you would imagine monetization features on Tumblr to work. The design had the following goals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use each component of the existing ecosystem for its strenghts.&lt;/li&gt;
&lt;li&gt;Find gaps in those, and improve each component so other efforts (including the Open Source users) can benefit.&lt;/li&gt;
&lt;li&gt;Leave minimal footprint. No new dashboards, no new systems.
Ideally, each subsequent billing need should be a configuration, not a custom project to be maintained.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The alternative architectures we were discussing did not meet these goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Combined backend for all Tumblr blogs, running on top of Stripe Connect (the obvious SAAS-like architecture with 4-5 bigger tables):

&lt;ul&gt;
&lt;li&gt;Does not utilize our infrastructure&lt;/li&gt;
&lt;li&gt;Does not contribute to other products&lt;/li&gt;
&lt;li&gt;Requires custom management UIs and new dashboards for Fraud, Systems, and Accounting teams&lt;/li&gt;
&lt;li&gt;Creates issues with In-App-Purchases&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Running “One Big WooCommerce” – a design where it’s still WooCommerce under the hood, but are payments are aggregated on one store, instead of per-merchant:

&lt;ul&gt;
&lt;li&gt;Cannot use WooCommerce Payments, requiring a custom payment gateway&lt;/li&gt;
&lt;li&gt;Requires us to write custom logic for splitting revenue across “merchants”&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In-App Purchases
&lt;/h2&gt;

&lt;p&gt;Running hundreds of thousands of WooCommerce sites is easy compared to the gymnastics we had to perform to enable In-App-Purchases in the app.&lt;/p&gt;

&lt;p&gt;We are effectively running a Marketplace on top of the Apple IAP API, and it’s not easy. I will describe it in a seperate post – subscribe to know where it’s ready:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://piszek.com/subscribe/"&gt;https://piszek.com/subscribe/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Some lessons learned
&lt;/h2&gt;

&lt;p&gt;This is a random assortment of lessons we learned:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Just because you find a clever way to do it, it is not always worth it. You have to design for ease of maintenance in the future&lt;/li&gt;
&lt;li&gt;Real problems are org-chart problems. Everything technical is solvable, but organizational issues are much harder to untangle.&lt;/li&gt;
&lt;li&gt;Chekhov’s function: If you deploy a function to a shared codebase, chances are somebody is going to use it without the same precautions as you would take. Sanitize your inputs.&lt;/li&gt;
&lt;li&gt;It’s always the cache.&lt;/li&gt;
&lt;li&gt;Tumblr users really want to share porn, while both Apple and Stripe really do not want that.&lt;/li&gt;
&lt;li&gt;Running 80 000 database updates per minute is a bad idea.&lt;/li&gt;
&lt;li&gt;Complexity grows at an exponential rate. If you have 3 untested features, when something breaks, you don’t know what you can depend on. Even if you build on a stable foundation, it’s best to do it one story at a time.&lt;/li&gt;
&lt;li&gt;When your &lt;code&gt;wp_users&lt;/code&gt; table is bigger than 300 million, than don’t create a user that owns more than 100 000 sites. As a role on each site is stored as a &lt;code&gt;user_meta&lt;/code&gt;, that giant entry will create cache timeouts.&lt;/li&gt;
&lt;li&gt;Composer autoloader filenames are incompatible with WordPress coding standards.&lt;/li&gt;
&lt;li&gt;WooCommerce boot order is a delicate beast.&lt;/li&gt;
&lt;li&gt;Your job is to code yourself out of the job&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Kudos
&lt;/h2&gt;

&lt;p&gt;I want to give huge huge huge kudos to my amazing teammates who made this challenging architecture a reality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/millerf"&gt;Fab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://magp.ie/"&gt;Eoin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/romarioraffington"&gt;Romario&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://n3f.com/"&gt;Brent&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the entire product team using this infrastructure to build amazing features like &lt;a href="https://staff.tumblr.com/post/689764170806312960/clack-clack-clack"&gt;crabs&lt;/a&gt;, so you can give crabs to other Tumblr users. It has been a joy to work with them.&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://piszek.com/2022/09/13/running-tumblr-on-woocommerce/"&gt;Running Tumblr on WooCommerce&lt;/a&gt; appeared first on &lt;a href="https://piszek.com"&gt;Artur Piszek&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>architecture</category>
      <category>wordpress</category>
      <category>woocommerce</category>
    </item>
    <item>
      <title>Synchronizing WordPress posts with Github README.md</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Mon, 15 Aug 2022 15:36:46 +0000</pubDate>
      <link>https://dev.to/artpi/synchronizing-wordpress-posts-with-github-readmemd-4ggf</link>
      <guid>https://dev.to/artpi/synchronizing-wordpress-posts-with-github-readmemd-4ggf</guid>
      <description>&lt;p&gt;I treat my WordPress blog as my “digital home” – THE place to collect my thoughts, describe projects, and &lt;a href="https://piszek.com/blog/"&gt;publish ideas&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It works very well for writing, but my coding projects feel better on Github. They also appreciate the documentation and descriptions I attach to them.&lt;/p&gt;

&lt;p&gt;Over time, my projects would get out of sync with their WordPress counterparts. I would add new features, change behavior or describe new use cases and would have to manually change that in WordPress, which I naturally forgot to do.&lt;/p&gt;

&lt;p&gt;I wrote this simple snippet to embed any Github .md file inside WordPress posts or pages. Content would be refreshed from Github every hour and displays inside the post like it was written on WordPress.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://piszek.com/roam/wp-roam-block/"&gt;Here is an example page&lt;/a&gt;. Almost the entire content is from the &lt;a href="https://github.com/artpi/wp-roam-block/blob/main/README.md"&gt;associated README.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to sync your WordPress posts with any markdown file on Github
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Use the attached code to introduce the GitHub markdown handler to your WordPress blog. Remember to also attach Parsedown.php!&lt;/li&gt;
&lt;li&gt;Create an amazing project solving the world’s most pressing problem, write a .md file describing it&lt;/li&gt;
&lt;li&gt;Copy the URL to Github markdown file&lt;/li&gt;
&lt;li&gt;Paste it to WordPress block editor, and see how it turns into the content from the file.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Here is the code:
&lt;/h2&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


</description>
      <category>programming</category>
      <category>github</category>
      <category>wordpress</category>
      <category>php</category>
    </item>
    <item>
      <title>Sort by Surprising</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Wed, 29 Jun 2022 09:25:00 +0000</pubDate>
      <link>https://dev.to/artpi/sort-by-surprising-45ne</link>
      <guid>https://dev.to/artpi/sort-by-surprising-45ne</guid>
      <description>&lt;p&gt;In the project management land, surprises are very costly. In the best case, they are introducing work you have not budgeted for, extending your timeline.&lt;/p&gt;

&lt;p&gt;In the worst case, you discover a dependency on external vendors, or internal teams. And that is blocking you &lt;strong&gt;right now&lt;/strong&gt;! Your team cannot proceed and can only sit idle and observe the roadblock removed.&lt;/p&gt;

&lt;p&gt;Since sitting empty-handed is corrosive, drains morale and kills momentum, you undertake some &lt;strong&gt;filler work&lt;/strong&gt; or some other project in the meantime. By the time your dependencies get resolved, you are deep in something else, which you have to switch back from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your total cost of the surprise is:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cost_Of_a_Surprise = Time_Spent_Waiting_on_Others + Time_Spent_Integrating_Their_Solution + Time_Switching_Back_From_Filler_Work&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple solution: Frontload the surprises
&lt;/h2&gt;

&lt;p&gt;There are many project management frameworks and philosophies, but my simple heuristic is to discover surprises, so &lt;strong&gt;work can be put in parallel as soon as possible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When kicking off the project, the natural instinct of engineers is double down on what they know, share that brilliant idea on how to deal with the X requirement, or use the technology Y they have been playing with in spare time. This compounds the problem, because it postpones dealing with uncertainty. The least known area holds the most surprises, and is usually the most underestimated.&lt;/p&gt;

&lt;p&gt;I fight these urges, starting any project with the &lt;strong&gt;least known area&lt;/strong&gt;  &lt;strong&gt;first&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We will make slower progress at the beginning,&lt;/li&gt;
&lt;li&gt;We cannot jump at those exciting ideas from the kickoff,&lt;/li&gt;
&lt;li&gt;We will discover undetected dependencies which we can start delegate right away&lt;/li&gt;
&lt;li&gt;We can adjust timeline much sooner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In plain SQL, &lt;code&gt;SELECT tasks FROM project ORDER BY surprising DESC;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The post &lt;a href="https://piszek.com/2022/06/29/sort-by-surprising/"&gt;Sort by Surprising&lt;/a&gt; appeared first on &lt;a href="https://piszek.com"&gt;Artur Piszek&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>leadership</category>
      <category>project</category>
      <category>management</category>
      <category>agile</category>
    </item>
    <item>
      <title>The price of free time: programmer’s guide to helping a Non-profit</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Sat, 22 Jan 2022 17:03:15 +0000</pubDate>
      <link>https://dev.to/artpi/the-price-of-free-time-programmers-guide-to-helping-a-non-profit-31a4</link>
      <guid>https://dev.to/artpi/the-price-of-free-time-programmers-guide-to-helping-a-non-profit-31a4</guid>
      <description>&lt;p&gt;&lt;em&gt;This piece was originally &lt;a href="https://Piszek.com"&gt;published on Piszek.com&lt;/a&gt;, where I write about Remote Work, being an effective professional, payment systems, psychology and solarpunk.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Congratulations! You have decided to help out a Non-Profit. Full of energy, and good intentions, you have embarked on a journey to use your professional skills to help a cause.&lt;/p&gt;

&lt;p&gt;It’s a win-win: Surely, with a better website / CRM / tech, they will be able to help a few more people. You, on the other hand, will meet interesting folk, do something purposeful (as opposed to optimizing button colors at your day job), and learn a few things.&lt;/p&gt;

&lt;p&gt;Here is what you need to know to not go insane:&lt;/p&gt;

&lt;h2&gt;
  
  
  The benefits of helping a Non-profit
&lt;/h2&gt;

&lt;p&gt;You probably have personal reasons to help a Non-profit. Working on hard problems with friends is one of the most fulfilling things you can do with your life. If you are not working on a world-changing startup and you need a respite from the drudgery of corporate existence, a Non-Profit may be your best next bet – the purpose and mission are plentiful.&lt;/p&gt;

&lt;p&gt;Non-profits are also a great place to meet interesting, like-minded people. Working side-by-side you can make real friends and create deeper connections, than you would build by exchanging the latest plots of TV shows over coffee at work.&lt;/p&gt;

&lt;h3&gt;
  
  
  But there also are powerful benefits directly translating to your career.
&lt;/h3&gt;

&lt;p&gt;My entire programming journey started from helping a Non-profit – a scout team I was a part of. I made my first website in 1998, graduated to building one for dad’s business, and later launched a WordPress web agency. Now I work at WordPress.com, periodically reporting to the creator of WordPress himself. During that journey, I helped my high school, a local TEDx chapter, and a non-profit supporting remote work.&lt;/p&gt;

&lt;p&gt;Working on projects is the best way to learn – you get to experiment with real-world problems and you get to try out different approaches and fail; building that tacit knowledge that makes one an expert.&lt;/p&gt;

&lt;p&gt;Since you are not paid for your contributions, there is a shared understanding of what can be expected of you in a Non-Profit. You have a mandate to play a little, try out things your way, and goof off. To further boost learning, it feels more like play than work, encoding the knowledge much more effectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  The traps
&lt;/h2&gt;

&lt;p&gt;As with everything in life, the downsides are directly correlated to the upsides. Yes, in a Non-Profit, you can be a bit unpredictable and inexperienced. It does not feel like work and you get a breather from a corporate feel of a professional workplace.&lt;/p&gt;

&lt;p&gt;But guess what – other people get to do that too. If you have just reserved a weekend to finish that signup page, and the people preparing the copy (texts) decided to be unprofessional – it suddenly becomes a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hero’s (that’s you) Journey
&lt;/h2&gt;

&lt;p&gt;Let’s assume you volunteered to create a website for your favorite Non-profit. Don’t be surprised, if the whole process goes like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You start full of energy and ideas.&lt;/li&gt;
&lt;li&gt;The non-profit is eager to launch a new website because they have project X coming up.&lt;/li&gt;
&lt;li&gt;Project X is the most important thing, and the website (meaning you) is a blocker.&lt;/li&gt;
&lt;li&gt;You jump straight into work! You cannot be a blocker, right? You ramp up and are ready to implement the most important piece.&lt;/li&gt;
&lt;li&gt;The texts and promotional materials are not ready, despite previous promises.&lt;/li&gt;
&lt;li&gt;You try to work around these requirements – project X is most important, right?&lt;/li&gt;
&lt;li&gt;You get a call. It seems that the “About the Team” page is most important now.&lt;/li&gt;
&lt;li&gt;Let’s do a photoshoot for the Team!&lt;/li&gt;
&lt;li&gt;You still don’t have materials for project X, but you got 10 pages of UI corrections, including a bigger logo, different button colors, and some creative ideas about the slider.&lt;/li&gt;
&lt;li&gt;You start implementing those changes, still have no materials about project X.&lt;/li&gt;
&lt;li&gt;Wait, there are changes to the changes now. Can you revert to the old button color?&lt;/li&gt;
&lt;li&gt;Sometime, after a few weeks, we finally got the Project X page to work.
The placeholder photos you chose are still there.
“About the Team” page that got 3 meetings, photoshoot, and 12 hours of your time has gotten a total of 100 visitors this month.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Things to watch out in a Non-profit
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The price of free time
&lt;/h3&gt;

&lt;p&gt;Professional environments have learned a long time ago, that time is money. If everybody is salaried, the easiest way to turn a profit is to stop wasting people’s time. The correlation is clear and obvious.&lt;/p&gt;

&lt;p&gt;I do realize that corporate environments waste mindblowing eons of their employees’ time. This is due to the scale. Big organisms being less nimble is a law of physics called inertia.&lt;/p&gt;

&lt;p&gt;Non-profits, however, have a peculiar relationship with money. They are called &lt;strong&gt;Non-Profits&lt;/strong&gt;. Duh! They get funded through donations, grants, and sometimes sales – but they are incentivized not to run a tight operation. Volunteers’ time is treated as free, so wastefulness is not controlled. It’s up to you to say no, which is hard because non-profits attract precisely the people least likely to defend their time.&lt;/p&gt;

&lt;h2&gt;
  
  
  It’s everybody else’s side-gig, too
&lt;/h2&gt;

&lt;p&gt;As I mentioned – you can learn, and experiment with new techniques and approaches. But other people do too. If your work depends on graphic design, don’t be surprised when the designer comes up with something out-of-the-box, which naturally will be harder for you to implement, than the run-of-the-mill website.&lt;/p&gt;

&lt;p&gt;Other people, like you, will cut corners. The designer has a family to feed, probably a day job and the thing called life. She can’t check every resolution, think about dimensions of headlines when you cram 100 characters in a title and give the proper attention to everything.&lt;/p&gt;

&lt;p&gt;Last, but not least – without salary, recognition becomes the currency. Don’t be surprised, that “about the team” is treated as the most important page on the entire website (even if the visitors don’t care) – this is the equity paid to volunteers. Being paid with recognition also drives some folk to seek more of that compensation – they will contribute to discussions, where they have not much expertise nor understanding. These are perfect bikeshedding conditions. Beware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Non-profits are passion-driven
&lt;/h2&gt;

&lt;p&gt;Most non-profits have a mission to fix a particular problem in the world. Hunger, poor education, lack of equality, climate change – these are all areas society is failing at and non-profits are stepping in to help.&lt;/p&gt;

&lt;p&gt;Many people are driven to work on these problems because they feel strongly about putting up with the collective screwups of society. Non-profits tend to attract people who approach most of the problems with passion and purpose, with no patience for tedious reasoning.&lt;/p&gt;

&lt;p&gt;This leads to: &lt;/p&gt;

&lt;h3&gt;
  
  
  Passion-driven-project-management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Urgency is the sole method of prioritization. Things are made urgent to ensure their completion, not because they actually are time-sensitive.&lt;/li&gt;
&lt;li&gt;Since urgency=priority, the priorities are fluid over time.&lt;/li&gt;
&lt;li&gt;Yesterday’s priority is forgotten today because somebody who feels more strongly comes in with more passion.&lt;/li&gt;
&lt;li&gt;Flashy things are more important than fulfilling the initial purpose. If you are working on a website, prepare for multiple CTAs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Non-profit survival techniques
&lt;/h2&gt;

&lt;p&gt;These techniques helped me stay sane while working within a few organizations.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find a senior member of the organization to “report to”. Ideally somebody with corporate experience, and some tenure inside the Non-Profit. You don’t want to report to a committee.&lt;/li&gt;
&lt;li&gt;Never agree to do anything ASAP. Chances are, that before you get to it – the original request will change or be forgotten. Save yourself the revert.
Bonus points for batching change requests into sprints.&lt;/li&gt;
&lt;li&gt;They will promise you texts, materials, and whatever else you’ll need. You WILL NOT get them on time. Plan accordingly.&lt;/li&gt;
&lt;li&gt;Record yourself changing stuff in the interface – this will be a good v1 for documentation so that everyone else can implement tiny changes themselves&lt;/li&gt;
&lt;li&gt;If you are creating a website – for goodness sake, use WordPress. It will save you from reinventing the wheel.

&lt;ul&gt;
&lt;li&gt;With WP, you have ready tutorials to send people to, so you don’t have to fix every typo yourself. Chances are, that other folks have WP experience too.&lt;/li&gt;
&lt;li&gt;The next person dealing with the system will know what to do with it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Use a ready template, don’t work with an empty canvas.
Yes, it will be less original than a custom-made design, but you will be able to get off the ground and focus on what’s important – content and functionality. You have no idea how many tiny details come together to make a template work.
Implementing custom design without an hourly rate will lead to an endless back-and-forth on every detail. It costs them nothing to throw in another change.
The constraints of an existing template work in your favor.&lt;/li&gt;
&lt;li&gt;Every statement you hear will be over-hyped – it’s a function of passion-driven project management. You have to do the mental math of halving the emotional charge of all statements.&lt;/li&gt;
&lt;li&gt;Remember to have fun. Despite unreasonable requests, the people you are working with are probably quite awesome. Don’t forget that, and schedule some time to meet them as people – not vendors of website updates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Working in a Non-Profit is a process of realizing that the corporate environment has its advantages and lessons to teach you as well.&lt;/p&gt;

&lt;p&gt;Coming to work on Monday to a well-oiled machine, where every cog (including you) is humming nicely, where the work flows seamlessly through the paths of well-established processes, where everything has its place is a refreshing experience. Of course, sometime around Wednesday you are sick of it all, yearning for the freedom and creativity you get to enjoy in your organization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://piszek.com"&gt;This post was first published on Piszek.com. Check out my site where you can find more articles like this one.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>programming</category>
      <category>volunteering</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your own Roam Research Quicknote REST API with Firebase</title>
      <dc:creator>Artur Piszek</dc:creator>
      <pubDate>Thu, 13 Jan 2022 18:33:02 +0000</pubDate>
      <link>https://dev.to/artpi/your-own-roam-research-quicknote-rest-api-with-firebase-5h80</link>
      <guid>https://dev.to/artpi/your-own-roam-research-quicknote-rest-api-with-firebase-5h80</guid>
      <description>&lt;p&gt;I love Roam Research (&lt;a href="https://piszek.com/roam/"&gt;have written a lot about it&lt;/a&gt;), but the biggest struggle for me is the lack of the proper REST API. I have tried &lt;a href="https://piszek.com/roam/roam-api/"&gt;faking it with Puppeteer&lt;/a&gt;, but I needed something quick for capturing the notes on the go.&lt;/p&gt;

&lt;p&gt;Now I can capture the notes from a variety of apps through IFTTT, or iOS shortcuts (including Siri on my watch), and I have been doing so for more than a year. &lt;a href="https://piszek.com/2019/11/14/y-u-bots/"&gt;As automation is important for me&lt;/a&gt;, this script has helped include roam into my workflows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hey Siri, make a note in Roam&lt;/p&gt;

&lt;p&gt;&lt;cite&gt;Cool, huh?&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2022/01/Zrzut-ekranu-2022-01-13-o-19.22.43.png?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kYQlnX_---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2022/01/Zrzut-ekranu-2022-01-13-o-19.22.43.png%3Fresize%3D620%252C181%26ssl%3D1" alt="" width="620" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;iOS shortcut or IFTTT makes a POST request to Firebase DB&lt;/li&gt;
&lt;li&gt;Every 10 minutes Roam checks that DB, fetches new notes and deletes them in DB.&lt;/li&gt;
&lt;li&gt;Any new note shows up in my current daily page, with “&lt;em&gt;#Inbox&lt;/em&gt;” tag&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From what I have been able to gather, this is similar to what &lt;a href="https://phonetonote.com/"&gt;phonetonote&lt;/a&gt; is doing, without the need for you to manage your own DB.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you need to make this work for you?
&lt;/h2&gt;

&lt;p&gt;You need 3 things: A Firebase project, a custom script in your Roam graph, and iOS shortcuts to start getting your data in the graph.&lt;/p&gt;

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

&lt;p&gt;Firebase is a Google Cloud offering that lets you store JSON in the cloud. With the usage you will be generating, you will be able to operate the project for free.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You need to set up a &lt;a href="https://firebase.google.com/products/realtime-database"&gt;Firebase Realtime Database&lt;/a&gt; project&lt;/li&gt;
&lt;li&gt;Once you set it up, you need to generate a database secret ( Project Settings -&amp;gt; Service Accounts -&amp;gt; Database Secrets)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2022/01/Zrzut-ekranu-2022-01-13-o-18.56.24.png?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fcJJM9fK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2022/01/Zrzut-ekranu-2022-01-13-o-18.56.24.png%3Fresize%3D620%252C400%26ssl%3D1" alt="" width="620" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Roam custom plugin
&lt;/h3&gt;

&lt;p&gt;This custom Roam plugin will pull data from this database – you can customize &lt;code&gt;path/to/quicknotes&lt;/code&gt; to whatever you like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://piszek.com/roam/roam-plugins/"&gt;Read here on how to install custom Roam plugins&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const fireBaseUrl = 'https://YOURPROJECT.firebaseio.com/path/to/quicknotes';
const fireBaseToken = '';

function zeroPad( data ) {
    if ( data &amp;lt; 10 ) {
        return '0' + data;
    }
    return '' + data;
}

function getTodayUid() {
    const d = new Date();
    return zeroPad( d.getMonth() + 1 ) + "-" + zeroPad( d.getDate() ) + "-" + d.getFullYear();
}
function importFromFirebase() {
    window.fetch( fireBaseUrl + '.json?auth=' + fireBaseToken )
        .then( response =&amp;gt; response.json() )
        .then( data =&amp;gt; {
            console.log( 'Firebase Import:', data );
            if ( ! data ) {
                return;
            }
            Object.keys( data ).forEach( key =&amp;gt; {
                const entry = data[key];
                if ( ! entry.string ) {
                    console.warn( 'The payload needs at least a string', entry );
                    return;
                }
                entry.string += ' #Inbox';
                window.roamAlphaAPI.createBlock( {
                    "location": {"parent-uid": getTodayUid(), "order": 0 }, 
                    "block": entry
                } );
                window.fetch( fireBaseUrl + '/' + key + '.json?auth=' + fireBaseToken, { method: 'DELETE' } );
            } );
        } );

    window.setTimeout( importFromFirebase, 10 * 60 * 1000 ); // Check for more notes every 10 minutes.
}

window.setTimeout( importFromFirebase, 60 * 1000 ); // We run this a minute after Roam starts.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Start POSTing your data
&lt;/h3&gt;

&lt;p&gt;Now you need to start throwing your data into the database and see it in your Roam graph!&lt;/p&gt;

&lt;h4&gt;
  
  
  iOS shortcuts
&lt;/h4&gt;

&lt;p&gt;Here is an iOS shortcut action that will save my data in this database. You can include it in your workflows (just remember to change Text to your token and YOURPROJECT to your project).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2022/01/IMG_5390.jpg?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--__nXwE2A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2022/01/IMG_5390.jpg%3Fresize%3D620%252C876%26ssl%3D1" alt="" width="620" height="876"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  IFTTT
&lt;/h4&gt;

&lt;p&gt;You can always include it in &lt;a href="https://ifttt.com"&gt;IFTTT&lt;/a&gt; to provide similar functionality, or make it save all your liked Youtube videos for example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2022/01/Zrzut-ekranu-2022-01-13-o-19.10.00.png?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--T3gtZCJU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2022/01/Zrzut-ekranu-2022-01-13-o-19.10.00.png%3Fresize%3D620%252C519%26ssl%3D1" alt="" width="620" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i0.wp.com/piszek.com/wp-content/uploads/2022/01/Zrzut-ekranu-2022-01-13-o-19.11.04.png?ssl=1"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--V0j82OhR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i0.wp.com/piszek.com/wp-content/uploads/2022/01/Zrzut-ekranu-2022-01-13-o-19.11.04.png%3Fresize%3D601%252C1024%26ssl%3D1" alt="" width="601" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enjoy!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>nocode</category>
      <category>roamresearch</category>
      <category>firebase</category>
      <category>ios</category>
    </item>
  </channel>
</rss>
