<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://peterish.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://peterish.com/" rel="alternate" type="text/html" /><updated>2026-03-29T08:40:46-05:00</updated><id>https://peterish.com/feed.xml</id><title type="html">Peterish</title><subtitle>Peter Gao&apos;s personal website. Hosts his notes on various subjects (CS, ML, Riichi Mahjong, etc.) as well as his graphic design portfolio.</subtitle><author><name>Peter Gao</name><email>peter1357908@hotmail.com</email></author><entry><title type="html">Full Stack Dev Notes MERN</title><link href="https://peterish.com/programming/full-stack-dev-note-MERNs/" rel="alternate" type="text/html" title="Full Stack Dev Notes MERN" /><published>2024-02-03T00:00:00-06:00</published><updated>2024-02-03T00:00:00-06:00</updated><id>https://peterish.com/programming/full-stack-dev-note-MERNs</id><content type="html" xml:base="https://peterish.com/programming/full-stack-dev-note-MERNs/"><![CDATA[<p>This post summarizes my findings from working on the <a href="https://assayplate.onrender.com">AssayPlate web app</a>.</p>

<ul>
  <li><a href="https://github.com/peter1357908/AssayPlate">GitHub repository</a></li>
  <li>REST <a href="https://github.com/peter1357908/AssayPlate/blob/main/server/README.md">API Documentation</a></li>
</ul>

<h1 id="outdatedbuggy-tutorials">Outdated/Buggy Tutorials</h1>

<p>This section is for developers new to the MERN stack.</p>

<p>First of all, do NOT follow <a href="https://www.mongodb.com/languages/mern-stack-tutorial">MongoDB’s tutorial on MERN stack</a> – it’s outdated in many ways. Some consequences of following this tutorial can be seen <a href="https://www.mongodb.com/community/forums/t/mern-stack-tutorial-guide-doesnt-work/222971/5">here</a> and <a href="https://stackoverflow.com/questions/75373419/mongoclient-connect-is-not-responding">here</a>.</p>

<p>This <a href="https://www.freecodecamp.org/news/how-to-secure-your-mern-stack-application/">FreeCodeCamp tutorial</a> has a good skeleton to build your app from, but it’s also riddled with bugs:</p>
<ul>
  <li>CORS and <code class="language-plaintext highlighter-rouge">react-cookies</code> bugs (details <a href="#react-cookies-quirks">here</a>)</li>
  <li><code class="language-plaintext highlighter-rouge">ToastContainer</code> needs to be moved like <a href="https://github.com/peter1357908/AssayPlate/blob/main/client/src/main.jsx">this</a>, lest it uses the wrong CSS</li>
  <li>it didn’t tell the readers to specify <code class="language-plaintext highlighter-rouge">TOKEN_KEY</code></li>
  <li>it incorrectly uses <code class="language-plaintext highlighter-rouge">bcryptjs</code> where it needs <code class="language-plaintext highlighter-rouge">bcrypt</code></li>
</ul>

<p>Another note: <code class="language-plaintext highlighter-rouge">create-react-app</code> is deprecated; use <code class="language-plaintext highlighter-rouge">vite</code> instead!</p>

<h1 id="where-to-deploy-the-backend">Where to deploy the backend?</h1>

<p>There are numerous free hosts for the static frontend; the real problem lies in where to deploy the backend.</p>

<p><a href="https://render.com/">Render</a> is probably the best free-forever solution, although it does <em>make your app sleep after a few minutes of idling</em> – waking up can easily take 40 seconds. I have not tested whether it support stateful apps (AssayPlate is stateless).</p>

<p><a href="https://adaptable.io/">Adaptable.io</a> not only makes your app sleep upon idling, it does NOT support stateful interactions (e.g., real-time connections). For example, it cannot functionally host the <a href="https://github.com/peter1357908/Resistance-Online-Server">Resistance Online server</a>.</p>

<h2 id="warning-against-flyio">Warning against Fly.io</h2>

<p>I had a very negative experience with <code class="language-plaintext highlighter-rouge">Fly.io</code>, and I would like to warn others who are exploring this option for hosting their backend.</p>

<p><a href="https://fly.io/">Fly.io</a> <em>supposedly</em> offers a one-time, free $5 credit for new users, but this was not my experience. It was actually a bit confusing – <strong>suspiciously so</strong>. When I registered with my GitHub account, I was prompted to add my credit card before I was allowed to deploy an app, and once I did, I immediately got auto-subscribed to their paid Hobby plan. The whole process made it look like I was just adding a credit card for verification, NOT for subscribing to their paid plan. In fact, someone else had the same experience around the time I “accidentally” subscribed to their paid plan and made <a href="https://community.fly.io/t/request-for-refund-and-plan-adjustment-due-to-unexpected-charges/18021">a post requesting refund that has still not gotten a public admin response</a>.</p>

<p>That wasn’t the only suspicious practice. A few days after hosting my backend on Fly.io, I started seeing charges for Additional RAM. After digging around, I found out it was because, <strong>even though I specified otherwise through their configuration web app</strong>, I ended up using the default configuration that uses more RAM than the free allowance. Again, <a href="https://community.fly.io/t/unexpected-ram-increase-in-shared-cpu-1x-after-deployment/17134">I’m not the only one complaining about this</a>. In fact, you see <a href="https://community.fly.io/t/unexpected-billing-for-free-plan/17484/4">posts about unexpected charges</a> every so often on their community forum; if anything, this implies their lack of willingness to make the billing process more intuitive/transparent.</p>

<h1 id="the-cookie-saga">The Cookie Saga</h1>

<p>I had a LOT of trouble with cookies when working on this project (browser not saving cookie, script not seeing the cookies visible to browser, etc.). Here is a list of things I learned, trying to resolve cookie-related issues for AssayPlate.</p>

<h2 id="how-to-debug-cookie-related-issues">how to debug cookie-related issues</h2>

<p>Use the browser’s Inspect tool and monitor the network exchanges. On Chrome, this would be the <code class="language-plaintext highlighter-rouge">Network</code> tab. Check out the cookie received in each response – the browser will include hints for why certain cookies are discarded. To check what cookies are <em>visible to the browser</em>, go to the <code class="language-plaintext highlighter-rouge">Application</code> tab, and find “Storage-&gt;Cookies”. Note: cookies that are visible to the browser <strong>may not be visible to your JavaScript code</strong>! See <a href="#different-domains">next section</a> for an example.</p>

<h2 id="different-domains">different domains?</h2>

<p>Initially, I hosted the backend at <code class="language-plaintext highlighter-rouge">https://assayplate-backend.onrender.com</code>, which I incorrectly assumed to be on the same domain as <code class="language-plaintext highlighter-rouge">https://assayplate.onrender.com</code>. This is because <code class="language-plaintext highlighter-rouge">onrender.com</code> is a public suffix (see <a href="https://publicsuffix.org/list/public_suffix_list.dat">here</a> for the entire list of public suffixes), so those two are intentionally treated as different domains.</p>

<p>Until I learned about the concept of public suffixes, I was configuring CORS and cookies as if the production backend and frontend were deployed on the same domain, which caused a great deal of bugs.</p>

<p>Now, knowing that the backend is deployed on a different domain from the frontend, we have different issues. As noted <a href="https://github.com/axios/axios/issues/295#issuecomment-381485257">here</a>, “Cross-domain cookies cannot be accessed. In case you are building a single page application and your server is on a different domain. You cannot access the cookies on your SPA. The withCredentials flag will just make sure that the network calls made include the cookies and accept any incoming cookies.”</p>

<p>For example, <code class="language-plaintext highlighter-rouge">react-cookies</code> would be useless if you want to handle cookies coming from a different domain. A heisenbug that appeared during my development was how, <code class="language-plaintext highlighter-rouge">react-cookies</code> was correctly handling cookies when in development, but not during production – turns out it’s because earlier in the local development, both the backend and frontend were hosted on <code class="language-plaintext highlighter-rouge">localhost</code>, i.e., the same domain, but in production, they were hosted on different domains, as noted above.</p>

<p>FINALLY, as <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Directives">noted in the MDN</a>, “Browsers block frontend JavaScript code from accessing the <code class="language-plaintext highlighter-rouge">Set-Cookie</code> header, as required by the Fetch spec, which defines <code class="language-plaintext highlighter-rouge">Set-Cookie</code> as a forbidden response-header name that must be filtered out from any response exposed to frontend code.” As a result, the frontend script cannot directly parse the <code class="language-plaintext highlighter-rouge">Set-Cookie</code> header, either.</p>

<p>In the end, <strong>I had to rely on the browser to handle the cookie exchanges</strong>, since my script cannot access cross-domain cookies through <code class="language-plaintext highlighter-rouge">react-cookies</code> nor parse the cookie headers directly. E.g., I had to make the frontend ask the backend to invalidate the cookie rather than having the frontend script delete the cookie locally.</p>

<h2 id="cookie-configuration-rules">cookie configuration rules</h2>

<p>there are some very important cookie configuration rules that, when violated, can lead to cookie-related bugs. Check <code class="language-plaintext highlighter-rouge">loginCookieOptions</code> <a href="https://github.com/peter1357908/AssayPlate/blob/main/server/Controllers/AuthController.js">here</a> for the correct cookie configurations for AssayPlate.</p>

<ul>
  <li>cookie configuration must be done correctly otherwise browser will reject the cookie (e.g., claiming that <code class="language-plaintext highlighter-rouge">sameSite</code> is <code class="language-plaintext highlighter-rouge">true</code> while sending a cookie from a different domain).</li>
  <li>setting <code class="language-plaintext highlighter-rouge">httpOnly</code> to <code class="language-plaintext highlighter-rouge">true</code> will make the cookie inaccessible to the JavaScript <code class="language-plaintext highlighter-rouge">Document.cookie</code> API, but this only matters when dealing with <code class="language-plaintext highlighter-rouge">sameSite</code> cookies, since the script cannot access cross-domain cookies regardless (see previous section)</li>
  <li><a href="https://developers.google.com/privacy-sandbox/3pcd#report-issues">Google is restricting third-party cookies</a>! This will soon break some cookie uses that don’t follow their new standard. Attempting to follow the new standard, I tried to set the <code class="language-plaintext highlighter-rouge">partitioned</code> flag in the cookie configuration, and ended up uncovering a bug with <code class="language-plaintext highlighter-rouge">express.js</code> where the flag couldn’t be set correctly (see <a href="https://github.com/expressjs/express/issues/5275#issuecomment-1920601045">here</a>).</li>
</ul>

<h2 id="react-cookies-quirks">react-cookies quirks</h2>

<p>even when we <em>can</em> use <code class="language-plaintext highlighter-rouge">react-cookies</code>, there are still some things to pay attention to, to avoid bugs:</p>

<ol>
  <li>use the <code class="language-plaintext highlighter-rouge">CookieProvider</code> wrapper! AND set the correct <code class="language-plaintext highlighter-rouge">defaultSetOptions</code>, especially the cookie path.</li>
  <li>call <code class="language-plaintext highlighter-rouge">useCookies</code> correctly… this is more of a <a href="#javascript-tips">JavaScript tip</a>, though. Don’t be dumb <a href="https://github.com/bendotcodes/cookies/issues/454">like me</a>.</li>
</ol>

<h1 id="cors-learning">CORS learning</h1>

<p>CORS is about <em>letting the browser</em> allow a script from site A to make requests to site B (site B has to define its CORS policy to allow requests from site A). The point is, the browser usually sends the user’s cookies for site B to site B whenever the browser makes a request to site B. If the request was a malicious request NOT made on the user’s behalf, but rather on site A’s behalf, then there is a problem – the cookies can contain authentication information; site A wouldn’t have been able to make requests to site B on its own; site A needed user’s cookies to access site B.</p>

<p>As a result, a number of APIs (e.g., <code class="language-plaintext highlighter-rouge">fetch()</code> and <code class="language-plaintext highlighter-rouge">XMLHttpRequest</code>) follow <code class="language-plaintext highlighter-rouge">same-origin-policy</code> – if the request for content is not by a script from the same origin (site B script requesting from site B), then the request is denied in the preflight check.</p>

<p>Note, again, it’s the browser and the server together that enforces this. When you manually make HTTP requests, you can choose to ignore CORS, fake your origin, etc, although this fact doesn’t help malicious parties that want to coax the browser into making illegitimate CORS requests.</p>

<h1 id="tips">Tips</h1>

<p>This section has some tips for good practices in full-stack development.</p>

<h2 id="http-tips">HTTP tips</h2>

<p>GET requests (or DELETE, etc.) shouldn’t have a body:
<a href="https://www.baeldung.com/http-get-with-body">https://www.baeldung.com/http-get-with-body</a>. Just use <code class="language-plaintext highlighter-rouge">POST</code> most of the time and structure the actual URLs in terms of CRUD (see this project’s <a href="https://github.com/peter1357908/AssayPlate/blob/main/server/README.md">API Documentation</a>)</p>

<h2 id="mongodbmongoose-tips">MongoDB/Mongoose tips</h2>

<p>Mongoose documentation can be confusing/lacking at times. Examples:</p>

<p>“Note: Like updateOne, Mongoose registers deleteOne middleware on Query.prototype.deleteOne by default. That means that Model.deleteOne() will trigger deleteOne hooks, and this will refer to a query. However, doc.deleteOne() does not fire deleteOne query middleware for legacy reasons. To register deleteOne middleware as document middleware, use schema.pre(‘deleteOne’, { document: true, query: false }).” <a href="https://mongoosejs.com/docs/middleware.html">Reference here</a>.</p>

<p>I coded a “smart” way to remove dead references upon population from document. It’s more efficient than the answer <a href="https://stackoverflow.com/a/44149737/21452015">here</a>, inspired by <a href="https://stackoverflow.com/a/74187454/21452015">this downvoted answer</a> in the same post, but does require <a href="https://stackoverflow.com/questions/24618584/mongoose-save-not-updating-value-in-an-array-in-database-document">marking as modified</a>. See <code class="language-plaintext highlighter-rouge">GetPlatesAndUsername</code> in <a href="https://github.com/peter1357908/AssayPlate/blob/main/server/Controllers/PlatesController.js">PlatesController.js here</a></p>

<h2 id="reactjs-tips">react.js tips</h2>

<ul>
  <li>to share states between components, <a href="https://react.dev/learn/sharing-state-between-components">lift them up</a>. For complex state management, use dedicated libraries like <a href="https://react-redux.js.org/">Redux</a>.</li>
  <li><a href="https://react.dev/learn/updating-arrays-in-state">state variables should be treated as immutable</a>. Not following this practice easily leads to bugs.</li>
</ul>

<h2 id="nodejs-tips">node.js tips</h2>

<p>The canonical way to check/set whether the code is running in production is via the <code class="language-plaintext highlighter-rouge">NODE_ENV</code> environment variable, as noted <a href="https://stackoverflow.com/a/10699297/21452015">here</a></p>

<h2 id="javascript-tips">JavaScript tips</h2>

<ul>
  <li><a href="https://stackoverflow.com/a/19146230/21452015">Empty arrays and objects are truthy</a> – be careful when using comparison shorthands!</li>
  <li>order matters when destructuring an array (as opposed to an object)! Here’s <a href="https://github.com/bendotcodes/cookies/issues/454">one way it led to a pretty annoying bug</a>, complicated by other issues with cookies mentioned in the <a href="#the-cookie-saga">earlier section</a></li>
</ul>

<h2 id="axios-tips">Axios tips</h2>

<p>Axios interceptor doesn’t natively support React hooks. If you <em>really</em> want to use it (e.g., to check all responses for a token expiration flag so you can log the user out), you can try following <a href="https://dev.to/arianhamdi/react-hooks-in-axios-interceptors-3e1h">this workaround</a>. For the record, I hate such workarounds – they tend to be inefficient and result in tech debt.</p>

<h2 id="react-draggable-tips">react-draggable tips</h2>

<p><code class="language-plaintext highlighter-rouge">react-draggable</code> currently behaves inconsistently on mobile for conditional renders. See <a href="https://github.com/react-grid-layout/react-draggable/issues/740">my GitHub issue here</a>.</p>]]></content><author><name>Peter Gao</name><email>peter1357908@hotmail.com</email></author><category term="programming" /><category term="Web Dev" /><category term="Full-stack" /><category term="MERN" /><category term="MongoDB" /><category term="Mongoose" /><category term="express.js" /><category term="react.js" /><category term="node.js" /><category term="stateless authentication" /><category term="REST API" /><category term="cookies" /><category term="CORS" /><summary type="html"><![CDATA[This post summarizes my findings from working on the AssayPlate web app.]]></summary></entry><entry><title type="html">Microservices Redundancy Elimination Notes</title><link href="https://peterish.com/programming/microservices-redundancy-elimination/" rel="alternate" type="text/html" title="Microservices Redundancy Elimination Notes" /><published>2024-02-03T00:00:00-06:00</published><updated>2024-02-03T00:00:00-06:00</updated><id>https://peterish.com/programming/microservices-redundancy-elimination</id><content type="html" xml:base="https://peterish.com/programming/microservices-redundancy-elimination/"><![CDATA[<p>This post is a summary of my notes from working on the “Redundancy Elimination in Microservice Communication” research project.</p>

<ul>
  <li><a href="https://res.cloudinary.com/djvg6ubiy/image/upload/v1706997340/peterish.com/Redundancy_Elimination_in_Microservice_Communication_c1innc.pdf">Project report paper here</a></li>
  <li><a href="https://github.com/peter1357908/HTTP-RE-for-Istio-BookInfo">my code here</a></li>
  <li><a href="https://github.com/JBYoshi/istio-proxy">Jonathan’s code here</a></li>
</ul>

<h1 id="tutorials-for-dev-tools">Tutorials for Dev Tools</h1>

<p>I had to learn to use all the tools from scratch, including Docker, Kubernetes, and Istio. Contrary to projects in my previous posts, I am developing on Windows 11 for this project. Here are some tutorials that I found helpful and up-to-date, in order:</p>

<ul>
  <li><a href="https://birthday.play-with-docker.com/istio-docker-desktop/">Running Istio with Kubernetes on Docker Desktop</a> (Docker Labs)</li>
  <li><a href="https://birthday.play-with-docker.com/kubernetes-docker-desktop/">Getting Started with Kubernetes on Docker Desktop</a> (Docker Labs)</li>
  <li><a href="https://medium.com/@salluu/setting-up-istio-with-docker-desktop-on-windows-10-c0f7108615c0">Setting up Istio with Docker-Desktop on Windows 10</a> (Medium post)</li>
  <li><a href="https://istio.io/latest/docs/setup/getting-started/">Getting Started</a> (official Istio guide)</li>
</ul>

<p>Regarding the official Istio guide, if we wanted, we can do step 3 of “Deploy the sample application” in Bash (e.g., Git Bash). We’ll be able to confirm the whole setup is working interactively later, either way.</p>

<p>The Medium guide says to forward port 80 to 8080 for some reason, but accessing BookInfo like this worked fine for me:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://localhost/productpage
</code></pre></div></div>
<p>(now we can access it from other devices in the same local network, after looking up host address with <code class="language-plaintext highlighter-rouge">ipconfig</code>)</p>

<h1 id="references-and-documentations">References and Documentations</h1>

<p>Here are some links to references/documentation that were very helpful for development:</p>

<ul>
  <li><a href="https://stackoverflow.com/questions/73025689/istio-envoy-filter-lua-updating-response-body-get-stuck">Using Lua filter to modify HTTP response body in Istio</a> (StackOverflow question)</li>
  <li><a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter.html">Lua Filter</a> (Envoy doc), which is one kind of <code class="language-plaintext highlighter-rouge">HTTP_FILTER</code> native to the Envoy proxy; in turn, Envoy proxy is the backbone of Istio.</li>
  <li><a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter#script-examples">Envoy Lua script examples</a> (Envoy doc)</li>
  <li><a href="https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/lua/v3/lua.proto">Lua (proto)</a> (Envoy doc)</li>
  <li><a href="https://istio.io/latest/docs/reference/config/networking/envoy-filter/#EnvoyFilter">EnvoyFilter Docs</a> (Istio doc)</li>
  <li><a href="https://github.com/istio/istio/wiki/EnvoyFilter-Samples">EnvoyFilter Samples</a> (Istio GitHub doc)</li>
</ul>

<h1 id="observations">Observations</h1>

<h2 id="docker-ram-usage">Docker RAM usage</h2>
<p>Docker was eating up my RAM. I don’t know why <code class="language-plaintext highlighter-rouge">vmmemWSL</code> still says it’s using around 4 GB of memory after I kill Docker Desktop. I ended up doing <code class="language-plaintext highlighter-rouge">wsl --shutdown</code> after I’m done developing for the day. There have been similar reports: <a href="https://github.com/microsoft/WSL/issues/8725">WSL2 + Docker causes severe memory leaks in vmmem process, consuming all my machine’s physical memory</a>.</p>

<h2 id="importing-lua-libraries">importing Lua libraries</h2>

<p>IBM Cloud has a <a href="https://github.com/ibm-cloud-architecture/tutorial-istio-envoy-lua-filters/tree/master">tutorial for writing Lua filters in Istio</a>, which involves loading external Lua libraries. As it turns out, importing external Lua library is NOT trivial; it requires updating the Docker image. References <a href="https://github.com/envoyproxy/envoy/issues/21900#issuecomment-1168467098">here</a> and <a href="https://www.atomiccommits.io/lua-libraries-in-istios-envoyfilter">here</a>.</p>

<h2 id="lua-filters-are-stateless">Lua filters are stateless</h2>

<p>Lua filters are stateless and can’t be used for e.g., caching. (I ended up just using it for per-request compression)</p>

<ul>
  <li><a href="https://github.com/envoyproxy/envoy/issues/4008#issuecomment-409632101">“Lua state is per worker”</a></li>
  <li><a href="https://github.com/envoyproxy/envoy/issues/4027#issuecomment-1041904063">“Shared state in lua would negate a potential requirement to utilize c++ simply to achieve shared state.”</a></li>
  <li>alternatively, build it in Rust/Go/… and compile to Wasm. References <a href="https://github.com/proxy-wasm/proxy-wasm-rust-sdk">here</a> and <a href="https://content.red-badger.com/resources/extending-istio-with-rust-and-webassembly">here</a>.</li>
</ul>

<h1 id="questions-about-envoy-filter">questions about Envoy Filter</h1>

<p>Here are some questions that I had trouble finding answers to, regarding configuring the Envoy Filter through <code class="language-plaintext highlighter-rouge">.yaml</code> files:</p>

<ol>
  <li>What is the difference between <code class="language-plaintext highlighter-rouge">envoy.filters.http.lua</code> and <code class="language-plaintext highlighter-rouge">envoy.lua</code> for <code class="language-plaintext highlighter-rouge">patch.value.name</code>?</li>
  <li><code class="language-plaintext highlighter-rouge">inlineCode</code> vs <code class="language-plaintext highlighter-rouge">inlineString</code>?</li>
</ol>]]></content><author><name>Peter Gao</name><email>peter1357908@hotmail.com</email></author><category term="programming" /><category term="Microservices" /><category term="Redundancy Elimination" /><category term="Kubernetes" /><category term="Docker" /><category term="Istio" /><category term="HTTP" /><category term="Lua" /><category term="Research" /><summary type="html"><![CDATA[This post is a summary of my notes from working on the “Redundancy Elimination in Microservice Communication” research project.]]></summary></entry><entry><title type="html">Jekyll Mahjong Dev Notes</title><link href="https://peterish.com/programming/jekyll-mahjong-dev-notes/" rel="alternate" type="text/html" title="Jekyll Mahjong Dev Notes" /><published>2023-08-13T00:00:00-05:00</published><updated>2023-08-13T00:00:00-05:00</updated><id>https://peterish.com/programming/jekyll-mahjong-dev-notes</id><content type="html" xml:base="https://peterish.com/programming/jekyll-mahjong-dev-notes/"><![CDATA[<p>My findings from working on the jekyll-mahjong plugin.</p>

<h1 id="jekyll-plugin-writing">Jekyll Plugin Writing</h1>

<h2 id="adding-static-files-to-site">adding static files to site</h2>

<p>Turns out that adding static files like the <code class="language-plaintext highlighter-rouge">.svg</code> files to the built site requires some fiddling around. Specifically, if the files didn’t exist before and are added by a <a href="https://jekyllrb.com/docs/plugins/generators/">Generator plugin</a> during build, the generator must specifically mark those files as static so they can be copied to the built site eventually. What’s more annoying is that if those files already exist before the Generator executes, even if the Generator updates those files, they must not be marked as static files again, otherwise Jekyll complains about duplicate static files. This is why I wrote the <code class="language-plaintext highlighter-rouge">copy_as_necessary</code> function in <a href="https://github.com/peter1357908/jekyll-mahjong/blob/d88527a4dc26b4b7ea99be1719486cb25d67aa17/lib/jekyll-mahjong-generator.rb"><code class="language-plaintext highlighter-rouge">jekyll-mahjong-generator</code></a></p>

<p>There is another potential pitfall in updating (overwriting) source files with a generator: the generator will run into an infinite loop if it updates files during re-builds of the site (updated files will then trigger re-builds again). That’s why I added in an <code class="language-plaintext highlighter-rouge">@has_run</code> flag to the generator to ensure that it only runs once per <code class="language-plaintext highlighter-rouge">jekyll serve</code>.</p>

<h1 id="scalability-vs-customization">Scalability V.S. Customization</h1>

<h2 id="styling-inside-svg">styling inside SVG?</h2>
<p>If we want to allow styling individual tags inside the SVGs through CSS (e.g., to allow customizing the tile designs’ colors), we need to embed the SVG text in the HTML directly. However, doing that is not scalable for <code class="language-plaintext highlighter-rouge">jekyll-mahjong</code>, since its usage typically involves displaying the same tile multiple times (we don’t want to embed the same whole SVG multiple times).</p>

<p>In general, if we do want to style inside SVG:</p>
<ul>
  <li>make sure the embedded SVGs are minimized (e.g., take out unnecessary data like Inkscape metadata)</li>
  <li>make the components selectable either through IDs (for when we are not reusing the component) or through classes:
    <ul>
      <li>utility to turn each Inkscape label to <code class="language-plaintext highlighter-rouge">id</code> <a href="https://www.reddit.com/r/Inkscape/comments/y8aw2n/comment/jd3drs3/?utm_source=share&amp;utm_medium=web2x&amp;context=3">here</a>.</li>
      <li>add classes to components in Inkscape via the XML Editor (hotkey: <code class="language-plaintext highlighter-rouge">ctrl + shift + x</code>)</li>
    </ul>
  </li>
</ul>

<h2 id="sideways-tiles-through-css">sideways tiles through CSS?</h2>
<p>The approach in <a href="https://github.com/peter1357908/jekyll-mahjong/releases/tag/v2.0.1"><code class="language-plaintext highlighter-rouge">v2+</code></a> to let CSS handle turning the tiles sideways also makes the display more computation-intensive than JUST displaying standalone sideways SVGs. However, this saves us the trouble of having to create and upload two versions of SVGs for every tile design, and also exposes the styling for customization.</p>

<h1 id="plugin-showcase">Plugin Showcase</h1>

<p>See <a href="/riichi-docs/jekyll-mahjong-plugin">this Riichi Doc</a>.</p>]]></content><author><name>Peter Gao</name><email>peter1357908@hotmail.com</email></author><category term="programming" /><category term="Ruby" /><category term="Jekyll" /><category term="Web Dev" /><category term="Riichi Mahjong" /><category term="Inkscape" /><summary type="html"><![CDATA[My findings from working on the jekyll-mahjong plugin.]]></summary></entry><entry><title type="html">Graphic Design Notes</title><link href="https://peterish.com/graphic-design/graphic-design-notes/" rel="alternate" type="text/html" title="Graphic Design Notes" /><published>2023-08-09T00:00:00-05:00</published><updated>2023-08-09T00:00:00-05:00</updated><id>https://peterish.com/graphic-design/graphic-design-notes</id><content type="html" xml:base="https://peterish.com/graphic-design/graphic-design-notes/"><![CDATA[<p>Tricks and how-tos I learned from my amateur graphic design experience.</p>

<h1 id="inkscape">Inkscape</h1>

<h2 id="distribute-objects-along-a-path">Distribute Objects Along a Path</h2>

<p>When you want to distribute multiple objects along a path, make sure to center-align all of them and group them (see below). Select the group and then the path. Next, use the extension:</p>

<p class="notice">Extensions -&gt; Generate From Path -&gt; Distribute Along Path (or “Scatter”, in older versions of Inkscape).</p>

<p>Select “If pattern is a group, pick group members”, and the “Sequentially” mode.</p>

<p class="align-center"><img src="https://res.cloudinary.com/djvg6ubiy/image/upload/v1691642819/Portfolio/My%20Tutorials/Distribute_Along_Path_Tutorial_u808pk.png" alt="Distribute_Along_Path_Tutorial" /></p>

<p>Note that you need to adjust parameters like the size of the objects, the shape of the path, etc. to get your desired look. In the demo above, you can see that the resulting spread wasn’t symmetrical and the group of objects is only half-way through the second repeat.</p>

<h2 id="create-tables-in-inkscape">Create Tables in Inkscape?</h2>

<p>Although you can use the “Construct Grid” path effect, it’s much easier to make and style the table in any spreadsheet tool like Excel and Google Sheets, export the table in <code class="language-plaintext highlighter-rouge">.pdf</code>, and then import the <code class="language-plaintext highlighter-rouge">.pdf</code> into Inkscape.</p>

<h2 id="rounding-corners-of-an-object">Rounding corners of an object?</h2>

<p>Select the object -&gt; Object to Path -&gt; Add Corners LPE -&gt; click and drag to select all desired corners -&gt; drag a corner and see!</p>

<h1 id="irfanview">IrfanView</h1>

<h2 id="tiled-images">Tiled Images</h2>
<p>Image -&gt; Create Tiled Images. This is good for printing 4 flyers on one page:<br />
<img src="https://res.cloudinary.com/djvg6ubiy/image/upload/c_scale,h_400/v1691635651/Longhorn%20Riichi/Finished%20Promo%20Products/Flyer4x_sdxc5e.png" alt="4x July 2023 Longhorn Riichi Flyers on one page" class="align-center" /></p>

<h2 id="merge-images">Merge Images</h2>
<p>Image -&gt; Merge Images. This is good for showcasing before-and-after, front-and-back, etc.:
<img src="https://res.cloudinary.com/djvg6ubiy/image/upload/v1691633486/Longhorn%20Riichi/Finished%20Promo%20Products/Yakuless_Behavior_Mockup_gdyqzb.jpg" alt="Longhorn Riichi Spring 2023 T-Shirt: Yakuless Behavior" class="align-center" /></p>

<h1 id="gimp">GIMP</h1>

<h2 id="healing-tool">Healing Tool</h2>

<p>This is useful for masking imperfections to ensure a consistent texture. Simply select a contiguous texture that contains no imperfection, paste it over any imperfection, and use healing tool to mask any inconsistencies at the copy-paste border.</p>

<p><img src="https://res.cloudinary.com/djvg6ubiy/image/upload/v1691643361/Portfolio/My%20Tutorials/Healing_Tool_Demo_zeryjk.jpg" alt="Healing Tool Demo" class="align-center" /></p>]]></content><author><name>Peter Gao</name><email>peter1357908@hotmail.com</email></author><category term="graphic-design" /><category term="Inkscape" /><category term="GIMP" /><category term="IrfanView" /><summary type="html"><![CDATA[Tricks and how-tos I learned from my amateur graphic design experience.]]></summary></entry><entry><title type="html">Jekyll Dev Notes</title><link href="https://peterish.com/programming/Jekyll-dev-notes/" rel="alternate" type="text/html" title="Jekyll Dev Notes" /><published>2023-08-08T00:00:00-05:00</published><updated>2023-08-08T00:00:00-05:00</updated><id>https://peterish.com/programming/Jekyll-dev-notes</id><content type="html" xml:base="https://peterish.com/programming/Jekyll-dev-notes/"><![CDATA[<p>My notes from working on <a href="https://longhornriichi.com/">longhornriichi.com</a> and <a href="https://peterish.com/">peterish.com</a>.</p>

<h1 id="programming">Programming</h1>

<p>Jekyll, Liquid, Bootstrap, and CSS.</p>

<h2 id="jekyll">Jekyll</h2>

<h3 id="testing-locally">testing locally</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle install
bundle exec jekyll serve --livereload
</code></pre></div></div>
<p>(might need <code class="language-plaintext highlighter-rouge">bundle add webrick</code> for Ruby 3.0 or later)</p>

<p>Then site is available on <a href="http://localhost:4000">http://localhost:4000</a></p>

<h3 id="serving-to-other-local-machines-in-wsl2">serving to other local machines in WSL2</h3>

<ol>
  <li>go into the <code class="language-plaintext highlighter-rouge">advanced settings</code> of <code class="language-plaintext highlighter-rouge">Windows Defender Firewall</code> and add an inbound rule that allows connecting to port <code class="language-plaintext highlighter-rouge">4000</code> via TCP. For more details, see this <a href="https://www.softwaretestinghelp.com/how-to-open-ports/">article</a>.</li>
  <li>find the WSL2’s IP (<strong>NOT</strong> its <code class="language-plaintext highlighter-rouge">vEthernet (WSL)</code> from <code class="language-plaintext highlighter-rouge">ipconfig</code> on PowerShell) with <code class="language-plaintext highlighter-rouge">hostname -I</code> in the WSL2 shell. Let’s say this is <code class="language-plaintext highlighter-rouge">a.b.c.d</code>.</li>
  <li>In PowerShell, <code class="language-plaintext highlighter-rouge">netsh interface portproxy set v4tov4 listenport=4000 listenaddress=0.0.0.0 connectport=4000 connectaddress=a.b.c.d</code></li>
  <li><code class="language-plaintext highlighter-rouge">bundle exec jekyll serve --host=0.0.0.0</code> (note that <code class="language-plaintext highlighter-rouge">--livereload</code> slows down loading A LOT for other devices on the network so it’s disabled here)</li>
  <li>find the host’s IPv4 address with <code class="language-plaintext highlighter-rouge">ipconfig</code> in PowerShell. Say it’s <code class="language-plaintext highlighter-rouge">192.168.x.y</code></li>
  <li>access the hosted Jekyll site from other local devices via <code class="language-plaintext highlighter-rouge">192.168.x.y:4000</code></li>
</ol>

<h4 id="portproxy"><code class="language-plaintext highlighter-rouge">portproxy</code></h4>
<ul>
  <li>as noted in <a href="https://superuser.com/a/1708945">this StackExchange response</a>, the default port forwarding for WSL2’s host device doesn’t work for other devices on the local network, and this is why we needed the <code class="language-plaintext highlighter-rouge">portproxy</code> steps above.</li>
  <li>Note that WSL2 IP may change upon restarting Windows, so we need to redo the <code class="language-plaintext highlighter-rouge">portproxy</code> steps every time WSL2 gets a new IP address. See related discussion <a href="https://gist.github.com/wllmsash/1636b86eed45e4024fb9b7ecd25378ce">here</a>. My WSL2’s IP has thus far remained the same across multiple restarts of both Windows 11 and WSL2 itself 🤷.</li>
  <li>use <code class="language-plaintext highlighter-rouge">netsh interface portproxy show all</code> to see all <code class="language-plaintext highlighter-rouge">portproxy</code> rules.</li>
</ul>

<h2 id="liquid">Liquid</h2>

<p>Iterating through a JSON object can be done as follows:</p>

<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">{%</span><span class="w"> </span><span class="nt">for</span><span class="w"> </span><span class="nv">social_key_value</span><span class="w"> </span><span class="nt">in</span><span class="w"> </span><span class="nv">site</span><span class="p">.</span><span class="nv">data</span><span class="p">.</span><span class="nv">socials</span><span class="w"> </span><span class="cp">%}</span>
    <span class="cp">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">social_name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">social_key_value</span><span class="p">.</span><span class="nf">first</span><span class="w"> </span><span class="cp">%}</span>
    <span class="cp">{%</span><span class="w"> </span><span class="nt">assign</span><span class="w"> </span><span class="nv">social_obj</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">social_key_value</span><span class="p">.</span><span class="nf">last</span><span class="w"> </span><span class="cp">%}</span>
    &lt;a href="<span class="cp">{{</span><span class="w"> </span><span class="nv">social_obj</span><span class="p">.</span><span class="nv">url</span><span class="w"> </span><span class="cp">}}</span>"&gt;&lt;img src="<span class="cp">{{</span><span class="w"> </span><span class="nv">social_obj</span><span class="p">.</span><span class="nv">image</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">relative_url</span><span class="w"> </span><span class="cp">}}</span>" title="<span class="cp">{{</span><span class="w"> </span><span class="nv">social_name</span><span class="w"> </span><span class="cp">}}</span>" alt="<span class="cp">{{</span><span class="w"> </span><span class="nv">social_name</span><span class="w"> </span><span class="cp">}}</span>" /&gt;&lt;/a&gt;
<span class="cp">{%</span><span class="w"> </span><span class="nt">endfor</span><span class="w"> </span><span class="cp">%}</span>
</code></pre></div></div>

<p>References: <a href="https://shopify.dev/docs/api/liquid/objects/for-loops#metafield-accessing-metafields-of-type-json">shopify doc</a> and <a href="https://stackoverflow.com/a/55094996/21452015">Stack Overflow answer</a>.</p>

<p>Also, you can escape Liquid processing with</p>
<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">{%-</span><span class="w"> </span><span class="nt">raw</span><span class="w"> </span><span class="cp">-%}</span>
<span class="cp">{%</span><span class="w"> </span><span class="nt">endraw</span><span class="w"> </span><span class="cp">%}</span>
</code></pre></div></div>
<p>(can you guess how I produced the code block above?)</p>

<h2 id="bootstrap">Bootstrap</h2>

<ul>
  <li>The <a href="https://getbootstrap.com/docs/4.0/layout/grid/">grid system</a> (<code class="language-plaintext highlighter-rouge">xs</code>, <code class="language-plaintext highlighter-rouge">sm</code>, <code class="language-plaintext highlighter-rouge">md</code>, <code class="language-plaintext highlighter-rouge">lg</code>, <code class="language-plaintext highlighter-rouge">xl</code>) seems to be at the core of the Bootstrap flow.</li>
  <li>the version of <code class="language-plaintext highlighter-rouge">Bootstrap</code> can be found in <code class="language-plaintext highlighter-rouge">_scss/bootstrap/bootstrap.scss</code> in <a href="https://github.com/Longhorn-Riichi/longhornriichi.com">our Jekyll site</a>.</li>
</ul>

<h2 id="sass--scss">SASS / SCSS</h2>

<p>Use <code class="language-plaintext highlighter-rouge">sass-migrator</code> to address deprecation warnings <a href="https://sass-lang.com/documentation/cli/migrator/#usage">like this</a>. For Jekyll purposes it’s likely just this, though:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sass-migrator division --verbose --migrate-deps _sass/*.scss
</code></pre></div></div>

<h1 id="gettinghosting-content">Getting/Hosting Content</h1>

<p>CloudFlare, Free Image Hosting,</p>

<h2 id="cloudflare">CloudFlare</h2>

<p>Use pull requests to allow <a href="https://developers.cloudflare.com/pages/platform/preview-deployments/">previewing deployments</a>. This deploys the site to a preview URL. Unfortunately this still <a href="https://community.cloudflare.com/t/cf-pages-limits-in-preview-deployment-environment/470740/2">counts against the 500 free monthly deploys</a>.</p>

<h2 id="free-image-hosting">Free Image Hosting</h2>
<h3 id="google-photos">Google Photos</h3>
<p>For a lightweight solution (minimal image hosting), we can <a href="https://www.labnol.org/embed/google/photos/">embed pictures from Google Photos</a>. This is not scalable but enough for <a href="https://longhornriichi.com/">longhornriichi.com</a> (it only needs to show a select few photos). We can adjust the size of the embedded image <a href="https://sites.google.com/site/picasaresources/google-photos-1/how-to-get-a-direct-link-to-an-image">by editing the link like this</a>.</p>

<h3 id="cloudinary">Cloudinary</h3>
<p>Given a greater image hosting demand (like <a href="https://peterish.com/">peterish.com</a>), we can use <a href="https://cloudinary.com/">Cloudinary</a> (I have to yet to figure out if there is a storage limit for the free tier). It has built in support for serving cropped, resized, etc. versions of an image (without affecting the uploaded, full image).</p>

<h3 id="not-imgur">NOT Imgur</h3>
<p class="notice--warning">Do NOT use Imgur as a CDN because <a href="https://help.imgur.com/hc/en-us/articles/115003264766-FAQ-for-Forum-Users-">this violates their TOS</a>.</p>

<p>They are ambiguous about hosting images through them on your blog, but best not to try your luck.</p>

<h2 id="free-illustration">Free Illustration</h2>

<p><a href="https://www.irasutoya.com/">いらすとや</a> has cute cliparts for free use; the interface is in Japanese, though.</p>]]></content><author><name>Peter Gao</name><email>peter1357908@hotmail.com</email></author><category term="programming" /><category term="Jekyll" /><category term="Web Dev" /><category term="CloudFlare" /><category term="WSL" /><summary type="html"><![CDATA[My notes from working on longhornriichi.com and peterish.com.]]></summary></entry><entry><title type="html">New Machine Setup</title><link href="https://peterish.com/programming/new-machine-setup/" rel="alternate" type="text/html" title="New Machine Setup" /><published>2023-08-08T00:00:00-05:00</published><updated>2023-08-08T00:00:00-05:00</updated><id>https://peterish.com/programming/new-machine-setup</id><content type="html" xml:base="https://peterish.com/programming/new-machine-setup/"><![CDATA[<p>Things to do on a new machine (e.g., new <a href="https://cloud.google.com/compute">GCE VM</a>).</p>

<h1 id="github">GitHub</h1>
<ol>
  <li><a href="https://git-scm.com/downloads">install git</a></li>
  <li>generate an SSH key for GitHub.
    <ul>
      <li>Bash
        <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  ssh-keygen <span class="nt">-t</span> ed25519 <span class="nt">-C</span> <span class="s2">"YOUR_EMAIL_HERE"</span>
  <span class="nb">eval</span> <span class="s2">"</span><span class="si">$(</span>ssh-agent <span class="nt">-s</span><span class="si">)</span><span class="s2">"</span>
  ssh-add ~/.ssh/id_ed25519
</code></pre></div>        </div>
      </li>
      <li>Powershell
        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="n">ssh-keygen</span><span class="w"> </span><span class="nt">-t</span><span class="w"> </span><span class="nx">ed25519</span><span class="w"> </span><span class="nt">-C</span><span class="w"> </span><span class="s2">"YOUR_EMAIL_HERE"</span><span class="w">
  </span><span class="n">start-ssh-agent</span><span class="w">
</span></code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>add the public key to GitHub: <code class="language-plaintext highlighter-rouge">cat ~/.ssh/id_ed25519.pub</code> <a href="https://github.com/settings/keys">here</a></li>
  <li>git config stuff:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> git config <span class="nt">--global</span> user.name <span class="s2">"YOUR_NAME_HERE"</span>
 git config <span class="nt">--global</span> user.email <span class="s2">"YOUR_EMAIL_HERE"</span>
 git config <span class="nt">--global</span> core.filemode <span class="nb">true</span>
</code></pre></div>    </div>
  </li>
  <li>(optional for Unix) add keychain: <code class="language-plaintext highlighter-rouge">sudo apt install keychain</code>, then add the following to <code class="language-plaintext highlighter-rouge">.bashrc</code>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c"># Use `keychain` to load my SSH key</span>
 /usr/bin/keychain <span class="nt">-q</span> <span class="nt">--nogui</span> <span class="nv">$HOME</span>/.ssh/id_ed25519
 <span class="nb">source</span> <span class="nv">$HOME</span>/.keychain/<span class="nv">$HOSTNAME</span><span class="nt">-sh</span>
</code></pre></div>    </div>
  </li>
</ol>

<h1 id="os-specific">OS-specific</h1>

<h2 id="wsl">WSL</h2>

<p>Add the following to <code class="language-plaintext highlighter-rouge">.bashrc</code>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># allow opening webpages with Chrome</span>
<span class="nb">export </span><span class="nv">BROWSER</span><span class="o">=</span><span class="s2">"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"</span>
</code></pre></div></div>
<p>This will allow opening URLs with <code class="language-plaintext highlighter-rouge">sensible-browser</code>, and was enough to make Chrome pop up e.g., when Google’s Python API wants to authenticate the user via a webpage prompt. To make <code class="language-plaintext highlighter-rouge">xdg-open</code> work additional fiddling is needed.</p>]]></content><author><name>Peter Gao</name><email>peter1357908@hotmail.com</email></author><category term="programming" /><category term="Unix" /><category term="WSL" /><category term="GitHub" /><summary type="html"><![CDATA[Things to do on a new machine (e.g., new GCE VM).]]></summary></entry><entry><title type="html">UvUManager Dev Notes</title><link href="https://peterish.com/programming/UvUManager-dev-notes/" rel="alternate" type="text/html" title="UvUManager Dev Notes" /><published>2023-08-07T00:00:00-05:00</published><updated>2023-08-07T00:00:00-05:00</updated><id>https://peterish.com/programming/UvUManager-dev-notes</id><content type="html" xml:base="https://peterish.com/programming/UvUManager-dev-notes/"><![CDATA[<p>A collection of notes from my experience developing and deploying my first ever Discord bot: the <a href="https://github.com/Longhorn-Riichi/Ronhorn-UvUManager">UvUManager</a>.</p>

<h1 id="uvumanager">UvUManager</h1>
<p><a href="https://github.com/Longhorn-Riichi/Ronhorn-UvUManager">UvUManager</a> is a Discord bot used for <a href="https://longhornriichi.com">Longhorn Riichi</a>’s special intercollegiate tournament “<a href="https://longhornriichi.com/events/">UTA vs UTD</a>”. The main technical challenges are:</p>
<ol>
  <li>Discord API (<a href="https://discordpy.readthedocs.io/en/stable/">discord.py</a> is relatively well documented but still have some ambiguities)</li>
  <li>Mahjong Soul API (there is no official API; I ended up using and contributing to <a href="https://github.com/RiichiNomi/mjsoul.py">mjsoul.py</a>)</li>
  <li>Google Sheets (the <a href="https://developers.google.com/sheets/api/guides/concepts">Official Google Sheets API</a> was a mess; <a href="https://github.com/burnash/gspread">gspread</a> was MUCH more user-friendly)</li>
</ol>

<p>The <a href="https://github.com/Longhorn-Riichi/Ronhorn-UvUManager">UvUManager</a> repo has information on how to obtain and set up the secrets for the 3 modules above.</p>

<h1 id="discordpy">discord.py</h1>
<ol>
  <li>Reloading an extension doesn’t re-import the Python files (i.e., to load the changed imported file, one must restart the bot altogether)</li>
  <li>Restarting the application in the same process doesn’t reload the <code class="language-plaintext highlighter-rouge">dotenv</code> variables (this is likely due to the fact that <code class="language-plaintext highlighter-rouge">dotenv</code> doesn’t override existing environment variables – the environment variable values remain the same as the previous process).</li>
  <li><code class="language-plaintext highlighter-rouge">ephemeral</code> behaves different for different <a href="https://discordpy.readthedocs.io/en/latest/interactions/api.html"><code class="language-plaintext highlighter-rouge">Interactions</code></a>. For <a href="https://discordpy.readthedocs.io/en/latest/interactions/api.html#component"><code class="language-plaintext highlighter-rouge">Component</code></a> interactions, the followup message can be ephemeral regardless of how <code class="language-plaintext highlighter-rouge">ephemeral</code> was set in <a href="https://discordpy.readthedocs.io/en/latest/interactions/api.html#discord.Interaction.followup"><code class="language-plaintext highlighter-rouge">defer()</code></a>, but for slash command interactions, the followup message’s <code class="language-plaintext highlighter-rouge">ephemeral</code> is governed by the <code class="language-plaintext highlighter-rouge">ephemeral</code> in <code class="language-plaintext highlighter-rouge">defer()</code>. More testing may be necessary.</li>
  <li>need to call <code class="language-plaintext highlighter-rouge">View.stop()</code> before deleting messages that have a <code class="language-plaintext highlighter-rouge">View</code>, especially if the <code class="language-plaintext highlighter-rouge">View</code> has a timeout (<code class="language-plaintext highlighter-rouge">View.stop</code> is a clean-up that cancels timeout tasks, etc.). This requirement isn’t listed on the <code class="language-plaintext highlighter-rouge">discord.py</code> <a href="https://discordpy.readthedocs.io/en/stable/interactions/api.html?highlight=view%20timeout#discord.ui.View.stop">doc</a> as of writing.</li>
</ol>

<h1 id="mahjong-soul-api">Mahjong Soul API</h1>

<h2 id="reverse-engineering-api">Reverse-engineering API</h2>
<ol>
  <li>The <code class="language-plaintext highlighter-rouge">.proto</code> file (like the one <a href="https://github.com/RiichiNomi/mjsoul.py/tree/main/proto">here</a>) contains all the readable and searchable Protobuf API. Developers typically need to search here for their desired interaction message with the Mahjong Soul server (and examine the format of the Request and Response objects).</li>
  <li>you can learn some protobuf field’s possible values with <a href="https://chrome.google.com/webstore/detail/websocket-frame-inspector/nlajeopfbepekemjhkjcbbnencojpaae">WebSocket Inspector</a> (e.g., the fact that Twitter OAuth2 protobuf request type field should be 10). You can decode Protobuf with <a href="https://protobuf-decoder.netlify.app/">this online decoder</a>, but it’s probably better to <a href="https://stackoverflow.com/a/35069365/21452015">decode with <code class="language-plaintext highlighter-rouge">protoc</code></a> given sensitive info and that you have the <code class="language-plaintext highlighter-rouge">.proto</code> file.</li>
</ol>

<h2 id="api-discoveries">API discoveries</h2>
<ol>
  <li><code class="language-plaintext highlighter-rouge">open_live</code> field of <code class="language-plaintext highlighter-rouge">createContestGame</code> doesn’t actually do anything. In fact, the same toggle on the management website doesn’t work either, as a result.</li>
  <li>known but <a href="https://github.com/RiichiNomi/mjsoul.py/issues/4">undocumented error</a>: <code class="language-plaintext highlighter-rouge">ERROR CODE 1209</code>, which happens when trying to pause an already paused tournament game</li>
  <li><a href="https://github.com/RiichiNomi/mjsoul.py">mjsoul.py</a> raises Exceptions if the response contains an Error object. I modified the relevant module locally (like <a href="https://github.com/Longhorn-Riichi/UvUManager/blob/6d095c4d10317178c21d6327a9292b9436f2d7dd/modules/mahjongsoul/contest_manager.py">this version</a>) so <code class="language-plaintext highlighter-rouge">GeneralMajsoulError</code> has a field <code class="language-plaintext highlighter-rouge">errorCode</code>, to help with catching and acting on specific errors (I’ll merge this change to the <a href="https://github.com/RiichiNomi/mjsoul.py">mjsoul.py</a> repo soon) .</li>
  <li>logging out through the tournament manager website (or closing the browser tab after logging in) also logs out the bot. Now the bot has to make a new WSS connection before retrying login – trying to login through the old WSS connection results in a <code class="language-plaintext highlighter-rouge">2504 : "ERR_CONTEST_MGR_HAS_LOGINED</code>. Okay this is further confirmed by the switch-contest behavior on browser – it also switched the contest for all WSS connections. It’s like there is only one state for each account no matter how many connections there are. In other words, each account can manage at most one contest at a time.</li>
</ol>

<h1 id="gce-vm">GCE VM</h1>

<h2 id="screen"><code class="language-plaintext highlighter-rouge">screen</code></h2>
<ol>
  <li>To run the bot without having it die on SSH disconnection, create a <code class="language-plaintext highlighter-rouge">screen</code> session. (could also use <code class="language-plaintext highlighter-rouge">nohup</code> as a more lightweight solution).</li>
  <li>while in a screen, use <code class="language-plaintext highlighter-rouge">^A^D</code> to detach from screen and let it run in the background.</li>
  <li><code class="language-plaintext highlighter-rouge">screen -ls</code> to list all screens, <code class="language-plaintext highlighter-rouge">screen -r</code> to resume a session, and <code class="language-plaintext highlighter-rouge">killall screen</code> to terminate all screens.</li>
  <li>GCE VM-specific bug: uploading files through SSH-in-browser while in a screen session crashes the SSH connection…</li>
</ol>

<h2 id="develop-with-local-ssh">Develop with local SSH</h2>

<p>I.e., not using the SSH-in-browser on Google Cloud.</p>

<h3 id="set-up-ssh-with-gcloud">Set up SSH with <code class="language-plaintext highlighter-rouge">gcloud</code></h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud auth login
gcloud compute config-ssh
</code></pre></div></div>

<p>it seems the default account for the GCE VM console SSH can be different from your local account: <a href="https://cloud.google.com/compute/docs/instances/ssh">doc</a>.</p>

<h3 id="ssh-with-vscode-for-gui-editor">SSH with VSCode for GUI Editor</h3>

<p>Guide <a href="https://learn.canceridc.dev/cookbook/virtual-machines/using-vs-code-with-gcp-vms">here</a>.</p>

<p class="notice--warning">WARNING: VSCode <em>can</em> crash your server! Relevant reports: <a href="https://medium.com/good-robot/use-visual-studio-code-remote-ssh-sftp-without-crashing-your-server-a1dc2ef0936d">here</a> and <a href="https://github.com/microsoft/vscode-remote-release/issues/2692">here</a>.</p>

<p>The crash causes the SSH connection to die and prevent further SSH connections into the VM of <em>any</em> form (e.g., Google’s SSH-in-browser). This can be resolved by restarting the VM (<code class="language-plaintext highlighter-rouge">STOP</code> and then <code class="language-plaintext highlighter-rouge">START</code>).</p>

<p>(can we figure out a better way to edit on VM with a GUI?)</p>]]></content><author><name>Peter Gao</name><email>peter1357908@hotmail.com</email></author><category term="programming" /><category term="Python" /><category term="Unix" /><category term="bot" /><category term="Discord" /><category term="Google Cloud" /><category term="Google Sheets" /><category term="Mahjong Soul" /><category term="Longhorn Riichi" /><category term="Riichi Mahjong" /><summary type="html"><![CDATA[A collection of notes from my experience developing and deploying my first ever Discord bot: the UvUManager.]]></summary></entry></feed>