Compare commits
	
		
			1 Commits
		
	
	
		
			master
			...
			restructur
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 06e0e6afe8 | 
							
								
								
									
										18
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								.drone.yml
									
									
									
									
									
								
							| @ -1,18 +0,0 @@ | ||||
| kind: pipeline | ||||
| type: docker | ||||
| name: build | ||||
| 
 | ||||
| steps: | ||||
|   - name: lint checks | ||||
|     image: node:14.16-alpine | ||||
|     commands: | ||||
|       - npm install | ||||
|       - npm run lint | ||||
|   - name: spellchecker checks | ||||
|     image: polpetta/spellchecker | ||||
|     commands: | ||||
|       - ./tools/spellchecker.sh en | ||||
|   - name: hugo build | ||||
|     image: klakegg/hugo:0.82.1-ext-alpine-ci | ||||
|     commands: | ||||
|       - hugo | ||||
							
								
								
									
										10
									
								
								.fleek.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.fleek.json
									
									
									
									
									
								
							| @ -1,10 +0,0 @@ | ||||
| { | ||||
|     "build": { | ||||
|         "image": "polpetta/fleek:latest", | ||||
|         "command": "git lfs install && git lfs pull && yarn && hugo", | ||||
|         "publicDir": "public", | ||||
|         "environment": { | ||||
|             "HUGO_DISABLELANGUAGES": "it" | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										86
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										86
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,12 +1,94 @@ | ||||
| # ---> macOS | ||||
| # General | ||||
| .DS_Store | ||||
| .AppleDouble | ||||
| .LSOverride | ||||
| 
 | ||||
| # Icon must end with two \r | ||||
| Icon | ||||
| 
 | ||||
| 
 | ||||
| # Thumbnails | ||||
| ._* | ||||
| 
 | ||||
| # Files that might appear in the root of a volume | ||||
| .DocumentRevisions-V100 | ||||
| .fseventsd | ||||
| .Spotlight-V100 | ||||
| .TemporaryItems | ||||
| .Trashes | ||||
| .VolumeIcon.icns | ||||
| .com.apple.timemachine.donotpresent | ||||
| 
 | ||||
| # Directories potentially created on remote AFP share | ||||
| .AppleDB | ||||
| .AppleDesktop | ||||
| Network Trash Folder | ||||
| Temporary Items | ||||
| .apdisk | ||||
| 
 | ||||
| # ---> Hugo | ||||
| # Generated files by hugo | ||||
| /public/ | ||||
| /resources/_gen/ | ||||
| /assets/jsconfig.json | ||||
| hugo_stats.json | ||||
| 
 | ||||
| # Executable may be added to repository | ||||
| hugo.exe | ||||
| hugo.darwin | ||||
| hugo.linux | ||||
| 
 | ||||
| node_modules/ | ||||
| .hugo_build.lock | ||||
| # Temporary lock file while building | ||||
| /.hugo_build.lock | ||||
| 
 | ||||
| # ---> Emacs | ||||
| # -*- mode: gitignore; -*- | ||||
| *~ | ||||
| \#*\# | ||||
| /.emacs.desktop | ||||
| /.emacs.desktop.lock | ||||
| *.elc | ||||
| auto-save-list | ||||
| tramp | ||||
| .\#* | ||||
| 
 | ||||
| # Org-mode | ||||
| .org-id-locations | ||||
| *_archive | ||||
| 
 | ||||
| # flymake-mode | ||||
| *_flymake.* | ||||
| 
 | ||||
| # eshell files | ||||
| /eshell/history | ||||
| /eshell/lastdir | ||||
| 
 | ||||
| # elpa packages | ||||
| /elpa/ | ||||
| 
 | ||||
| # reftex files | ||||
| *.rel | ||||
| 
 | ||||
| # AUCTeX auto folder | ||||
| /auto/ | ||||
| 
 | ||||
| # cask packages | ||||
| .cask/ | ||||
| dist/ | ||||
| 
 | ||||
| # Flycheck | ||||
| flycheck_*.el | ||||
| 
 | ||||
| # server auth directory | ||||
| /server/ | ||||
| 
 | ||||
| # projectiles files | ||||
| .projectile | ||||
| 
 | ||||
| # directory configuration | ||||
| .dir-locals.el | ||||
| 
 | ||||
| # network security | ||||
| /network-security.data | ||||
| /rss.xml | ||||
|  | ||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +0,0 @@ | ||||
| [submodule "themes/hermit"] | ||||
| 	path = themes/hermit | ||||
| 	url = https://github.com/Track3/hermit.git | ||||
| @ -1,39 +0,0 @@ | ||||
| # Disable some built-in rules | ||||
| config: | ||||
|   no-inline-html: false | ||||
|   MD013: | ||||
|     code_block_line_length: 120 | ||||
|   MD024: | ||||
|     allow_different_nesting: true | ||||
| #  no-trailing-spaces: false | ||||
| #  no-multiple-blanks: false | ||||
| 
 | ||||
| # Include a custom rule package | ||||
| #customRules: | ||||
| #  - markdownlint-rule-titlecase | ||||
| 
 | ||||
| # Fix any fixable errors | ||||
| fix: false | ||||
| 
 | ||||
| # Define a custom front matter pattern | ||||
| #frontMatter: "<head>[^]*<\/head>" | ||||
| 
 | ||||
| # Define glob expressions to ignore | ||||
| ignores: | ||||
|   - "*.autogen.md" | ||||
| 
 | ||||
| # Use a plugin to recognize math | ||||
| #markdownItPlugins: | ||||
| #  - | ||||
| #    - "@iktakahiro/markdown-it-katex" | ||||
| 
 | ||||
| # Disable inline config comments | ||||
| noInlineConfig: true | ||||
| 
 | ||||
| # Disable progress on stdout | ||||
| noProgress: true | ||||
| 
 | ||||
| # Use a specific formatter | ||||
| outputFormatters: | ||||
|   - | ||||
|     - markdownlint-cli2-formatter-default | ||||
| @ -1,8 +0,0 @@ | ||||
| FROM fleek/hugo:node-16 | ||||
| 
 | ||||
| LABEL MAINTAINER="Davide Polonio poloniodavide@gmail.com" | ||||
| LABEL DESCRIPTION="Docker image for deploying website on IPFS via fleek" | ||||
| 
 | ||||
| RUN apt-get update \ | ||||
|  && apt-get install -y git-lfs \ | ||||
|  && rm -rf /var/lib/apt/lists/* | ||||
							
								
								
									
										16
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| .PHONY: all | ||||
| .DEFAULT_GOAL := all | ||||
| 
 | ||||
| all: clean html | ||||
| 
 | ||||
| html: | ||||
| 	@./publish.el | ||||
| 
 | ||||
| watch: | ||||
| 	@fswatch -0 path | while read -d "" event \
 | ||||
| 		do; | ||||
| 			./publish.el "${event}"; | ||||
| 		done | ||||
| 
 | ||||
| clean: | ||||
| 	@rm -rf ./dist/ | ||||
| @ -1,6 +0,0 @@ | ||||
| --- | ||||
| title: "{{ replace .Name "-" " " | title }}" | ||||
| date: {{ .Date }} | ||||
| draft: true | ||||
| --- | ||||
| 
 | ||||
							
								
								
									
										73
									
								
								config.toml
									
									
									
									
									
								
							
							
						
						
									
										73
									
								
								config.toml
									
									
									
									
									
								
							| @ -1,73 +0,0 @@ | ||||
| baseURL = "https://bitdispenser.dev/" | ||||
| languageCode = "en-us" | ||||
| defaultContentLanguage = "en" | ||||
| title = "Bitdispenser" | ||||
| theme = "hermit" | ||||
| #enableGitInfo = true | ||||
| pygmentsCodefences  = true | ||||
| pygmentsUseClasses  = true | ||||
| rssLimit = 15 | ||||
| copyright = "This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License." | ||||
| enableEmoji = true | ||||
| relativeUrls = true | ||||
| 
 | ||||
| [author] | ||||
|   name = "Davide Polonio" | ||||
| 
 | ||||
| [taxonomies] | ||||
|   tag = "tags" | ||||
| 
 | ||||
| [params] | ||||
|   dateform        = "2 Jan 2006" | ||||
|   dateformShort   = "2 Jan" | ||||
|   dateformNum     = "02-01-2006" | ||||
|   dateformNumTime = "02-01-2006" | ||||
|   themeColor = "#494f5c" | ||||
|   homeSubtitle = "Yet another ordered dispenser of bits" | ||||
|   footerCopyright = ' · <a href="http://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener">CC BY-SA 4.0</a>' | ||||
|   justifyContent = true | ||||
|   relatedPosts = true | ||||
|   code_copy_button = true | ||||
| 
 | ||||
|   [[params.social]] | ||||
|     name = "email" | ||||
|     url = "mailto:davide+bitdispenser@poldebra.me" | ||||
|   [[params.social]] | ||||
|     name = "github" | ||||
|     url = "https://github.com/Polpetta" | ||||
|   [[params.social]] | ||||
|     name = "stackoverflow" | ||||
|     url = "https://stackoverflow.com/users/9306378/polpetta" | ||||
|   [[params.social]] | ||||
|     name = "linkedin" | ||||
|     url = "https://www.linkedin.com/in/davidepolonio" | ||||
| 
 | ||||
| [languages] | ||||
|   [languages.en] | ||||
|     languageName = "English" | ||||
|     contentDir = "content/english" | ||||
|     title = "Bitdispenser" | ||||
|     homeSubtitle = "Yet another ordered dispenser of bits" | ||||
|     weight = 0 | ||||
|     [[languages.en.menu.main]] | ||||
|         name = "Posts" | ||||
|         url = "posts/" | ||||
|         weight = 10 | ||||
|     [[languages.en.menu.main]] | ||||
|         name = "About me" | ||||
|         url = "about-me" | ||||
|         weight = 20 | ||||
|   [languages.it] | ||||
|     languageName = "Italiano" | ||||
|     contentDir = "content/italian" | ||||
|     title = "Bitdispenser" | ||||
|     homeSubtitle = "L'ennesimo distributore ordinato di bit" | ||||
|     weight = 10 | ||||
|     [[languages.it.menu.main]] | ||||
|       name = "Articoli" | ||||
|       url = "articoli/" | ||||
|       weight = 10 | ||||
|     [[languages.it.menu.main]] | ||||
|       name = "Riguardo l'Autore" | ||||
|       url = "riguardo-l-autore" | ||||
|       weight = 20 | ||||
| @ -1,56 +0,0 @@ | ||||
| --- | ||||
| title: "About me" | ||||
| --- | ||||
| 
 | ||||
| Hi all! I am Davide Polonio, a programmer and devOps engineer. I am born in 1994 | ||||
| and started messing with computers since I was 8 (my first OS was the only and | ||||
| glorious Windows 95), and from there I continued my path to learn about | ||||
| computers and how to ~~break~~ tweak them. I graduated at university of Padova | ||||
| with a master degree in computer science in 2018. | ||||
| 
 | ||||
| I prefer back-end stuff (I usually end up using Java, Golang, Rust and Bash to | ||||
| gluing stuff around), especially automating releases with Continuous Integration | ||||
| and Continuous Delivery. I create and manage pipelines for Jenkins (and I | ||||
| currently administer Jenkins as well), but I also know other CI systems like | ||||
| Github Actions, Travis CI, Drone CI (or the now clone Woodpecker). I enjoy | ||||
| container technologies, starting from Docker, Podman and going on to | ||||
| orchestrators like Docker Swarm, Kubernetes and Nomad (which I would like to | ||||
| learn more of the lasts two). If I can, I use Infrastructure as Code (IaC) tools | ||||
| like Terraform and Packer to create and maintain infrastructure on cloud | ||||
| providers, like AWS or Scaleway (I also maintain [Podman plugin for | ||||
| Packer](https://github.com/Polpetta/packer-plugin-podman)). Automation is | ||||
| usually my preferred way, since it helps developers focus only on the business | ||||
| product instead of messing with tasks that can be done by a machine. Finally, I | ||||
| am interested in development processes like (but not only) Agile, Scrum, | ||||
| Waterfall. I think the right way to develop a product starts from how you | ||||
| organize your workflow and what processes you put in place in order to keep the | ||||
| _development machine_ going. | ||||
| 
 | ||||
| I am an open source enthusiast, so when I am able in my free time (or paid time | ||||
| if it is an open source business project) I like to contribute to open source | ||||
| projects and discover new ones. I believe in the added values that open source | ||||
| software (and especially free software) can give respect to closed ones. My | ||||
| dream job would be to contribute to one of the many open source I like full | ||||
| time! 🤩 | ||||
| 
 | ||||
| I'm not only into programming though! I like to practice sport. In particular, I | ||||
| am a black belt Judo and I teach it at the local dojo were I live. I know how to | ||||
| ski from when I was I child, and if possible I enjoy passing some time in the | ||||
| middle of mountains to admire the beauty of nature in winter. Finally, I like to | ||||
| play tabletops games and I enjoy things like table football, billiards, bowling | ||||
| (although I suck at the last one 😅). | ||||
| 
 | ||||
| If you are interested in getting in touch with me (or to get my resume) you can | ||||
| send me an e-mail (I prefer this way) at | ||||
| [davide@poldebra.me](mailto:davide+bitdispenser@poldebra.me) or you can contact | ||||
| me through my [LinkedIn](https://www.linkedin.com/in/davidepolonio/) profile. | ||||
| 
 | ||||
| ## Why this website and this blog | ||||
| 
 | ||||
| As I have already written at the beginning of [my first blog post]({{< ref | ||||
| "/hello-world.md" >}}) I liked the idea of a place where I can share my thoughts | ||||
| regarding technologies, personal experiences and what not. This blog is open | ||||
| source, and all my content is licensed under CC BY-SA 4.0. The repository can be | ||||
| found either on [my personal Gitea | ||||
| instance](https://git.poldebra.me/polpetta/bitdispenser.dev) or on [Github | ||||
| (mirrored)](https://github.com/Polpetta/bitdispenser.dev). | ||||
| @ -1,150 +0,0 @@ | ||||
| --- | ||||
| title: Hello world! | ||||
| description: The very first article of this blog | ||||
| tags: | ||||
| - blog | ||||
| - tech | ||||
| - hugo | ||||
| date: 2021-05-18T22:22:00+02:00 | ||||
| --- | ||||
| 
 | ||||
| If you are reading this it means either you are trying to understand who I am | ||||
| (are you a recruiter by any chance?) or you are simply bored. In any way, this | ||||
| is the very first blog post, and it means you have come to the end of the road | ||||
| (since this is the beginning). I always loved to have a place where I can write | ||||
| down my thoughts about a new technology, a framework or what I did in order to | ||||
| achieve a goal in a hobby project. That is not all though: I was searching for a | ||||
| place where I was also able to express my ideas regarding modern dilemmas like | ||||
| privacy issues and decentralization. I am not the kind of guy who likes social | ||||
| networks, so I never had the chance to express them. Until now. | ||||
| 
 | ||||
| ## How this blog is built | ||||
| 
 | ||||
| ### The website | ||||
| 
 | ||||
| This blog is a very simple [Hugo](https://gohugo.io/) website. If you do not | ||||
| know what it is, its basically a static website generator. It takes Markdown | ||||
| documents and converts them in HTML pages. If you applied a specific theme then | ||||
| it builds the page according to that theme. Hugo has a huge selection of themes | ||||
| in its [dedicated themes page](https://themes.gohugo.io/), so basically picking | ||||
| up one and starting from there is very simple. If you are curious about how I | ||||
| made it, you can check out the source at my [personal git server | ||||
| instance](https://git.poldebra.me/polpetta/bitdispenser.dev), where I started | ||||
| hosting my code when [Microsoft bought | ||||
| Github](https://news.microsoft.com/announcement/microsoft-acquires-github/) (you | ||||
| can find a copy of the repository on Github too, visiting [my | ||||
| profile](https://github.com/Polpetta)). I have simply picked up the simpler and | ||||
| cleanest blog theme out there, following Ludwig Mies van der Rohe's idea: | ||||
| 
 | ||||
| > less is more | ||||
| 
 | ||||
| It will be a success if more than two people actually starts reading what I | ||||
| write here, at least I want them to read this blog without having their | ||||
| eyes bleeding with an extravagant color combination. | ||||
| 
 | ||||
| Last but not least, the website icon is literally the Team Fortress 2 dispenser | ||||
| (one of the games I love and one of the first game I started playing online as a | ||||
| kid when a decent connection at home was finally available), taken from the RED | ||||
| team and generated via a [favicon generator](https://realfavicongenerator.net/). | ||||
| The name of the website partially came out from that, and from the fact that | ||||
| basically every website is a dispenser of bits, and it is only thanks to our | ||||
| beloved browsers we are able to actually "consume" what is distributed in the | ||||
| first place. | ||||
| 
 | ||||
| {{< figure src="/content/hello-world/engiwithdispenser.png" alt=`Engineer with | ||||
| his dispenser` caption=`Engineer class with a dispenser. Thanks to [Team | ||||
| Fortress wiki](https://wiki.teamfortress.com) for providing the image I | ||||
| shamelessly downloaded from them` >}} | ||||
| 
 | ||||
| I hope Valve will not sue me for taking that asset as my website favicon. I | ||||
| swear I will change it, a day. Pinky promise. | ||||
| 
 | ||||
| ### Hosting | ||||
| 
 | ||||
| The real problem of hosting a website now day is not how to build it (as you can | ||||
| see) but _where_ you can host it. There are plenty of cloud provides: AWS, | ||||
| Google Cloud, DigitalOcean, Scaleway, etc... every day there is a new one | ||||
| popping up. They all offers the possibility to host your website pretty easily, | ||||
| especially if the website is a static one like this (e.g. using a S3 bucket). | ||||
| Since I like challenges and I also like to learn new stuff, I though that | ||||
| hosting the website in this way was boring. At the same time, I wanted to have a | ||||
| good uptime and to not meddle too much under the hood. My (dream) requirements | ||||
| were: | ||||
| 
 | ||||
| * always up | ||||
| * good response time | ||||
| * possibility to host as much data as I want | ||||
| * using a hosting free as in beer and (possibly) that could use free as in | ||||
|   freedom technologies | ||||
| 
 | ||||
| Now, reading this I imagine you are thinking I am going crazy, and maybe I am, but | ||||
| that is not the case. In fact, multiple weeks prior to writing this article, | ||||
| while trying to kill the boredom caused by COVID-19 lockdown, I discovered | ||||
| [IPFS](https://ipfs.io/). I already heard of it at University, but I never | ||||
| bothered too much to understand what was about. I though "well, it surely is | ||||
| some sort of filesystem". I was somewhat right, but not the way I thought. | ||||
| 
 | ||||
| IPFS acts like a peer-to-peer network, where nodes hash the content they want to | ||||
| share to let other nodes grab it. You can grab this content using your local | ||||
| node or using one of the many available gateways. Nodes can "pin" a file too, in | ||||
| order to keep it locally and to serve it to other nodes. If a file gets pinned | ||||
| by different nodes and gains traction it basically can not be deleted from the | ||||
| web. | ||||
| 
 | ||||
| #### Choosing the right tools | ||||
| 
 | ||||
| Surely, as [the IPFS documentation | ||||
| describes](https://docs.ipfs.io/how-to/websites-on-ipfs/multipage-website/), I | ||||
| could have done it by myself. But there is a but. Holding all the infrastructure | ||||
| manually means sacrifice the "always up" and "good response time" thingy for two | ||||
| simple motives: | ||||
| 
 | ||||
| 1. the [server where I host my drafts & codebase](https://git.poldebra.me) is | ||||
|    hosted on a very small machine (2GB of RAM and 2vCPU), very easy to kill with | ||||
|    the slightest of loads | ||||
| 2. in order to achieve a good response time, a CDN or some sort of caching is | ||||
|    necessary (even if the application is stored in a distributed file system) | ||||
| 
 | ||||
| {{< figure src="/content/hello-world/fleek.png" caption=`Fleek website, that I | ||||
| used to automate my deployment on IPFS and my DNS update` alt=`The fleek website | ||||
| screenshot` class="right" >}} | ||||
| 
 | ||||
| Finally, in order to achieve full automation with DNS updates, I would have | ||||
| needed to implement and use NameCheap APIs (currently it is the DNS provider I | ||||
| use for most of my websites). "What's the difficulty?" one would ask. [Here is | ||||
| the official documentation](https://www.namecheap.com/support/api/intro/), and | ||||
| even if the APIs look promising, while studying them my will to live decreased a | ||||
| little bit, and so I decided that if I wanted to get up and running with less | ||||
| maintenance as possible, with a good uptime while having the maximum | ||||
| automation possible I needed to rely on a dedicated service. Luckily | ||||
| [fleek.co](https://fleek.co) was what I was searching for. They currently | ||||
| provide the possibility to buy a domain from their website, giving them all the | ||||
| hassle of updating the new website on IPFS, distributing it, refreshing a very | ||||
| possible CDN and finally to update the DNS records accordingly. This, as you can | ||||
| imagine, provides multiple benefits: | ||||
| 
 | ||||
| * I do not have to care about my very little dev machine getting hugged to death | ||||
|   by request in the remote case any of my posts get any attention | ||||
| * I do not have to focus on automating the process someone else has already done | ||||
|   for me | ||||
| * I can focus on writing posts after dinner instead of scratching my head trying | ||||
|   to understand why the website does not load/the DNS is not properly updated | ||||
|    | ||||
| The only downside to this approach is that Fleek provides limitations on how | ||||
| much data and bandwidth you can host. At the time of writing, you can only host | ||||
| up to 3GB (enough for this website) and have a 50GB bandwidth (that is fine for | ||||
| now) for the free version. [Upgrading you account](https://fleek.co/pricing/) to | ||||
| one of the available plans give you extra space and bandwidth. | ||||
| 
 | ||||
| ## Future improvements | ||||
| 
 | ||||
| For sure, this is only the beginning. Having an automatic workflow of spell | ||||
| check, deployment and release would be the first milestone. Future features for | ||||
| this website could be an automatic posting of every new article on a dedicated | ||||
| Mastodon bot, so that people can possibly discuss about my thoughts on the | ||||
| fediverse, a decentralized network. | ||||
| 
 | ||||
| To conclude, the frequency of this blog will be...whenever I have time to post | ||||
| :grin: Of course I need content before posting something, and this require some | ||||
| time for myself for experimenting with new technologies and learning new stuff, | ||||
| so I do not expect very much posting, but only time will tell! | ||||
| @ -1,255 +0,0 @@ | ||||
| --- | ||||
| title: "Building a Telegram bot in Rust: a journey through Songlify" | ||||
| description: An adventure through Rust and Telegram | ||||
| tags: | ||||
| - blog | ||||
| - tech | ||||
| - rust | ||||
| - telegram | ||||
| - songlify | ||||
| date: 2022-01-05T17:01:00+02:00 | ||||
| --- | ||||
| 
 | ||||
| Some time passed since the last article I wrote there. A lot of stuff happened | ||||
| meanwhile, especially with COVID, but here we are again. While busy dealing with | ||||
| the mess of real life tasks, three months ago I started to write a little bot | ||||
| for Telegram in Rust. It is a simple one, but I consider the journey interesting | ||||
| and worth of writing it down. If I add new features worth mentioning I will | ||||
| start a series about it, maybe. | ||||
| 
 | ||||
| ## Telegram bots | ||||
| 
 | ||||
| Telegram bots are not something new to me and nowadays are pretty much easy to | ||||
| make, so I consider them like a gym where to try out new technologies and | ||||
| experiment with stuff. I wrote plentiful of them, some of those are open source | ||||
| like for example <https://github.com/Augugrumi/TorreArchimedeBot> (which is | ||||
| currently broken 😭) that was useful when going to University, because it | ||||
| scraped the university free room web page and from there it was able to tell you | ||||
| which rooms where without any lessons and for how much time, allowing you to | ||||
| easily find a place where to study with your mates (yep, we didn't like library | ||||
| too much). | ||||
| 
 | ||||
| {{< figure src="/content/songlify/telegramscreen.png" alt=`A screenshot of | ||||
| TorreArchimedeBot in action.` caption=`A screenshot of TorreArchimedeBot in | ||||
| action.` >}} | ||||
| 
 | ||||
| Also another one bot worthy of mention is | ||||
| <https://github.com/Polpetta/RedditToTelegram>, that allowed our D&D group to | ||||
| receive push notifications of our private Subreddit in our Telegram group. | ||||
| 
 | ||||
| As you can see, all of these bots are quite simple, but they have the added | ||||
| value of teaching you some new programming concepts, technologies or frameworks | ||||
| that can be later applied in something that can be more production environment. | ||||
| 
 | ||||
| ## Rust | ||||
| 
 | ||||
| I started to approach Rust many years ago (I do not remember exactly when). | ||||
| First interaction with it was quite interesting to say at least: there were way | ||||
| less compiler features (for example now the compiler is able to understand | ||||
| object lifetime at compile time most of the time alone, without specifying them) | ||||
| that made it a... _not-so-pleasant programming experience_. It had potential | ||||
| thought, so by following Rust news I picked it up last year again, noticing that | ||||
| now it has improved a lot and it is more pleasant to write. Meanwhile, also | ||||
| JetBrains developed a good support for IntelliJ, so now it is even possible to | ||||
| debug and perform every operation directly from your IDE UI. | ||||
| 
 | ||||
| ## Making the two worlds collide: Rust + Telegram | ||||
| 
 | ||||
| One of the features I wanted to learn this time regarding Rust was the | ||||
| asynchronous support it offers. Rust started to have `async` support with | ||||
| [Tokio](https://tokio.rs/) framework, and recently the Rust team started to | ||||
| build the asynchronous functionality inside Rust itself. Even if in the first | ||||
| steps, it looks promising and the idea of a low-level language, without GC, with | ||||
| automatic memory management and so much safety having asynchronous support is | ||||
| exciting to me! 🥳 So the only option left, at this point, was to start messing | ||||
| around with it. I started by picking up one of the many frameworks that provides | ||||
| a layer for the Telegram APIs, [Teloxide](https://github.com/teloxide/teloxide). | ||||
| In particular, as you can see from its _README_, one of the examples starts by | ||||
| using `#[tokio:main]` macro: | ||||
| 
 | ||||
| ```rust | ||||
| use teloxide::prelude::*; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     teloxide::enable_logging!(); | ||||
|     log::info!("Starting dices_bot..."); | ||||
| 
 | ||||
|     let bot = Bot::from_env().auto_send(); | ||||
| 
 | ||||
|     teloxide::repl(bot, |message| async move { | ||||
|         message.answer_dice().await?; | ||||
|         respond(()) | ||||
|     }) | ||||
|     .await; | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| This was the reason I picked it up, given that it looked the most promising by | ||||
| the time I started the project. | ||||
| 
 | ||||
| ### Building _Songlify_ | ||||
| 
 | ||||
| So, after choosing what was going to use to build the bot, I needed a _reason_ | ||||
| to build it. | ||||
| 
 | ||||
| With my friends we usually share a lot of songs (via Spotify links), so I | ||||
| thought it was a good idea to build a bot around it. I integrated a Spotify API | ||||
| library in it and started hacking up a bot. | ||||
| 
 | ||||
| > ⚠ Note that at the time of writing I have just noticed that the library I use | ||||
| > for speaking with Spotify, [aspotify](https://crates.io/crates/aspotify) has | ||||
| > been deprecated in favour of [rspotify](https://crates.io/crates/rspotify) | ||||
| 
 | ||||
| The first bot version was something very simple, and it was a single-file | ||||
| program with nothing very fancy (I have written it in a night): | ||||
| 
 | ||||
| ```rust | ||||
| use crate::SpotifyURL::Track; | ||||
| use aspotify::{Client, ClientCredentials}; | ||||
| use teloxide::prelude::*; | ||||
| 
 | ||||
| enum SpotifyURL { | ||||
|     Track(String), | ||||
| } | ||||
| 
 | ||||
| fn get_spotify_entry(url: &str) -> Option<SpotifyURL> { | ||||
|     if url.contains("https://open.spotify.com/track/") { | ||||
|         let track_id = url.rsplit('/').next().and_then(|x| x.split('?').next()); | ||||
|         return match track_id { | ||||
|             Some(id) => Some(SpotifyURL::Track(id.to_string())), | ||||
|             None => None, | ||||
|         }; | ||||
|     } | ||||
|     return None; | ||||
| } | ||||
| 
 | ||||
| struct TrackInfo { | ||||
|     name: String, | ||||
|     artist: Vec<String>, | ||||
| } | ||||
| 
 | ||||
| async fn get_spotify_track(spotify: Box<Client>, id: &String) -> Option<TrackInfo> { | ||||
|     match spotify.tracks().get_track(id.as_str(), None).await { | ||||
|         Ok(track) => Some(TrackInfo { | ||||
|             name: track.data.name, | ||||
|             artist: track.data.artists.iter().map(|x| x.name.clone()).collect(), | ||||
|         }), | ||||
|         Err(_e) => None, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     teloxide::enable_logging!(); | ||||
|     log::info!("Starting Songlify..."); | ||||
| 
 | ||||
|     let bot = Bot::from_env().auto_send(); | ||||
|     teloxide::repl(bot, |message| async move { | ||||
|         let spotify_creds = | ||||
|             ClientCredentials::from_env().expect("CLIENT_ID and CLIENT_SECRET not found."); | ||||
|         let spotify_client = Box::new(Client::new(spotify_creds)); | ||||
| 
 | ||||
|         log::info!("Connected to Spotify"); | ||||
|         let text = message.update.text().and_then(get_spotify_entry); | ||||
|         match text { | ||||
|             Some(spotify) => match spotify { | ||||
|                 Track(id) => { | ||||
|                     let track_info = get_spotify_track(spotify_client, &id).await; | ||||
|                     match track_info { | ||||
|                         Some(info) => { | ||||
|                             let reply = format!( | ||||
|                                 "Track information:\n\ | ||||
|                                 Track name: {}\n\ | ||||
|                                 Artists: {}", | ||||
|                                 info.name, | ||||
|                                 info.artist.join(", ") | ||||
|                             ); | ||||
|                             Some(message.reply_to(reply).await?) | ||||
|                         } | ||||
|                         None => None, | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             None => None, | ||||
|         }; | ||||
|         respond(()) | ||||
|     }) | ||||
|     .await; | ||||
| 
 | ||||
|     log::info!("Exiting..."); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| As you can see, basically every time a request arrived to the bot, login to | ||||
| Spotify was performed and track information and name retrieved from there. Of | ||||
| course this was only the beginning (also you can "appreciate" the number of | ||||
| nested blocks there...). Now the bot supports albums and playlists too, with the | ||||
| possibility to go through each song in the playlist and collect general | ||||
| information such as how many artists are in that playlist, how many songs and | ||||
| other little information like that. If you see the [bot | ||||
| repository](https://git.poldebra.me/polpetta/Songlify) you can see now that | ||||
| Spotify functions live in a separate module. | ||||
| 
 | ||||
| #### Packaging and distribution | ||||
| 
 | ||||
| The obvious choice for a software like that was to incorporate it into a OCI | ||||
| image. I wrote a very simple Dockerfile that, once the program was built, took | ||||
| the artifact and using the [multi-stage Docker build | ||||
| functionality](https://docs.docker.com/develop/develop-images/multistage-build/) | ||||
| and put it into a separate container, in order to avoid having build | ||||
| dependencies inside the final image. I used the images distributed by the | ||||
| [Distroless project](gcr.io/distroless/) (you can find the source on their | ||||
| [Github repository](https://github.com/GoogleContainerTools/distroless)) in | ||||
| order to obtain the smallest possible image. The final result? | ||||
| 
 | ||||
| ```txt | ||||
| λ ~/Desktop/git/songlify/ docker images | ||||
| REPOSITORY               TAG                    IMAGE ID       CREATED          SIZE | ||||
| test/test                latest                 8ac7a7018719   5 seconds ago    34MB | ||||
| <none>                   <none>                 4bc7fb0699e0   12 seconds ago   1.53GB | ||||
| rust                     1.56.1-slim-bullseye   d3e070c5ffa7   6 weeks ago      667MB | ||||
| gcr.io/distroless/base   latest-amd64           24787c1cd2e4   52 years ago     20.2MB | ||||
| ``` | ||||
| 
 | ||||
| A part from the 52 years old image pulled from `gcr`, you can see that | ||||
| `test/test` (actually Songlify) is only of **34MB**. Not much if you consider | ||||
| that inside that image there are shared dynamic libraries to make the executable | ||||
| able to run, which by default weights 20MB. A plus of these images is that they | ||||
| do not run as root user and they do not have any shell of bash integrated, | ||||
| making a possible surface attack smaller (not that Docker is secure anyway...). | ||||
| I upload the images on Docker Hub, where you can find them here | ||||
| <https://hub.docker.com/r/polpetta/songlify> | ||||
| 
 | ||||
| Finally, to run the bot I use a very simple docker-compose definition, that can | ||||
| be found in my [server-dotfiles | ||||
| repository](https://git.poldebra.me/polpetta/server-dotfiles/src/commit/7e7e1780b2db45f475510c49bf1a2f9e76c4c166/songlify/docker-compose.yml). | ||||
| This allows me to easily upgrade the bot by just changing the version and | ||||
| running `docker-compose up -d`. | ||||
| 
 | ||||
| #### Plans for the future | ||||
| 
 | ||||
| Currently I work on the bot only when I feel like I wanna add something. An | ||||
| interesting feature to add could be to insert a persistence layer (using a | ||||
| database for instance) and add various stats (which is the most shared song? Who | ||||
| _is that guy_ that shares the most songs in a group?). Persistence can be | ||||
| achieved quite easily by using [Diesel](https://diesel.rs/), an ORM compatible | ||||
| with various databases. | ||||
| 
 | ||||
| Another cool feature could be to add the _inline bot_ functionality, where you | ||||
| can search for songs directly in Spotify. Since I currently have a domain | ||||
| available for that I could set it up for receive web-hook notifications, instead | ||||
| of performing polling like the bot is currently doing (one requisite for inline | ||||
| bots is indeed to receive web-hooks). There are platforms like Heroku where you | ||||
| could make the bot run, but currently I prefer to use my box since it gives me | ||||
| more flexibility. Experimenting with Heroku could lead to cool results though | ||||
| 😏. | ||||
| 
 | ||||
| Finally, link translation could be something very useful. I have a friend that | ||||
| does not use Spotify but prefers to listen to music via YouTube. So an | ||||
| interesting feature would be, given a Spotify link, to "convert" it into a | ||||
| YouTube link and, of course, _vice-versa_. This could lead to translate | ||||
| playlists and albums too into YouTube-based playlists, which of course could be | ||||
| very useful if you are trying to avoid the infamous _vendor lock-in_, in this | ||||
| case being stuck with Spotify because all you music collection, saved songs, etc | ||||
| is there. | ||||
| @ -1,5 +0,0 @@ | ||||
| --- | ||||
| Title: "Riguardo l'Autore" | ||||
| --- | ||||
| 
 | ||||
| Prova 😀 | ||||
| @ -1,7 +0,0 @@ | ||||
| <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> | ||||
| <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> | ||||
| <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> | ||||
| <link rel="manifest" href="/site.webmanifest"> | ||||
| <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> | ||||
| <meta name="msapplication-TileColor" content="#da532c"> | ||||
| <meta name="theme-color" content="#ffffff"> | ||||
							
								
								
									
										60
									
								
								org/about-me.org
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								org/about-me.org
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| Hi all! I am Davide Polonio, a programmer and devOps engineer. I am born | ||||
| in 1994 and started messing with computers since I was 8 (my first OS | ||||
| was the only and glorious Windows 95), and from there I continued my | ||||
| path to learn about computers and how to +break+ tweak them. I graduated | ||||
| at university of Padova with a master degree in computer science | ||||
| in 2018. | ||||
| 
 | ||||
| I prefer back-end stuff (I usually end up using Java, Golang, Rust and | ||||
| Bash to gluing stuff around), especially automating releases with | ||||
| Continuous Integration and Continuous Delivery. I create and manage | ||||
| pipelines for Jenkins (and I currently administer Jenkins as well), but | ||||
| I also know other CI systems like Github Actions, Travis CI, Drone CI | ||||
| (or the now clone Woodpecker). I enjoy container technologies, starting | ||||
| from Docker, Podman and going on to orchestrators like Docker Swarm, | ||||
| Kubernetes and Nomad (which I would like to learn more of the lasts | ||||
| two). If I can, I use Infrastructure as Code (IaC) tools like Terraform | ||||
| and Packer to create and maintain infrastructure on cloud providers, | ||||
| like AWS or Scaleway (I also maintain | ||||
| [[https://github.com/Polpetta/packer-plugin-podman][Podman plugin for Packer]]). | ||||
| Automation is usually my preferred way, since it helps developers focus | ||||
| only on the business product instead of messing with tasks that can be | ||||
| done by a machine. Finally, I am interested in development processes | ||||
| like (but not only) Agile, Scrum, Waterfall. I think the right way to | ||||
| develop a product starts from how you organize your workflow and what | ||||
| processes you put in place in order to keep the /development machine/ | ||||
| going. | ||||
| 
 | ||||
| I am an open source enthusiast, so when I am able in my free time (or | ||||
| paid time if it is an open source business project) I like to contribute | ||||
| to open source projects and discover new ones. I believe in the added | ||||
| values that open source software (and especially free software) can give | ||||
| respect to closed ones. My dream job would be to contribute to one of | ||||
| the many open source I like full time! 🤩 | ||||
| 
 | ||||
| I'm not only into programming though! I like to practice sport. In | ||||
| particular, I am a black belt Judo and I teach it at the local dojo were | ||||
| I live. I know how to ski from when I was I child, and if possible I | ||||
| enjoy passing some time in the middle of mountains to admire the beauty | ||||
| of nature in winter. Finally, I like to play tabletops games and I enjoy | ||||
| things like table football, billiards, bowling (although I suck at the | ||||
| last one 😅). | ||||
| 
 | ||||
| If you are interested in getting in touch with me (or to get my resume) | ||||
| you can send me an e-mail (I prefer this way) at | ||||
| [[mailto:davide+bitdispenser@poldebra.me][davide@poldebra.me]] or you | ||||
| can contact me through my | ||||
| [[https://www.linkedin.com/in/davidepolonio/][LinkedIn]] profile. | ||||
| 
 | ||||
| ** Why this website and this blog | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: why-this-website-and-this-blog | ||||
| :END: | ||||
| As I have already written at the beginning of [my first blog post]({{< | ||||
| ref “/hello-world.md” >}}) I liked the idea of a place where I can share | ||||
| my thoughts regarding technologies, personal experiences and what not. | ||||
| This blog is open source, and all my content is licensed under CC BY-SA | ||||
| 4.0. The repository can be found either on | ||||
| [[https://git.poldebra.me/polpetta/bitdispenser.dev][my personal Gitea instance]] | ||||
| or on | ||||
| [[https://github.com/Polpetta/bitdispenser.dev][Github (mirrored)]]. | ||||
							
								
								
									
										6
									
								
								org/index.org
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								org/index.org
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| #+TITLE: Bitdispenser homepage | ||||
| #+AUTHOR: Davide Polonio | ||||
| 
 | ||||
| Welcome | ||||
| 
 | ||||
| cmdfklvdfm | ||||
							
								
								
									
										166
									
								
								org/posts/hello-world.org
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								org/posts/hello-world.org
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,166 @@ | ||||
| If you are reading this it means either you are trying to understand who | ||||
| I am (are you a recruiter by any chance?) or you are simply bored. In | ||||
| any way, this is the very first blog post, and it means you have come to | ||||
| the end of the road (since this is the beginning). I always loved to | ||||
| have a place where I can write down my thoughts about a new technology, | ||||
| a framework or what I did in order to achieve a goal in a hobby project. | ||||
| That is not all though: I was searching for a place where I was also | ||||
| able to express my ideas regarding modern dilemmas like privacy issues | ||||
| and decentralization. I am not the kind of guy who likes social | ||||
| networks, so I never had the chance to express them. Until now. | ||||
| 
 | ||||
| ** How this blog is built | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: how-this-blog-is-built | ||||
| :END: | ||||
| *** The website | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: the-website | ||||
| :END: | ||||
| This blog is a very simple [[https://gohugo.io/][Hugo]] website. If you | ||||
| do not know what it is, its basically a static website generator. It | ||||
| takes Markdown documents and converts them in HTML pages. If you applied | ||||
| a specific theme then it builds the page according to that theme. Hugo | ||||
| has a huge selection of themes in its | ||||
| [[https://themes.gohugo.io/][dedicated themes page]], so basically | ||||
| picking up one and starting from there is very simple. If you are | ||||
| curious about how I made it, you can check out the source at my | ||||
| [[https://git.poldebra.me/polpetta/bitdispenser.dev][personal git server instance]], | ||||
| where I started hosting my code when | ||||
| [[https://news.microsoft.com/announcement/microsoft-acquires-github/][Microsoft bought Github]] | ||||
| (you can find a copy of the repository on Github too, visiting | ||||
| [[https://github.com/Polpetta][my profile]]). I have simply picked up | ||||
| the simpler and cleanest blog theme out there, following Ludwig Mies van | ||||
| der Rohe's idea: | ||||
| 
 | ||||
| #+begin_quote | ||||
| less is more | ||||
| #+end_quote | ||||
| 
 | ||||
| It will be a success if more than two people actually starts reading | ||||
| what I write here, at least I want them to read this blog without having | ||||
| their eyes bleeding with an extravagant color combination. | ||||
| 
 | ||||
| Last but not least, the website icon is literally the Team Fortress 2 | ||||
| dispenser (one of the games I love and one of the first game I started | ||||
| playing online as a kid when a decent connection at home was finally | ||||
| available), taken from the RED team and generated via a | ||||
| [[https://realfavicongenerator.net/][favicon generator]]. The name of | ||||
| the website partially came out from that, and from the fact that | ||||
| basically every website is a dispenser of bits, and it is only thanks to | ||||
| our beloved browsers we are able to actually “consume” what is | ||||
| distributed in the first place. | ||||
| 
 | ||||
| #+CAPTION: Engineer class with a dispenser. Thanks to [[https://wiki.teamfortress.com][Team Fortress Wiki]] for providing the image I shamelessly downloaded from them | ||||
| #+NAME:   fig:Engineer with his dispenser | ||||
| [[../media/hello-world/engiwithdispenser.png]] | ||||
| 
 | ||||
| I hope Valve will not sue me for taking that asset as my website | ||||
| favicon. I swear I will change it, a day. Pinky promise. | ||||
| 
 | ||||
| *** Hosting | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: hosting | ||||
| :END: | ||||
| The real problem of hosting a website now day is not how to build it (as | ||||
| you can see) but /where/ you can host it. There are plenty of cloud | ||||
| provides: AWS, Google Cloud, DigitalOcean, Scaleway, etc... every day | ||||
| there is a new one popping up. They all offers the possibility to host | ||||
| your website pretty easily, especially if the website is a static one | ||||
| like this (e.g. using a S3 bucket). Since I like challenges and I also | ||||
| like to learn new stuff, I though that hosting the website in this way | ||||
| was boring. At the same time, I wanted to have a good uptime and to not | ||||
| meddle too much under the hood. My (dream) requirements were: | ||||
| 
 | ||||
| - always up | ||||
| - good response time | ||||
| - possibility to host as much data as I want | ||||
| - using a hosting free as in beer and (possibly) that could use free as | ||||
|   in freedom technologies | ||||
| 
 | ||||
| Now, reading this I imagine you are thinking I am going crazy, and maybe | ||||
| I am, but that is not the case. In fact, multiple weeks prior to writing | ||||
| this article, while trying to kill the boredom caused by COVID-19 | ||||
| lockdown, I discovered [[https://ipfs.io/][IPFS]]. I already heard of it | ||||
| at University, but I never bothered too much to understand what was | ||||
| about. I though “well, it surely is some sort of filesystem”. I was | ||||
| somewhat right, but not the way I thought. | ||||
| 
 | ||||
| IPFS acts like a peer-to-peer network, where nodes hash the content they | ||||
| want to share to let other nodes grab it. You can grab this content | ||||
| using your local node or using one of the many available gateways. Nodes | ||||
| can “pin” a file too, in order to keep it locally and to serve it to | ||||
| other nodes. If a file gets pinned by different nodes and gains traction | ||||
| it basically can not be deleted from the web. | ||||
| 
 | ||||
| **** Choosing the right tools | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: choosing-the-right-tools | ||||
| :END: | ||||
| Surely, as | ||||
| [[https://docs.ipfs.io/how-to/websites-on-ipfs/multipage-website/][the IPFS documentation describes]], | ||||
| I could have done it by myself. But there is a but. Holding all the | ||||
| infrastructure manually means sacrifice the “always up” and “good | ||||
| response time” thingy for two simple motives: | ||||
| 
 | ||||
| 1. the | ||||
|    [[https://git.poldebra.me][server where I host my drafts & codebase]] | ||||
|    is hosted on a very small machine (2GB of RAM and 2vCPU), very easy | ||||
|    to kill with the slightest of loads | ||||
| 2. in order to achieve a good response time, a CDN or some sort of | ||||
|    caching is necessary (even if the application is stored in a | ||||
|    distributed file system) | ||||
| 
 | ||||
| 
 | ||||
| #+CAPTION: Fleek website, that I used to automate my deployment on IPFS and my DNS update | ||||
| #+NAME:   fig:The fleek website screenshot | ||||
| #+ATTR_HTML: :width 600px | ||||
| [[../media/hello-world/fleek.png]] | ||||
| 
 | ||||
| Finally, in order to achieve full automation with DNS updates, I would | ||||
| have needed to implement and use NameCheap APIs (currently it is the DNS | ||||
| provider I use for most of my websites). “What's the difficulty?” one | ||||
| would ask. | ||||
| [[https://www.namecheap.com/support/api/intro/][Here is the official documentation]], | ||||
| and even if the APIs look promising, while studying them my will to live | ||||
| decreased a little bit, and so I decided that if I wanted to get up and | ||||
| running with less maintenance as possible, with a good uptime while | ||||
| having the maximum automation possible I needed to rely on a dedicated | ||||
| service. Luckily [[https://fleek.co][fleek.co]] was what I was searching | ||||
| for. They currently provide the possibility to buy a domain from their | ||||
| website, giving them all the hassle of updating the new website on IPFS, | ||||
| distributing it, refreshing a very possible CDN and finally to update | ||||
| the DNS records accordingly. This, as you can imagine, provides multiple | ||||
| benefits: | ||||
| 
 | ||||
| - I do not have to care about my very little dev machine getting hugged | ||||
|   to death by request in the remote case any of my posts get any | ||||
|   attention | ||||
| - I do not have to focus on automating the process someone else has | ||||
|   already done for me | ||||
| - I can focus on writing posts after dinner instead of scratching my | ||||
|   head trying to understand why the website does not load/the DNS is not | ||||
|   properly updated | ||||
| 
 | ||||
| The only downside to this approach is that Fleek provides limitations on | ||||
| how much data and bandwidth you can host. At the time of writing, you | ||||
| can only host up to 3GB (enough for this website) and have a 50GB | ||||
| bandwidth (that is fine for now) for the free version. | ||||
| [[https://fleek.co/pricing/][Upgrading you account]] to one of the | ||||
| available plans give you extra space and bandwidth. | ||||
| 
 | ||||
| ** Future improvements | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: future-improvements | ||||
| :END: | ||||
| For sure, this is only the beginning. Having an automatic workflow of | ||||
| spell check, deployment and release would be the first milestone. Future | ||||
| features for this website could be an automatic posting of every new | ||||
| article on a dedicated Mastodon bot, so that people can possibly discuss | ||||
| about my thoughts on the fediverse, a decentralized network. | ||||
| 
 | ||||
| To conclude, the frequency of this blog will be...whenever I have time | ||||
| to post :grin: Of course I need content before posting something, and | ||||
| this require some time for myself for experimenting with new | ||||
| technologies and learning new stuff, so I do not expect very much | ||||
| posting, but only time will tell! | ||||
							
								
								
									
										272
									
								
								org/posts/songlify-1.org
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								org/posts/songlify-1.org
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,272 @@ | ||||
| Some time passed since the last article I wrote there. A lot of stuff | ||||
| happened meanwhile, especially with COVID, but here we are again. While | ||||
| busy dealing with the mess of real life tasks, three months ago I | ||||
| started to write a little bot for Telegram in Rust. It is a simple one, | ||||
| but I consider the journey interesting and worth of writing it down. If | ||||
| I add new features worth mentioning I will start a series about it, | ||||
| maybe. | ||||
| 
 | ||||
| ** Telegram bots | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: telegram-bots | ||||
| :END: | ||||
| Telegram bots are not something new to me and nowadays are pretty much | ||||
| easy to make, so I consider them like a gym where to try out new | ||||
| technologies and experiment with stuff. I wrote plentiful of them, some | ||||
| of those are open source like for example | ||||
| [[https://github.com/Augugrumi/TorreArchimedeBot]] (which is currently | ||||
| broken 😭) that was useful when going to University, because it scraped | ||||
| the university free room web page and from there it was able to tell you | ||||
| which rooms where without any lessons and for how much time, allowing | ||||
| you to easily find a place where to study with your mates (yep, we | ||||
| didn't like library too much). | ||||
| 
 | ||||
| 
 | ||||
| #+CAPTION: A screenshot of TorreArchimedeBot in action. | ||||
| #+NAME:   fig:A screenshot of TorreArchimedeBot in action. | ||||
| #+ATTR_HTML: :width 600px | ||||
| [[../media/songlify/telegramscreen.png]] | ||||
| 
 | ||||
| Also another one bot worthy of mention is | ||||
| [[https://github.com/Polpetta/RedditToTelegram]], that allowed our D&D | ||||
| group to receive push notifications of our private Subreddit in our | ||||
| Telegram group. | ||||
| 
 | ||||
| As you can see, all of these bots are quite simple, but they have the | ||||
| added value of teaching you some new programming concepts, technologies | ||||
| or frameworks that can be later applied in something that can be more | ||||
| production environment. | ||||
| 
 | ||||
| ** Rust | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: rust | ||||
| :END: | ||||
| I started to approach Rust many years ago (I do not remember exactly | ||||
| when). First interaction with it was quite interesting to say at least: | ||||
| there were way less compiler features (for example now the compiler is | ||||
| able to understand object lifetime at compile time most of the time | ||||
| alone, without specifying them) that made it a... /not-so-pleasant | ||||
| programming experience/. It had potential thought, so by following Rust | ||||
| news I picked it up last year again, noticing that now it has improved a | ||||
| lot and it is more pleasant to write. Meanwhile, also JetBrains | ||||
| developed a good support for IntelliJ, so now it is even possible to | ||||
| debug and perform every operation directly from your IDE UI. | ||||
| 
 | ||||
| ** Making the two worlds collide: Rust + Telegram | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: making-the-two-worlds-collide-rust-telegram | ||||
| :END: | ||||
| One of the features I wanted to learn this time regarding Rust was the | ||||
| asynchronous support it offers. Rust started to have =async= support | ||||
| with [[https://tokio.rs/][Tokio]] framework, and recently the Rust team | ||||
| started to build the asynchronous functionality inside Rust itself. Even | ||||
| if in the first steps, it looks promising and the idea of a low-level | ||||
| language, without GC, with automatic memory management and so much | ||||
| safety having asynchronous support is exciting to me! 🥳 So the only | ||||
| option left, at this point, was to start messing around with it. I | ||||
| started by picking up one of the many frameworks that provides a layer | ||||
| for the Telegram APIs, | ||||
| [[https://github.com/teloxide/teloxide][Teloxide]]. In particular, as | ||||
| you can see from its /README/, one of the examples starts by using | ||||
| =#[tokio:main]= macro: | ||||
| 
 | ||||
| #+begin_src rust | ||||
| use teloxide::prelude::*; | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     teloxide::enable_logging!(); | ||||
|     log::info!("Starting dices_bot..."); | ||||
| 
 | ||||
|     let bot = Bot::from_env().auto_send(); | ||||
| 
 | ||||
|     teloxide::repl(bot, |message| async move { | ||||
|         message.answer_dice().await?; | ||||
|         respond(()) | ||||
|     }) | ||||
|     .await; | ||||
| } | ||||
| #+end_src | ||||
| 
 | ||||
| This was the reason I picked it up, given that it looked the most | ||||
| promising by the time I started the project. | ||||
| 
 | ||||
| *** Building /Songlify/ | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: building-songlify | ||||
| :END: | ||||
| So, after choosing what was going to use to build the bot, I needed a | ||||
| /reason/ to build it. | ||||
| 
 | ||||
| With my friends we usually share a lot of songs (via Spotify links), so | ||||
| I thought it was a good idea to build a bot around it. I integrated a | ||||
| Spotify API library in it and started hacking up a bot. | ||||
| 
 | ||||
| #+begin_quote | ||||
| ⚠ Note that at the time of writing I have just noticed that the library | ||||
| I use for speaking with Spotify, | ||||
| [[https://crates.io/crates/aspotify][aspotify]] has been deprecated in | ||||
| favour of [[https://crates.io/crates/rspotify][rspotify]] | ||||
| #+end_quote | ||||
| 
 | ||||
| The first bot version was something very simple, and it was a | ||||
| single-file program with nothing very fancy (I have written it in a | ||||
| night): | ||||
| 
 | ||||
| #+begin_src rust | ||||
| use crate::SpotifyURL::Track; | ||||
| use aspotify::{Client, ClientCredentials}; | ||||
| use teloxide::prelude::*; | ||||
| 
 | ||||
| enum SpotifyURL { | ||||
|     Track(String), | ||||
| } | ||||
| 
 | ||||
| fn get_spotify_entry(url: &str) -> Option<SpotifyURL> { | ||||
|     if url.contains("https://open.spotify.com/track/") { | ||||
|         let track_id = url.rsplit('/').next().and_then(|x| x.split('?').next()); | ||||
|         return match track_id { | ||||
|             Some(id) => Some(SpotifyURL::Track(id.to_string())), | ||||
|             None => None, | ||||
|         }; | ||||
|     } | ||||
|     return None; | ||||
| } | ||||
| 
 | ||||
| struct TrackInfo { | ||||
|     name: String, | ||||
|     artist: Vec<String>, | ||||
| } | ||||
| 
 | ||||
| async fn get_spotify_track(spotify: Box<Client>, id: &String) -> Option<TrackInfo> { | ||||
|     match spotify.tracks().get_track(id.as_str(), None).await { | ||||
|         Ok(track) => Some(TrackInfo { | ||||
|             name: track.data.name, | ||||
|             artist: track.data.artists.iter().map(|x| x.name.clone()).collect(), | ||||
|         }), | ||||
|         Err(_e) => None, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     teloxide::enable_logging!(); | ||||
|     log::info!("Starting Songlify..."); | ||||
| 
 | ||||
|     let bot = Bot::from_env().auto_send(); | ||||
|     teloxide::repl(bot, |message| async move { | ||||
|         let spotify_creds = | ||||
|             ClientCredentials::from_env().expect("CLIENT_ID and CLIENT_SECRET not found."); | ||||
|         let spotify_client = Box::new(Client::new(spotify_creds)); | ||||
| 
 | ||||
|         log::info!("Connected to Spotify"); | ||||
|         let text = message.update.text().and_then(get_spotify_entry); | ||||
|         match text { | ||||
|             Some(spotify) => match spotify { | ||||
|                 Track(id) => { | ||||
|                     let track_info = get_spotify_track(spotify_client, &id).await; | ||||
|                     match track_info { | ||||
|                         Some(info) => { | ||||
|                             let reply = format!( | ||||
|                                 "Track information:\n\ | ||||
|                                 Track name: {}\n\ | ||||
|                                 Artists: {}", | ||||
|                                 info.name, | ||||
|                                 info.artist.join(", ") | ||||
|                             ); | ||||
|                             Some(message.reply_to(reply).await?) | ||||
|                         } | ||||
|                         None => None, | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             None => None, | ||||
|         }; | ||||
|         respond(()) | ||||
|     }) | ||||
|     .await; | ||||
| 
 | ||||
|     log::info!("Exiting..."); | ||||
| } | ||||
| #+end_src | ||||
| 
 | ||||
| As you can see, basically every time a request arrived to the bot, login | ||||
| to Spotify was performed and track information and name retrieved from | ||||
| there. Of course this was only the beginning (also you can “appreciate” | ||||
| the number of nested blocks there...). Now the bot supports albums and | ||||
| playlists too, with the possibility to go through each song in the | ||||
| playlist and collect general information such as how many artists are in | ||||
| that playlist, how many songs and other little information like that. If | ||||
| you see the | ||||
| [[https://git.poldebra.me/polpetta/Songlify][bot repository]] you can | ||||
| see now that Spotify functions live in a separate module. | ||||
| 
 | ||||
| **** Packaging and distribution | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: packaging-and-distribution | ||||
| :END: | ||||
| The obvious choice for a software like that was to incorporate it into a | ||||
| OCI image. I wrote a very simple Dockerfile that, once the program was | ||||
| built, took the artifact and using the | ||||
| [[https://docs.docker.com/develop/develop-images/multistage-build/][multi-stage Docker build functionality]] | ||||
| and put it into a separate container, in order to avoid having build | ||||
| dependencies inside the final image. I used the images distributed by | ||||
| the [[file:gcr.io/distroless/][Distroless project]] (you can find the | ||||
| source on their | ||||
| [[https://github.com/GoogleContainerTools/distroless][Github repository]]) | ||||
| in order to obtain the smallest possible image. The final result? | ||||
| 
 | ||||
| #+begin_src txt | ||||
| λ ~/Desktop/git/songlify/ docker images | ||||
| REPOSITORY               TAG                    IMAGE ID       CREATED          SIZE | ||||
| test/test                latest                 8ac7a7018719   5 seconds ago    34MB | ||||
| <none>                   <none>                 4bc7fb0699e0   12 seconds ago   1.53GB | ||||
| rust                     1.56.1-slim-bullseye   d3e070c5ffa7   6 weeks ago      667MB | ||||
| gcr.io/distroless/base   latest-amd64           24787c1cd2e4   52 years ago     20.2MB | ||||
| #+end_src | ||||
| 
 | ||||
| A part from the 52 years old image pulled from =gcr=, you can see that | ||||
| =test/test= (actually Songlify) is only of *34MB*. Not much if you | ||||
| consider that inside that image there are shared dynamic libraries to | ||||
| make the executable able to run, which by default weights 20MB. A plus | ||||
| of these images is that they do not run as root user and they do not | ||||
| have any shell of bash integrated, making a possible surface attack | ||||
| smaller (not that Docker is secure anyway...). I upload the images on | ||||
| Docker Hub, where you can find them here | ||||
| [[https://hub.docker.com/r/polpetta/songlify]] | ||||
| 
 | ||||
| Finally, to run the bot I use a very simple docker-compose definition, | ||||
| that can be found in my | ||||
| [[https://git.poldebra.me/polpetta/server-dotfiles/src/commit/7e7e1780b2db45f475510c49bf1a2f9e76c4c166/songlify/docker-compose.yml][server-dotfiles repository]]. | ||||
| This allows me to easily upgrade the bot by just changing the version | ||||
| and running =docker-compose up -d=. | ||||
| 
 | ||||
| **** Plans for the future | ||||
| :PROPERTIES: | ||||
| :CUSTOM_ID: plans-for-the-future | ||||
| :END: | ||||
| Currently I work on the bot only when I feel like I wanna add something. | ||||
| An interesting feature to add could be to insert a persistence layer | ||||
| (using a database for instance) and add various stats (which is the most | ||||
| shared song? Who /is that guy/ that shares the most songs in a group?). | ||||
| Persistence can be achieved quite easily by using | ||||
| [[https://diesel.rs/][Diesel]], an ORM compatible with various | ||||
| databases. | ||||
| 
 | ||||
| Another cool feature could be to add the /inline bot/ functionality, | ||||
| where you can search for songs directly in Spotify. Since I currently | ||||
| have a domain available for that I could set it up for receive web-hook | ||||
| notifications, instead of performing polling like the bot is currently | ||||
| doing (one requisite for inline bots is indeed to receive web-hooks). | ||||
| There are platforms like Heroku where you could make the bot run, but | ||||
| currently I prefer to use my box since it gives me more flexibility. | ||||
| Experimenting with Heroku could lead to cool results though 😏. | ||||
| 
 | ||||
| Finally, link translation could be something very useful. I have a | ||||
| friend that does not use Spotify but prefers to listen to music via | ||||
| YouTube. So an interesting feature would be, given a Spotify link, to | ||||
| “convert” it into a YouTube link and, of course, /vice-versa/. This | ||||
| could lead to translate playlists and albums too into YouTube-based | ||||
| playlists, which of course could be very useful if you are trying to | ||||
| avoid the infamous /vendor lock-in/, in this case being stuck with | ||||
| Spotify because all you music collection, saved songs, etc is there. | ||||
							
								
								
									
										0
									
								
								org/rss.org
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								org/rss.org
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										739
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										739
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,739 +0,0 @@ | ||||
| { | ||||
|     "name": "bitdispenser.dev", | ||||
|     "version": "0.0.1", | ||||
|     "lockfileVersion": 2, | ||||
|     "requires": true, | ||||
|     "packages": { | ||||
|         "": { | ||||
|             "name": "bitdispenser.dev", | ||||
|             "version": "0.0.1", | ||||
|             "license": "CC-BY-SA-4.0", | ||||
|             "devDependencies": { | ||||
|                 "markdownlint-cli2": "0.0.10" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@nodelib/fs.scandir": { | ||||
|             "version": "2.1.4", | ||||
|             "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", | ||||
|             "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@nodelib/fs.stat": "2.0.4", | ||||
|                 "run-parallel": "^1.1.9" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@nodelib/fs.stat": { | ||||
|             "version": "2.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", | ||||
|             "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">= 8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@nodelib/fs.walk": { | ||||
|             "version": "1.2.6", | ||||
|             "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", | ||||
|             "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@nodelib/fs.scandir": "2.1.4", | ||||
|                 "fastq": "^1.6.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/argparse": { | ||||
|             "version": "1.0.10", | ||||
|             "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", | ||||
|             "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "sprintf-js": "~1.0.2" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/array-union": { | ||||
|             "version": "2.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", | ||||
|             "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/braces": { | ||||
|             "version": "3.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", | ||||
|             "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "fill-range": "^7.0.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/dir-glob": { | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", | ||||
|             "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "path-type": "^4.0.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/entities": { | ||||
|             "version": "2.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", | ||||
|             "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/fast-glob": { | ||||
|             "version": "3.2.5", | ||||
|             "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", | ||||
|             "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "@nodelib/fs.stat": "^2.0.2", | ||||
|                 "@nodelib/fs.walk": "^1.2.3", | ||||
|                 "glob-parent": "^5.1.0", | ||||
|                 "merge2": "^1.3.0", | ||||
|                 "micromatch": "^4.0.2", | ||||
|                 "picomatch": "^2.2.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/fastq": { | ||||
|             "version": "1.11.0", | ||||
|             "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", | ||||
|             "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "reusify": "^1.0.4" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/fill-range": { | ||||
|             "version": "7.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", | ||||
|             "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "to-regex-range": "^5.0.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/glob-parent": { | ||||
|             "version": "5.1.2", | ||||
|             "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", | ||||
|             "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "is-glob": "^4.0.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/globby": { | ||||
|             "version": "11.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", | ||||
|             "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "array-union": "^2.1.0", | ||||
|                 "dir-glob": "^3.0.1", | ||||
|                 "fast-glob": "^3.1.1", | ||||
|                 "ignore": "^5.1.4", | ||||
|                 "merge2": "^1.3.0", | ||||
|                 "slash": "^3.0.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=10" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/ignore": { | ||||
|             "version": "5.1.8", | ||||
|             "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", | ||||
|             "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">= 4" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/is-extglob": { | ||||
|             "version": "2.1.1", | ||||
|             "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", | ||||
|             "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=0.10.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/is-glob": { | ||||
|             "version": "4.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", | ||||
|             "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "is-extglob": "^2.1.1" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=0.10.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/is-number": { | ||||
|             "version": "7.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", | ||||
|             "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=0.12.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/linkify-it": { | ||||
|             "version": "3.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz", | ||||
|             "integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "uc.micro": "^1.0.1" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/markdown-it": { | ||||
|             "version": "11.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.0.tgz", | ||||
|             "integrity": "sha512-+CvOnmbSubmQFSA9dKz1BRiaSMV7rhexl3sngKqFyXSagoA3fBdJQ8oZWtRy2knXdpDXaBw44euz37DeJQ9asg==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "argparse": "^1.0.7", | ||||
|                 "entities": "~2.0.0", | ||||
|                 "linkify-it": "^3.0.1", | ||||
|                 "mdurl": "^1.0.1", | ||||
|                 "uc.micro": "^1.0.5" | ||||
|             }, | ||||
|             "bin": { | ||||
|                 "markdown-it": "bin/markdown-it.js" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/markdownlint": { | ||||
|             "version": "0.21.1", | ||||
|             "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.21.1.tgz", | ||||
|             "integrity": "sha512-8kc88w5dyEzlmOWIElp8J17qBgzouOQfJ0LhCcpBFrwgyYK6JTKvILsk4FCEkiNqHkTxwxopT2RS2DYb/10qqg==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "markdown-it": "11.0.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=10" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/markdownlint-cli2": { | ||||
|             "version": "0.0.10", | ||||
|             "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.0.10.tgz", | ||||
|             "integrity": "sha512-tuWKncGgg0ic9YIWzPu1w2upoGNF/CQwb+CMzJmB8LMM2MhFKTUQSDyUt0kM022qs1R7KjxFXG7y5FBmqGVhUQ==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "globby": "~11.0.1", | ||||
|                 "markdownlint": "~0.21.0", | ||||
|                 "markdownlint-cli2-formatter-default": "~0.0.1", | ||||
|                 "markdownlint-rule-helpers": "~0.12.0", | ||||
|                 "micromatch": "~4.0.2", | ||||
|                 "strip-json-comments": "~3.1.1", | ||||
|                 "yaml": "~1.10.0" | ||||
|             }, | ||||
|             "bin": { | ||||
|                 "markdownlint-cli2": "markdownlint-cli2.js" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=10.17.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/markdownlint-cli2-formatter-default": { | ||||
|             "version": "0.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.2.tgz", | ||||
|             "integrity": "sha512-jIz1X3SIC8sX4NDFqQFUXL+JEtfnDoN4i+xocEu+etcxGX455pHb6sx86f/yVk4mKJ2o7aNe2ydSx9an22BfBg==", | ||||
|             "dev": true, | ||||
|             "peerDependencies": { | ||||
|                 "markdownlint-cli2": ">=0.0.4" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/markdownlint-rule-helpers": { | ||||
|             "version": "0.12.0", | ||||
|             "resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.12.0.tgz", | ||||
|             "integrity": "sha512-Q7qfAk+AJvx82ZY52OByC4yjoQYryOZt6D8TKrZJIwCfhZvcj8vCQNuwDqILushtDBTvGFmUPq+uhOb1KIMi6A==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/mdurl": { | ||||
|             "version": "1.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", | ||||
|             "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/merge2": { | ||||
|             "version": "1.4.1", | ||||
|             "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", | ||||
|             "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">= 8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/micromatch": { | ||||
|             "version": "4.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", | ||||
|             "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "braces": "^3.0.1", | ||||
|                 "picomatch": "^2.2.3" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=8.6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/path-type": { | ||||
|             "version": "4.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", | ||||
|             "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/picomatch": { | ||||
|             "version": "2.2.3", | ||||
|             "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", | ||||
|             "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=8.6" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/jonschlinkert" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/queue-microtask": { | ||||
|             "version": "1.2.3", | ||||
|             "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", | ||||
|             "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", | ||||
|             "dev": true, | ||||
|             "funding": [ | ||||
|                 { | ||||
|                     "type": "github", | ||||
|                     "url": "https://github.com/sponsors/feross" | ||||
|                 }, | ||||
|                 { | ||||
|                     "type": "patreon", | ||||
|                     "url": "https://www.patreon.com/feross" | ||||
|                 }, | ||||
|                 { | ||||
|                     "type": "consulting", | ||||
|                     "url": "https://feross.org/support" | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         "node_modules/reusify": { | ||||
|             "version": "1.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", | ||||
|             "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "iojs": ">=1.0.0", | ||||
|                 "node": ">=0.10.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/run-parallel": { | ||||
|             "version": "1.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", | ||||
|             "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", | ||||
|             "dev": true, | ||||
|             "funding": [ | ||||
|                 { | ||||
|                     "type": "github", | ||||
|                     "url": "https://github.com/sponsors/feross" | ||||
|                 }, | ||||
|                 { | ||||
|                     "type": "patreon", | ||||
|                     "url": "https://www.patreon.com/feross" | ||||
|                 }, | ||||
|                 { | ||||
|                     "type": "consulting", | ||||
|                     "url": "https://feross.org/support" | ||||
|                 } | ||||
|             ], | ||||
|             "dependencies": { | ||||
|                 "queue-microtask": "^1.2.2" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/slash": { | ||||
|             "version": "3.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", | ||||
|             "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=8" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/sprintf-js": { | ||||
|             "version": "1.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", | ||||
|             "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/strip-json-comments": { | ||||
|             "version": "3.1.1", | ||||
|             "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", | ||||
|             "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">=8" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/sponsors/sindresorhus" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/to-regex-range": { | ||||
|             "version": "5.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | ||||
|             "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "is-number": "^7.0.0" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">=8.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/uc.micro": { | ||||
|             "version": "1.0.6", | ||||
|             "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", | ||||
|             "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "node_modules/yaml": { | ||||
|             "version": "1.10.2", | ||||
|             "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", | ||||
|             "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", | ||||
|             "dev": true, | ||||
|             "engines": { | ||||
|                 "node": ">= 6" | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@nodelib/fs.scandir": { | ||||
|             "version": "2.1.4", | ||||
|             "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", | ||||
|             "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "@nodelib/fs.stat": "2.0.4", | ||||
|                 "run-parallel": "^1.1.9" | ||||
|             } | ||||
|         }, | ||||
|         "@nodelib/fs.stat": { | ||||
|             "version": "2.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", | ||||
|             "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "@nodelib/fs.walk": { | ||||
|             "version": "1.2.6", | ||||
|             "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", | ||||
|             "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "@nodelib/fs.scandir": "2.1.4", | ||||
|                 "fastq": "^1.6.0" | ||||
|             } | ||||
|         }, | ||||
|         "argparse": { | ||||
|             "version": "1.0.10", | ||||
|             "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", | ||||
|             "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "sprintf-js": "~1.0.2" | ||||
|             } | ||||
|         }, | ||||
|         "array-union": { | ||||
|             "version": "2.1.0", | ||||
|             "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", | ||||
|             "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "braces": { | ||||
|             "version": "3.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", | ||||
|             "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "fill-range": "^7.0.1" | ||||
|             } | ||||
|         }, | ||||
|         "dir-glob": { | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", | ||||
|             "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "path-type": "^4.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "entities": { | ||||
|             "version": "2.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", | ||||
|             "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "fast-glob": { | ||||
|             "version": "3.2.5", | ||||
|             "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", | ||||
|             "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "@nodelib/fs.stat": "^2.0.2", | ||||
|                 "@nodelib/fs.walk": "^1.2.3", | ||||
|                 "glob-parent": "^5.1.0", | ||||
|                 "merge2": "^1.3.0", | ||||
|                 "micromatch": "^4.0.2", | ||||
|                 "picomatch": "^2.2.1" | ||||
|             } | ||||
|         }, | ||||
|         "fastq": { | ||||
|             "version": "1.11.0", | ||||
|             "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", | ||||
|             "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "reusify": "^1.0.4" | ||||
|             } | ||||
|         }, | ||||
|         "fill-range": { | ||||
|             "version": "7.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", | ||||
|             "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "to-regex-range": "^5.0.1" | ||||
|             } | ||||
|         }, | ||||
|         "glob-parent": { | ||||
|             "version": "5.1.2", | ||||
|             "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", | ||||
|             "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "is-glob": "^4.0.1" | ||||
|             } | ||||
|         }, | ||||
|         "globby": { | ||||
|             "version": "11.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", | ||||
|             "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "array-union": "^2.1.0", | ||||
|                 "dir-glob": "^3.0.1", | ||||
|                 "fast-glob": "^3.1.1", | ||||
|                 "ignore": "^5.1.4", | ||||
|                 "merge2": "^1.3.0", | ||||
|                 "slash": "^3.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "ignore": { | ||||
|             "version": "5.1.8", | ||||
|             "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", | ||||
|             "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "is-extglob": { | ||||
|             "version": "2.1.1", | ||||
|             "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", | ||||
|             "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "is-glob": { | ||||
|             "version": "4.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", | ||||
|             "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "is-extglob": "^2.1.1" | ||||
|             } | ||||
|         }, | ||||
|         "is-number": { | ||||
|             "version": "7.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", | ||||
|             "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "linkify-it": { | ||||
|             "version": "3.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz", | ||||
|             "integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "uc.micro": "^1.0.1" | ||||
|             } | ||||
|         }, | ||||
|         "markdown-it": { | ||||
|             "version": "11.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.0.tgz", | ||||
|             "integrity": "sha512-+CvOnmbSubmQFSA9dKz1BRiaSMV7rhexl3sngKqFyXSagoA3fBdJQ8oZWtRy2knXdpDXaBw44euz37DeJQ9asg==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "argparse": "^1.0.7", | ||||
|                 "entities": "~2.0.0", | ||||
|                 "linkify-it": "^3.0.1", | ||||
|                 "mdurl": "^1.0.1", | ||||
|                 "uc.micro": "^1.0.5" | ||||
|             } | ||||
|         }, | ||||
|         "markdownlint": { | ||||
|             "version": "0.21.1", | ||||
|             "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.21.1.tgz", | ||||
|             "integrity": "sha512-8kc88w5dyEzlmOWIElp8J17qBgzouOQfJ0LhCcpBFrwgyYK6JTKvILsk4FCEkiNqHkTxwxopT2RS2DYb/10qqg==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "markdown-it": "11.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "markdownlint-cli2": { | ||||
|             "version": "0.0.10", | ||||
|             "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.0.10.tgz", | ||||
|             "integrity": "sha512-tuWKncGgg0ic9YIWzPu1w2upoGNF/CQwb+CMzJmB8LMM2MhFKTUQSDyUt0kM022qs1R7KjxFXG7y5FBmqGVhUQ==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "globby": "~11.0.1", | ||||
|                 "markdownlint": "~0.21.0", | ||||
|                 "markdownlint-cli2-formatter-default": "~0.0.1", | ||||
|                 "markdownlint-rule-helpers": "~0.12.0", | ||||
|                 "micromatch": "~4.0.2", | ||||
|                 "strip-json-comments": "~3.1.1", | ||||
|                 "yaml": "~1.10.0" | ||||
|             } | ||||
|         }, | ||||
|         "markdownlint-cli2-formatter-default": { | ||||
|             "version": "0.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.2.tgz", | ||||
|             "integrity": "sha512-jIz1X3SIC8sX4NDFqQFUXL+JEtfnDoN4i+xocEu+etcxGX455pHb6sx86f/yVk4mKJ2o7aNe2ydSx9an22BfBg==", | ||||
|             "dev": true, | ||||
|             "requires": {} | ||||
|         }, | ||||
|         "markdownlint-rule-helpers": { | ||||
|             "version": "0.12.0", | ||||
|             "resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.12.0.tgz", | ||||
|             "integrity": "sha512-Q7qfAk+AJvx82ZY52OByC4yjoQYryOZt6D8TKrZJIwCfhZvcj8vCQNuwDqILushtDBTvGFmUPq+uhOb1KIMi6A==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "mdurl": { | ||||
|             "version": "1.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", | ||||
|             "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "merge2": { | ||||
|             "version": "1.4.1", | ||||
|             "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", | ||||
|             "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "micromatch": { | ||||
|             "version": "4.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", | ||||
|             "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "braces": "^3.0.1", | ||||
|                 "picomatch": "^2.2.3" | ||||
|             } | ||||
|         }, | ||||
|         "path-type": { | ||||
|             "version": "4.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", | ||||
|             "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "picomatch": { | ||||
|             "version": "2.2.3", | ||||
|             "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", | ||||
|             "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "queue-microtask": { | ||||
|             "version": "1.2.3", | ||||
|             "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", | ||||
|             "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "reusify": { | ||||
|             "version": "1.0.4", | ||||
|             "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", | ||||
|             "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "run-parallel": { | ||||
|             "version": "1.2.0", | ||||
|             "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", | ||||
|             "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "queue-microtask": "^1.2.2" | ||||
|             } | ||||
|         }, | ||||
|         "slash": { | ||||
|             "version": "3.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", | ||||
|             "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "sprintf-js": { | ||||
|             "version": "1.0.3", | ||||
|             "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", | ||||
|             "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "strip-json-comments": { | ||||
|             "version": "3.1.1", | ||||
|             "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", | ||||
|             "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "to-regex-range": { | ||||
|             "version": "5.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", | ||||
|             "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", | ||||
|             "dev": true, | ||||
|             "requires": { | ||||
|                 "is-number": "^7.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "uc.micro": { | ||||
|             "version": "1.0.6", | ||||
|             "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", | ||||
|             "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", | ||||
|             "dev": true | ||||
|         }, | ||||
|         "yaml": { | ||||
|             "version": "1.10.2", | ||||
|             "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", | ||||
|             "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", | ||||
|             "dev": true | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								package.json
									
									
									
									
									
								
							| @ -1,19 +0,0 @@ | ||||
| { | ||||
|     "name": "bitdispenser.dev", | ||||
|     "version": "0.0.1", | ||||
|     "description": "Personal Hugo website", | ||||
|     "main": "none.js", | ||||
|     "scripts": { | ||||
|         "lint": "markdownlint-cli2 content/", | ||||
| 	      "spellchecker": "bash tools/spellchecker.sh en" | ||||
|     }, | ||||
|     "repository": { | ||||
|         "type": "git", | ||||
|         "url": "git+https://git.poldebra.me/polpetta/bitdispenser.dev.git" | ||||
|     }, | ||||
|     "author": "Polonio Davide <davide@poldebra.me>", | ||||
|     "license": "CC-BY-SA-4.0", | ||||
|     "devDependencies": { | ||||
|         "markdownlint-cli2": "0.0.10" | ||||
|     } | ||||
| } | ||||
							
								
								
									
										80
									
								
								publish.el
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										80
									
								
								publish.el
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,80 @@ | ||||
| #!/usr/bin/env -S emacs -x | ||||
| 
 | ||||
| (require 'package) | ||||
| (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) | ||||
| (package-initialize) | ||||
| 
 | ||||
| ;; Install ox-rss if not present | ||||
| (unless (package-installed-p 'ox-rss) | ||||
|   (package-refresh-contents) | ||||
|   (package-install 'ox-rss)) | ||||
| 
 | ||||
| (require 'ox-publish) | ||||
| (require 'ox-rss) | ||||
| (setq org-publish-project-alist | ||||
|       '(("blog-posts" | ||||
|          :base-directory "org/posts/" | ||||
|          :base-extension "org" | ||||
|          :publishing-directory "dist/p/" | ||||
|          :recursive t | ||||
|          :publishing-function org-html-publish-to-html | ||||
|          :org-html-preamble nil | ||||
|          :html-self-link-headlines nil | ||||
|          :org-export-with-title nil | ||||
|          :org-export-with-toc nil | ||||
|          :org-export-with-section-numbers nil | ||||
|          :org-export-with-properties nil | ||||
|          :org-export-with-tags nil | ||||
|          :org-export-with-date t | ||||
|          :org-export-with-time-stamp-file t | ||||
|          :org-export-with-tables t | ||||
|          :org-html-table-use-header-tags-for-first-column nil | ||||
|          ) | ||||
|         ("blog-pages" | ||||
|          :base-directory "org/" | ||||
|          :base-extension "org" | ||||
|          :publishing-directory "dist/" | ||||
|          :exclude "posts/\\|rss\\.org" | ||||
|          :recursive t | ||||
|          :publishing-function org-html-publish-to-html | ||||
|          :org-html-preamble nil | ||||
|          :html-self-link-headlines nil | ||||
|          :org-export-with-title nil | ||||
|          :org-export-with-toc nil | ||||
|          :org-export-with-section-numbers nil | ||||
|          :org-export-with-tags nil | ||||
|          :org-html-table-use-header-tags-for-first-column nil | ||||
|          ) | ||||
|         ("blog-media" | ||||
|          :base-directory "org/media" | ||||
|          :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf" | ||||
|          :publishing-directory "dist/media/" | ||||
|          :recursive t | ||||
|          :publishing-function org-publish-attachment | ||||
|          ) | ||||
|         ("blog-static" | ||||
|          :base-directory "static/" | ||||
|          :base-extension "*" | ||||
|          :publishing-directory "dist/media/" | ||||
|          :recursive nil | ||||
|          :publishing-function org-publish-attachment | ||||
|          ) | ||||
|         ("blog-rss" | ||||
|          :base-directory "org/posts/" | ||||
|          :base-extension "org" | ||||
|          :recursive nil | ||||
|          :exclude ".*" | ||||
|          :include ("../rss.org") | ||||
|          :publishing-function (org-rss-publish-to-rss) | ||||
|          :publishing-directory "dist/" | ||||
|          :with-toc nil | ||||
|          :section-numbers nil | ||||
|          :html-link-use-abs-url t | ||||
|          :html-link-home "https://bitdispenser.dev/" | ||||
|          :title "Bitdispenser RSS" | ||||
|          ) | ||||
|         ("blog" | ||||
|          :components ("blog-posts" "blog-pages" "blog-media" "blog-static" "blog-rss")) | ||||
|         )) | ||||
| 
 | ||||
| (org-publish "blog" t) | ||||
							
								
								
									
										7
									
								
								templates/post.org
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								templates/post.org
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| #+options: html-link-use-abs-url:nil html-postamble:nil html-preamble:nil | ||||
| #+options: html-scripts:t html-style:nil html5-fancy:t tex:t | ||||
| #+options: tags:t title:nil toc:nil num:0 date:t | ||||
| #+html_doctype: html5 | ||||
| #+html_container: div | ||||
| #+html_head: <link rel="stylesheet" type="text/css" href="css/style.css" /> | ||||
| #+creator: Davide Polonio | ||||
| @ -1 +0,0 @@ | ||||
| Subproject commit 2dc35c5c6a52168a3a7b35c5ad51209f40a851cf | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user