Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

Installing Command & Conquer Generals Zero Hour on GNU/Linux

Box art of Command and Conquer Generals Zero Hour with GNU and Wine overlaid

Overview

The Command and Conquer Generals Zero Hour cds need to both be mounted, and the contents copied to a single directory. Run the setup.exe from that combined directory.

wine /mnt/both-cds/setup.exe

That process needs to be done the exact same way for both Generals and Generals Zero Hour.

Install the Zero Hour patch 1.04.

Set wine to Windows 98 compatibility.

tell Wine to use virtual desktop of any size. You can set the options.ini below for setting the used resolution.

modify ~/Documents/Command\ and\ Conquer\ Generals\ Zero\ Hour\ Data/options.ini for widescreen.

AntiAliasing = 4
BuildingOcclusion = yes
CampaignDifficulty = 0
DrawScrollAnchor = 
DynamicLOD = yes
ExtraAnimations = yes
GameSpyIPAddress = 0.0.0.0
Gamma = 50
HeatEffects = yes
IPAddress = 0.0.0.0
IdealStaticGameLOD = Low
LanguageFilter = false
MaxParticleCount = 5000
MoveScrollAnchor = 
MusicVolume = 76
Resolution = 1920 1080
Retaliation = yes
SFX3DVolume = 79
SFXVolume = 71
ScrollFactor = 50
SendDelay = no
ShowSoftWaterEdge = yes
ShowTrees = yes
StaticGameLOD = High
TextureReduction = 0
UseAlternateMouse = no
UseCloudMap = yes
UseDoubleClickAttackMove = no
UseLightMap = yes
UseShadowDecals = yes
UseShadowVolumes = yes
VoiceVolume = 70

Observe the Resolution setting.

Copy in the GameData.ini to drive_c/Program Files/EA Games/Command & Conquer Generals Zero Hour/Data/INI.

Modify GAMEDATA.INI into your "Command & Conquer Generals Zero Hour\Data\INI" folder. (typically under Program Files) Edit MaxCameraHeight to adjust the FOV. Default setting is 310. Setting it to 410 is approximately the same as a 4:3 monitor, and 510+ gives you a widescreen advantage.

References

  1. Command & Conquer Zero-hour startup crash fix
  2. WineHQ - Command & Conquer: Generals Zero Hour: 1.04
  3. guidance from CC ZH 1.04 widescreen patch file - C&C: Generals Zero Hour - Mod DB

ThinkCentre MAC address all zeros after power loss

I eventually saw that my system didn't have a MAC address for the network card!

$ ip link show ens33 | grep ether
2: ens33: <DOWN>
    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff

I was getting desperate, and booted into the BIOS and disabled the onboard ethernet card, and rebooted. Then I rebooted into bios again and re-enabled the network card, and then back into the OS, and now my network card was working again!

Fix debhelper autoconf input should be named configure.ac not configure.in

[  455s] make: 'build' is up to date.
[  455s]  fakeroot debian/rules binary
[  455s] dh binary
[  455s]    dh_update_autotools_config
[  462s]    dh_autoreconf
[  474s] autoreconf: warning: autoconf input should be named 'configure.ac', not 'configure.in'
[  474s] autoreconf: error: configure.in: AC_INIT not found; not an autoconf script?
[  485s] dh_autoreconf: error: autoreconf -f -i returned exit code 1
[  486s] make: *** [debian/rules:7: binary] Error 25
[  486s] dpkg-buildpackage: error: fakeroot debian/rules binary subprocess returned exit status 2
[  486s] ### VM INTERACTION START ###
[  487s] Powering off.
[  487s] [  465.787492] reboot: Power down
[  487s] ### VM INTERACTION END ###
[  487s]
[  487s] lamb64 failed "build _service:extract_file:waterfox+devuan.dsc" at Wed Sep  1 12:17:25 UTC 2021.

After some Internet searching I learned that the problem is because one of the changes to debhelper now tries to run autoreconf, but this Mozilla-based package doesn't need it run. The fix is actually just a one-line change to debian/rules:

diff --git a/waterfox/debian/rules b/waterfox/debian/rules
index 7e4cff5..7a68fcd 100755
--- a/waterfox/debian/rules
+++ b/waterfox/debian/rules
@@ -4,7 +4,7 @@
 export SHELL=/bin/bash

 %:
-       dh $@
+       dh $@ --without autoreconf

My fix to my waterfox source package is very simple.

Convert b6i to iso

Afte some Internet searching, I came across cdemu for GNU/Linux that can convert the files the way I want. I use Devuan Ceres (the unstable release), so I wanted to rebuild the packages for my exact libs.

So I used the OBS to build them: subproject cdemu. I had to borrow one hack that the Ubuntu team came up with, to suit the needs of systemd-free Devuan.

For my headless VM where I was running these tasks, I had to start a virtual X server. In a GNU screen:

Xvfb :1

In another screen, run:

export DISPLAY=:1
sudo modprobe vhba
sudo chmod 0666 /dev/vhba_ctl
cdemu-daemon &

The daemon insists on using X11, so I had to do it this way. But now you can use client commands. I loaded my .b6t file and then listed it to make sure it was available to my system.

cdemu load 0 /path/to/file.b6t
cdemu device-mapping

The output should resemble:

$ cdemu device-mapping
Device mapping:
DEV   SCSI CD-ROM     SCSI generic
0     /dev/sr1        /dev/sg2

And now you can mount the contents, and then write a new iso file.

sudo mount -v /dev/sr1 /mnt/cdrom
DISKLABEL="$( sudo file -s /dev/sr1 | awk -F\' '{print $2}' )" ; echo "${DISKLABEL}"
mkisofs -V "${DISKLABEL}" -J -rock -o ~/disc.iso /mnt/cdrom

Generate tag cloud for static site

A tag cloud is just a fun way to present data that is loosely measured and displayed in varying weights of importance. And it was a fun, amusing challenge, er, copy-paste job.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/bin/sh
# File: generate-tag-cloud.sh
# Locatiosn:
#    doc7-01a:/usr/local/bin/generate-tag-cloud.sh
#    server1:/mnt/public/Support/Programs/nikola/scripts/for-doc7-01a
# Author: bgstack15
# Startdate: 2021-08-30 09:35
# Title: Generate tag cloud
# Purpose: generate an html sidebar-worthy tag cloud of the article tags on my static site.
# Usage:
#    generate-tag-cloud.sh
#    Called by deploy-part2.sh which is called by deploy.sh on server1
# References:
#    https://dev.to/alvaromontoro/create-a-tag-cloud-with-html-and-css-1e90
# Improve:
# Dependencies:
#    `nikola build` has already run
#    the nikola conf.py points to tagcloud.html
#    awk, bc

INDIR=/var/www/blog
test -z "${TAG_COUNT}" && TAG_COUNT=30
test -z "${TRIM_TAGS_SMALLER_THAN}" && TRIM_TAGS_SMALLER_THAN=3
test -z "${LINK_STRING}" && LINK_STRING="/blog/categories/\${tag}"
test -z "${MAX_SCALE}" && MAX_SCALE=9 # so 1 to 9
test -z "${SHOW_COUNTS}" && SHOW_COUNTS=1   # set undefined 
test -z "${OUT_CSS}" && OUT_CSS=/var/www/blog/tagcloud.css
test -z "${OUT_HTML}" && OUT_HTML=/var/www/blog/tagcloud.html

# FUNCTIONS
math_scale() {
   # call: math_scale "${weights}" "${MAX_SCALE}" "${MIN_SCALE}"
   # math scale is fun to technically implement, but not very useful for what we are doing.
   _weights="${1}"
   _outmax="${2}"
   _outmin="${3}"
   # we need to build a scale for min-max of the given numbers, to scale from 1-9 for the data-weights.
   max="$( echo "${_weights}" | awk 'BEGIN{a=0}{if($2>a){a=$2}} END{print a}' )"
   min="$( echo "${_weights}" | awk 'BEGIN{a=50}{if($2<a){a=$2}} END{print a}' )"
   #echo "So we need to scale ${min}-${max} down to 1-${MAX_SCALE}" 1>&2
   echo "${_weights}" | sort -k2 | awk -v "outmax=${_outmax}" -v "outmin=${_outmin}" -v "max=${max}" -v "min=${min}" '{a=int(($2-min)/(max-min)*((outmax-outmin)+outmin));print $1,a,$2}'
   #/usr/bin/printf "%d\n" "$( printf '%s\n' \
   #   "(${_value}-${_min})/(${_max}-${_min})*((${_outmax}-${_outmin})+${_outmin})" | bc )" 2>/dev/null
}

smooth_scale() {
   # goal: list all items in weight order, then add an integer that scales as the list increments, where this new integer scales from 1-MAX
   # call: smooth_scale "${weights}" "${MAX_SCALE}"
   _weights="${1}"
   _max_scale="${2}"
   _max="$( echo "${_weights}" | wc -l )"
   echo "${weights}" | sort -k2 | awk -v "max_scale=${_max_scale}" -v "max=${_max}" '{a=int((NR/max)*max_scale);print $1,a,$2}'
}

# MAIN
cd "${INDIR}"
# Nikola generates "article:tag" contents which we can parse easily
# This outputs "ansible 24\ncentos 14\ncli 14\n"
weights="$( grep -h --include '*html' -riIE 'article:tag' . | awk '{print $3}' | awk -F'"' -v"t=${TRIM_TAGS_SMALLER_THAN}" '{a[$2]++} END {for(i in a){if(a[i]>=t){print i,a[i]}}}' | sort -k2 -n -r | head -n"${TAG_COUNT}" )"

# Choose how to scale the sizes of the words. smooth_scale is better.
#out_weights="$( math_scale "${weights}" "${MAX_SCALE}" 1 )"
out_weights="$( smooth_scale "${weights}" "${MAX_SCALE}" 1 )"

# Sort here. I might change my mind about how to sort them. Wordpress sorts the visible tags alphabetically.
# column 1 is name, 2 is the scaled weight, 3 is raw number
sorted_weights="$( echo "${out_weights}" | sort -k1 )"
#echo "${sorted_weights}" 1>&2

# build css
{
   cat <<'EOF'
ul.tagcloud a[data-weight="0"] { --size: 1; }
ul.tagcloud a[data-weight="1"] { --size: 2; }
ul.tagcloud a[data-weight="2"] { --size: 3; }
ul.tagcloud a[data-weight="3"] { --size: 4; }
ul.tagcloud a[data-weight="4"] { --size: 5; }
ul.tagcloud a[data-weight="5"] { --size: 6; }
ul.tagcloud a[data-weight="6"] { --size: 7; }
ul.tagcloud a[data-weight="7"] { --size: 8; }
ul.tagcloud a[data-weight="8"] { --size: 9; }
ul.tagcloud a[data-weight="9"] { --size: 10; }
ul.tagcloud a[data-weight="10"] { --size: 11; }

ul.tagcloud {
   list-style: none;
   padding-left: 0;
   display: flex;
   flex-wrap: wrap;
   align-items: center;
   justify-content: center;
}

ul.tagcloud a {
   color: #a33;
   display: block;
   padding: 0.125rem 0.125rem;
   text-decoration: none;
   position: relative;
   --size: 2;
   font-size: calc(var(--size) * 0.125rem + .6666rem);
}

ul.tagcloud[show-data-value] a::after {
   content: " (" attr(data-value) ")";
   font-size: 1rem;
}

ul.tagcloud a::before {
   content: "";
   position: absolute;
   top: 0;
   left: 50%;
   width: 0;
   height: 100%;
   background: #000;
   transform: translate(-50%, 0);
   opacity: 0.15;
   transition: width 0.25s;
}

ul.tagcloud a:focus::before,
ul.tagcloud a:hover::before {
  width: 100%;
}

@media (prefers-reduced-motion) {
  ul.tagcloud * {
    transition: none !important;
  }
}
EOF
} > "${OUT_CSS}"

# build html
{
   # the base target=_parent enables the links to control the page outside this iframe-included tagcloud.html
   echo "<html><head><base target=\"_parent\"><link rel=\"stylesheet\" href=\"tagcloud.css\"></head><body>"
   echo "<ul class=\"tagcloud\" role=\"navigation\" ${SHOW_COUNTS:+show-data-value}>"
   echo "${sorted_weights}" | while read tag weight value ;
   do
      eval this_link="${LINK_STRING}"
      echo "   <li><a href=\"${this_link}\" data-weight=\"${weight}\" data-value=\"${value}\">${tag}</a></li>"
   done
   echo "</ul>"
   echo "</body></html>"
} > "${OUT_HTML}"

My nikola config file defines the sidebar with an iframe (so sue me) that loads this html file.

Deploy my Nikola site, part 2

So file deploy-part2.sh executes on the web server. It's called by the first script from the first server.

#!/bin/sh
# File: deploy-part2.sh
# Locations:
#    doc7-01a:/usr/local/bin/
#    server1:/var/server1/shares/public/Support/Programs/nikola/scripts/
# Author: bgstack15
# Startdate: 2021-08-29 16:56
# Title: Script that extracts tarfile of static site for knowledgebase
# Purpose: Automation of updating the blog
# History:
# Usage:
#    depoy-part2.sh /tmp/kb2.blog.2021-08-29T165805.tgz
#    called by deploy-kb2.sh
# Reference:
# Improve:
# Dependencies:
#    tarball of knowledgebase blog
# Documentation: /mnt/public/Suppot/Programs/nikola/blog-README.md
set -x
OUTDIR=/var/www/blog
TARFILE="${1}" # should be similar to /tmp/kb2.blog.2021-08-29T165805.tgz
if ! test -f "${TARFILE}" ;
then
   echo "Unable to find tarfile ${TARFILE}. Aborted."
   exit 1
fi
test "${USER}" != "blog" && { sudo mkdir -p "${OUTDIR}" ; sudo chown blog.nginx "${OUTDIR}" ; sudo chmod 0775 "${OUTDIR}" ; }
cd "$( dirname "${OUTDIR}" )"
{
   {
      tar -zxf "${TARFILE}"
      # for some reason we cannot utime, even though this is xfs. Whatever. Don't care.
   } 2>&1 1>&3 | grep -viE 'Cannot utime|Exiting with failure status' 1>&2
} 3>&1
test "${USER}" != "blog" && { sudo chown -R blog.nginx "${OUTDIR}" ; }
chmod -R u=rwX,g=rwX,o=rX "${OUTDIR}"
/usr/local/bin/generate-tag-cloud.sh

Walkthrough

I'm so pleased that I don't have to trigger nginx to reload or anything! That's the beauty of a static site: the contents are served from disk, and that is all (well, minus the javascript comments stuff).

I ran into some weird problem where tar was throwing extraneous errors about cannot utime. And I didn't care enough to fix it, so I just wrap around it and exclude that error message.

And then I call generate-tag-cloud.sh which makes the nifty tag cloud you see in the right sidebar.

Scheduling my blog updates

So because I don't actually write these posts at the date they appear on the site (or are listed as when they appeared on the site), I need to schedule running my build and deploy scripts at those times.

Nikola has a feature for THE_FUTURE_IS_NOW, but I do want to wait until the appointed time before content is visible.

So my plan is to use a custom script to check for files whose scheduled publication time is in the same quarter-hour as this invocation of the cron job.

So in cron:

14,29,44,59  *  *  *  *  blog sh /var/server1/shares/public/Support/Programs/nikola/scripts/follow-schedule.sh 1>>/var/server1/shares/public/Support/Systems/server1/var/log/nikola_schedule/log 2>&1

Notice how in this cron entry I actually rely on cron to send the output to log files. This is a departure from the norm.

And file follow-schedule.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/bin/sh
# Startdate: 2021-09-01 21:38
# Purpose: find nikola posts that should be released now and then build and deploy
# Usage:
#    ./follow-schedule.sh "2021-09-03 09:16"
# Design assumption:
#    expecting only one post per quarter-hour!
#    due to ssh possibility, cannot rely on DEBUG, VERBOSE, APPLY, DRYRUN environment variables. Just always be verbose.
# Dependencies:
#    plecho from bgscripts-core
# Documentation:
#    /mnt/public/Support/Programs/nikola/blog-README.md
# flow: find anything scheduled within this 15-minute block
if ! test "$( hostname -s )" = "server1" ;
then
   echo "Connecting to server1..."
   ssh server1 /mnt/public/Support/Programs/nikola/scripts/follow-schedule.sh
else

if ! test "${USER}" = "blog" ;
then
   echo "Switching to user blog..."
   sudo su blog -c '/mnt/public/Support/Programs/nikola/scripts/follow-schedule.sh'
else

echo "START nikola scheduled job" | plecho
INDIR=/mnt/public/Support/Programs/nikola/kb2
test -n "${1}" && DATE_AND_HOUR="${1}"
test -z "${DATE_AND_HOUR}" && DATE_AND_HOUR="$( date "+%F %H" )"
test -n "${2}" && MINUTE="${2}"
test -z "${MINUTE}" && MINUTE="$( date "+%M" )"
export DATE_AND_HOUR
export THISDATE="$( echo "${DATE_AND_HOUR}" | awk '{print $1}' )"
results="$( grep --include '*.md' -riIE '^\.\. date: [0-9]{4}(-[0-9]{2}){2}' "${INDIR}" 2>/dev/null | grep "${DATE_AND_HOUR}" 2>/dev/null )"
NOW_QUARTER="$( printf '%s\n' "scale=0;${MINUTE}/15" | bc )"
echo "Evaluating date+hour ${DATE_AND_HOUR} with quarter-hour ${NOW_QUARTER}"
if test -n "${results}" ;
then
   POST_QUARTER="$( printf "%s\n" "scale=0;$( echo "${results}" | awk '{print $NF}' | awk -F':' '{print $2}' )/15" | bc )"
   if test "${POST_QUARTER}" = "${NOW_QUARTER}" ;
   then
      tf="$( echo "${results}" | sed -r -e 's/:\.\..*$//;' )"
      scheduled_time="$( echo "${results}" | awk '{print $NF}' )"
      echo "Found a post for this quarter-hour: ${tf} at ${scheduled_time}"
      # determine if needs to be moved to YYYY/MM
      YYYY_MM_DD="$( echo "${THISDATE}" | awk -F'-' 'BEGIN{OFS="/"} {print $1,$2,$3}' )"
      if ! echo "${tf}" | grep -qiE "${YYYY_MM_DD}" ;
      then
         echo "mv \"${tf}\" \"${INDIR}/posts/${YYYY_MM_DD}\""
         mkdir -p "${INDIR}/posts/${YYYY_MM_DD}"
         mv "${tf}" "${INDIR}/posts/${YYYY_MM_DD}/"
      else
         echo "File \"${tf}\" does not need to be moved, but it is scheduled for now."
      fi
      # and now run build and deploy
      echo "Now will build and deploy."
      /mnt/public/Support/Programs/nikola/scripts/build.sh && \
      /mnt/public/Support/Programs/nikola/scripts/deploy.sh
   else # this quarter-hour
      echo "No files found for this quarter-hour."
   fi # end-if this quarter-hour
fi # end-if any 
echo "STOP nikola scheduled job" | plecho

# end-if-not-user-blog
fi

# end-if-not-server1
fi

Deploy my nikola site

So after I have built the site with nikola, I send up a tarball over ssh and extract it on the site. There's probably a million ways to do this, and I picked a simple one off the top of my head.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/bin/sh
# File: deploy.sh
# Locations:
#    server1:/var/server1/shares/public/Support/Programs/nikola/scripts/deploy.sh
# Author: bgstack15
# Startdate: 2021-08-29
# Title: Deploy static site to bgstack15.ddns.net/blog/
# Purpose: Automation of updating the blog
# History:
# Usage:
#    deploy-kb2.sh
# Reference:
# Improve:
# Dependencies:
#   wireguard vpn to doc7-01a
#   output of `nikola build` in /mnt/public/Support/Programs/nikola/kb2-output/
# Documentation:
#    /mnt/public/Support/Programs/nikola/blog-README.md
INDIR=/mnt/public/Support/Programs/nikola/blog
OUTDIR=/var/www/blog
TARFILE=/tmp/kb2.blog.$( date "+%FT%H%M%S" ).tgz
if test "$( hostname -s )" != "server1" ;
then
   echo "Connecting to server1..."
   ssh server1 /mnt/public/Support/Programs/nikola/scripts/deploy.sh
else
   echo "Now on server1."
   if test "${USER}" != "blog" ;
   then
      sudo su blog <<-EOF
      chmod -R g=rwX "${INDIR}" 2>/dev/null
      tar -zcf "${TARFILE}" -C "$( dirname "${INDIR}" )" blog
      scp -p "${TARFILE}" blog@doc7-01a:/tmp
      ssh -i ~/.ssh/id_rsa blog@doc7-01a /usr/local/bin/deploy-part2.sh "${TARFILE}"
EOF
   else
      # already user blog
      chmod -R g=rwX "${INDIR}" 2>/dev/null
      tar -zcf "${TARFILE}" -C "$( dirname "${INDIR}" )" blog
      scp -p "${TARFILE}" blog@doc7-01a:/tmp
      ssh -i ~/.ssh/id_rsa blog@doc7-01a /usr/local/bin/deploy-part2.sh "${TARFILE}"
   fi
fi

Just like that build script, this script can be run from anywhere in my infrastructure because it will ssh to server1 first, if necessary.

Automatic build script for my static site

Building the site

As part of my nikola-powered static website, I have to actually "compile" or build the pages of the site.

The process for nikola is rather basic.

nikola build

Well, that sounds easy right? Well, you have to make sure you use the right python venv, and cd to the correct path. My entire logic consists of build.sh.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/sh
# Startdate: 2021-08-29 21:20
# Purpose: run `nikola build` for knowledgebase
# Dependencies:
#    nikola installed by pip for user blog on server1
INDIR=/var/server1/shares/public/Support/Programs/nikola/kb2
if test "$( hostname -s )" != "server1" ;
then
   echo "Connecting to server1..."
   ssh server1 /mnt/public/Support/Programs/nikola/scripts/build.sh
else
   echo "Now on server1."
   if test "${USER}" != "blog" ;
   then
      sudo su blog <<-EOF
      source ~/nikola/bin/activate
      cd $INDIR
      nikola build
EOF
   else
      source ~/nikola/bin/activate
      cd $INDIR
      nikola build
   fi # end-if-not-user-blog
fi # end-if-not-server1

The script sshes to the main file server, and then switches user, and then runs the nikola build command.

Fix Isso comments from old url to new url

While isso has migration steps for Wordpress comments which work well, my url changed. I had to perform some transformations of the comment urls (i.e., which comments show up on which blog post) and comment author links. To facilitate this process, I wrote a python script.

fix-isso-comments.py script

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#!/usr/bin/env python3
# File: fix-isso-comments.py
# Location: https://gitlab.com/bgstack15/former-gists
# Author: bgstack15
# Startdate: 2021-08-26
# SPDX-License-Identifier: GPL-3.0
# Title: Fix URLs in Isso for Importing Wordpress Blog
# Purpose: Make it possible for a oneliner to fix the virtual paths for comments that isso imported from wordpress
# History:
# Usage:
#    ./fix-isso-comments.py --help
#    # Fix comments for migration from wordpress.com to nikola site at /blog/
#    ./fix-isso-comments.py -a -v --dbfile isso.db -m "" -N "/blog/posts"
#    # Fix pingback website entries
#    ./fix-isso-comments.py -a -v --dbfile isso.db -m "https://bgstack15.wordpress.com/" -N "/blog/posts/" --action pingbacks
# Reference:
# Improve:
#    Write a new function that fixes comments posted by other posts on this blog (which is called "pingback" in Wordpress)
# Documentation:
#    This is partially a general library for sqlite, but only for the one function that changes the values within a single column.
import sqlite3, argparse

# Functions
def dict_factory(cursor, row):
    d = {}
    for idx, col in enumerate(cursor.description):
        d[col[0]] = row[idx]
    return d

def open_sqlite_as_dict(dbfile,table):
   con = sqlite3.connect(dbfile)
   response = []
   # load contents as a list of dicts
   con.row_factory = dict_factory
   cur = con.cursor()
   cur.execute(f"select * from {table}")
   a = 0
   while a is not None:
      a = cur.fetchone()
      if a is not None:
         response.append(a)
   return response, con

def replace_value_in_column_in_sqlite(dbfile,verbose=False,dryrun=True,table="",pk="id",column="",matchvalue="",newvalue=""):
   mylist, con = open_sqlite_as_dict(dbfile,table)
   for item in mylist:
      try:
         item[column]=item[column].replace(matchvalue,newvalue,1)
      except:
         pass
      substr = ""
      for key in item.keys():
         #if key != pk: # only excluding primary key is not enough
         if key == column:
            substr = substr + f", {key} = '{item[key]}'"
      substr=substr.lstrip(", ") # strip leading comma
      command = f"UPDATE {table} SET {substr} WHERE {pk} = {item[pk]} OR {pk} = '{item[pk]}'"
      if verbose:
         print(command)
      if not dryrun:
         con.execute(command)
   if not dryrun:
      print("Applying changes.")
      con.commit()

def fix_thread_urls(dbfile,verbose=False,dryrun=True,matchvalue="",newvalue=""):
   return replace_value_in_column_in_sqlite(
      dbfile=dbfile,
      verbose=verbose,
      dryrun=dryrun,
      table="threads",
      pk="id",
      column="uri",
      matchvalue=matchvalue,
      newvalue=newvalue
   )

def fix_pingbacks(dbfile,verbose=False,dryrun=True,matchvalue="",newvalue=""):
   return replace_value_in_column_in_sqlite(
      dbfile=dbfile,
      verbose=verbose,
      dryrun=dryrun,
      table="comments",
      pk="id",
      column="website",
      matchvalue=matchvalue,
      newvalue=newvalue
   )

# Parse arguments
parser = argparse.ArgumentParser()
parser.add_argument("--dbfile","-d", help="sqlite3 database file")
parser.add_argument("--verbose","-v", action="store_true", help="Verbose.")
dryrun_group = parser.add_mutually_exclusive_group()
dryrun_group.add_argument("--dryrun","--dry-run","-n", action='store_true', help="Dry run only. Default.")
dryrun_group.add_argument("--apply","-a", action='store_true', help="Apply changes.")
parser.add_argument("--match","-m", required=True,help="String within value to be replaced.")
parser.add_argument("--newvalue","--new-value","-N", required=True,help="String to be inserted.")
parser.add_argument("--action","-A",required=False,choices=["threads","pingbacks"],default="threads")

args = parser.parse_args()
print(args)
dbfile = args.dbfile
verbose = args.verbose
dryrun = not args.apply # yes, dryrun = not args.apply does what we want.
matchvalue = args.match
newvalue = args.newvalue
action = args.action

# Main
if "threads" == action:
   fix_thread_urls(
      dbfile=dbfile,
      verbose=verbose,
      dryrun=dryrun,
      matchvalue=matchvalue,
      newvalue=newvalue
   )
elif "pingbacks" == action:
   fix_pingbacks(
      dbfile=dbfile,
      verbose=verbose,
      dryrun=dryrun,
      matchvalue=matchvalue,
      newvalue=newvalue
   )
else:
   print(f"Invalid action: {action}. Aborted.")

The documentation in the script explains how to use it, as well as the previous blog post.

Step 1 is to fix the comment thread urls, so the right comments show up on the blog posts where they belong. Because my previous wordpress instance was served at virtual path / but my new site is served at /blog/, we need to fix every comment url.

/usr/local/bin/fix-isso-comments.py -a -v --dbfile isso.kb2.db -m "" -N "/blog/posts"

Observe how here we add the "/blog/posts" to Isso contents. Any relative links stored in the source files for nikola itself get parsed by nikola and prepended with the baseurl that nikola is configured with. But isso doesn't read that and doesn't care; it needs the full virtual path to each page.

Fix pingback website entries. In Wordpress, a pingback is where a blog post comments on any link that is a Wordpress post. That way, the author of the wordpress blog that was linked to has an option to show the reverse link. It can get spammy, and I assume it was an option, but I had left it on. And so any links I have in the comments need to be updated to the new urls.

/usr/local/bin/fix-isso-comments.py -a -v --dbfile isso.kb2.db -m "https://bgstack15.wordpress.com/" -N "/blog/posts/" --action pingbacks