From 01363ba35a5a07431f21b26199fcab42710e09c7 Mon Sep 17 00:00:00 2001 From: DM Date: Tue, 31 Oct 2023 18:19:46 +0800 Subject: [PATCH] feat: support watched profiles following list has new activity can be call snap's notify --- packages/snap/snap.manifest.json | 2 +- packages/snap/src/crossbell.ts | 24 +++++-- packages/snap/src/farcaster.ts | 28 ++++++-- packages/snap/src/fetch.ts | 89 +++++++++++++++++++++-- packages/snap/src/index.ts | 32 ++++++++- packages/snap/src/lens.ts | 15 +++- packages/snap/src/notify.test.ts | 120 +++++++++++++++++++++++++++++++ 7 files changed, 291 insertions(+), 19 deletions(-) create mode 100644 packages/snap/src/notify.test.ts diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 535261a..8c10d26 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/NaturalSelectionLabs/RSS3-MetaMask-Snap.git" }, "source": { - "shasum": "l+dcwmstbQR/2SVBE97+stCQ/PxlzKW1aR81AQmQtVY=", + "shasum": "LzxqoM9rAkicT97UYNE2inSY4KoE/532tFOShDZgR5U=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snap/src/crossbell.ts b/packages/snap/src/crossbell.ts index d39354d..db72df7 100644 --- a/packages/snap/src/crossbell.ts +++ b/packages/snap/src/crossbell.ts @@ -1,5 +1,6 @@ import { isValidWalletAddress } from './utils'; -import { getMultiple } from './fetch'; +import { diffMonitor, getMultiple } from './fetch'; +import { SocialMonitor } from './state'; import { Platform, TProfile, TRelationChainResult } from '.'; const API = `https://indexer.crossbell.io/v1`; @@ -136,8 +137,14 @@ async function getCharacterId(handle: string) { * Retrieves the followers for the given character ID from the Crossbell API. * * @param id - The character ID to retrieve the followers for. + * @param olderMonitor - The older monitor. + * @param handle - The handle. */ -export async function getFollowingByCharacterId(id: string) { +export async function getFollowingByCharacterId( + id: string, + olderMonitor: SocialMonitor, + handle: string, +) { const following: TProfile[] = []; let hasNextPage = true; let cursor: string | undefined; @@ -184,10 +191,17 @@ export async function getFollowingByCharacterId(id: string) { }); if (findOut) { + const lastActivities = diffMonitor( + olderMonitor, + findOut.activities, + handle, + item.handle, + ); + return { ...item, activities: findOut.activities, - lastActivities: findOut.oldActivities, + lastActivities, }; } } @@ -217,11 +231,13 @@ export function format(profiles: TCSBProfile[]): TProfile[] { * Retrieves the relation chain for the given handle from the Crossbell API. * * @param handle - The handle to retrieve the relation chain for. + * @param oldMonitor - The old monitor. * @param fetchMethod - The method to use to fetch the following. * @returns The relation chain for the given handle. */ export async function handler( handle: string, + oldMonitor: SocialMonitor, fetchMethod: typeof getFollowingByCharacterId = getFollowingByCharacterId, ): Promise { // 1. Get owner profile @@ -265,7 +281,7 @@ export async function handler( } // 2. Get following - const following = await fetchMethod(data.characterId); + const following = await fetchMethod(data.characterId, oldMonitor, handle); // 3. Return result return { diff --git a/packages/snap/src/farcaster.ts b/packages/snap/src/farcaster.ts index ef441f3..41ef4e0 100644 --- a/packages/snap/src/farcaster.ts +++ b/packages/snap/src/farcaster.ts @@ -1,4 +1,5 @@ -import { getMultiple } from './fetch'; +import { diffMonitor, getMultiple } from './fetch'; +import { SocialMonitor } from './state'; import { Platform, TProfile, TRelationChainResult } from '.'; const API = 'https://api.warpcast.com/v2'; @@ -142,9 +143,15 @@ export async function format(data: TFarcasterUser[]): Promise { * Returns the following profiles for a given Farcaster ID. * * @param fid - The Farcaster ID to get the following profiles for. + * @param olderMonitor - The older monitor. + * @param handle - The handle. * @returns An array of TProfile objects representing the following profiles. */ -export async function getFollowingByFid(fid: number) { +export async function getFollowingByFid( + fid: number, + olderMonitor: SocialMonitor, + handle: string, +) { let cursor: string | undefined; let hasNextPage = true; const following: TProfile[] = []; @@ -179,10 +186,17 @@ export async function getFollowingByFid(fid: number) { }); if (findOut) { + const lastActivities = diffMonitor( + olderMonitor, + findOut.activities, + handle, + item.handle, + ); + return { ...item, activities: findOut.activities, - lastActivities: findOut.oldActivities, + lastActivities, }; } } @@ -195,9 +209,13 @@ export async function getFollowingByFid(fid: number) { * Returns the relation chain for a given Farcaster handle. * * @param handle - The Farcaster handle to get the relation chain for. + * @param olderMonitor - The older monitor. * @returns The relation chain for the given Farcaster handle. */ -export async function handler(handle: string): Promise { +export async function handler( + handle: string, + olderMonitor: SocialMonitor, +): Promise { const owner = await getOwnerProfileByUsername(handle); if (!owner.fid) { return { @@ -211,7 +229,7 @@ export async function handler(handle: string): Promise { }; } - const following = await getFollowingByFid(owner.fid); + const following = await getFollowingByFid(owner.fid, olderMonitor, handle); return { owner: { handle: owner.handle, diff --git a/packages/snap/src/fetch.ts b/packages/snap/src/fetch.ts index 52fa21b..a73d248 100644 --- a/packages/snap/src/fetch.ts +++ b/packages/snap/src/fetch.ts @@ -7,7 +7,7 @@ import { } from '@rss3/js-sdk'; import { themePlain } from '@rss3/js-sdk/lib/readable/activity/theme'; -import { CronActivity } from './state'; +import { CronActivity, SocialMonitor } from './state'; export const getSocialActivitiesUrl = (address: string) => `https://testnet.rss3.io/data/accounts/${address}/activities?tag=social&direction=out`; @@ -98,8 +98,8 @@ export async function getMultiple(addresses: string[]) { const executeAddresses = filtedAddresses; - // 1 day ago - const timestamp = moment().subtract(1, 'day').unix(); + // 1 hour ago + const timestamp = moment().subtract(1, 'hour').unix(); while (hasNextPage) { const params = { action_limit: 10, @@ -151,13 +151,92 @@ export async function getMultiple(addresses: string[]) { return { owner: addr, activities: groupBy, - oldActivities: [], }; } return { owner: addr, activities: [], - oldActivities: [], }; }); } + +/** + * Social monitor. + * + * @param olderMonitor - The old monitor. + * @param newer - The newer cron activities. + * @param ownerHandle - The owner handle. + * @param handle - The handle. + * @returns The difference between the cached social activities and the fetched social activities. + */ +export function diffMonitor( + olderMonitor: SocialMonitor, + newer: CronActivity[], + ownerHandle: string, + handle: string, +) { + let lastActivities: CronActivity[] = newer; + + const cachedProfiles = olderMonitor.watchedProfiles?.find( + (wProfile) => + wProfile.owner.handle.toLocaleLowerCase() === + ownerHandle.toLocaleLowerCase(), + ); + if (cachedProfiles) { + const cachedProfilesFollowingProfiles = cachedProfiles.following; + if (cachedProfilesFollowingProfiles) { + const cachedProfilesFollowingProfile = + cachedProfilesFollowingProfiles.find( + (cProfile) => + cProfile.handle.toLocaleLowerCase() === handle.toLocaleLowerCase(), + ); + if (cachedProfilesFollowingProfile) { + const cachedProfilesFollowingProfilesActivities = + cachedProfilesFollowingProfile.activities; + + if (cachedProfilesFollowingProfilesActivities !== undefined) { + lastActivities = newer + .map((activity) => { + if ( + !cachedProfilesFollowingProfilesActivities.some( + (cacheActivity) => cacheActivity.id === activity.id, + ) + ) { + return activity; + } + return null; + }) + .filter((item) => item !== null) as CronActivity[]; + } + } + } + } + return lastActivities; +} + +// /** +// * Get the last updated following social activities. +// * +// * @param monitors - The social monitors. +// */ +// export function getLastUpdatedFollowingSocialActivities( +// monitors: SocialMonitor[], +// ) { +// const followingSocialActivities: CronActivity[] = []; +// monitors.forEach((monitor) => { +// if (monitor.watchedProfiles) { +// monitor.watchedProfiles.forEach((wProfile) => { +// if (wProfile.following) { +// wProfile.following.forEach((followingProfile) => { +// if (followingProfile.lastActivities) { +// followingSocialActivities.push( +// ...followingProfile.lastActivities, +// ); +// } +// }); +// } +// }); +// } +// }); +// return followingSocialActivities; +// } diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index 07d9ef5..7ed7a4f 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -458,7 +458,7 @@ export const onCronjob: OnCronjobHandler = async ({ request }) => { const watchedProfiles: TRelationChainResult[] = []; const promises = handles.map(async (exec) => { if (exec?.handle) { - const fol = await exec.execute(exec.handle); + const fol = await exec.execute(exec.handle, item); if (fol) { watchedProfiles.push(fol); } @@ -470,12 +470,40 @@ export const onCronjob: OnCronjobHandler = async ({ request }) => { watchedProfiles, }; }); - const monitor = await Promise.all(monitorPromises); await setState({ ...state, monitor, }); + + // notify the latest social activities by the monitor + const content: any[] = []; + monitor.forEach((item) => { + item.watchedProfiles?.forEach((profile) => { + const lastActivities = profile.following?.flatMap( + (follower) => + follower.lastActivities?.map((activity) => activity.text) ?? [], + ); + lastActivities?.length && + content.push( + heading( + `${profile.owner.handle}'s following has new activities.`, + ), + text(lastActivities.join('\n')), + divider(), + ); + }); + }); + + if (content.length > 0) { + await snap.request({ + method: 'snap_dialog', + params: { + type: DialogType.Alert, + content: panel(content), + }, + }); + } return true; } diff --git a/packages/snap/src/lens.ts b/packages/snap/src/lens.ts index 441e7c0..58a646a 100644 --- a/packages/snap/src/lens.ts +++ b/packages/snap/src/lens.ts @@ -1,6 +1,7 @@ import { Client, cacheExchange, fetchExchange, gql } from '@urql/core'; import { isValidWalletAddress } from './utils'; -import { getMultiple } from './fetch'; +import { diffMonitor, getMultiple } from './fetch'; +import { SocialMonitor } from './state'; import { TRelationChainResult, type TProfile, Platform } from '.'; // only need handle, ownedBy and picture. @@ -180,12 +181,14 @@ export async function getAddressByHandle(handle: string) { * Returns an object containing information about a user's relation chain. * * @param handle - The user's handle. + * @param olderMonitor - The older monitor. * @param limit - The pagination limit. * @param queryMethod - The query method. * @returns An object containing the user's relation chain information. */ export async function handler( handle: string, + olderMonitor: SocialMonitor, limit = 20, queryMethod: typeof query = query, ): Promise { @@ -231,6 +234,7 @@ export async function handler( .map((item) => item.address) .filter((addr) => addr !== undefined) as string[]; const fetchedAddresses = await getMultiple(addresses); + const fetchedFollowing = following.map((item) => { if (item.address !== undefined) { const findOut = fetchedAddresses.find((addr) => { @@ -241,10 +245,17 @@ export async function handler( }); if (findOut) { + const lastActivities = diffMonitor( + olderMonitor, + findOut.activities, + handle, + item.handle, + ); + return { ...item, activities: findOut.activities, - lastActivities: findOut.oldActivities, + lastActivities, }; } } diff --git a/packages/snap/src/notify.test.ts b/packages/snap/src/notify.test.ts new file mode 100644 index 0000000..3db8a18 --- /dev/null +++ b/packages/snap/src/notify.test.ts @@ -0,0 +1,120 @@ +import { divider, heading, panel, text } from '@metamask/snaps-ui'; +import { SocialMonitor } from './state'; +import { Platform } from '.'; + +describe('check query profile', () => { + it('should return the profiles', async () => { + const monitors: SocialMonitor[] = [ + { + search: 'henryqw.eth', + profiles: [ + { + address: '0x827431510a5d249ce4fdb7f00c83a3353f471848', + handle: 'henryqw.lens', + name: 'henryqw.lens', + platform: 'Lens', + network: 'polygon', + url: 'https://lenster.xyz/u/henryqw.lens', + profileURI: [ + 'https://ipfs.io/ipfs/QmW8p2NuAEbuSgzGrR5zYXezQpyoMHHY3RwHTaCDLSUj5c', + ], + }, + ], + latestUpdateTime: '2023/10/31 06:00:00', + watchedProfiles: [ + { + owner: { + handle: 'henryqw.lens', + address: '0x827431510a5D249cE4fdB7F00C83a3353F471848', + avatar: + 'https://ik.imagekit.io/lens/media-snapshot/2b02c7846b00ac1923659e2a559ec597c83da5a82f232368047647d1ec8b3835.png', + }, + platform: Platform.Lens, + status: true, + message: 'success', + following: [ + { + handle: 'vitalik.lens', + address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + avatar: + 'https://ik.imagekit.io/lens/media-snapshot/d2762e3b5f2532c648feec96bf590923ea6c3783fee428cbb694936ce62962e0.jpg', + activities: [ + { + id: '0x000000000000000000000000adddbb314369cba162d10a997c2487d3583124c9', + text: 'vitalik.eth published a post "Different types of layer 2s" on Farcaster Oct 31, 2023', + owner: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + }, + ], + lastActivities: [ + { + id: '0x000000000000000000000000adddbb314369cba162d10a997c2487d3583124c9', + text: 'vitalik.eth published a post "Different types of layer 2s" on Farcaster Oct 31, 2023', + owner: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + }, + ], + }, + { + handle: 'dmoosocool.lens', + address: '0xE584Ca8F30b93b3Ed47270297a3E920e2D6D25f0', + avatar: + 'https://ik.imagekit.io/lens/media-snapshot/f947319b5f4c063e88cda751fde67a259d96ecca1558db949696374f583fa3f2.png', + activities: [], + lastActivities: [], + }, + { + handle: 'brucexc.lens', + address: '0x23c46e912b34C09c4bCC97F4eD7cDd762cee408A', + avatar: + 'https://ik.imagekit.io/lens/media-snapshot/76c6afcf170723465c2cb3b1494367b8d7a683fbfbb04f2210524bef2d4fca48.png', + activities: [], + lastActivities: [], + }, + { + handle: 'lensprotocol', + address: '0x05092cF69BDD435f7Ba4B8eF97c9CAecF2BA69AD', + avatar: + 'https://ik.imagekit.io/lens/media-snapshot/6d21d1544a4c303a3a407b9756071386955b76a3b091fded5731ca049604994a.png', + activities: [], + lastActivities: [], + }, + { + handle: 'stani.lens', + address: '0x7241DDDec3A6aF367882eAF9651b87E1C7549Dff', + avatar: + 'https://ik.imagekit.io/lens/media-snapshot/e3adfb7046a549480a92c63de2d431f1ced8e516ea285970267c4dc24f941856.png', + activities: [], + lastActivities: [], + }, + { + handle: 'usagi.lens', + address: '0xDA048BED40d40B1EBd9239Cdf56ca0c2F018ae65', + avatar: + 'https://ik.imagekit.io/lens/media-snapshot/bfa48a4324e29d62d46a27cbcfd506f9592021eaf063dbdfbaba90cbe9444c71.jpg', + activities: [], + lastActivities: [], + }, + ], + }, + ], + }, + ]; + + const content: any[] = []; + monitors.forEach((item) => { + item.watchedProfiles?.forEach((profile) => { + const lastActivities = profile.following?.flatMap( + (follower) => + follower.lastActivities?.map((activity) => activity.text) ?? [], + ); + lastActivities?.length && + content.push( + heading(`${profile.owner.handle}'s following has new activities.`), + text(lastActivities.join('\n')), + divider(), + ); + }); + }); + + expect(panel(content)).toStrictEqual([]); + }); +});