and has 0 comments

  A Very Punchable Face is an attempt to answer the question "Who is Colin Jost?" by small sketch like chapters that have very little to do with each other and also seem to have not very much to say about Colin himself. Some of the more interesting or personal issues are just ignored or assumed known by the reader. If you don't already know who he is and what he did in life, some of the passages in the book won't make any sense to you. Also, isn't it obnoxious to write a "memoir" when you're 38?

  So who is Colin Jost? He is a guy whose greatest fear is to be mediocre. Understandable since he went to Harvard, married Scarlett Johansson and wrote and hosted for Saturday Night Live. Who in his shoes wouldn't, right?

  I usually enjoy self biographical works because they are deeply personal, and while I enjoyed reading this book, it didn't feel that personal. It was filled with jokes, but they didn't do anything for the story. They were there just because Jost is used to think of jokes all the time. It held some personal anecdotes, but mostly event descriptions, with little interior revelation of personal thoughts and feelings and intentions. Of all of the chapters I loved the one about his mother most, even if it had nothing to say about Colin himself. And I swear he speaks more about the times he shat himself than, let's say, Scarlett!

  Bottom line: The book doesn't say anything you probably thought you were going to read it for. The rest is amusing, but felt like a series of sketches and not something to convey how a person feels inside and experiences life. Also the writing was rather... well... mediocre.

and has 0 comments

  Daniel Suarez is a trailblazer: he takes technology in its infancy and creates stories about how it could be used today, given a little bit of determination and perhaps insanity. He is a competent writer, paying more attention to events and dialogue than to character development. This makes the books packed with ideas and information, but less lyrical, let's say. They also start brilliantly, with a new angle on a situation that could be happening today without big leaps in technology or stretches of imagination, yet kind of go over the edge towards the ending, become less plausible. Suarez's view of the state of the world and human nature in general is both optimistic and terribly dark.

  Delta-V follows the same pattern, this time focusing on space mining, a subject that I am personally really interested in. What would happen if someone would ignore the bureaucracy and the ethical bog in which Earth is mired in and instead just push the boundaries, dare to do where others barely dare to dream? What if we would use the money we throw every day on wars and maintaining an artificial system of wealth and politics on something that lifts us all?

  I liked the technical aspects of the book, but less the interactions between people and the way events unfolded. The story raises many interesting points, but fails to raise something more important: hope. The plot is akin to those high stakes James Bond chases, thrilling, but implausible, letting me feel like it would be crazy to even try. Delta-V leaves a bitter sweet taste after reading: to know what is possible with just a little commitment and to know that the world is poised to stop you at every point for the simple reason that it must justify its existence and protect its pecking order.

  I liked Daemon more, but this one has a subject that is closer to my heart.

and has 0 comments

  The Abyss Beyond Dreams ended with Bienvenido being thrown out of the Void and outside the very galaxy. A different set of heroes now need to battle Fallers, idiotic government people and spacetime to save the world!

   A Night Without Stars is almost as good as the first one. It brings new challenges, a slightly different setup, other characters. In a way, it's pretty much a separate book. And while it follows the plethora of different people, each doing their own thing, it keeps the entire narrative together and consistent. Still had parts and leaps of logic that felt a bit lazy, but the main flow of the story was captivating and the characters sympathetic.

   But, being the actual end of a story and being a Peter F. Hamilton book, it doesn't end on a cliffhanger, but as abruptly as falling off the cliff. To give you a taste: the fate of the Void is resolved in less than a paragraph. The end of the book introduces no less than three different alien races, each with their own few paragraphs. It was like Hamilton was saying "Hey, glad you enjoyed the book. I also had this list of ideas while writing it. I'll list them at the end and let you think about the possibilities as homework".

  Bottom line: if you are a Hamilton fan (or you like good hard science fantasy) there is no force that will stop you reading these two books. I even felt like they were slightly better written than the ones before, even if a bit less carefully. However, the cold turkey endings of these stories stop me from feeling like I want more. It's like enjoying a high speed car ride and hitting a tree. It was fun while it lasted, but you don't feel like driving now.

and has 0 comments

  The Void trilogy brought us the captivating idea of an area of the galaxy that has different properties than the rest, a place where electricity and electronics don't work well, but people have psychic abilities. Also steampunk heroes that fight the system and have superpowers. 

  Well, The Abyss Beyond Dreams is also set in the Void, but on another planet. It starts with Nigel and Paula discovering the cache of telepathic recording of "Edeard's dreams" and Peter F. Hamilton makes fun of his own work by having Nigel tear up at the end of consuming them because it was such an exact hero's journey. I understand Hamilton's embarrassment as I remember reading the books and waiting impatiently for the hard sciency part of the book to finish so I can see what Edeard was up to, which is the opposite of what I normally do.

  Anyway, Nigel goes into the Void to mess it up, as it engulfs more and more of the galaxy to fuel its function, and he arrives on a planet in an early industrial stage of development and that is ruled by a bureaucratic government. So he encourages a Trotsky-like movement in order to reach his goals.

  To me the book was very entertaining, I've read it in a few days, and I also think is one of the good Hamilton books. It's not hard to spot the logical errors in it. I saw clearly how he wanted to create a new story, this time examining other aspects of human psychology and sociology by dissecting a socialist revolution, and so he paid less attention to other sides of it. But it's a book, a hard science fantasy story! It is not perfect and still pretty cool.

  I also liked that the two parts of this story, one being this book, the other A Night Without Stars, were almost standalone, with different characters doing things in different ages. The ending of the book is abrupt as it usually is with PFH, but not as jarring as other of his books, nor ending in a terrible cliffhanger, nor like the end of the second book... :) And this time it's not a trilogy, but a duology. Hurrah for self contained stories!

  Bottom line: good read, I didn't realize how much I was missing reading some of Peter F's stories until I started reading this.

and has 0 comments

In 1859, Charles Darwin was publishing "On the Origin of Species by Means of Natural Selection, or the Preservation of Favoured Races in the Struggle for Life", a book that was proposing a theory that was logical, but against every cultural idea that people adhered to at the time. It said that people were animals and that the only force that pushes for change and therefore speciation, is evolution. People went mad with anger: we are not monkeys! Evolution is really easy to understand though, especially if you are not thinking of people or even of live things. Bear with me, while I make this point, before we move to the real theme of this post.

Imagine a chair maker. He buys the wood, carves the various components used to make chairs, then puts them together and sells them. He makes normal chairs: four legs supporting a platform and then a backrest. Whenever people need chairs, they come to him. Now, another chair maker comes into town. He figures that a chair is almost as stable with three legs as it would be with four. So he starts making three legged chairs, which cost less to make, so he sells them cheaper. Most of the people who need chairs start buying from him. In this case the force that applies pressure on the chairs is the public wanting sturdy, comfortable and cheap chairs and the cause of change is the design of the chair maker. The four leg chair maker will either switch to three legs, make more beautiful chairs or find a different production method if he wants to stay in business.

And before you tell me that I am explaining evolution through intelligent design, I will agree with that point. Because evolution is not something that denies intelligent design, it works with or without it just as well. It's a logical outcome of existing conditions and the rules that govern the environment. It has nothing to do with gods and nothing against them. If you have a population of things that can multiply in a way that allows for change, be it random or designed, and there is a pressure that limits the growth of the number of things, whether random or designed, then evolution takes place, favoring some variants (species or races) and disfavoring others. It happens every time men prefer blue eyes to brown ones in women, whenever women prefer tall men over bulky ones, whenever people make chairs or when there is pressure applied to the growth of a virus.

Yes, we reached the real point of the post: the characteristics of a virus in the human population will depend on the amount and direction of pressure we apply on its growth. Let's take some examples, shall we?

When we open schools because the Covid virus doesn't affect young children, but we limit or forbid adults gathering or going places, we put pressure on the virus to grow inside children. It's not complicated at all. Just like the chair maker, if the virus survives it only does it where it can spread. Because you can't stand your own children being children in your house with you and/or because you somehow believe that the linear and continuous application of regimented education without any breaks is more important than your children's health, you get a variant of virus that spreads through children. If you're lucky, it will come home with them and make just you sick. If you're unlucky, it will kill your children, too.

When we let political and economic pressure dictate the response to a viral outbreak, thus letting the virus spread unchecked through the population, you exponentially increase the chance of mutation (remember the multiplication which allows for change? That's called mutation in biology) thus getting more virus variants. Some of them will be more lethal, some of them will not. It's a throw of a dice that you should never have thrown. If after this horrid year of 2020 we start getting vaccinated and there is a variant out there that can infect vaccinated people, then it will spread through the entire population like we did absolutely nothing. And this happens whether your country implements a full lockdown or not, because other countries don't. Viruses care nothing about borders. So you're not losing money or political clout in the short term, but medium and long term you are losing big time.

This brings us to the last example and you won't like it. It goes like this: if you vaccinate people starting with the sick and elderly, and without even testing them first, you will have more chances of vaccinating already infected people. That means that while the vaccine will make your body reject a specific type of virus, that virus is already multiplying inside you and - yes, you guessed it - if any of them mutate into something that the vaccine did not prepare for, then it will be selected faster for evolution and survival, thus increasing the chances for a virus variant that the vaccine is ineffective for. A vaccine is the true long term solution for any viral outbreak: it uniformly limits the spread of the virus at scale with minimal cost. But only if applied uniformly!

This is not medical science that I am explaining here, it's simple logical progression from a given point applying a set of rules. When people address the issue of a viral epidemic by discussing their legal or moral rights, the existence or nonexistence of various deities, by considering the economy or advantages for various political parties or even some crackpot conspiracy or their personal comfort, they are missing the point. All you can do, as a person, group or government, is to alter your behavior so that the pressure you apply or do not apply leads to the best result for you and your people. The application of logic does not invalidate your beliefs, unless your believe that logic is wrong, which just makes you stupid.

The virus doesn't care what you believe or what you think. It will just move forward on the path of least resistance. It's your job to carve that path so that it leads where you want it to lead.

Whatever you read on Facebook also obeys the rules of evolution. So do media reports and politics. If they can't spread, they will die, so they will mutate into something that spreads and infects more readily. Your job is again, to act according to your own interest, to decide where you want to take things. Where will you apply the pressure and where will you go soft?

As I was saying in a post from a year ago, a virus (or a meme) tends to become more infections, but less damaging in time. Again, it's pure logic. It needs to spread better and therefore not kill its host too quickly or limit its mobility and thus its ability to infect others. But that only applies on a virus that is left unchecked. Once you apply pressure, you change the rules, it changes direction. All I am saying here is push it towards where you want it to go!

and has 0 comments

  Plain Bad Heroines is a lesbian gothic book, as it features unexplainable horror and almost everyone in it is gay and female. Should I call it sapphic gothic? It has the benefit of scaring you twice as much if you're homophobic, I guess. The first thing you notice is how well the book is written. Emily M. Danforth is clearly a talented author and she must be praised for it. I continued to read the book until I finished it mostly because of how well it was written. However, while I am a fan of intermingling stories and self referential prose, most of these stories bored me to tears.

  It is possible, though, that I was not connected to the subtleties of the book, as I was frequently falling asleep while reading it and starting to read it again from a random point that seemed vaguely familiar. I mean, it is a book about the making of a movie that is based on a book, itself inspired by Capote's unfinished work, that researched the spooky happenings at an isolate manor which was being used as a girls only school in the 1900's and where said happenings were being associated with a book written by an outspoken bixsexual feminist who wrote a confessional memoir. And that's just the synopsis. It talks about life in the glamourous Los Angeles movie scene, about societal gossip and the history of Truman Capote, the more or less overt lesbianism accepted at the beginning of the 20th century in high class educational institutions, book writing, sexting and flirtation, life under the spotlight with crazed fans following your every move, witchcraft, even a nod to Lovecraft.

  The thing that bothered me about the storytelling was that most of these subjects were not interesting to me. They felt neither very personal nor technical. As a book inspired by two others, one a shockingly honest autobiography and the other a shockingly honest autopsy of high social circles, Plain Bad Heroines felt really subdued. The scenes that most elicited emotion out of me were neither the lesbian romance, nor the behind the movie scenes machinations, nor the old timey 1900 era rich manor life. It was the witchy curse scenes, which in the end had a very underwhelming explanation. It felt like a book about nothing, going in circles around the point that it was trying to reach, but never getting there. It was a rim job!

  Bottom line: probably more subtle that I understood it to be, it might be just the thing to read if you're gay or into the socialite L.A. life. To me it was difficult to finish and find an interest in.

So I wanted to use the button on my Android headset to control the audio of the application I was currently using: a book reader or a music player or something similar like that. And instead an annoying assistant was blinging annoyingly and impotently in my headphones, while my music was still on. It is not at all obvious what does that and how to stop it. And there are two assistants on my phone: Google Assistant and Bixby. Turns out you only need to care about one.

Here is what I tried and didn't work:

  • disable assistants
    • Bixby can't even be disabled
    • Google Assistant can be disabled, but by that it means it doesn't listen to your voice commands anymore, buttons are fair game
  • apps to change the action for the headset button
    • they either don't work right or can't work when the screen is locked
    • the "many apps" recommended by some articles are not even in the store anymore
    • they need full control permissions on your phone

So, here is what you need to do:

  1. have your headphones connected to your phone
  2. open Google application
  3. tap the three dot More button
  4. go to Settings
  5. select Google Assistant
  6. go to Devices (it's a section in blue on my phone)
  7. you should see Wired headphones (or maybe something else depending on your headset type) - tap it
  8. uncheck the option Get help from Google


That's it! You don't have to disable the assistant on your headphones, but your headphones in your assistant. And this is regardless of whether the assistant is "off" or "on". Now you can listen to your music and books in peace.

"But, what about Bixby?" you will ask. As far as I can see, Bixby is something that comes over Google Assistant. I did disable some stuff in it, but I doubt it was a problem to begin with.

and has 2 comments

Update Aug 28 2023: the book has finally been uploaded to the Internet Archive in both PDF and epub formats!

  I have been working on this adventure computer game that is a tribute to the history of adventure stories. One important part of that history is what we call a gamebook, a printed work that allows the reader to choose one of several paths to complete the story. Because of very aggressive marketing and copyright tactics, this is now almost absorbed by the Choose Your Own Adventure brand and for sure Bantam Books (now Random House) would like us to think that they invented the concept. In fact, the man who sold the idea to them, Edward Packard, was not even born when two American women collaborated on what is now credited as the first book in the genre: Consider the Consequences! from 1930.

  Now, imagine that you would know what is the first book in a literary genre, like the first horror book, or the first romance. You would expect to find pages and pages written about it, you would think others have mentioned it in their works in the field and you would certainly trust to be able to find it somewhere to read. Well, Consider the Consequences! has almost disappeared. A book that probably has a dozen copies left in the world and is carefully (yet greedily) hoarded by a few libraries and collectors.

  Go on, search for it on the web, the place where everybody talks about everything. You will probably find it on Amazon, where it is unavailable, and on Goodreads, where you have one rating and one review. Perhaps you would find mentions of it on a site called Gamebooks, which only seems fair, on a blog called Renga in Blue and a long tweet from a James Ryan. Then there are some context mentions and that is it! The first ever instance of a book in an entire genre is about to go extinct!

  Now, I don't know if it was any good or not. That's kind of the point, I can't judge this work because I can't find it anywhere. If I had lived in the US or the UK maybe I could have read it in the library of some university, although that is just a possibility and not something that I would expect to be able to do. I don't even know if it is in the public domain or not. The U.S. legislation says conflicting things and something written in 1930 may perhaps become free of copyright in 2026.

  And the authors were the real deal: activists, suffragettes and all that. Perhaps I should complain that the patriarchy is trying to stifle the roots of feminine literature and then something would happen. It's astounding, really.

  Update, September 3 2021: Andy Mabbett left a comment about a new Wikipedia entry for the book. Thanks, Andy!

  From the link, a partial play in a radio show, where you can get a feel of what the book was like. Revolutionary for the genre, to say the least! Here it is:

  [youtube:SWCu6PnK5ls]

and has 0 comments

  The Mother Code is not a terrible book, but it is certainly not a good one. It has problems of structure, story and characters. But worst of all, it is really not about motherhood.

  Imagine a world where Uncle Sam "tests" a biological weapon somewhere in Afghanistan, only later realising that they have doomed the whole of humanity. Their solution is not to create self sustained habitats that eschew the issue, going to another planet, moving to Antarctica. Instead they focus on two avenues: finding a cure and creating a fleet of robots that can incubate, raise and protect children that have been genetically manipulated to resist the disease. Yes, because that is doable if you put (just) a team of people to work. It gets worse. The only mention of other countries is in about three or four paragraphs. They don't exist in the American mind, other than an afterthought, and indeed that's the political response of the government in the book, working in secret even knowing they are the cause and that other countries might help. And of course, all the children are American, as are the personalities of the "mothers".

  And really, you might accuse me for nitpicking here, I mean, I've read a lot of bad or naive stories over the years, why be so upset with this? But Carole Stivers decided to also show what happens in the future in parallel to the epidemic story, thus eliminating any thrill of what might happen. The core of any sci-fi story, the what-if, was halved in the moment she presented the end at the same time with the beginning. And later on, when there was another serious question about the future of humanity, the author again chooses to show her cards early and resolve the tension before it even started building.

  So what's left? A deep and interesting analysis of what it means to be a mother, explored through the eyes of the children raised by machines? No. That's just an afterthought, instead the focus being on a group of people that just... exist, with no real consequences to the story until the very end. I understand the dilemma of the editor: should they remove the superfluous writing, thus ending up with a short story, or should they leave it in in hope people would buy "a book" and thus pay for their salary.

  And the whole "mother code" thing is barely touched upon, which is so very sad, because the concept of a software developer trying to understand and code a nurturing mother is amazing! Yet that part takes just a few chapters and it doesn't really feel like what would happen to a software person.

  Bottom line: a really good idea, wasted on a subpar book that buries it under a lot of unnecessary story and forgettable characters.

and has 0 comments

  I always appreciate autobiographies for the glimpse they offer into another person's life. Double points for something that is well written and, if anything, A Promised Land is well written. Barack Obama's voice makes everything feel present and personal, which is the hallmark of a good biography. Yet one has to wonder how much of the story has been left out, how many personal failures have been explained away by a personality affected by the hubris of being the president, particularly given the several sections of the book where political figures were criticized for speaking out of turn or saying too much to the wrong people. So am I conflicted about this. I liked reading the book and I am glad to have had a taste of the experience of being a president, but I cannot take anything else at factual value.

  The book only covers the first four years of the presidential term, starting before the Democrat candidate elections and ending with bin Laden's death. It portrays Obama as an idealist, a reasonable man, one of those few people that need the world to make sense. He tells the story of him becoming a candidate almost like he got caught up in a current.

  He then tells stories about his Democrat colleagues and Republican adversaries, taking great care to talk as nice of them as possible. Notable exceptions are Sarah Palin, which Obama sees as the prototype Trump: an uneducated know-nothing who gained political capital not despite, but because of her ignorance, and Mitch McConnell, who is for all intents and purposes the Palpatine of the story. Trump is also mentioned at the end, deliberately almost like a footnote and depicted as a mindless buffoon.

  Reading the book from start, when the hero is a young idealist who believes in America and its political system, to end, when the hero is a battered soldier fighting economic collapse, terrorism, Republican lies and suicidal policies meant to counter him personally, felt painful. A good kind of pain, like the one (I assume :) ) one would get after a good workout, but pain nonetheless. It also started as a manifesto of hope and kind of ended in a bunch of apologetic explanations on why the good things that he did were not noticed by people and why the good things he did not do had good reasons for not getting done.

  I liked that Obama is a politician who hates the way politics work. I loved that he is a principled man as it is my personal belief that only well thought and agreed principles should guide important decisions, not personal feelings. I liked that the book did not focus on racism or social justice and the few passages about that were well argumented and put in context.

  I don't think being president helped him a lot, it sounded like it was one of those soul sucking jobs that people get into for money and prestige and the hope of achieving something, but that get them exhausted and lifeless at the end. Even for a positive and hopeful person, his book leaves a bitter taste of disappointment in how the world works.

My conclusion: it is a long book, 1700 ebook pages, and it only covers the first term of his presidency and a few years before as a politician. It is well written and I imagine the audio book, which is narrated by Obama himself, has an even stronger impact. I liked that he tried to present himself accurately, with strengths and weaknesses, qualities and flaws. I do believe, though, that the narrative of the book and especially they way he sees himself is a bit fantastic. Some of the chapters felt like rationalizations of past failures. There were valid reasons for that, but they were failures nonetheless and it felt like he refused to own responsibility for some of them. I recommend the book, but for someone not interested in politics, it may feel a bit boring.

  Update: more analysis shows that the change was not because of the OR clauses, but because of some debug OPTIONs that I had used. This post is thus wrong.

Original post:

  So I had this stored procedure that would calculate counts from a table, based on a specific column which was also indexed. Something like this:

SELECT Code, COUNT(*) as Nr 
FROM MyTable

  The code would take the result of this query and only use the counts for some of the codes, let's say 'A', 'B' and 'C'. There was also a large number of instructions that had a NULL Code. So the obvious optimizations was:

SELECT Code, COUNT(*) as Nr 
FROM MyTable
WHERE Code IN ('A','B','C')

  And it worked, however I was getting this annoying warning in the execution plan: "Operator used tempdb to spill data during execution". What the hell was that?

  Long story short, I found a very nice SO answer that explains it: SQL Server cardinality estimation can use two types of statistics to guess how many rows will get through a predicate filter:

  • about the column on average using the density vector
  • about particular values for that column using the histogram

When a literal is used, the cardinality estimator can search for that literal in the histogram. When a parameter is used, its value is not evaluated until after cardinality estimation, so the CE has to use column averages in the density vector.

  Probably, behind the scenes, ('A','B','C') is treated as a variable, so it only uses the density vector. Also, because how the IN operator is implemented, what happens to the query next is very different than replacing it with a bunch of ORs:

SELECT Code, COUNT(*) as Nr 
FROM MyTable
WHERE (Code='A' OR Code='B' OR Code='C')

  Not only the warning disappeared, but the execution time was greatly reduced!  

  You see, the query here is simplified a lot, but in real life it was part of a larger one, including complicated joins and multiple conditions. With an IN clause, the execution plan would only show me one query, containing multiple joins and covering all of the rows returned. By using OR clauses, the execution plan would show me three different queries, one for each code.

  This means that in certain situations, this strategy might not work, especially if the conditions are not disjunct and have rows that meet multiple ones. I am also astonished that for such a simple IN clause, the engine did not know to translate it automatically into a series of ORs! My intent as a developer is clear and the implementation should just take that and turn it into the most effective query possible.

  I usually tell people to avoid using OR clauses (and instead try to use ANDs) or query on values that are different (try for equality instead) or using NOT IN. And the reason is again the execution plan and how you cannot prove a general negative claim. Even so, I've always assumed that IN will work as a series or ORs. My reasoning was that, in case of an OR, the engine would have to do something like an expensive DISTINCT operation, something like this: 

SELECT *
FROM MyTable
WHERE (Code='A' OR Code='B')

-- the above should equate to
SELECT *
FROM MyTable
WHERE Code='A'
UNION -- not a disjunct union so engine would have to eliminate duplicates
SELECT *
FROM MyTable
WHERE Code='B'

-- therefore an optimal query is
SELECT *
FROM MyTable
WHERE Code='A'
UNION ALL - disjunct union
SELECT *
FROM MyTable
WHERE Code='B'
-- assuming that there are no rows that meet both conditions (code A and code B)

  In this case, however, SQL did manage to understand that the conditions were disjunct so it split the work into three, correctly using the index and each of them being quite efficient.

  I learned something today!

and has 0 comments

Just saying.

and has 0 comments

  Rhythm of War feels like a setup for something, like an interlude. Also, while it largely expands the scope of the story, it relies a lot of existing characters and their stories in previous books, without doing that smart thing Brandon Sanderson usually does to remind people what they were. I don't even know if it's possible with this large of a story. And remember, this is just the fourth in a ten book series!

  And so my reading of this book suffered from two things: I didn't quite remember what everything was about and the story just got too large! Meaning that I have to choose whether I care about individual characters and the sides they take or if I see everything as a big saga where people don't really matter. At this point, though, the choice is very difficult to make.

  To summarize, I loved the book, but not as much as I remember liking the previous three. The pace was slower, the implications grander, but also not reaching closure. A lot more characters, types of spren, gods, realms and bindings were revealed, but now I have to wait another two years to see what they were actually about. And Kaladin's pain now went into some weird directions, like battle schock and psychological help and accepting limitations in order to go forward. Was everyone depressed in 2020?! I thought Sanderson was immune to depression.

  Anyway, it seems to me that I will have to plan well the arrival of the fifth book in the series, probably by rereading the first four. Only then will the story click as it should and not make me feel like a stupid Taravangian.

  I was working on a grid display and I had to properly sort date columns. The value provided was not a datetime, but instead a string like "20 Jan 2017" or "01 Feb 2020". Obviously sorting them alphabetically would not be very useful. So what I did was implement a custom sorting function that first parsed the strings as dates, then compared them. Easy enough, particularly since the Date object in Javascript has a Parse function that understands this format.

  The problem came with a string with the value "01 Jan 0001" which appeared randomly among the existing values. I first thought it was an error being thrown somewhere, or that it would not parse this string or even that it would be an overflow. It was none of that. Instead, it was about handling the year part.

  A little context first:

Date.parse('01 Jan 0001') //978300000000
new Date(0) //Thu Jan 01 1970 00:00:00

Date.parse('01 Jan 1950') //-631159200000
new Date(Date.parse('01 Jan 1950')) //Sun Jan 01 1950 00:00:00

Date.parse('31 Dec 49 23:59:59.999') //2524600799999
Date.parse('1 Jan 50 00:00:00.000') //-631159200000

new Date(Date.parse('01 Jan 0001')) //Mon Jan 01 2001 00:00:00

  The first two lines almost had me convinced Javascript does not handle dates lower than 1970. The next two lines disproved that and made me think it was a case of numerical overflow. The next two demonstrated it was not so. Now look closely at the last line. What? 2001?

  The problem was with the handling of years that are numerically smaller than 50. The parser assumes we used a two digit year and translates it into Date.parse('01 Jan 01') which would be 2001. We get a glimpse into how it works, too, because everything between 50 and 99 would be translated into 19xx and everything between 00 and 49 is considered 20xx.

  Note that .NET does not have this problem, correctly making the difference between a 2 digit and 4 digit year.

  Hope it helps people.

Update: due to popular demand, I've added Tyrion as a Github repo.

Update 2: due to more popular demand, I even made the repo public. :) Sorry about that.

Intro

  Discord is something I have only vaguely heard about and when a friend told me he used it for chat with friends, I installed it, too. I was pleasantly surprised to see it is a very usable and free chat application, which combines feature of IRC, other messenger applications and a bit of Slack. You can create servers and add channels to them, for example, where you can determine the rights of people and so on. What sets Discord apart from anything, perhaps other than Slack, is the level of "integration", the ability to programatically interact with it. So I create a "bot", a program which stays active and responds to user chat messages and can take action. This post is about how to do that.

  Before you implement a bot you obviously need:

  All of this has been done to death and you can follow the links above to learn how to do it. Before we continue, a little something that might not be obvious: you can edit a Discord chat invite so that it never expires, as it is the one on this blog now.

Writing code

One can write a bot in a multitude of programming languages, but I am a .NET dev, so Discord.NET it is. Note that this is an "unofficial" library, so it may not (and it is not) completely in sync with all the options that the Discord API provides. One such feature, for example, is multiple attachments to a message. But I digress.

Since my blog is also written in ASP.NET Core, it made sense to add the bot code to that. Also, in order to make it all clean code, I will use dependency injection as much as possible and use the built-in system for commands, even if it is quite rudimentary.

Step 1 - making dependencies available

We are going to need these dependencies:

  • DiscordSocketClient - the client to connect to Discord
  • CommandService - the service managing commands
  • BotSettings - a class used to hold settings and configuration
  • BotService - the bot itself, which we are going to make implement IHostedService so we can add it as a hosted service in ASP.Net

In order to keep things separated, I will not add all of this in Startup, instead encapsulating them into a Bootstrap class:

public static class Bootstrap
{
    public static IWebHostBuilder UseDiscordBot(this IWebHostBuilder builder)
    {
        return builder.ConfigureServices(services =>
        {
            services
                .AddSingleton<BotSettings>()
                .AddSingleton<DiscordSocketClient>()
                .AddSingleton<CommandService>()
                .AddHostedService<BotService>();
        });
    }
}

This allows me to add the bot simply in CreateWebHostBuilder as: 

WebHost.CreateDefaultBuilder(args)
   .UseStartup<Startup>()
   .UseKestrel(a => a.AddServerHeader = false)
   .UseDiscordBot();

Step 2 - the settings

The BotSettings class will be used not only to hold information, but also communicate it between classes. Each Discord chat bot needs an access token to connect and we can add that as a configuration value in appsettings.config:

{
  ...
  "DiscordBot": {
	"Token":"<the token value>"
  },
  ...
}
public class BotSettings
{
    public BotSettings(IConfiguration config, IHostingEnvironment hostingEnvironment)
    {
        Token = config.GetValue<string>("DiscordBot:Token");
        RootPath = hostingEnvironment.WebRootPath;
        BotEnabled = true;
    }

    public string Token { get; }
    public string RootPath { get; }
    public bool BotEnabled { get; set; }
}

As you can see, no fancy class for getting the config, nor do we use IOptions or anything like that. We only need to get the token value once, let's keep it simple. I've added the RootPath because you might want to use it to access files on the local file system. The other property is a setting for enabling or disabling the functionality of the bot.

Step 3 - the bot skeleton

Here is the skeleton for a bot. It doesn't change much outside the MessageReceived and CommandReceived code.

public class BotService : IHostedService, IDisposable
{
    private readonly DiscordSocketClient _client;
    private readonly CommandService _commandService;
    private readonly IServiceProvider _services;
    private readonly BotSettings _settings;

    public BotService(DiscordSocketClient client,
        CommandService commandService,
        IServiceProvider services,
        BotSettings settings)
    {
        _client = client;
        _commandService = commandService;
        _services = services;
        _settings = settings;
    }

    // The hosted service has started
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        _client.Ready += Ready;
        _client.MessageReceived += MessageReceived;
        _commandService.CommandExecuted += CommandExecuted;
        _client.Log += Log;
        _commandService.Log += Log;
        // look for classes implementing ModuleBase to load commands from
        await _commandService.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
        // log in to Discord, using the provided token
        await _client.LoginAsync(TokenType.Bot, _settings.Token);
        // start bot
        await _client.StartAsync();
    }

    // logging
    private async Task Log(LogMessage arg)
    {
        // do some logging
    }

    // bot has connected and it's ready to work
    private async Task Ready()
    {
        // some random stuff you can do once the bot is online: 

        // set status to online
        await _client.SetStatusAsync(UserStatus.Online);
        // Discord started as a game chat service, so it has the option to show what games you are playing
        // Here the bot will display "Playing dead" while listening
        await _client.SetGameAsync("dead", "https://siderite.dev", ActivityType.Listening);
    }
    private async Task MessageReceived(SocketMessage msg)
    {
        // message retrieved
    }
    private async Task CommandExecuted(Optional<CommandInfo> command, ICommandContext context, IResult result)
    {
        // a command execution was attempted
    }

    // the hosted service is stopping
    public async Task StopAsync(CancellationToken cancellationToken)
    {
        await _client.SetGameAsync(null);
        await _client.SetStatusAsync(UserStatus.Offline);
        await _client.StopAsync();
        _client.Log -= Log;
        _client.Ready -= Ready;
        _client.MessageReceived -= MessageReceived;
        _commandService.Log -= Log;
        _commandService.CommandExecuted -= CommandExecuted;
    }


    public void Dispose()
    {
        _client?.Dispose();
    }
}

Step 4 - adding commands

In order to add commands to the bot, you must do the following:

  • create a class to inherit from ModuleBase
  • add public methods that are decorated with the CommandAttribute
  • don't forget to call commandService.AddModuleAsync like above

Here is an example of an enable/disable command class:

public class BotCommands:ModuleBase
{
    private readonly BotSettings _settings;

    public BotCommands(BotSettings settings)
    {
        _settings = settings;
    }

    [Command("bot")]
    public async Task Bot([Remainder]string rest)
    {
        if (string.Equals(rest, "enable",StringComparison.OrdinalIgnoreCase))
        {
            _settings.BotEnabled = true;
        }
        if (string.Equals(rest, "disable", StringComparison.OrdinalIgnoreCase))
        {
            _settings.BotEnabled = false;
        }
        await this.Context.Channel.SendMessageAsync("Bot is "
            + (_settings.BotEnabled ? "enabled" : "disabled"));
    }
}

When the bot command will be issued, then the state of the bot will be sent as a message to the chat. If the parameter of the command is enable or disable, the state will also be changed accordingly.

Yet, in order for this command to work, we need to add code to the bot MessageReceived method: 

private async Task MessageReceived(SocketMessage msg)
{
    // do not process bot messages or system messages
    if (msg.Author.IsBot || msg.Source != MessageSource.User) return;
    // only process this type of message
    var message = msg as SocketUserMessage;
    if (message == null) return;
    // match the message if it starts with R2D2
    var match = Regex.Match(message.Content, @"^\s*R2D2\s+", RegexOptions.IgnoreCase);
    int? pos = null;
    if (match.Success)
    {
        // this is an R2D2 command, everything after the match is the command text
        pos = match.Length;
    }
    else if (message.Channel is IPrivateChannel)
    {
        // this is a command sent directly to the private channel of the bot, 
        // don't expect to start with R2D2 at all, just execute it
        pos = 0;
    }
    if (pos.HasValue)
    {
        // this is a command, execute it
        var context = new SocketCommandContext(_client, message);
        await _commandService.ExecuteAsync(context, message.Content.Substring(pos.Value), _services);
    }
    else
    {
        // processing of messages that are not commands
        if (_settings.BotEnabled)
        {
            // if the bot is enabled and people are talking about it, show an image and say "beep beep"
            if (message.Content.Contains("R2D2",StringComparison.OrdinalIgnoreCase))
            {
                await message.Channel.SendFileAsync(_settings.RootPath + "/img/R2D2.gif", "Beep beep!", true);
            }
        }
    }
}

This code will forward commands to the command service if message starts with R2D2, else, if bot is enabled, will send replies with the R2D2 picture and saying beep beep to messages that contain R2D2.

Step 5 - handling command results

Command execution may end in one of three states:

  • command is not recognized
  • command has failed
  • command has succeeded

Here is a CommandExecuted event handler that takes these into account:

private async Task CommandExecuted(Optional<CommandInfo> command, ICommandContext context, IResult result)
{
    // if a command isn't found
    if (!command.IsSpecified)
    {
        await context.Message.AddReactionAsync(new Emoji("🤨")); // eyebrow raised emoji
        return;
    }

    // log failure to the console 
    if (!result.IsSuccess)
    {
        await Log(new LogMessage(LogSeverity.Error, nameof(CommandExecuted), $"Error: {result.ErrorReason}"));
        return;
    }
    // react to message
    await context.Message.AddReactionAsync(new Emoji("🤖")); // robot emoji
}

Note that the command info object does not expose a result value, other than success and failure.

Conclusion

This post has shown you how to create a Discord chat bot in .NET and add it to an ASP.Net Core web site as a hosted service. You may see the result by joining this blog's chat and giving commands to Tyr, the chat's bot:

  • play
  • fart
  • use metric or imperial units in messages
  • use Yahoo Messenger emoticons in messages
  • whatever else I will add in it when I get in the mood :)