Adding the bot to Matrix server

I have my own matrix server for my own use. Usually it is used by me a bit by my family and most of it by my own services to sent me some notifications.

Lately I started playing with nanobot. It is personal AI assistant like OpenClaw. I wanted to be able to chat with it in my own Matrix via my own phone. To do that I needed to create new user dedicated

I find it a bit confusing that there is no central web UI that allow you to do that. Ok, since privacy and security is their utmost concern maybe there did it like that so that you have to have direct control of the server that is running it. OK that is one way to do it securely but also it is a bit obscure. You have to remember where it is, how docker container is named (there are several) and remember exact command you have for run, with exact name of parameters. Since it is very rare occurrence having a need to do that (it is not like I am constantly changing users), I have a hard time to remember that.

I created snippet of bash script that need to be run in order to do that. I have matrix running on docker compose with separate directory for all the data.

# navigate to directory
cd /opt/matrix
sudo docker compose exec matrix-synapse /bin/bash
register_new_matrix_user -u newusername -p very-secure-password1 -c /data/homeserver.yaml

This creates the user. In order to connect to server as new user you still need to login. In theory it should be possible to do that via web client and extract device id and access token from the client itself. It is possible and might actually work. But it is also possible to do that via API and it is much better since you can easily regenrate that data. And you will probably need that since token that is in use will be active but if user will not be active via that token for some time it will be invalidated. And then using client will be much more inconvenient then just API call via curl for example.

Here is another bash script that retrieves access token via API:

curl -XPOST -d '{"type":"m.login.password", "user":"newusername", "password":"very-secure-password1"}' "https://matrix.domain/_matrix/client/r0/login"

Of course user name and its password need to be the same as in previous script.

This will return JSON similar to:

{
    "access_token": "QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd",
    "home_server": "localhost",
    "user_id": "@matrix.domain:newusername"
}

That is all. Though I am not sure how device id need to be retrieved/regenerated without some client. Or even if it need to be communicated to the server at all prior to login. Anyway one time login via client and retrieving device id from the client it is enough. I won’t change and access token can be changed via running a script again fairly easily.

Running my own AI assistant.

I recently started experimenting with my own AI asisstant. I decided to skip on OpenClaw for being massive slop of 400k LOC of vibe coded monstrosity. Looking trough web I found that there is also NanoClaw which seems much better but looks like it is tied to Anthropic service and I like owning my own data and I self host what I can.

That left me with Nanobot being my only option from bigger open source projects that are (relatively) known, are under active development and have big user base.

First I did setup do some research about possible integration of locally hosted model. Nanobot have configuration options for custom, OpenAPI compatible provider that should be fine with llama.cpp server. Also it have dedicated provider for vllm which is also compatible with OpenAI API.

I did some research to which model would be suitable to run in agentic mode and Qwen 3.5 have very good opinions. Unfortunately it is fairly new and is not integrated with all the tools yet – at the time of writing this I could not make it work with llama.cpp. This is not terrible since vllm server seems to be better choice for running a server – it is more performant and have dedicated Docker images. Also Amd have page for vllm on docker with their rocm libraries so it seemed like better choice for tests on my PC with Radeon RX 7900 XTX with 24GB of VRAM.

Qwen 3.5 did not run with vllm on its official docker images. It failed with ‘uknown’ architecture of model. I did not wanted to setup my own local environment of vllm with the latest versions of libraries because it also requires to serum AMD Radeon drivers and ROCm libraries installed – which for now is terrible experience on Debian. AMD officially supports only Ubuntu and Fedora.

Because of that I decided to run another model from Qwen family. I did some tests and:

  • Qwen 3 0.6B – was very fast but also felt a bit dumb.
  • Qwen 3 1.7B and Qwen 3 4B felt much smarter but they are still pretty small and I wanted to try bigger models.
  • Qwen 3 Next – I could not fit it into GPU.
  • Qwen 3 Coder 30B – it was running with very small context.
  • Qwen3-VL-30B ThinkingQwen3-VL-30B Instruct running image capability seemed a bit wasteful since I did not had an use case for that. Also thinking version is fun to read through the response to learn how those models operate but it is slow (because of number of generated tokens) and would be very annoying for agentic use.
  • Qwen3-30B-A3B-Instruct-2507 – felt like about right to test few things. Quntized version of it: cyankiwi/Qwen3-30B-A3B-Instruct-2507-AWQ-4bit ran pretty fast, felt capable in responses and could fit with smaller context in my 24GB of VRAM.

Unfortunatelly right now Nanobot have some problems with running on Matrix which is a bit sad since I am running my own server so it would be perfect integration. But I can always chat with it and I also setup separate email account as an alternative way of communication – I can always send an email from anywhere!

Right now I was able just to make few tests but it feels great to have my own personal assistant, virtual entity living in my own hardware, waiting for me to ask it for help!

Moving /boot partition

For a while I had a problem with my boot partition being too small. It was fine for one kernel image but not for two. So whenever kernel was updated it was failing because of lack of space. Five years ago 500MB was enough but not nowadays.

Today I decided to finally fix that.

First, I did created new LVM volume for /boot:

sudo lvcreate -L 1G -n boot root-vg

Then I mounted it and copied contents of old /boot into it

sudo mount /dev/mapper/root--vg-boot /mnt/boot
sudo rsync -avp /boot /mnt/boot

After that I edited /etc/fstab and changed old boot partition to new boot partition and then rebooted the machine.

Then I did update Grub:

sudo update-grub

Everything went well so I removed old partition as no longer necessary.

And this time PC failed to start.

Being in Grub emergency shell I managed to boot my machine by doing:

set root=(lvm,root--vg-boot)
linux /vmlinuz-6.18.9+deb14-amd64 root=/dev/mapper/root--vg-root
initrd initrd.img-6.18.9+deb14-amd64
boot

Which meant that system was fine, but Grub was misconfigured.

I tried to fix that by reinstalling grub (which according to quick web search should fix the issue) but I was unable to find an example that would work on my setup. I kept getting:

grub-install: error: cannot find EFI directory.

Finally after inspecting manpage for grub-install I did use additional switch:

sudo grub-install --efi-directory=/boot/efi /dev/nvme0n1

This did work and I was finally able to see GRUB welcome page and booted my PC just fine.

Purging smtp services from Debian

Lately I purchased mew dedicated server with intention to use it as my new running mail email server – mailcow, my blog, and some other services. When I was migrating mailcow suite everything was fine except postfix image failed to start.

I had problem like that already on Debian and usually I was dealing with that by disabling exim:

sudo systemctl disable exim.service

This was fine till major Debian update that was reinstalling exim and causing it to steal port 25 again. So this time I decided to uinstall it completely.

sudo apt remove exim4-daemon-light

This removed exim but mailcow again failed to start! To my surprise inspecting open ports shown that port 25 is being used again. This time it was service called couriertcpd. I decided to get rid of this one too, though I had to first search for the name of the package.

sudo apt remove courier-mta

And after that I did again discover that port 25 is busy again! This time exim was back!

Well I had only myself to blame since I was accepting what apt was proposing to do without actually reading the proposition of changes. Every remove of one smtp package was coming up with yet another alternative to deliver emails.

I understand that it is baked into the system that it needs local mail server for some administration purposes, but installing packages during removal is for me a bit too much.

I started to do

sudo apt purge exim4-* courier-*

and it kept with yet another and another alternative:

  • dma
  • estmp
  • mstmp
  • nullmailer
  • opensmtpd
  • postfix
  • sendmail-cf
  • ssmtp

So doing:

sudo apt purge exim4-* courier-* dma esmtp msmtp nullmailer opensmtpd postfix sendmail-cf ssmtp

finally removed all SMTP services that could steal port 25 from mailcow.

After doing small cleanup

sudo apt autoremove

that removed all unnecessary dependencies that were installed with purged mail services, I finally had free 25 port and mailcow was able to start.

Enhancing your .NET API with query language

HTTP://LING

What is query language? SQL is Structured Query Language and you use it to query your database for data you are interested in. It does not have to do anything with your API. If you ever were working with big XML files you probably used XQuery or XPath, query languages designed to XML for data. There are specialized languages that are designed to work with APIs to query them for data. For example, you probably heard about GraphQL or even used it when you were working with some API.

Do you need one for you API?

If you try to use online marketplace search and enter “SPF”, for example, you will see a lot of sun creams in the result. But you are interested in transceivers for your new 100G switch instead. You will try limit search to category of “electronics” or “networking”. In the query string of the browser page you maybe will see something like:

  • "?_nkw=spf&_from=R40"
  • "k=spf&i=computers&crid=YIPWAZPBD3BT&sprefix=spf%2Ccomputers%2C87&ref=nb_sb_noss"
  • "networking-4413?string=spf"

This is very hard to decipher and not a problem if you are developing a website. Most users do not care about those strange signs in the browser address. But if you are developing an API, this does matter. Those “strange letters and numbers” is part of your product! Ease of use for every product is very important. Even if this is very technical product, for technically apt people.

I was working on commercial and non-commercial applications that were trying to reinvent API query language in some way. In example API or website had few query string parameters for filters (like “name” or “date” or “active=true”) then somewhere in application logic it would be translated into SQL query parameters:

dbContext.Persons.Where(p => p.Name.ToLower().Contains(name)).ToList()

eventsStorage.Events.Where(e => e.Date.Date == date).ToList();

ordersRepository.Find(o => o.Status.ToString().ToLower() == "active").ToArray();

Which is fine. But when project grows and consists of more parts and more modules, it is hard to keep things consistent and lack of consistency is a problem for users. They do not care if feature or microservice was developed two years ago and still is using ‘old approach ™’. Concise approach to search across your API is much more important.

You can do that using query language. GraphQL, as already mentioned is a possibility, but it also allows your users to create very complex, heavy and time-consuming queries. Personally, I find it very hard to use. There is also Dynamic Linq and I saw it being used in one project I worked for. But probably allowing your users to write dynamic code for your application to run is not the best idea. There are also libraries, like RQL.NET, but when you search for .NET query language you will get results connected to Linq mostly. This means that for .NET simplest solution would be to use Linq or something similar for API query language. This is the logical solution.

HLinq: Linq to HTTP

HLinq is single Nuget package that allows you to add query language capabilities into your API very easily. Syntax is also quite clear to understand, especially if you already used Linq before.

For example filtering is done with following syntax:

where[ x.name == John ]                                                            - search for people with name "John"
where[ x.name != John ]                                                            - search for people with name not being "John"
where[ x.id == 1 ]                                                                 - search for specific record by its Id
where[ x.id == 77774169-BB9D-4DF9-A4A7-52019C4A445D ]                              - the same as above but with GUID instead
where[ x.name == John Smith ]                                                      - name search but with space inside
where[ x.email == null ]                                                           - look for people that have no email set
where[ x.number == 1 ]                                                             - integral value search
where[ x.Number == 1.2323 ]                                                        - exact fractional number value search
where[ x.Number > 1.2322 && x.Number < 1.2324 ]                                    - look for range of numbers
where[ x.placed == 2025-10-05T19:43:07.3693705Z ]                                  - exact date time filter
where[ x.placed==2011-01-30 15:55:20.433+01:00 ]                                   - date time and zone filter
where[ x.dateOfBirth >= 2010-08-31 ]                                               - search records with date greater or equal than specified value
where[ x.name.EndsWith(s) || x.Name.StartsWith(d) ]                                - search with either one condition OR another condition
where[ x.status == active && x.count > 10 ]                                        - both conditions must be TRUE
where[ ( x.Id==1 || x.Name.StartsWith(d) ) && x.dateOfBith >= 2010-08-31 00:00 ) ] - search with AND and OR logical operators applied to conditions or group of conditions enclosed with '(' ')'
where[ x.name.contains(Han) ]                                                      - case-sensitive 'like' search
where[ x.name.contains(han, StringComparison.InvariantCultureIgnoreCase) ]         - case-insensitive search using memory-only collection search
where[ ilike(x.name, han) ]                                                        - case-insensitive search using DB specific function
where[x.name==John]                                                                - white space is optional
where[                                                                             - white space really does not matter!
  (
     p.name.contains(Kowalski)
     && p.name.startsWith(Jan)
  )
  ||
  p.dateOfBirth > 2010-01-01
]
where[ x.name=="John " ]                                                           - except for search parameters values
where[ x.Name.Contains(Jan) ]                                                      - casing of properties and methods does not matter 

Also, very useful for users is ability to order records in a specific way:

orderBy[x.name]                                         - order by name in ascending order
orderByDescending[x.name]                               - the same but in reverse order
orderBy[x.age].thenBy[x.lastActive]                     - order by two properties, first by age, then by last activity date
orderBy[x.age].thenByDescending[x.lastActive]           - the same but most active users put on top
orderByDescending[x.age].thenByDescending[x.lastActive] - the same but oldest users first

It is possible to count number of records in your query:

count[]                              - count all records
where[x.date <= 2020-01-01].count[]  - count only records that satisfy condition

If you are building front end or some kind of client of your API, it is necessary to limit number of records returned or only fetch records from specific offset.

take[200]            - return only 200 records
skip[1000].take[200] - take only 200 records but from offset 1000

There is also possibility to transform response, to limit it to only specific properties that user is interested in.

select[ x.name ]                                       - return only one property
select[ x.name, x.age ]                                - return only two properties
select[ fullName = x.name, x.age ]                     - the same two properties but name will be renamed to "fullName"
select[ fullName = x.name, active=true ]               - you can add syntactic property to the response
where[ x.age > 22 ].select[ fullName = x.name ]        - get names of people that are older than specific age
select[ fullName = x.name ].where[ x.age > 22 ]        - but this will not work because age does not exist after applying "select[]" transformation

All of this is relatively easy to understand on the first glance. Or at least easier to grasp than something like that (RQL.NET):

var rqlExpression = {
  filter: {
    "isDone": 1,
    "$or":[
        {"updatedAt": {"$lt": "2020/01/02", "$gt":1577836800}},
      ],
  },
  limit : 1000,
  offset: 0,
  sort:["-updatedAt"],
}

Or json query of GraphQL:

{
  search(text: "an") {
    __typename
    ... on Human {
      name
    }
  }
}

Install HLinq

Using HLinq in your API is quite simple. Install Nuget package in your project:

<PackageReference Include="HamsterWheel.HLinq.AspNet" Version="0.4.0" />

After that add initialization of HLinq, in your Program.cs file or similar place when you have access to instance of ServicesCollection:

services.ConfigureHLinq();

Then you need to add HLinqQuery<MyEntity> model into your endpoint which is actual HLinq query mapped from HTTP Query String into type safe structure. MyEntity type is important to be the actual type your API exposes to the clients.

app.MapGet("/endpoint",
    (DbContext dbContext, HLinqQuery<Superhero> query, CancellationToken cancellationToken) =>
    {
        var queryable = dbContext.MySet; 
        return query.ApplyTo(queryable, cancellationToken);
    });

If you are using Controllers instead of Minimal API you can do this instead:

[HttpGet("my-endpoint")]
public IActionResult GetData(HLinqQuery<MyEntity> query, CancellationToken cancellationToken) =>
    Ok(query.ApplyTo(dbContext.MySet, cancellationToken));

And that is it!

You can now call this endpoint with HLinq query filters, ordering, paging and selects! This will work with db collections and in memory collections or any other collections backed by IQueryable.

For example if you have endpoint that responds with the collection of persons serialized from following type:

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; } = null!;
    public string LastName { get; set; } = null!;
    public string Email { get; set; } = null!;
    public string? IpAddress { get; set; }
}

If you add HLinq query model to that endpoint, apply it to a collection and return transformed collection to the client.

For example if you will call this endpoint without HLinq query at all, it will return everything all 1000 at most records if collection is bigger. This is a safety mechanism to prevent user from fetching your entire database in few requests (though it may be configured to be different value).

If you call it with:

?count[]

You will get a response that is just an integral value.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Thu, 30 Oct 2025 09:30:12 GMT
Server: Kestrel
Transfer-Encoding: chunked

2000

If you call it with:

?where[x.firstName.contains(tra)]

It will return all persons with first name containing ‘tra’.

[
  {
    "id": 546,
    "firstName": "Demetra",
    "lastName": "Kauffman",
    "email": "dkauffmanf5@wufoo.com",
    "ipAddress": "6.84.100.186"
  },
  {
    "id": 965,
    "firstName": "Anitra",
    "lastName": "MacGraith",
    "email": "amacgraithqs@mozilla.org",
    "ipAddress": "241.235.111.149"
  }
]

If you need wildcard search done on DB query side it can be done with DB function. I.e. For PgSql it is "?where[ilike(x.firstName,tra%)]" but since character ‘%’ cannot be placed directly in HTTP url it have to be encoded as %25.

?where[ilike(x.firstName,tra%25)]

This is equivalent to doing EF query like below.

persons.Where(x => EF.Functions.ILike(x.FirstName, "tra%"));

And both will be translated directly to similar SQL:

SELECT p."Id", p."Email", p."FirstName", p."IpAddress", p."LastName"
FROM "Persons" AS p
WHERE p."FirstName" ILIKE 'tra%'
LIMIT @__p_0

And you will get response:

[
  {
    "id": 46,
    "firstName": "Travers",
    "lastName": "Carsberg",
    "email": "tcarsberg19@yandex.ru",
    "ipAddress": "156.239.72.190"
  },
  {
    "id": 1001,
    "firstName": "Tracey",
    "lastName": "Bellard",
    "email": "tbellardrs@wikipedia.org",
    "ipAddress": "145.78.92.113"
  }
]

If collection that your API is exposing to the client is not DB function, similar case-insensitive search can be done with the following query:

?where[x.name.contains(b, StringComparison.InvariantCultureIgnoreCase)]

This is similar to Linq query:

persons.Where(p => p.Name.Contains("b", StringComparison.InvariantCultureIgnoreCase))

Conclusion

HLinq does not offer all the capabilities of Linq, for example grouping for now is not supported (but it is on the roadmap!), but it is pretty powerful tool as it is now. It is also designed to be easily configurable and extensible, but I think the biggest upside is ease of use in already existing Asp.NET application.