|
| 1 | +import { logger } from '@poppinss/cliui' |
| 2 | +import { createCommand } from 'commander' |
| 3 | +import { JikeClient, limit } from 'jike-sdk/node' |
| 4 | +import { format } from 'date-fns' |
| 5 | +import { displayImage } from '../utils/terminal' |
| 6 | +import { displayUser, filterUsers } from '../utils/user' |
| 7 | +import type { Spinner } from '@poppinss/cliui/build/src/Logger/Spinner' |
| 8 | +import type { Entity } from 'jike-sdk/node' |
| 9 | + |
| 10 | +interface NotificationOptions { |
| 11 | + avatar?: boolean |
| 12 | + image?: boolean |
| 13 | + count?: number |
| 14 | +} |
| 15 | + |
| 16 | +export const msg = createCommand('msg') |
| 17 | + .description('display notifications') |
| 18 | + .aliases(['message', 'notification']) |
| 19 | + .option('--no-avatar', 'do not show avatar') |
| 20 | + .option('--no-image', 'do not show image') |
| 21 | + .option('-c, --count <count>', 'notification max count', '30') |
| 22 | + .action(() => { |
| 23 | + const opts = msg.opts<NotificationOptions>() |
| 24 | + showNotifications(opts) |
| 25 | + }) |
| 26 | + |
| 27 | +const showNotifications = async (opts: NotificationOptions) => { |
| 28 | + const [user] = filterUsers() |
| 29 | + const client = JikeClient.fromJSON(user) |
| 30 | + |
| 31 | + const count = +(opts.count ?? 30) |
| 32 | + const spinner = logger.await('Loading notifications...') |
| 33 | + const notifications = await client |
| 34 | + .queryNotifications({ |
| 35 | + limit: limit.limitMaxCount(count), |
| 36 | + onNextPage: (page, key, data) => { |
| 37 | + spinner.update( |
| 38 | + `Loading notifications... (page ${page}, ${+( |
| 39 | + (data.length / count) * |
| 40 | + 100 |
| 41 | + ).toFixed(2)}%)` |
| 42 | + ) |
| 43 | + }, |
| 44 | + }) |
| 45 | + .finally(() => spinner.stop()) |
| 46 | + |
| 47 | + logger.success('Loading notifications done!') |
| 48 | + |
| 49 | + { |
| 50 | + const divider = logger.colors.gray('─'.repeat(process.stdout.columns || 30)) |
| 51 | + |
| 52 | + let spinner: Spinner | undefined |
| 53 | + if (opts.image) spinner = logger.await('Downloading images') |
| 54 | + |
| 55 | + const texts = ( |
| 56 | + await Promise.all( |
| 57 | + notifications.map((n) => |
| 58 | + renderNotification(n, opts).then((result) => [...result, divider]) |
| 59 | + ) |
| 60 | + ).finally(() => spinner?.stop()) |
| 61 | + ).flat() |
| 62 | + texts.unshift(divider) |
| 63 | + |
| 64 | + process.stdout.write(`${texts.join('\n')}\n`) |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +const EMPTY_PLACEHOLDER = '(EMPTY)' |
| 69 | + |
| 70 | +async function renderNotification( |
| 71 | + n: Entity.Notification, |
| 72 | + { avatar, image }: NotificationOptions |
| 73 | +): Promise<string[]> { |
| 74 | + const users = n.actionItem?.users ?? [] |
| 75 | + let usersText = |
| 76 | + users.length > 1 |
| 77 | + ? `${users.map((user) => displayUser(user)).join(', ')} ` |
| 78 | + : users[0] |
| 79 | + ? `${displayUser(users[0], true)} ` |
| 80 | + : '-' |
| 81 | + const bio = logger.colors.gray(users[0].bio ?? '') |
| 82 | + |
| 83 | + const usersCount = n.actionItem?.usersCount |
| 84 | + if (typeof usersCount === 'number' && users.length !== usersCount) { |
| 85 | + usersText += `等 ${usersCount} 人` |
| 86 | + } |
| 87 | + const content = n.actionItem?.content |
| 88 | + let refContent = n.referenceItem?.content?.trim() || '(empty)' |
| 89 | + if (refContent.length > 100) { |
| 90 | + refContent = `${refContent.slice(0, 100)}...` |
| 91 | + } |
| 92 | + |
| 93 | + const userAvatarUrl = users[0]?.avatarImage.smallPicUrl |
| 94 | + const userAvatar = (height: number) => { |
| 95 | + if (!avatar || !image) return EMPTY_PLACEHOLDER |
| 96 | + return displayImage(userAvatarUrl, height).then(({ result }) => result) |
| 97 | + } |
| 98 | + |
| 99 | + const referenceImageUrl = n.referenceItem?.referenceImageUrl |
| 100 | + const referenceImage = (height = 8) => { |
| 101 | + if (!image || !referenceImageUrl) return EMPTY_PLACEHOLDER |
| 102 | + return displayImage(referenceImageUrl, height).then(({ result }) => result) |
| 103 | + } |
| 104 | + |
| 105 | + let texts = await renderStory() |
| 106 | + texts ||= await renderPersonalUpdate() |
| 107 | + texts ||= await renderUser() |
| 108 | + texts ||= await renderAvatar() |
| 109 | + |
| 110 | + if (texts) { |
| 111 | + const timeStr = format(new Date(n.createdAt), 'yyyy-MM-dd HH:mm:ss') |
| 112 | + texts.unshift(logger.colors.gray(timeStr)) |
| 113 | + return texts.filter((text) => text.trim() !== EMPTY_PLACEHOLDER) |
| 114 | + } else { |
| 115 | + warnUnknownType(n) |
| 116 | + return [] |
| 117 | + } |
| 118 | + |
| 119 | + async function renderStory(): Promise<string[] | undefined> { |
| 120 | + switch (n.type) { |
| 121 | + case 'LIKE_STORY': |
| 122 | + return [`👍 ${usersText}赞了你的日记`, await referenceImage()] |
| 123 | + case 'REPLIED_TO_STORY_COMMENT': |
| 124 | + return [await userAvatar(4), `📨 ${usersText}回复了你的留言`, content] |
| 125 | + case 'COMMENT_STORY': |
| 126 | + return [ |
| 127 | + `📨 ${usersText}给你的日记留言了:`, |
| 128 | + content, |
| 129 | + await referenceImage(), |
| 130 | + ] |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + async function renderPersonalUpdate(): Promise<string[] | undefined> { |
| 135 | + switch (n.type) { |
| 136 | + case 'LIKE_PERSONAL_UPDATE': |
| 137 | + return [`👍 ${usersText}赞了你的动态:`, refContent] |
| 138 | + case 'COMMENT_PERSONAL_UPDATE': |
| 139 | + return [await userAvatar(4), `📨 ${usersText}评论了你`, content] |
| 140 | + case 'REPLIED_TO_PERSONAL_UPDATE_COMMENT': |
| 141 | + return [await userAvatar(4), `📨 ${usersText}回复了你的评论`, content] |
| 142 | + case 'LIKE_PERSONAL_UPDATE_COMMENT': |
| 143 | + return [`👍 ${usersText}赞了你的评论:`, refContent] |
| 144 | + case 'COMMENT_AND_REPOST': |
| 145 | + return [`📨 ${usersText}评论并转发了你:`, content] |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + async function renderUser(): Promise<string[] | undefined> { |
| 150 | + switch (n.type) { |
| 151 | + case 'USER_RESPECT': |
| 152 | + return [`🎉 ${usersText}夸了夸你`, content] |
| 153 | + case 'MENTION': |
| 154 | + return [`👋 ${usersText}@了你:`, content] |
| 155 | + case 'USER_FOLLOWED': |
| 156 | + return [await userAvatar(4), bio, `✨ ${usersText}关注了你`] |
| 157 | + case 'USER_SILENT_FOLLOWED': |
| 158 | + return [ |
| 159 | + await userAvatar(4), |
| 160 | + bio, |
| 161 | + `✨ ${usersText}关注了你(静默关注 🤔)`, |
| 162 | + ] |
| 163 | + case 'PERSONAL_UPDATE_REPOSTED': |
| 164 | + return [ |
| 165 | + await userAvatar(4), |
| 166 | + `🔗 ${usersText}转发了你的动态`, |
| 167 | + refContent, |
| 168 | + ] |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + async function renderAvatar(): Promise<string[] | undefined> { |
| 173 | + switch (n.type) { |
| 174 | + case 'LIKE_AVATAR': |
| 175 | + return [await referenceImage(4), `👍 ${usersText}赞了你的头像`] |
| 176 | + case 'AVATAR_GREET': |
| 177 | + return [await referenceImage(4), `👉 ${usersText}赞了你的头像`] |
| 178 | + } |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +const warnUnknownType = (n: Entity.Notification) => { |
| 183 | + console.log(n) |
| 184 | + const info = [n.type, n.actionType, n.actionItem.type].join('||') |
| 185 | + logger.warning( |
| 186 | + `Unknown notification: ${info}. Please send it to developer, thanks!` |
| 187 | + ) |
| 188 | +} |
0 commit comments