Migrated your moments from HappyFeed to HappyNotes

First, get your moments data

(function () {
  const MAX_WAIT_TIME = 50000; // If the button is always not there, we wait at most 50 seconds.
  const CHECK_INTERVAL = 1000; // If the button is not there, we do the same check after 1 scond
  const buttonSelector = '#__next > main > div > div.MomentHistory_moment-history__inner__a9etG > button';

  // Initially finding the button
  const button = document.querySelector(buttonSelector);
  if (button) {
    button.click();
    console.log('Found the button and clicked');
  } else {
    console.log('No Load button at all');
    return
  }

  // 当前已等待的时间
  let elapsedTime = 0;

  // 定时器开始循环查找,立即获得 intervalId
  const intervalId = setInterval(() => {
    elapsedTime += CHECK_INTERVAL;

    // Searching the Load button
    const retryButton = document.querySelector(buttonSelector);
    if (retryButton) {
      retryButton.click();
      elapsedTime = 0
      console.log('Found the button and clicked');
    } else {
      console.log('Could not find the button, continue searching...');
    }

    // If we could no longer find the button in MAX_WAIT_TIME, we know the work has been done.
    if (elapsedTime >= MAX_WAIT_TIME) {
      console.error('Could not find the button any more, stopping....');
      clearInterval(intervalId); // Stop the interval
    }
  }, CHECK_INTERVAL); // check if we can find the button every second
})()

Second, extract the data into a JSON object from the long page we get by clicking the Load button again and again by the script above

const diaryEntries = [];
const listItems = document.querySelectorAll('.MomentListItem_moment-list__item__d5UJL');

// Helper function:Convert date into yyyy-MM-dd format
function formatDate(dateString) {
    const months = {
        January: "01", February: "02", March: "03", April: "04", May: "05", June: "06",
        July: "07", August: "08", September: "09", October: "10", November: "11", December: "12"
    };
    const [month, day, year] = dateString.split(" ");
    return `${year}-${months[month]}-${day.padStart(2, '0')}`;
}

listItems.forEach(item => {
    const dateElement = item.querySelector('.MomentListItem_moment-item__date__XfMA8');
    const contentElement = item.querySelector('.MomentListItem_moment-item__text__F5V2C > p');
    const photoElement = item.querySelector('.MomentImage_root__uLWy7 img');
    const videoElement = item.querySelector('.MomentListItem_moment-item__video__16FUa video');

    const entry = {
        pdate: dateElement ? formatDate(dateElement.textContent.replace('Posted ', '').trim()) : null,
        content: contentElement ? contentElement.textContent.trim() : '',
        photo: ''
    };

    if (photoElement) {
        entry.photo = photoElement.getAttribute('src');
    } else if (videoElement) {
        const thumbnail = videoElement.getAttribute('poster');
        entry.photo = thumbnail ? thumbnail.trim() : '';
    }

    diaryEntries.push(entry);
    diaryEntries.reverse(); // make sure the old data is at the beginning
});

Third, send the moments data to HappyNotes by the following script

// Your personal token
const token = "eyJh...";

// API endpoint
const apiUrl = "https://happynotes-api.shukebeta.com/note/post";

// Function to send a note
async function sendNote(note) {
    const contentWithPhoto = note.photo 
        ? `${note.content}\n\n ![image](${note.photo})`
        : note.content;

    const payload = {
        isprivate: true,
        isMarkdown: true,
        publishDateTime: note.pdate,
        timezoneId: "Pacific/Auckland",
        content: contentWithPhoto
    };

    try {
        const response = await fetch(apiUrl, {
            method: "POST",
            headers: {
                Authorization: `Bearer ${token}`,
                "Content-Type": "application/json"
            },
            body: JSON.stringify(payload)
        });

        if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
        }

        console.log(`Note sent successfully: ${await response.text()}`);
    } catch (error) {
        console.error(`Error sending note: ${error.message}`);
    }
}

// Send notes one by one
(async () => {
    var date = ''
    var minutes = '00'
    for (const note of diaryEntries) {
        if (note.pdate != date) {
           date = note.pdate
           minutes = '00'
        } else {
           minutes = (+minutes + 10) + ''
        }
        note.pdate += ` 20:${minutes}:00`
        await sendNote(note);
    }
})();

You need to get the token by a manually login. Don't share the token to anyone else! Anyone who gets the token can access your account and data. Follow the following steps to get your personal token.

Open the DevTools on Chrome or Firefox, then do a fresh login.

image

Solving AutoMapper Errors When Mapping Fields That Don't Exist in the Target Class

When working with AutoMapper, you may encounter errors when trying to map properties from a source object to a target object, especially if the source contains properties that don't exist in the target. This is a common pitfall, but it’s easy to resolve once you understand the root cause.

The Issue:

Consider the scenario where we have a PostNoteRequest class containing two new fields: PublishDateTime and TimezoneId. These fields are necessary for calculating the CreatedAt property of the target Note class, but the Note class doesn’t have PublishDateTime or TimezoneId at all.

Here’s a typical AutoMapper mapping that results in an error:

CreateMap<PostNoteRequest, Note>()
    .ForMember(m => m.CreatedAt, _ => _.MapFrom((src,dst) =>
    {
        // Logic to calculate CreatedAt based on PublishDateTime and TimezoneId
    }));

Even though we're calculating CreatedAt based on the source properties, AutoMapper will try to map PublishDateTime and TimezoneId directly from the source to the target, resulting in an error like this:

Error mapping types.
Mapping types:
PostNoteRequest -> Note
HappyNotes.Models.PostNoteRequest -> HappyNotes.Entities.Note

The issue arises because AutoMapper expects all properties mentioned in the MapFrom expression to exist in both the source and target objects.

The Cause:

AutoMapper doesn't know how to handle fields (PublishDateTime and TimezoneId) that don’t exist in the target class (Note). The moment we reference these fields directly in the delegate passed to MapFrom, AutoMapper assumes they need to be mapped.

The Solution:

To solve this, we have two main approaches:

1. Use a Custom Value Resolver

A custom value resolver allows us to decouple the logic of calculating CreatedAt from the mapping process. It ensures that we don't reference non-existent fields in the target object.

public class CreatedAtResolver : IValueResolver<PostNoteRequest, Note, long>
{
    public long Resolve(PostNoteRequest source, Note destination, long member, ResolutionContext context)
    {
        if (!string.IsNullOrWhiteSpace(source.PublishDateTime) && !string.IsNullOrWhiteSpace(source.TimezoneId))
        {
            return DateTime.UtcNow.ToUnixTimeSeconds();
        }
        
        var dateStr = source.PublishDateTime;
        if (dateStr.Length == 10) dateStr += " 20:00:00";

        DateTime date = DateTime.ParseExact(dateStr, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
        TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(source.TimezoneId);

        return TimeZoneInfo.ConvertTime(new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, DateTimeKind.Unspecified), timeZone).ToUnixTimeSeconds();
    }
}

// In your mapping configuration
CreateMap<PostNoteRequest, Note>()
    .ForMember(m => m.CreatedAt, opt => opt.MapFrom<CreatedAtResolver>());

This approach encapsulates the logic for CreatedAt calculation in a resolver, which avoids directly referencing PublishDateTime or TimezoneId in the target object.

2. Use ConstructUsing for Manual Construction

Another way to handle this is by manually constructing the target object in the ConstructUsing method, ensuring CreatedAt is set correctly without relying on AutoMapper’s default property mapping behavior.

CreateMap<PostNoteRequest, Note>()
    .ForMember(m => m.CreatedAt, opt => opt.MapFrom((src, dst) =>
    {
        var dateStr = src.PublishDateTime;
        if (dateStr.Length == 10) dateStr += " 20:00:00";
        
        DateTime date = DateTime.ParseExact(dateStr, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
        TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(src.TimezoneId);
        
        return TimeZoneInfo.ConvertTime(new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, DateTimeKind.Unspecified), timeZone).ToUnixTimeSeconds();
    }))
    .AfterMap((src, dst) =>
    {
        dst.Tags = string.Join(" ", dst.TagList);
        dst.Content = dst.IsLong ? src.Content.GetShort() : src.Content;
    });

This method allows you to explicitly handle the CreatedAt logic while avoiding AutoMapper’s attempt to map non-existent properties from the source object.

Conclusion:

AutoMapper is a powerful tool, but it expects the properties referenced in its mapping expressions to exist in both the source and target objects. When you need to perform custom logic (like calculating CreatedAt based on other properties), using custom value resolvers or manual construction can help you avoid errors and maintain clear and maintainable code.

By understanding how AutoMapper expects the mapping to work, you can prevent issues related to missing properties and ensure your mappings are both efficient and correct.

书摘: 人间相亲故事

2024-06-14 18:05:45 毕业后的恋爱里,女友有次幽怨地说,她期待的是回家时有一盏为她亮着的灯。于是,他毅然放弃了深圳的稳定工作来到广州,从零开始打拼。但爱情的甜蜜期很快过去,女友开始天天计算着工资和房价,

2024-06-13 15:37:39 他曾经奋不顾身的浪漫变成了鲁莽、冲动和毫无计划…… 注: 怎么说呢,这话又对又不对。这话有理,但谁说都不违和,可从那个人嘴里说出来就好没理。

2024-06-13 15:39:37 他原本“希望与对方有话题可以聊,价值观至少要匹配”,但一年下来却发现,“很难找到能好好说话的人”。

2024-06-13 20:09:38 小林是他的初恋女友,也是他的情感启蒙老师。29岁的陆昱辰,像新生儿颤巍巍地迈出了第一步——跨进了一个没有爸妈、只有他和小林的平行空间。他们一起度过了将近一年的时光,逛遍了彼此都很熟悉的北京、看遍了新上线的电影,还去丽江等地旅行。他们也吵架,又和好。他们谈论朋友,讨论工作,唯独不谈论未来。

2024-06-14 08:35:31 我远远地看着,看东子和一个刚刚认识两天的姑娘结婚。我远远地看着,看东子和一个刚刚认识两天的姑娘结婚。

2024-06-14 08:43:00 我远远地看着。看东子和一个刚刚认识两天的姑娘,并排站在装饰得极浪漫温馨的礼台中央,表白、宣誓、拥抱、互换婚戒,相视而立,亲友哄笑着把两个人的脸颊贴在一起,他们也各自捆绑住了彼此始料未及的今生。

书摘:毛澤東私人醫生回憶錄

2024-01-14 17:00:37
帝王權勢讓帝王享有最大的奢侈──生活簡單。毛大部分的時間要不在床上,要不在私人游泳池旁休憩。

2024-01-14 17:05:31
毛告訴李醫生,美國對中國的企圖一向具有正面意義。

2024-01-14 17:11:36
毛發展的理想失敗了。但在這個他所毀滅的國家中,他握有絕對權力。

2024-01-14 17:17:13
此書給我們另一種教訓。它描述過度膨脹的權力,如何驅策其擁有者進入一種黑暗的深淵:在深淵中,偉大的夢想只能導致滔天罪行。

2024-01-14 18:20:00
雪泥鴻爪

2024-01-14 18:21:33
一九八九年三月,我點檢行篋,取出了舊記和帶來的全部資料,開始了寫作生活。這一方面是對嫻的永久的紀念。另一方面,身在美國,就可以將這些年的所見所聞,秉筆直書,無需避諱,加以發表。如果讀過這本書以後,讀者能夠更加珍惜自己的理想和所嚮往的幸福的生活,那將是我和嫻多年來的最大願望。

2024-01-14 18:25:33
對蔣介石,雖然終生為敵,但並不持完全否定的態度。他認為蔣有強烈的民族自尊心,不俯首貼耳聽命於美國。

2024-01-14 18:29:26
寮國領袖凱山的合照

…more

书摘:抱歉,我动了你的脑子:一位神经外科医生的悲喜

2024-08-01 07:28:37
神经外科的手术就像是一次独自的徒手攀岩,这是一个人的旅程,当你作为主刀医生开始手术的那一刻起,就注定要独自完成,因为没有任何的退路。手术是这样,人生又何尝不是如此呢?

2024-08-01 07:40:31
我想不起在医学院读过的关于“类风湿性关节炎”的文章,但我记得心神烦忧、手指扭曲、再也无法捏面团做饼干给孙子吃的老祖母

2024-08-01 07:41:09
失败,往往比成功更具有教育意义

2024-08-01 07:41:36
一位病人的死亡比五十次的救援成功更能塑造医生的心理,促进他成长。

2024-08-01 08:30:58
第一条规则:当你的脑壳被打开,跟空气接触过之后,你就再也不是从前的你了。

2024-08-01 08:18:34
设计师只管机器的表现,从没考虑过维修容不容易、方不方便。”

2024-08-01 08:30:41
第二条规则:只有别人操刀的手术,才叫小手术。如果负责动手术的人是你,这就是大手术。永远不要忘记这一点。”

2024-08-01 08:31:19
第三条规则同样适用于要动脑部或椎间盘手术的病人:只要病人还活着,你永远有办法把他弄得更惨。

…more