Posts in category “Programming”

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.

How to Skip a Collection Run in Postman Based on Response Conditions

When working with Postman to automate API testing, you might encounter scenarios where you want to conditionally skip the execution of subsequent requests in a collection. This can be useful if a certain condition isn't met in your API responses. Here's a guide on how to achieve this using Postman's scripting capabilities.

Scenario

Imagine you have a collection where the first request returns a response like this:

{
    "content": "True",
    "statusCode": 200,
    "message": "Global value got from DbConfig!"
}

You want to skip all subsequent requests if the content field is "False".

Step-by-Step Guide

  1. Set Up Your Collection: Ensure your collection is organized with the request you want to evaluate placed at the beginning.

  2. Write a Post-Request Script: In the test script section of the first request, you'll write a script to check the content field and decide whether to continue the collection run.

    // Parse the response
    let response = pm.response.json();
    
    // Check the content value
    if (response.content === "False") {
        pm.execution.setNextRequest(null); // Skip the remaining requests
    }
    

    This script checks if the content is "False". If it is, pm.setNextRequest(null) stops all subsequent requests from running.

  3. Test the Flow: Run your collection to see the logic in action. If the condition is met (i.e., content is "False"), the collection run will halt after the first request.

Explanation

  • pm.response.json(): This method parses the JSON response from your request.
  • pm.setNextRequest(null): This function is used to stop further requests in the collection run. If the condition isn't met, the collection continues with all remaining requests in their original order.

Benefits

  • Efficiency: Avoid unnecessary API calls when certain conditions aren’t met, saving time and resources.
  • Control: Gain greater control over your testing workflows by dynamically determining execution paths.

Conclusion

Using pm.execution.setNextRequest(null) in a test script provides a straightforward way to control the flow of your Postman collection runs based on specific conditions in your API responses. This technique can be a powerful tool in optimizing your automated testing processes.

Feel free to customize the logic to fit your specific needs!

详解:import { ref, onMounted, watch } from 'vue'

一个朋友说 Gemini最近的进步已经挺大了,于是我就试验了一下。 我问Gemini:你能否用简洁的汉语详细解释一下import { ref, onMounted, watch } from 'vue'这一语句import的这三个对象?它们是什么,做什么用,好的实践是什么?

Gemini的回答令我非常满意,因此我把它发表在这里。它的回答是:

好的,我来详细解释一下这段代码中导入的三个对象:

import { ref, onMounted, watch } from 'vue'

解释:

  • import:

    • 这是 JavaScript 中的导入语句,用于从其他模块(这里就是 Vue.js)中引入需要的功能。
    • 想象一下,你在搭建积木,import 就相当于你从积木盒里拿出特定的积木块。
  • ref, onMounted, watch:

    • 这些都是 Vue.js 提供的三个核心功能:

      • ref:

        • 作用: 创建一个响应式的引用。
        • 比喻: 想象一个盒子,你把一个数据放进去,这个盒子就变成了一个 ref。当盒子里面的数据发生变化时,Vue 会自动追踪到,并触发相应的更新。
        • 使用场景: 用于创建响应式的基本数据,比如一个计数器、一个输入框的值等。
      • onMounted:

        • 作用: 在组件挂载到 DOM 后执行一次回调函数。
        • 比喻: 就像一个新房子建好了,你就可以搬进去开始装修了。onMounted 就是在你搬进去之后执行的第一个动作,比如打开窗户通风。
        • 使用场景: 用于执行一些初始化操作,比如获取数据、设置定时器等。
      • watch:

        • 作用: 监听一个值的改变,并在值改变时执行回调函数。
        • 比喻: 就像你设置了一个监控摄像头,一旦监控到有物体移动,就会触发报警。watch 就是这个监控摄像头,它会持续监控一个值,一旦值发生变化,就会执行你设定的操作。
        • 使用场景: 用于实现一些复杂的逻辑,比如根据数据的变化动态更新视图、进行表单验证等。

总结:

  • ref 用于创建响应式数据。
  • onMounted 用于在组件挂载后执行初始化操作。
  • watch 用于监听数据的变化并执行相应的操作。

好的实践:

  • 合理使用 ref: 不要过度使用 ref,只对需要响应式的数据进行包裹。
  • 在 onMounted 中进行异步操作: 将获取数据、设置定时器等异步操作放在 onMounted 中,确保组件已经挂载到 DOM 中。
  • watch 的使用场景: watch 更适合监听复杂逻辑或需要深度比较的情况。对于简单的计算属性,可以使用 computed

示例:

import { ref, onMounted, watch } from 'vue'

const count = ref(0)

onMounted(() => {
  // 在组件挂载后获取数据
  fetch('/api/data')
    .then(response => response.json())
    .then(data => {
      // 更新数据
      count.value = data.count
    })
})

watch(count, (newValue, oldValue) => {
  console.log('count has changed:', newValue, oldValue)
})

更深入的学习:

如果你想更深入地了解这些概念,可以参考 Vue.js 的官方文档。

希望这个解释能帮助你更好地理解这三个对象的作用和用法。如果你还有其他问题,欢迎随时提出。

我觉得AI非常善于把概念性的事情用简洁准确的方式说清楚,在这一点上,真可以说它的智慧出于人类而胜于人类了。

[Solution] The argument type 'Consumer' can't be assigned to the parameter type 'PreferredSizeWidget?'.

The error occurs because AppBar expects a widget that implements the PreferredSizeWidget interface, but Consumer<NoteModel> does not directly implement this interface. To solve this, you need to return an AppBar from within the Consumer builder method.

Here’s how you can do it:

Scaffold(
  appBar: PreferredSize(
    preferredSize: Size.fromHeight(kToolbarHeight),
    child: Consumer<NoteModel>(
      builder: (context, noteModel, child) {
        return AppBar(
          title: Text(
            'Your Title',
            style: TextStyle(
              color: noteModel.isPrivate ? Colors.red : Colors.green,
            ),
          ),
        );
      },
    ),
  ),
  body: // Your other widgets,
);

In this approach, I wrapped the Consumer<NoteModel> inside a PreferredSize widget to ensure it adheres to the PreferredSizeWidget interface required by appBar.

This should resolve the error while allowing you to update only the AppBar based on changes in your NoteModel.

Glory to ChatGPT!