index.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. require('dotenv').config();
  2. const { BskyAgent, RichText } = require('@atproto/api');
  3. const BasePlatformService = require('./utils/BasePlatformService');
  4. const { getDb } = require('./utils/MongoDBConnector');
  5. const BLUESKY_IDENTIFIER = process.env.BLUESKY_IDENTIFIER || '';
  6. const BLUESKY_APP_PASSWORD = process.env.BLUESKY_APP_PASSWORD || '';
  7. const BLUESKY_SERVICE = process.env.BLUESKY_SERVICE || 'https://bsky.social';
  8. class BlueskyService extends BasePlatformService {
  9. constructor() {
  10. super('bluesky');
  11. this.agent = new BskyAgent({ service: BLUESKY_SERVICE });
  12. this.loggedIn = false;
  13. }
  14. async _ensureLoggedIn() {
  15. if (this.loggedIn) return;
  16. if (!BLUESKY_IDENTIFIER || !BLUESKY_APP_PASSWORD) {
  17. throw new Error('Bluesky credentials not configured');
  18. }
  19. await this.agent.login({
  20. identifier: BLUESKY_IDENTIFIER,
  21. password: BLUESKY_APP_PASSWORD,
  22. });
  23. this.loggedIn = true;
  24. }
  25. async getStatus() {
  26. if (!BLUESKY_IDENTIFIER || !BLUESKY_APP_PASSWORD) {
  27. return { connected: false, platform: 'bluesky', error: 'Credentials not configured' };
  28. }
  29. try {
  30. await this._ensureLoggedIn();
  31. const profile = await this.agent.getProfile({ actor: BLUESKY_IDENTIFIER });
  32. return {
  33. connected: true,
  34. platform: 'bluesky',
  35. username: profile.data.handle,
  36. displayName: profile.data.displayName,
  37. avatar: profile.data.avatar,
  38. };
  39. } catch (err) {
  40. this.loggedIn = false;
  41. return { connected: false, platform: 'bluesky', error: err.message };
  42. }
  43. }
  44. async fetchFeed({ limit = 50 } = {}) {
  45. await this._ensureLoggedIn();
  46. const response = await this.agent.getTimeline({ limit: Number(limit) });
  47. const items = response.data.feed
  48. .filter((entry) => entry.post)
  49. .map((entry) => {
  50. const post = entry.post;
  51. const author = post.author;
  52. const record = post.record;
  53. return this.normalizeFeedItem({
  54. originalId: post.uri,
  55. author: {
  56. name: author.displayName || author.handle,
  57. username: author.handle,
  58. avatar: author.avatar,
  59. profileUrl: `https://bsky.app/profile/${author.handle}`,
  60. },
  61. content: record.text || '',
  62. media: _extractMedia(post.embed),
  63. platformTags: _extractTags(record.facets),
  64. metrics: {
  65. likes: post.likeCount || 0,
  66. shares: post.repostCount || 0,
  67. comments: post.replyCount || 0,
  68. },
  69. url: `https://bsky.app/profile/${author.handle}/post/${post.uri.split('/').pop()}`,
  70. createdAt: record.createdAt,
  71. });
  72. });
  73. // MongoDB'ye kaydet (upsert)
  74. try {
  75. const db = await getDb();
  76. const col = db.collection('feeds');
  77. for (const item of items) {
  78. await col.updateOne(
  79. { platform: 'bluesky', originalId: item.originalId },
  80. { $set: item },
  81. { upsert: true }
  82. );
  83. }
  84. } catch (err) {
  85. this.app.log.error({ action: 'feed_write', platform: 'bluesky', outcome: 'failure', err: err.message });
  86. }
  87. return items;
  88. }
  89. async publishPost({ content, media = [], firstComment } = {}) {
  90. await this._ensureLoggedIn();
  91. const rt = new RichText({ text: content });
  92. await rt.detectFacets(this.agent);
  93. const postData = { text: rt.text, facets: rt.facets };
  94. if (media.length > 0) {
  95. postData.embed = { $type: 'app.bsky.embed.images', images: media };
  96. }
  97. const result = await this.agent.post(postData);
  98. if (firstComment?.trim()) {
  99. try {
  100. const commentRt = new RichText({ text: firstComment.trim() });
  101. await commentRt.detectFacets(this.agent);
  102. await this.agent.post({
  103. text: commentRt.text,
  104. facets: commentRt.facets,
  105. reply: {
  106. root: { uri: result.uri, cid: result.cid },
  107. parent: { uri: result.uri, cid: result.cid },
  108. },
  109. });
  110. this.app.log.info({ action: 'first_comment', platform: 'bluesky', uri: result.uri, outcome: 'success' });
  111. } catch (err) {
  112. this.app.log.warn({ action: 'first_comment', platform: 'bluesky', uri: result.uri, outcome: 'failure', err: err.message });
  113. }
  114. }
  115. return { uri: result.uri, cid: result.cid };
  116. }
  117. }
  118. function _extractMedia(embed) {
  119. if (!embed) return [];
  120. if (embed.$type === 'app.bsky.embed.images#view') {
  121. return (embed.images || []).map((img) => ({
  122. url: img.fullsize || img.thumb,
  123. type: 'image',
  124. thumbnail: img.thumb,
  125. alt: img.alt,
  126. }));
  127. }
  128. return [];
  129. }
  130. function _extractTags(facets = []) {
  131. if (!facets) return [];
  132. return facets
  133. .filter((f) => f.features?.some((feat) => feat.$type === 'app.bsky.richtext.facet#tag'))
  134. .flatMap((f) => f.features.filter((feat) => feat.$type === 'app.bsky.richtext.facet#tag').map((feat) => feat.tag));
  135. }
  136. const service = new BlueskyService();
  137. service.start(process.env.PORT || 3004);