bluesky-social / atproto

Social networking technology created by Bluesky
Other
6.88k stars 486 forks source link

PDS how to change app view CDN url? #3018

Open lovemyliwu opened 5 days ago

lovemyliwu commented 5 days ago

Is your feature request related to a problem? Please describe. I saw PDS_BSKY_APP_VIEW_CDN_URL_PATTERN can config, but only effect to local viewer, I need customize the cdn url from app view level.

Describe the solution you'd like

when app view render cdn url, consider the requester customize config through header, for example PDS-CFG-CDN-URL-PATTERN=https://thecdn.customize.com/%s/%s/%s

Describe alternatives you've considered

Additional context

lovemyliwu commented 4 days ago

some detail clue related to app view api call

config.cdnUrl => process.env.BSKY_CDN_URL || process.env.BSKY_IMG_URI_ENDPOINT => https://cdn.bsky.app/img/
config.videoPlaylistUrlPattern => process.env.BSKY_VIDEO_PLAYLIST_URL_PATTERN => https://video.bsky.app/watch/%s/%s/playlist.m3u8
config.videoThumbnailUrlPattern => process.env.BSKY_VIDEO_THUMBNAIL_URL_PATTERN => https://video.cdn.bsky.app/hls/%s/%s/thumbnail.jpg

!https://github.com/bluesky-social/atproto/blob/main/packages/bsky/src/index.ts#L159
index
    => call lexicon create server => call create xrpc server with schema define as variable => define namespace(it is a functino to define handler to xrpc server)
    => call API with lexicon server => call detail api init => call lexicon server namespace(finished define handler to xrpc server)

!https://github.com/bluesky-social/atproto/blob/main/packages/xrpc-server/src/server.ts#L113
xrpc-server-define-method
    => call add method => call add route => define middlewares variable => call create handler as handler variable
        => call express get or post defne method with middlewares variable and handler variable

!https://github.com/bluesky-social/atproto/blob/main/packages/xrpc-server/src/server.ts#L236
xrpc-server-define-create-handler
    => define validator for input and output => return dynamic defined function

!https://github.com/bluesky-social/atproto/blob/main/packages/xrpc-server/src/server.ts#L257
express-get-or-post-define-handler
    => decode query params => validate input => define request context => check rate limit
        => call detail api handler => validate output => encode output
            => call express response json method
lovemyliwu commented 3 days ago

detail api handler render url by call ‎Views.post from presentation function.

the Views.embed will call imgUriBuilder or videoUriBuilder to generate url from Views.post function.

at viewer level, it can't see the express reqeust object because it is a non-route function, and since the call chain is very deepth, we should not pass the request to viewer.

I asked AI how to get request object from a non-route function, it said:


在Node.js的Express框架中,如果你想在非路由处理函数中获取当前的request header,而不显式地传递req对象,可以使用AsyncLocalStorage来实现。AsyncLocalStorage是Node.js的一个内置模块,它提供了一种机制来保持异步操作的上下文。

以下是如何使用AsyncLocalStorage来在非路由处理函数中获取当前的request header的步骤:

  1. 创建一个AsyncLocalStorage实例: 在你的应用中创建一个AsyncLocalStorage的实例,这将用于存储和访问请求上下文。

    const { AsyncLocalStorage } = require('async_hooks');
    const asyncLocalStorage = new AsyncLocalStorage();
  2. 在请求处理中设置上下文: 在Express的中间件中,使用asyncLocalStorage.run()方法来设置当前请求的上下文。你可以将请求对象或其他需要在请求生命周期内访问的数据存储在上下文中。

    app.use((req, res, next) => {
     asyncLocalStorage.run(req, () => {
       next();
     });
    });
  3. 在非路由处理函数中访问上下文: 在任何异步操作中,你可以使用asyncLocalStorage.getStore()来获取当前请求的上下文,从而访问request header。

    function someAsyncFunction() {
     const req = asyncLocalStorage.getStore();
     if (req) {
       // 访问请求头
       const headerValue = req.headers['some-header'];
       console.log(headerValue);
     }
    }
  4. 确保上下文的正确性: 由于Node.js的事件循环和异步操作的特性,确保在异步操作完成之前上下文不会被破坏是很重要的。AsyncLocalStorage会自动处理这些情况,确保每个异步操作都能访问到正确的上下文。

请注意,AsyncLocalStorage是在Node.js v13.10.0和v12.17.0中引入的,因此你需要确保你的Node.js版本至少是这些版本之一。此外,使用AsyncLocalStorage可能会带来一定的性能开销,但在大多数情况下,这种开销是可以接受的。

lovemyliwu commented 2 days ago

currently, I will proxy the bluesky api server, after research, these api contains CDN url

lex:app.bsky.feed.defs#postView
    => lex:app.bsky.feed.defs#feedViewPost
        => app.bsky.feed.getActorLikes
        => app.bsky.feed.getAuthorFeed
        => app.bsky.feed.getFeed
        => app.bsky.feed.getListFeed
        => app.bsky.feed.getTimeline
    => lex:app.bsky.feed.defs#replyRef
        => lex:app.bsky.feed.defs#feedViewPost
    => lex:app.bsky.feed.defs#threadViewPost
        => lex:app.bsky.feed.defs#threadViewPost
        => app.bsky.feed.getPostThread
    => app.bsky.feed.getPosts
    => app.bsky.feed.getQuotes
    => app.bsky.feed.searchPosts
lovemyliwu commented 2 days ago

for somebody want to workaround like me, create a cf worker to replace bluesky cdn url to yourself CDN, then update the pds.env file field PDS_BSKY_APP_VIEW_URL

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

function needModify(pathname) {
  let api_names = [
    'app.bsky.feed.getActorLikes',
    'app.bsky.feed.getAuthorFeed',
    'app.bsky.feed.getFeed',
    'app.bsky.feed.getListFeed',
    'app.bsky.feed.getTimeline',
    'app.bsky.feed.getPostThread',
    'app.bsky.feed.getPosts',
    'app.bsky.feed.getQuotes',
    'app.bsky.feed.searchPosts',
    // below related to avatar
    'app.bsky.actor.getProfile',
    'app.bsky.actor.getProfiles',
    'app.bsky.actor.getSuggestions',
    'app.bsky.actor.searchActors',
    'app.bsky.actor.searchActorsTypeahead',
    'app.bsky.feed.getActorFeeds',
    'app.bsky.feed.getFeedGenerator',
    'app.bsky.feed.getFeedGenerators',
    'app.bsky.feed.getLikes',
    'app.bsky.feed.getRepostedBy',
    'app.bsky.feed.getSuggestedFeeds',
    'app.bsky.graph.getActorStarterPacks',
    'app.bsky.graph.getBlocks',
    'app.bsky.graph.getFollowers',
    'app.bsky.graph.getFollows',
    'app.bsky.graph.getKnownFollowers',
    'app.bsky.graph.getList',
    'app.bsky.graph.getListBlocks',
    'app.bsky.graph.getListMutes',
    'app.bsky.graph.getLists',
    'app.bsky.graph.getMutes',
    'app.bsky.graph.getStarterPack',
    'app.bsky.graph.getSuggestedFollowsByActor',
    'app.bsky.graph.searchStarterPacks',
    'app.bsky.labeler.getServices',
    'app.bsky.notification.listNotifications',
    'app.bsky.unspecced.getPopularFeedGenerators',
    'chat.bsky.convo.getConvo',
    'chat.bsky.convo.getConvoForMembers',
    'chat.bsky.convo.getLog',
    'chat.bsky.convo.getMessages',
    'chat.bsky.convo.listConvos',
    'chat.bsky.convo.muteConvo',
    'chat.bsky.convo.sendMessage',
    'chat.bsky.convo.sendMessageBatch',
    'chat.bsky.convo.unmuteConvo',
    'chat.bsky.convo.updateRead',
    'chat.bsky.moderation.getMessageContext'
  ]
  return api_names.includes(pathname.substring('/xrpc/'.length))
}

function replaceCDN_URL(full_url) {
  let url = new URL(full_url)
  url.pathname = `/${url.host}${url.pathname}`
  url.host = 'YOUR-CDN-HOST'
  return url.toString()
}

async function handleRequest(request) {
  const url = new URL(request.url)
  url.hostname = 'api.bsky.app'
  const newRequest = new Request(url, request)
  let response = await fetch(newRequest)
  const responseClone = response.clone();

  // 检查响应内容类型是否为JSON
  if (needModify(url.pathname) && response.headers.get('content-type').includes('application/json')) {
    // 读取响应文本
    const json = await responseClone.json();
    let cdn_img_url = 'https://cdn.bsky.app/img/';
    let cdn_video_playlist_url = 'https://video.bsky.app/watch/';
    let cdn_video_thumbnail_url = 'https://video.cdn.bsky.app/hls/'

    // 使用递归函数遍历并替换JSON中的特定字符串
    function replaceInJson(obj) {
      Object.keys(obj).forEach(key => {
        if (typeof obj[key] === 'object' && obj[key] !== null) {
          replaceInJson(obj[key]);
        } else if (typeof obj[key] === 'string' && (obj[key].startsWith(cdn_img_url) || obj[key].startsWith(cdn_video_playlist_url) || obj[key].startsWith(cdn_video_thumbnail_url))) {
          obj[key] = replaceCDN_URL(obj[key]);
        }
      });
    }

    // 替换JSON中的字符串
    replaceInJson(json);

    // 将修改后的JSON转换为字符串
    const modifiedJsonString = JSON.stringify(json);

    // 返回新的响应,保持原始响应的headers
    return new Response(modifiedJsonString, {
      status: response.status,
      headers: response.headers
    });
  } else {
    // 如果不是JSON响应,直接返回原始响应
    return response;
  }
}