initial import

This commit is contained in:
Kyle Isom 2023-05-06 00:14:37 -07:00
commit 353cf2cf8a
14 changed files with 1078 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
nlink.conf

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# nomadlink
nomadlink is a tool for syncing a Nomad RSS feed to pnut.
[Nomad](https://nomad.wntrmute.net/)
[Pnut](https://beta.pnut.io/)
The original version was running as a cronjob on a Raspberry Pi. I've
modified it to run on a gokrazy instance.
## Previous version instruction
### Raspberry Pi setup
1. [Install go](https://www.e-tinkers.com/2019/06/better-way-to-install-golang-go-on-raspberry-pi/).
2. Install mercurial: `sudo apt install mercurial`
3. Fetch the repository and `cd` to the directory: `hg clone https://hg.sr.ht/~kisom/nlink && cd nlink`
4. Make the config directory: `mkdir -p ~/.config/nomadlink`
5. Create a new Pnut dev app from the [Pnut developer page](https://pnut.io/dev).
6. Copy the `creds.json` file to `~/.config/nomadlink/creds.json` and fill it out with the correct details from the Pnut dev page.
7. Run `go build`.
8. Verify the build works by running `./nlink -i -m`. This will create the database and mark all the posts you currently have on nomad as already posted.
9. Copy `nlink` to `/usr/local/bin`: `sudo cp nlink /usr/local/bin/`
10. Edit your crontab (`crontab -e`) and add the following line:
```
*/15 * * * * /usr/local/bin/nlink -c /home/$USER/.config/nomadlink/creds.json -d /home/$USER/.config/nomadlink/nomadlink.db
```
11. Eventually, posts should start showing up.
### Optional
12. Install postfix and set it up as a local site.
```
sudo apt install postfix bsd-mailx
```

12
go.mod Normal file
View File

@ -0,0 +1,12 @@
module git.wntrmute.dev/kyle/nlink
go 1.15
require (
git.sr.ht/~thrrgilag/woodstock v0.0.0-20210714032038-b22c4f10cc34
git.wntrmute.dev/kyle/goutils v1.6.6
github.com/anaskhan96/soup v1.2.4
github.com/mattn/go-sqlite3 v1.10.0
github.com/minio/minio-go/v7 v7.0.52
github.com/mmcdole/gofeed v1.1.3
)

185
go.sum Normal file
View File

@ -0,0 +1,185 @@
bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4=
git.sr.ht/~thrrgilag/woodstock v0.0.0-20210714032038-b22c4f10cc34 h1:nTH4UWrZTwEhkFvuR4Y4vV6KMSoZuF+3qFmgGs9Vm2I=
git.sr.ht/~thrrgilag/woodstock v0.0.0-20210714032038-b22c4f10cc34/go.mod h1:giZm2mNVkz9BeA31LtUC2gwIYfUSUPQINp+tI8EGFTc=
git.wntrmute.dev/kyle/goutils v1.6.6 h1:CRCBlmSXOTkShbqC6j9lgxh4lb+khzc2zpIJYGQJtnc=
git.wntrmute.dev/kyle/goutils v1.6.6/go.mod h1:p0m2YprqMXkqtxTPKCiRcmgYo/D/9DtAIRfNVFE3JBg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/anaskhan96/soup v1.2.4 h1:or+sKs9QbzJGZVTYFmTs2VBateEywoq00a6K14z331E=
github.com/anaskhan96/soup v1.2.4/go.mod h1:6YnEp9A2yywlYdM4EgDz9NEHclocMepEtku7wg6Cq3s=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY=
github.com/cloudflare/cfssl v1.5.0/go.mod h1:sPPkBS5L8l8sRc/IOO1jG51Xb34u+TYhL6P//JdODMQ=
github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4=
github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw=
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.52 h1:8XhG36F6oKQUDDSuz6dY3rioMzovKjW40W6ANuN0Dps=
github.com/minio/minio-go/v7 v7.0.52/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mmcdole/gofeed v1.1.3 h1:pdrvMb18jMSLidGp8j0pLvc9IGziX4vbmvVqmLH6z8o=
github.com/mmcdole/gofeed v1.1.3/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE=
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.12.0/go.mod h1:fUqqXB5vEgVCZ131L+9say31RAri6aF6KDViawhxKK8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is=
github.com/zmap/zcrypto v0.0.0-20200513165325-16679db567ff/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4=
github.com/zmap/zcrypto v0.0.0-20200911161511-43ff0ea04f21/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4=
github.com/zmap/zlint/v2 v2.2.1/go.mod h1:ixPWsdq8qLxYRpNUTbcKig3R7WgmspsHGLhCCs6rFAM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200124225646-8b5121be2f68/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

127
main.go Normal file
View File

@ -0,0 +1,127 @@
package main
import (
"flag"
"path/filepath"
"time"
"git.sr.ht/~thrrgilag/woodstock"
"git.wntrmute.dev/kyle/nlink/nomad"
"git.wntrmute.dev/kyle/goutils/config"
"git.wntrmute.dev/kyle/goutils/die"
log "git.wntrmute.dev/kyle/goutils/syslog"
)
var defaultRSS = "https://nomad.wntrmute.net/u/kyle.rss"
func defaultPath(file string) string {
return filepath.Join("/perm", "nlink", file)
}
func fetchItems(db *nomad.DB, markOnly bool) error {
rssFeed := nomad.NewURLSource(config.Get("local_rss_feed_url"))
items, err := nomad.FetchRSS(rssFeed)
if err != nil {
return nil
}
items, err = db.Filter(nil, items)
if err != nil {
return err
}
if items == nil {
return nil
}
if markOnly {
tx, err := db.Begin()
if err != nil {
return err
}
for _, item := range items {
if err = db.Mark(tx, item); err != nil {
return err
}
}
tx.Commit()
return err
}
client := woodstock.NewClient(config.Get("pnut_id"), config.Get("pnut_secret"))
client.SetAccessToken(config.Get(""))
for _, item := range items {
p := nomad.NewPost(item)
err = p.Fetch()
die.If(err)
err = p.Post(client)
die.If(err)
err = db.Mark(nil, item)
}
return nil
}
const interval = 5 * time.Minute
func main() {
var (
configFile string
interval time.Duration
initDB bool
level string
markOnly bool
)
flag.StringVar(&configFile, "f", defaultPath("nlink.conf"), "`path` to config file")
flag.BoolVar(&initDB, "i", false, "initialize a new DB")
flag.StringVar(&level, "l", "INFO", "log level")
flag.BoolVar(&markOnly, "m", false, "only mark posts as having been posted")
flag.Parse()
logOpts := &log.Options{
Level: level,
Tag: "nlink",
Facility: "daemon",
WriteSyslog: true,
}
if err := log.Setup(logOpts); err != nil {
die.If(err)
}
log.Debugf("loading config file %s", configFile)
if err := config.LoadFile(configFile); err != nil {
log.Fatal(err)
}
if initDB {
db := nomad.NewDB(config.Get("local_database"))
err := db.Create()
die.If(err)
db.Close()
}
db := nomad.NewDB(config.Get("local_database"))
err := db.Connect()
die.If(err)
defer db.Close()
if markOnly {
if err = fetchItems(db, markOnly); err != nil {
log.Fatal(err)
}
return
}
for {
err = fetchItems(db, markOnly)
log.Info(err)
time.Sleep(interval)
}
}

119
nomad/db.go Normal file
View File

@ -0,0 +1,119 @@
package nomad
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
type Executor interface {
Exec(stmt string, args ...interface{}) (sql.Result, error)
Query(stmt string, args ...interface{}) (*sql.Rows, error)
}
func buildArgs(n int) string {
s := ""
for i := 0; i < n; i++ {
s += "?,"
}
return s[:len(s)-1]
}
type DB struct {
path string
db *sql.DB
}
func NewDB(path string) *DB {
return &DB{path: path}
}
func (db *DB) Connect() (err error) {
if db.db != nil {
return nil
}
db.db, err = sql.Open("sqlite3", db.path)
return err
}
func (db *DB) Close() error {
if db.db == nil {
return nil
}
err := db.db.Close()
db.db = nil
return err
}
func (db *DB) Create() (err error) {
err = db.Connect()
if err != nil {
return err
}
_, err = db.db.Exec(createSQL)
return err
}
func (db *DB) Begin() (tx *sql.Tx, err error) {
err = db.Connect()
if err != nil {
return tx, err
}
return db.db.Begin()
}
func (db *DB) executor(tx *sql.Tx) Executor {
if tx != nil {
return tx
}
return db.db
}
func (db *DB) Mark(tx *sql.Tx, item Item) (err error) {
_, err = db.executor(tx).Exec(markSQL, item.URL.ID(), item.Title, item.PubDate)
return err
}
func (db *DB) Filter(tx *sql.Tx, items []Item) (newItems []Item, err error) {
seen := map[string]bool{}
args := buildArgs(len(items))
stmt := fmt.Sprintf(filterSQLTpl, args)
urls := make([]interface{}, 0, len(items))
for _, item := range items {
urls = append(urls, item.URL.ID())
}
rows, err := db.executor(tx).Query(stmt, urls...)
if err != nil {
return nil, err
}
for rows.Next() {
var url string
err = rows.Scan(&url)
if err != nil {
return nil, err
}
seen[url] = true
}
for _, item := range items {
if seen[item.URL.ID()] {
continue
}
newItems = append(newItems, item)
}
// Reverse the list of items so that they are in reverse chronological order.
nitems := len(items) - 1
for i := range items {
j := nitems - i
items[i], items[j] = items[j], items[i]
}
return newItems, nil
}

11
nomad/db_test.go Normal file
View File

@ -0,0 +1,11 @@
package nomad
import "testing"
func TestBuildArgs(t *testing.T) {
expected := "?,?,?"
args := buildArgs(3)
if expected != args {
t.Fatalf("have '%s', want '%s'", args, expected)
}
}

51
nomad/item.go Normal file
View File

@ -0,0 +1,51 @@
package nomad
import (
"sort"
"github.com/mmcdole/gofeed"
)
type Item struct {
URL Source
Title string
PubDate int64
}
type itemList []Item
func (l itemList) Len() int {
return len(l)
}
func (l itemList) Less(i, j int) bool {
return l[i].PubDate < l[j].PubDate
}
func (l itemList) Swap(i, j int) {
l[i], l[j] = l[j], l[i]
}
func FetchRSS(feedSource Source) (items []Item, err error) {
rd, err := feedSource.Fetch()
if err != nil {
return nil, err
}
feedParser := gofeed.NewParser()
feed, err := feedParser.ParseString(rd)
if err != nil {
return nil, err
}
for _, item := range feed.Items {
items = append(items, Item{
URL: NewSoupSource(item.Link),
Title: item.Title,
PubDate: item.PublishedParsed.Unix(),
})
}
sort.Sort(itemList(items))
return items, nil
}

104
nomad/item_test.go Normal file
View File

@ -0,0 +1,104 @@
package nomad
import "testing"
var sampleRSS = `<?xml version='1.0' encoding='UTF-8'?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>kyle@ nomad</title>
<link>https://nomad.wntrmute.net/u/kyle.rss</link>
<description>An experiment in MMS micropublishing</description>
<atom:link href="https://nomad.wntrmute.net/u/kyle.rss" rel="self"/>
<docs>http://www.rssboard.org/rss-specification</docs>
<generator>python-feedgen</generator>
<lastBuildDate>Thu, 04 Nov 2021 16:44:41 +0000</lastBuildDate>
<item>
<title>kyle @ 2021-11-03 10:36:44 PDT: At least not with images, I guess.</title>
<link>https://nomad.wntrmute.net/p/1996</link>
<description>At least not with images, I guess.</description>
<guid isPermaLink="true">https://nomad.wntrmute.net/p/1996</guid>
<pubDate>Wed, 03 Nov 2021 10:36:44 -0700</pubDate>
</item>
<item>
<title>kyle @ 2021-11-03 10:35:53 PDT: This thing isn't working at all...</title>
<link>https://nomad.wntrmute.net/p/1995</link>
<description>This thing isn't working at all...</description>
<guid isPermaLink="true">https://nomad.wntrmute.net/p/1995</guid>
<pubDate>Wed, 03 Nov 2021 10:35:53 -0700</pubDate>
</item>
<item>
<title>kyle @ 2021-11-03 02:38:48 PDT: And a plain text post too...</title>
<link>https://nomad.wntrmute.net/p/1994</link>
<description>And a plain text post too...</description>
<guid isPermaLink="true">https://nomad.wntrmute.net/p/1994</guid>
<pubDate>Wed, 03 Nov 2021 02:38:48 -0700</pubDate>
</item>
<item>
<title>kyle @ 2021-11-03 02:23:29 PDT: Working on a thing that needs a test post so heres a picture of...</title>
<link>https://nomad.wntrmute.net/p/1993</link>
<description>Working on a thing that needs a test post so heres a picture of some mead.</description>
<guid isPermaLink="true">https://nomad.wntrmute.net/p/1993</guid>
<enclosure url="https://nomad.sfo2.cdn.digitaloceanspaces.com/MM95aee9af66546a4ef9b66ea8168b359c.jpg" length="194792" type="image/jpeg"/>
<pubDate>Wed, 03 Nov 2021 02:23:29 -0700</pubDate>
</item>
<item>
<title>kyle @ 2021-10-31 22:44:56 PDT: Fall colours in Oakland.</title>
<link>https://nomad.wntrmute.net/p/1991</link>
<description>Fall colours in Oakland.</description>
<guid isPermaLink="true">https://nomad.wntrmute.net/p/1991</guid>
<enclosure url="https://nomad.sfo2.cdn.digitaloceanspaces.com/MM7095b4db336c7d03b23f2cd702eb0e0a.jpg" length="426006" type="image/jpeg"/>
<pubDate>Sun, 31 Oct 2021 22:44:56 -0700</pubDate>
</item>
<item>
<title>kyle @ 2021-10-25 20:02:11 PDT: I am the operator of my pocket ... operator.</title>
<link>https://nomad.wntrmute.net/p/1990</link>
<description>I am the operator of my pocket ... operator.</description>
<guid isPermaLink="true">https://nomad.wntrmute.net/p/1990</guid>
<enclosure url="https://nomad.sfo2.cdn.digitaloceanspaces.com/MMe8e7ad7b08b18e9df53ac97d87c8faa0.jpg" length="232659" type="image/jpeg"/>
<pubDate>Mon, 25 Oct 2021 20:02:11 -0700</pubDate>
</item>
<item>
<title>kyle @ 2021-10-24 19:28:09 PDT: The Yeti being a backup battery that I got with a solar panel fo...</title>
<link>https://nomad.wntrmute.net/p/1989</link>
<description>The Yeti being a backup battery that I got with a solar panel for offgrid stuff, good test for it though </description>
<guid isPermaLink="true">https://nomad.wntrmute.net/p/1989</guid>
<pubDate>Sun, 24 Oct 2021 19:28:09 -0700</pubDate>
</item>
<item>
<title>kyle @ 2021-10-24 19:27:14 PDT: Power's out, fortunately I have the Yeti</title>
<link>https://nomad.wntrmute.net/p/1988</link>
<description>Power's out, fortunately I have the Yeti</description>
<guid isPermaLink="true">https://nomad.wntrmute.net/p/1988</guid>
<pubDate>Sun, 24 Oct 2021 19:27:14 -0700</pubDate>
</item>
<item>
<title>kyle @ 2021-10-24 13:22:47 PDT: Finally exploring the trail behind my apartment, of course in th...</title>
<link>https://nomad.wntrmute.net/p/1986</link>
<description>Finally exploring the trail behind my apartment, of course in the rain.</description>
<guid isPermaLink="true">https://nomad.wntrmute.net/p/1986</guid>
<enclosure url="https://nomad.sfo2.cdn.digitaloceanspaces.com/MM6f13e74faa6d643d654507c5bd9d8f53.jpg" length="622207" type="image/jpeg"/>
<pubDate>Sun, 24 Oct 2021 13:22:47 -0700</pubDate>
</item>
<item>
<title>kyle @ 2021-10-24 09:33:15 PDT: From todays Readwise email</title>
<link>https://nomad.wntrmute.net/p/1984</link>
<description>From todays Readwise email</description>
<guid isPermaLink="true">https://nomad.wntrmute.net/p/1984</guid>
<enclosure url="https://nomad.sfo2.cdn.digitaloceanspaces.com/MMcc9d528ead93949ea21dd4b4aff21836.jpg" length="53704" type="image/jpeg"/>
<pubDate>Sun, 24 Oct 2021 09:33:15 -0700</pubDate>
</item>
</channel>
</rss>
`
func TestFetchRSS(t *testing.T) {
source := NewStringSource(sampleRSS)
items, err := FetchRSS(source)
if err != nil {
t.Fatal(err)
}
if len(items) == 0 {
t.Fatal("no items parsed from RSS")
}
}

126
nomad/post.go Normal file
View File

@ -0,0 +1,126 @@
package nomad
import (
"bytes"
"errors"
"fmt"
"path/filepath"
"git.sr.ht/~thrrgilag/woodstock"
"github.com/anaskhan96/soup"
)
func nomadLink(url Source) string {
return fmt.Sprintf("[nomad.wntrmute.net](%s)", url.ID())
}
type sel struct {
selectors []string
}
func selector(selectors ...string) sel {
return sel{selectors: selectors}
}
func find(root soup.Root, attr string, selectors ...sel) (string, bool) {
result := root
for _, selector := range selectors {
result = result.Find(selector.selectors...)
if result.Pointer == nil {
return "", false
}
}
if attr == "" {
text := result.Text()
return text, text != ""
}
value, hasAttr := result.Attrs()[attr]
return value, hasAttr
}
type Post struct {
Image Source
Body string
URL Source
}
func NewPost(item Item) Post {
return Post{
Body: nomadLink(item.URL),
URL: item.URL,
}
}
func (p *Post) Fetch() error {
pageSource, err := p.URL.Fetch()
if err != nil {
return err
}
root := soup.HTMLParse(pageSource)
body, hasBody := find(root, "", selector("p", "class", "entry-body"))
if hasBody {
p.Body = body + " " + p.Body
}
imageURL, hasImageURL := find(
root, "src",
selector("div", "class", "entry-image"),
selector("img"))
if hasImageURL {
p.Image = NewURLSource(imageURL)
}
return nil
}
func (p Post) Post(client *woodstock.Client) error {
pnutPost := woodstock.NewPost{
Text: p.Body,
}
if p.Image != nil {
imageData, err := p.Image.FetchBytes()
if err != nil {
return err
}
params := map[string]string{
"name": filepath.Base(p.Image.ID()),
"is_public": "1",
"type": "photo",
}
r := bytes.NewBuffer(imageData)
file, err := client.CreateFile(params, r)
if err != nil {
panic(err.Error())
return err
}
pnutPost.Raw = []woodstock.Raw{pnutOembedRaw(file)}
}
result, err := client.Post(pnutPost)
if err != nil {
return err
}
if result.Meta.ErrorMessage != "" {
return errors.New("woodstock: " + result.Meta.ErrorMessage)
}
return nil
}
func pnutOembedRaw(file woodstock.FileResult) woodstock.Raw {
fval := map[string]string{
"file_id": file.Data.ID,
"file_token": file.Data.FileToken,
"format": "oembed",
}
value := map[string]map[string]string{"+io.pnut.core.file": fval}
return woodstock.Raw{Type: "io.pnut.core.oembed", Value: value}
}

97
nomad/post_test.go Normal file
View File

@ -0,0 +1,97 @@
package nomad
import (
"testing"
)
var pageData = `<!DOCTYPE html>
<html lang="en">
<head>
<title>Nomad: An MMS µblog</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property="og:title" content="Finally exploring the trail behind my apartment, of course in the rain." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://nomad.wntrmute.net/p/1986" />
<meta property="og:image" content="https://nomad.sfo2.cdn.digitaloceanspaces.com/MM6f13e74faa6d643d654507c5bd9d8f53.jpg" />
<link href="/index.rss" type="application/rss+xml" rel="alternate" title="Nomad feed" />
<link href="/static/favicon.ico" rel="shortcut icon">
<link href="/static/style.css" rel="stylesheet">
</head>
<body>
<div id="wrap">
<main>
<header>
<h1><a href="/">Nomad</a></h1>
</header>
<section>
<article class="entry entry-single" id="post-1986">
<div class="entry-details">
<span class="entry-author"><a href="/u/kyle">kyle</a></span>
</div>
<div class="entry-image">
<!-- TODO: how do we provide a useful alt? -->
<a href="https://nomad.sfo2.cdn.digitaloceanspaces.com/MM6f13e74faa6d643d654507c5bd9d8f53.jpg" target="_blank" rel="noopener">
<img src="https://nomad.sfo2.cdn.digitaloceanspaces.com/MM6f13e74faa6d643d654507c5bd9d8f53.jpg" alt="MMS attachment"/>
</a>
</div>
<p class="entry-body">Finally exploring the trail behind my apartment, of course in the rain.</p>
<div class="entry-details">
<time datetime="2021-10-24 13:22:47 PDT">
posted 2021-10-24 13:22:47 PDT
</time>
</div>
</article>
</section>
<footer>
<p>
<a href="/about">Nomad</a> is an experiment in MMS micropublishing,
and is bolted together with
<a href="https://www.palletsprojects.com/p/flask/">Flask</a>
and <a href="https://twilio.com/">Twilio</a>; it was inspired by
<a href="https://craigmod.com/essays/sms_publishing/">Craig Mod's experiment</a>.
It is being built by
<a class="h-card u-url" rel="me" href="https://ai6ua.net/">Kyle</a> and
<a class="h-card u-url" rel="me" href="https://wallyjones.com/">Wally</a>.
</p>
<p><small>Nomad v78</small></p>
</footer>
</main>
</div>
</body>
</html>
`
func TestFetch(t *testing.T) {
p := Post{
Body: nomadLink(NewURLSource("https://localhost")),
URL: NewStringSource(pageData),
}
err := p.Fetch()
if err != nil {
t.Fatal(err)
}
expectedBody := "Finally exploring the trail behind my apartment, of course in the rain. [nomad.wntrmute.net](https://localhost)"
expectedImage := "https://nomad.sfo2.cdn.digitaloceanspaces.com/MM6f13e74faa6d643d654507c5bd9d8f53.jpg"
if p.Body != expectedBody {
t.Fatalf("have '%s', want '%s'", p.Body, expectedBody)
}
if p.Image.ID() != expectedImage {
t.Fatalf("have '%s', want '%s'", p.Image, expectedImage)
}
}

114
nomad/source.go Normal file
View File

@ -0,0 +1,114 @@
package nomad
import (
"crypto/sha256"
"fmt"
"io/ioutil"
"net/http"
"github.com/anaskhan96/soup"
)
// Source provides data for the various parsing functions.
type Source interface {
Fetch() (string, error)
FetchBytes() ([]byte, error)
ID() string
String() string
}
// URLSource fetches data from an HTTP resource.
type URLSource struct {
url string
}
func (u URLSource) FetchBytes() ([]byte, error) {
resp, err := http.Get(u.url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, err
}
func (u URLSource) Fetch() (string, error) {
body, err := u.FetchBytes()
if err != nil {
return "", err
}
return string(body), nil
}
func (u URLSource) ID() string {
return u.url
}
func (u URLSource) String() string {
return u.url
}
func NewURLSource(url string) URLSource {
return URLSource{url: url}
}
type StringSource struct {
s string
}
func (s StringSource) Fetch() (string, error) {
return s.String(), nil
}
func (s StringSource) FetchBytes() ([]byte, error) {
return []byte(s.s), nil
}
func NewStringSource(s string) *StringSource {
return &StringSource{s: s}
}
func (s StringSource) ID() string {
h := sha256.Sum256([]byte(s.s))
return fmt.Sprintf("%s", h[:])
}
func (s StringSource) String() string {
return s.s
}
// SoupSource uses the soup package to fetch data.
type SoupSource struct {
url string
}
func (s SoupSource) Fetch() (string, error) {
return soup.Get(s.url)
}
func (s SoupSource) FetchBytes() ([]byte, error) {
body, err := s.Fetch()
if err != nil {
return nil, err
}
return []byte(body), nil
}
func (s SoupSource) ID() string {
return s.url
}
func (s SoupSource) String() string {
return s.url
}
func NewSoupSource(url string) SoupSource {
return SoupSource{url: url}
}

11
nomad/sql.go Normal file
View File

@ -0,0 +1,11 @@
package nomad
var createSQL = `CREATE TABLE IF NOT EXISTS posts (
url TEXT UNIQUE NOT NULL PRIMARY KEY,
title TEXT UNIQUE NOT NULL,
pub_date INTEGER NOT NULL
);`
var markSQL = `INSERT INTO posts (url, title, pub_date) VALUES (?, ?, ?);`
var filterSQLTpl = `SELECT url FROM posts WHERE url IN (%s)`

73
storage.go Normal file
View File

@ -0,0 +1,73 @@
package main
import (
"context"
"io"
"os"
"git.wntrmute.dev/kyle/goutils/config"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var minioClient *minio.Client
func connectMinio() (err error) {
minioClient, err = minio.New(config.Get("minio_host"), &minio.Options{
Creds: credentials.NewStaticV4(
config.Get("minio_access_key"),
config.Get("minio_secret_key"), ""),
Secure: true,
})
return err
}
func restoreDatabase(path string) error {
obj, err := minioClient.GetObject(
context.Background(),
config.Get("minio_bucket"),
"nlink.db",
minio.GetObjectOptions{},
)
if err != nil {
return err
}
dbFile, err := os.Create(path)
if err != nil {
return err
}
defer dbFile.Close()
_, err = io.Copy(dbFile, obj)
return err
}
func saveDatabase(path string) error {
dbFile, err := os.Open(path)
if err != nil {
return err
}
defer dbFile.Close()
fi, err := dbFile.Stat()
if err != nil {
return err
}
_, err = minioClient.PutObject(
context.Background(),
config.Get("minio_bucket"),
"nlink.db",
dbFile,
fi.Size(),
minio.PutObjectOptions{ContentType: "application/octet-stream"},
)
if err != nil {
return err
}
return nil
}