Skip to content

Commit

Permalink
feat(link-preview): add support for link preview for tumblr posts
Browse files Browse the repository at this point in the history
  • Loading branch information
Nurguly Ashyrov committed Mar 20, 2024
1 parent 7638625 commit 1914707
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 14 deletions.
37 changes: 28 additions & 9 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
import type { Readable } from 'stream'

import { TumblrClient } from './network-api'
import type { AuthCredentialsWithDuration, AuthCredentialsWithExpiration } from './types'
import { mapBlogToUser, mapCurrentUser, mapMessage, mapMessageContentForNewConversations, mapMessageContentToOutgoingMessage, mapPaginatedMessages, mapPaginatedThreads, mapThread } from './mappers'
import type { AuthCredentialsWithDuration, AuthCredentialsWithExpiration, Poster } from './types'
import { mapBlogToUser, mapCurrentUser, mapMessage, mapMessageContentForNewConversations, mapMessageContentToOutgoingMessage, mapPaginatedMessages, mapPaginatedThreads, mapThread, parseTumblrPostUrl } from './mappers'

export default class TumblrPlatformAPI implements PlatformAPI {
readonly network = new TumblrClient()
Expand Down Expand Up @@ -123,11 +123,7 @@ export default class TumblrPlatformAPI implements PlatformAPI {

sendMessage = async (threadID: ThreadID, content: MessageContent): Promise<boolean | Message[]> => {
const currentUser = await this.network.getCurrentUser()
if (!currentUser?.activeBlog?.uuid) {
throw Error('User credentials are absent. Try reauthenticating.')
}

const body = await mapMessageContentToOutgoingMessage(threadID, currentUser.activeBlog, content)
const body = await mapMessageContentToOutgoingMessage(threadID, currentUser.activeBlog, content, this.network)
const response = await this.network.sendMessage(body)
return response.json.messages.data.map(message => mapMessage(message, currentUser.activeBlog))
}
Expand Down Expand Up @@ -161,7 +157,7 @@ export default class TumblrPlatformAPI implements PlatformAPI {

createThread = async (userIDs: UserID[], title?: string, messageText?: string): Promise<boolean | Thread> => {
const currentUser = await this.network.getCurrentUser()
const body = await mapMessageContentForNewConversations([currentUser.activeBlog.uuid, ...userIDs], { text: messageText })
const body = await mapMessageContentForNewConversations([currentUser.activeBlog.uuid, ...userIDs], { text: messageText }, this.network)
const response = await this.network.createConversation(body)
return mapThread(response.json, currentUser)
}
Expand Down Expand Up @@ -194,7 +190,30 @@ export default class TumblrPlatformAPI implements PlatformAPI {

removeReaction?: (threadID: ThreadID, messageID: MessageID, reactionKey: string) => Awaitable<void>

getLinkPreview?: (link: string) => Awaitable<MessageLink | undefined>
getLinkPreview = async (link: string): Promise<MessageLink | undefined> => {
const { blogName, postId } = parseTumblrPostUrl(link)
// We only support a link preview for tumblr post pages.
if (!blogName || !postId) {
return
}

const { json: urlInfo } = await this.network.getUrlInfo(link)
if (!urlInfo) {
return
}

const poster = urlInfo.poster?.[0] || {} as Poster
return {
url: urlInfo.displayUrl || urlInfo.url,
img: poster.url,
imgSize: {
width: poster.width || 192,
height: poster.height || 192,
},
title: urlInfo.title,
summary: urlInfo.description,
}
}

addParticipant?: (threadID: ThreadID, participantID: UserID) => Awaitable<void>

Expand Down
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export const API_URLS = {
* Marks the conversation as read
*/
MARK_AS_READ: `${API_URL}/conversations/mark_as_read`,

/**
* Extracts url info for preview
*/
URL_INFO: `${API_URL}/url_info`,
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const info: PlatformInfo = {
Attribute.SUBSCRIBE_TO_THREAD_SELECTION,
Attribute.SINGLE_THREAD_CREATION_REQUIRES_MESSAGE,
Attribute.SUPPORTS_REPORT_THREAD,
Attribute.CAN_FETCH_LINK_PREVIEW,
Attribute.CAN_REMOVE_LINK_PREVIEW,
]),
loginMode: 'browser',
browserLogins: [
Expand Down
54 changes: 51 additions & 3 deletions src/mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Blog, ApiLinks, OutgoingMessage, OutgoingMessageToCreateConversation,
} from './types'
import { UNTITLED_BLOG } from './constants'
import type { TumblrClient } from './network-api'

const mapUserSocialAttributes = (blog: Blog): UserSocialAttributes => {
const social: UserSocialAttributes = {
Expand Down Expand Up @@ -243,7 +244,32 @@ export const mapPaginatedThreads = ({
oldestCursor: links?.next?.href || links?.prev?.href,
})

export const mapMessageContentToOutgoingMessage = async (conversationId: string, blog: { uuid: string }, content: MessageContent): Promise<OutgoingMessage> => {
/**
* Extracts the blog name and a post id from a tumblr post url.
*
* Handles different variations of the post urls. Like:
* 'https://nurguly.tumblr.com/729164932829052928',
* 'http://nurguly.tumblr.com/729164932829052928',
* 'https://tumblr.com/nurguly/729164932829052928',
* 'https://tumblr.com/nurguly/729164932829052928?source=share',
* 'https://tumblr.com/nurguly/729164932829052928/seo-friendly-name-of-the-post',
* 'https://www.tumblr.com/nurguly/729164932829052928',
* 'https://nu_rg-uly.tumblr.com/729164932829052928',
* 'https://tumblr.com/nu_rg-uly/729164932829052928',
* 'https://texts.com',
* 'automattic',
* '',
*/
export const parseTumblrPostUrl = (url = ''): { blogName?: string, postId?: string } => {
const result = url.matchAll(/(?:http|https)?:\/\/(?:www\.)?(?:(?<blogNameAsSubdomain>[0-9,a-z,A-Z_-]+)\.)?tumblr\.com\/(?:(?<blogNameAsPath>[0-9,a-z,A-Z_-]+)\/)?(?<postId>\d+).*/g)
const { blogNameAsSubdomain, blogNameAsPath, postId } = [...result][0]?.groups || {}
return {
blogName: blogNameAsSubdomain || blogNameAsPath,
postId,
}
}

export const mapMessageContentToOutgoingMessage = async (conversationId: string, blog: { uuid: string }, content: MessageContent, network: TumblrClient): Promise<OutgoingMessage> => {
if (content.filePath || content.fileBuffer) {
let data: File | Buffer
let filename = ''
Expand All @@ -263,6 +289,28 @@ export const mapMessageContentToOutgoingMessage = async (conversationId: string,
}
}

const { link, includePreview } = content.links?.[0] || { includePreview: false, link: '' }
const { blogName, postId } = parseTumblrPostUrl(link)
if (includePreview && link && blogName && postId) {
const { json: urlInfo } = await network.getUrlInfo(link)
const { json: postBlog } = await network.getBlogInfo(blogName)
if (urlInfo && postBlog) {
const posterType = urlInfo.poster?.[0]?.type || ''
return {
type: 'POSTREF',
conversation_id: conversationId,
message: '',
participant: blog.uuid,
context: posterType === 'image/gif' ? 'messaging-gif' : 'post-chrome',
post: {
id: postId,
blog: postBlog.uuid,
type: 'post',
},
}
}
}

return {
conversation_id: conversationId,
type: 'TEXT',
Expand All @@ -271,8 +319,8 @@ export const mapMessageContentToOutgoingMessage = async (conversationId: string,
}
}

export const mapMessageContentForNewConversations = async (participants: string[], content: MessageContent): Promise<OutgoingMessageToCreateConversation> => {
const outgoingMessage = await mapMessageContentToOutgoingMessage('', { uuid: participants[0] }, content)
export const mapMessageContentForNewConversations = async (participants: string[], content: MessageContent, network: TumblrClient): Promise<OutgoingMessageToCreateConversation> => {
const outgoingMessage = await mapMessageContentToOutgoingMessage('', { uuid: participants[0] }, content, network)
delete outgoingMessage.conversation_id
return {
...outgoingMessage,
Expand Down
17 changes: 17 additions & 0 deletions src/network-api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
Message,
Blog,
OutgoingMessageToCreateConversation,
UrlInfo,
} from '../types'
import ConversationsChannel from './conversation-channel'
import { camelCaseKeys } from './word-case'
Expand Down Expand Up @@ -529,4 +530,20 @@ export class TumblrClient {
this.eventCallback(events)
}
}

getUrlInfo = async (url: string) => {
const response = await this.fetch<{ content: UrlInfo }>(`${API_URLS.URL_INFO}?url=${encodeURI(url)}`)
return {
...response,
json: response.json.response.content,
}
}

getBlogInfo = async (blogName: string) => {
const response = await this.fetch<{ blog: Blog }>(`${API_URLS.BASE}/blog/${blogName}/info`)
return {
...response,
json: response.json.response.blog,
}
}
}
34 changes: 32 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,29 @@ interface OutgoingMessageImage {
filename: string
}

export type OutgoingMessage = OutgoingMessageText | OutgoingMessageImage
interface OutgoingMessagePost {
type: 'POSTREF'
conversation_id: string
message: string
participant: string
context: 'messaging-gif' | 'post-chrome' | 'fast-post-chrome'
post: {
id: string
blog: string
type: 'post'
}
}

export type OutgoingMessage = OutgoingMessageText | OutgoingMessageImage | OutgoingMessagePost

type OutgoingMessageForNewConversation<T extends { conversation_id: string }> = Omit<T, 'conversation_id'> & {
participants: string[]
}

export type OutgoingMessageToCreateConversation =
OutgoingMessageForNewConversation<OutgoingMessageImage> |
OutgoingMessageForNewConversation<OutgoingMessageText>
OutgoingMessageForNewConversation<OutgoingMessageText> |
OutgoingMessageForNewConversation<OutgoingMessagePost>

export type GIFPost = Post & {
type: 'image'
Expand Down Expand Up @@ -237,3 +251,19 @@ export interface UnreadCountsResponse {
}
}
}

export interface UrlInfo {
url?: string
displayUrl?: string
title?: string
description?: string
siteName?: string
poster: Poster[]
}

export interface Poster {
type: MimeType
width: number
height: number
url: string
}

0 comments on commit 1914707

Please sign in to comment.