Sing's Log

Knowledge worth sharing


  • Home

  • Tags

  • Timeline

  • Portfolio

  • Search

Resolving IntelliJ 'Cannot Resolve Symbol' Error

Posted on 2023-11-21 |

If you’ve encountered the Cannot resolve symbol... error in IntelliJ IDEA after switching branches, you’re not alone. This frustrating issue can occur even when your code is error-free and compiles successfully from the command line.

Solution

To resolve this issue, follow these steps:

  1. Expand the Maven plugin in IntelliJ IDEA’s sidebar.
  2. Click the Reload All Maven Projects button with the refresh icon.

By following these steps, you can make the Cannot resolve symbol... error vanish and continue coding smoothly. This simple fix can save you time and frustration.

Reference

For more details and community discussions on this issue, check out this Stack Overflow thread.

Automating Google Calendar from Gmail Content

Posted on 2023-11-08 | Edited on 2023-11-21 |

As a software engineer based in Dublin, Ireland, I often find myself juggling a busy schedule. Managing my fitness classes, work meetings, and personal commitments can be quite a challenge. To streamline this process, I decided to embark on a project to automatically create Google Calendar events from Gympass confirmation emails. In this blog post, I’ll walk you through the steps and code I used to accomplish this task.

The Problem

Gympass, a popular fitness service, sends confirmation emails whenever I book a fitness class. These emails contain valuable information about the class, such as the date, time, location, and instructor. Manually adding these details to my Google Calendar was becoming a time-consuming task.

The Solution

To automate this process, I created a Google Apps Script that runs in my Gmail account. This script scans my inbox for Gympass confirmation emails, extracts the relevant information, and creates corresponding Google Calendar events. Here’s an overview of how it works:

  1. Create a Google Apps Script Project:

    • Open your Gmail account in a web browser.
    • Click on the “Apps Script” icon in the top-right corner (it looks like a square with a pencil).
    • This will open the Google Apps Script editor.
  2. Write the Script:

    • In the Google Apps Script editor, copy and paste the script code provided in this blog post.
  3. Save and Name the Project:

    • Give your project a name by clicking on “Untitled Project” at the top left and entering a name.
  4. Authorization:

    • The script will need authorization to access your Gmail and Google Calendar. Click on the run button (▶️) in the script editor.
    • Follow the prompts to grant the necessary permissions to the script.
  5. Trigger the Script:

    • To automate the script’s execution, you can set up triggers. Click on the clock icon ⏰ in the script editor.
    • Create a new trigger that specifies when and how often the script should run. You might want it to run periodically, such as every hour.
  6. Email Search: The script starts by searching your inbox for emails from `[email protected]`. This ensures that it only processes Gympass emails.

  7. Email Parsing: For each Gympass email found, the script extracts the email’s subject and plain text body.

  8. Cancellation Handling: If the email subject indicates that you canceled a booking, the script performs a cancellation action (you can customize this based on your business logic).

  9. Data Extraction: For non-cancellation emails, the script removes any unnecessary footer text and URLs from the email body.

  10. Event Details Extraction: Using regular expressions, the script extracts the date, time, location, and event title from the cleaned email body.

  11. Month-to-Number Conversion: It converts the month name to a numeric value for creating a Date object.

  12. Event Creation: With all the details in hand, the script creates a new Google Calendar event. It includes the event title, start and end times, location, and a Google Maps link to the event’s location.

  13. Duplicate Event Check: Before creating an event, the script checks if an event with the same date, time, title, and location already exists in the calendar to avoid duplicates.

Code Organization

To keep the code clean and maintainable, I divided it into several functions:

  • extractAndCreateCalendarEvent: The main function that orchestrates the entire process.
  • extractEventDetails: Extracts event details from the email body using regular expressions.
  • monthToNumber: Converts month names to numeric values.
  • createCalendarEvent: Creates a new Google Calendar event.
  • isEventExist: Checks if an event with the same details already exists.
  • removeFooterAndUrls: Removes unnecessary footer text and URLs from the email body.
  • cancelEvent: Placeholder for handling event cancellations (customize based on your needs).
  • parseTime: Parses time in HH:mm a format and returns it as milliseconds.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// Main function to process emails and create calendar events
const extractAndCreateCalendarEvent = () => {
const searchString = "from:[email protected]";
const threads = GmailApp.search(searchString);

for (let i = 0; i < threads.length; i++) {
const messages = threads[i].getMessages();
for (let j = 0; j < messages.length; j++) {
const message = messages[j];
const subject = message.getSubject();
const body = message.getPlainBody();

if (subject.includes("You canceled your booking")) {
cancelEvent(body);
} else {
const cleanedBody = removeFooterAndUrls(body);
const eventDetails = extractEventDetails(cleanedBody);

if (eventDetails) {
createCalendarEvent(eventDetails, cleanedBody);
}
}
}
}
};

// Extract the event details from the email body
const extractEventDetails = (body) => {
const dateRegex = /(\w+), ([A-Za-z]+) (\d+)th at (\d+:\d+ [APap][Mm]) \(GMT\)/;
const locationRegex = /In-person • (.+)/;
const titleRegex = /(.*?) will see you/;
const timeRegex = /(\d+:\d+ [APap][Mm]) - (\d+:\d+ [APap][Mm]) \(GMT\)/;

const dateMatch = body.match(dateRegex);
const locationMatch = body.match(locationRegex);
const titleMatch = body.match(titleRegex);
const timeMatch = body.match(timeRegex);

if (dateMatch && locationMatch && titleMatch && timeMatch) {
const dayOfWeek = dateMatch[1];
const month = dateMatch[2];
const day = dateMatch[3];
const eventTimeStart = timeMatch[1];
const eventTimeEnd = timeMatch[2];
const location = locationMatch[1];
const eventTitle = titleMatch[1];

return {
dayOfWeek,
month,
day,
eventTimeStart,
eventTimeEnd,
location,
eventTitle,
};
}

return null;
};

// Convert month name to a numeric value
const monthToNumber = (month) => {
const monthMap = {
"January": 0,
"February": 1,
"March": 2,
"April": 3,
"May": 4,
"June": 5,
"July": 6,
"August": 7,
"September": 8,
"October": 9,
"November": 10,
"December": 11
};
return monthMap[month];
};

// Create a Google Calendar event
const createCalendarEvent = (eventDetails, cleanedContent) => {
const {
dayOfWeek,
month,
day,
eventTimeStart,
eventTimeEnd,
location,
eventTitle,
} = eventDetails;

const monthNumber = monthToNumber(month);

const eventDate = new Date(new Date().getFullYear(), monthNumber, day, 0, 0, 0);
const eventStartTime = new Date(new Date().getFullYear(), monthNumber, day, 0, 0, 0);
eventStartTime.setMilliseconds(parseTime(eventTimeStart));

const eventEndTime = new Date(new Date().getFullYear(), monthNumber, day, 0, 0, 0);
eventEndTime.setMilliseconds(parseTime(eventTimeEnd));

const gymName = eventTitle;
const mapsUrl = 'https://www.google.com/maps/search/' + encodeURIComponent(`${gymName} ${location}`);
const description = `Google Maps Location URL: ${mapsUrl}\n${cleanedContent}`;

if (!isEventExist(eventDate, eventEndTime, eventTitle, location)) {
const calendar = CalendarApp.getDefaultCalendar();
calendar.createEvent(eventTitle, eventStartTime, eventEndTime, { location, description });
}
};

// Check if an event already exists
const isEventExist = (startDate, endDate, title, location) => {
const calendar = CalendarApp.getDefaultCalendar();
const events = calendar.getEvents(startDate, endDate);
for (let i = 0; i < events.length; i++) {
if (events[i].getTitle() === title && events[i].getLocation() === location) {
return true;
}
}
return false;
};

// Remove footer text and URLs
const removeFooterAndUrls = (body) => {
const footerRegex = /Help\n<.*?>[\s\S]*$/;
const cleanedBody = body.replace(footerRegex, '');
const urlRegex = /<[^>]*>/g;
const cleanedContent = cleanedBody.replace(urlRegex, '');
return cleanedContent;
};


// Cancel an event
const cancelEvent = (body) => {
// Parse the content of the cancellation email and perform the cancellation action as needed
// You can identify the event to cancel based on the information in the email content
// The specific implementation of this depends on your business logic
};

// Helper function to parse time in HH:mm a format and return it as milliseconds
const parseTime = (timeString) => {
const [time, amPm] = timeString.split(' ');
const [hours, minutes] = time.split(':');
let hoursInt = parseInt(hours, 10);
const minutesInt = parseInt(minutes, 10);

if (amPm.toLowerCase() === 'pm' && hoursInt !== 12) {
hoursInt += 12;
} else if (amPm.toLowerCase() === 'am' && hoursInt === 12) {
hoursInt = 0;
}

return (hoursInt * 60 + minutesInt) * 60000; // Convert to milliseconds
};

Conclusion

Automating the creation of Google Calendar events from Gympass emails has significantly reduced the time and effort required to manage fitness classes and appointments. With this script running in the background, you can focus on your workouts and let technology take care of the scheduling.

By following the steps outlined in this blog post, you can set up your own automated email-to-calendar integration and adapt it to your specific needs. Happy coding!

Nuphy keyboard with Karabinar Elements

Posted on 2023-11-07 | Edited on 2023-11-21 |

Introduction

I recently purchased the Nuphy Air75 keyboard and encountered an issue with Karabinar Elements mapping. In this post, I’ll explain how to resolve this problem by enabling the modify events feature in Karabinar Elements.

Solution

To make your Nuphy keyboard work with Karabinar Elements, follow these steps:

  1. Open Karabinar-Elements Settings.
  2. Navigate to the Devices tab.
  3. Enable the Modify events option for the Nuphy Keyboard.

Additional Information

If you’re interested in purchasing the Air75 keyboard, you can use my affiliate link to get a 10% discount: Nuphy Affiliate Link.

Here’s a top view of my Air75 keyboard:

mybatis - Cannot determine value type from string, Cannot convert string to Timestamp

Posted on 2023-10-17 | Edited on 2023-11-21 |

Problem

These error occurs when selecting columns from the database that don’t match the model, for example:

1
org.springframework.dao.DataIntegrityViolationException: Error attempting to get column 'name' from result set.  Cause: java.sql.SQLDataException: Cannot determine value type from string 'singming'
1
org.springframework.dao.DataIntegrityViolationException: Error attempting to get column 'type' from result set.  Cause: java.sql.SQLDataException: Cannot convert string 'MALE' to java.sql.Timestamp value

Solution

Ensure your SQL query only selects columns that exist in your Java model to avoid type mismatches and data integrity issues.

Explanation

The error looks ridiculous, it is because mybatis is trying to map results from sql to some fields in the java model. e.g. mapping a name VARCHAR(255) to Date createdTime even if you add the jdbcType mapping in the <resultMap>
MyBatis expects a direct mapping between columns in your SQL query and properties in your Java model. If your SQL query selects columns that don’t exist in your model, you can encounter type mismatches errors above.

To resolve this:

  1. Review your SQL queries: Ensure that you only select columns that have corresponding properties in your Java model.

  2. Use aliases: If your SQL query retrieves columns with different names but equivalent data, use aliases to match them to the model properties.

Example

1
2
3
4
5
6
7
8
9
10
11
12
<!-- MyBatis XML Mapper -->

<!-- Result Map -->
<resultMap id="userResultMap" type="User">
<result property="name" column="name" />
<result property="age" column="age" />
</resultMap>

<!-- SQL Query -->
<select id="selectUsers" resultMap="userResultMap">
SELECT name, age FROM users
</select>
1
2
3
4
5
// Java model
class User {
private String name;
private int age;
}

In this example, the SQL query and Java model are aligned, resulting in a smooth mapping without type mismatches.

Synchronizing Remote Terraform Backend States

Posted on 2023-09-05 | Edited on 2023-11-21 |

Introduction

When cloning a project from a remote source, it’s crucial to synchronize the Terraform states from the backend to maintain consistency and accuracy.

Syncing Remote Backend State: A Quick Guide

Follow these steps to efficiently sync your remote backend state:

1. Initialize and Install Dependencies

First, initiate the process and update dependencies using the following command:

1
2
3
4
terraform init

# If you need to upgrade dependencies
terraform init --upgrade

2. Streamline Variable Input

Instead of manually inputting or exporting variables each time, simplify the process by creating a terraform.tfvars file:

1
echo "foo=bar" > terraform.tfvars

3. Removing Items Not Present in Remote

In cases where items are removed from the remote but haven’t been reflected in the state, use the following command:

1
terraform state rm <resource>.<name>

4. Importing New Items to Sync State

If new items have been added but aren’t yet synced with the state, consult the provider’s documentation to confirm import support. Then use the provided syntax:

1
terraform import <resource>.<name> <id>

Conclusion

Properly synchronizing Terraform backend states ensures your project’s integrity and consistency, especially when working with remote repositories. Following these concise steps will streamline your workflow and enhance your overall development process.

Training a Model to Solve CAPTCHA Audio Challenges

Posted on 2023-07-02 | Edited on 2023-11-21 |

In this blog post, we will explore how to train a machine learning model to solve CAPTCHA audio challenges. CAPTCHA is a widely used security measure on the web to prevent automated bots from accessing websites. It presents users with various tests to determine if they are human, one of which is the audio challenge. By training a model to solve these audio challenges, we can automate the process and improve user experience. Let’s dive into the code and understand how it works.

The Setup

Before we get into the code, let’s understand the directory structure and dependencies required for this project. Here’s an overview:

  1. login-captcha-example: This directory contains the audio files used for training and testing the model.
  2. material: This directory contains the labeled training audio files. Each audio file represents a digit in the captcha.
  3. svm_model.pkl: This is the trained SVM model file that will be generated during training.
  4. temp: This directory will be used to store temporary audio files generated during the prediction process.

To get started, make sure you have the following dependencies installed:

  • os: A module for interacting with the operating system.
  • librosa: A library for audio and music signal analysis.
  • numpy: A library for mathematical operations on multi-dimensional arrays.
  • sklearn.svm: The SVM (Support Vector Machine) model implementation from scikit-learn.
  • sklearn.metrics: A module for evaluating the performance of machine learning models.
  • subprocess: A module for spawning new processes and executing system commands.
  • string.Template: A class for string templating.
  • joblib: A library for serializing Python objects to disk and loading them back into memory.

Code Walkthrough

Read more »

Remove duplicate documents from Mongodb by keys

Posted on 2022-11-17 | Edited on 2023-11-21 |

Background

To ensure there is no duplicate documents, we could create an unique index.
However, it will throw error when there are duplicates entries in the collection:

MongoDB cannot create a unique index on the specified index field(s) if the collection already contains data that would violate the unique constraint for the index.

Remove the duplicates by keys

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const collection = db.collection('MyCollection');
const operations: Promise<any>[] = [];
console.time('aggregation');

await collection
.aggregate([
{
$group: {
_id: {
key1: '$key1',
key2: '$key2',
},
dups: {
$push: '$_id',
},
count: {
$sum: 1,
},
},
},
{
$match: {
_id: {
$ne: null,
},
count: {
$gt: 1,
},
},
},
])
.forEach((doc) => {
console.log(doc);
doc.dups.slice(1).forEach((duplicateId: string) => {
operations.push(collection.deleteOne({ _id: duplicateId }));
});
});

console.timeEnd('aggregation');

console.time('remove duplicate');
await Promise.all(operations);
console.timeEnd('remove duplicate');

Create the unique index

1
await collection.createIndex({ key1: 1, key2: 1 }, { unique: true, name: 'unique_index' });

Mongodb connection test inside a container

Posted on 2022-07-28 | Edited on 2023-11-21 |

Background

To test the connection between a container and the mongoDB.

Steps

  1. Install mongo client

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # Install required package
    apt-get install gnupg

    # import the MongoDB public GPG Key
    wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | apt-key add -

    # Create a /etc/apt/sources.list.d/mongodb-org-6.0.list file for MongoDB
    echo "deb http://repo.mongodb.org/apt/debian buster/mongodb-org/6.0 main" | tee /etc/apt/sources.list.d/mongodb-org-6.0.list

    # Reload local package database.
    apt-get update

    # Install the MongoDB packages.
    apt-get install -y mongodb-org
  2. Test the connection

    1
    2
    3
    4
    5
    # Login to mongo (Use mongo or mongosh, depends on the mongodb package version)
    mongosh "mongodb://username:[email protected]:5/dbname?authSource=admin"

    # Check the DB name
    db

Reference

  • https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-debian/#install-mongodb-community-edition

Build Puppeteer Docker image to run inside K8s cluster

Posted on 2022-07-27 | Edited on 2023-11-21 |

Background

  • To run Puppeteer, you can’t just use an official Nodejs image and npm install puppeteer
  • Below is the Dockerfile that I have tried and proved as a working solution
  • More information see the Puppeteer official troubleshooting

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
FROM alpine

# Installs latest Chromium (100) package.
RUN apk add --no-cache \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont \
nodejs \
yarn

# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

COPY package.json yarn.lock ./
RUN yarn

# Puppeteer v13.5.0 works with Chromium 100.
# RUN yarn add [email protected]

# Add user so we don't need --no-sandbox.
RUN addgroup -S pptruser && adduser -S -G pptruser pptruser \
&& mkdir -p /home/pptruser/Downloads /app \
&& chown -R pptruser:pptruser /home/pptruser \
&& chown -R pptruser:pptruser /app

# Run everything after as non-privileged user.
USER pptruser

WORKDIR /app

COPY . .

EXPOSE 3003
CMD ["yarn", "start"]

Enlarge Click Area by CSS pseudo element

Posted on 2022-06-06 | Edited on 2023-11-21 |

An utility class .enlarge-click-area to enlarge the click area of any elements.

Plain CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.enlarge-click-area {
position: relative;
}
.enlarge-click-area::after {
content: "";
display: block;
position: absolute;
left: 0;
top: 0;
width: 160%;
height: 160%;
/* How to calculate 18.75%:
Extra width for left-hand side: (160% - 100%) / 2 = 30%
Transform percentage will relative to the width: 30% / 160% = 18.75%
*/
transform: translate(-18.75%, -18.75%);
}

SCSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=enlargeClickArea($ratio: 2)
position: relative

&:after
$offset: percentage((1 - $ratio) / 2 / $ratio)

content: ''
display: block
position: absolute
left: 0
top: 0
width: percentage($ratio)
height: percentage($ratio)
transform: translate($offset, $offset)
12…9
Sing Ming Chen

Sing Ming Chen

Sing's log, a developer's blog

83 posts
232 tags
GitHub E-Mail Linkedin Facebook StackOverflow
© 2023 Sing Ming Chen
Powered by Hexo v3.9.0
|
Theme — NexT.Gemini v6.3.0