Skip to content

Commit 6edc471

Browse files
authored
Merge pull request #27 from komplexb/feat/image-upload-2025
feat: OneNote Image to Telegram Photo Upload
2 parents 70eb39c + cb34414 commit 6edc471

File tree

4 files changed

+204
-19
lines changed

4 files changed

+204
-19
lines changed

eventData.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"onenoteSettings": {
33
"notebookName": "2nd Brain",
44
"sectionName": "Test",
5-
"isSequential": false
5+
"isSequential": true
66
},
77
"messageSettings": {
88
"channelHandle": "@notifyer_quotes_dev",

lib/notify.js

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const { htmlToMarkdown } = require('./markdown')
22
const HTMLParser = require('node-html-parser')
33
const { markdownv2 } = require('telegram-format')
4-
const { getNoteContents } = require('./onenote')
4+
const { getNoteContents, extractFirstImage, downloadImage } = require('./onenote')
55

66
const fetch = require('superagent')
77

@@ -146,33 +146,69 @@ async function withTelegram(note, messageSettings, login = false) {
146146
} = messageSettings
147147
const msg = await structureMessage({ note, titlePrefix, isLogin: login })
148148

149-
let text = markdownv2.bold(markdownv2.escape(msg.prefix))
150-
text += markdownv2.escape(`\n\n`)
151-
text += markdownv2.underline(markdownv2.escape(msg.title))
152-
text += markdownv2.escape(`\n\n`)
153-
text += msg.body
154-
155-
if(showEditLink) {
156-
// const openInOnenote = markdownv2.url("Open In OneNote", markdownv2.escape(msg.url))
157-
text += markdownv2.escape(`\n\n`)
158-
text += markdownv2.url("Edit note", msg.webUrl)
159-
}
160-
161149
return new Promise(async(resolve, reject) => {
162150
try {
151+
// Handle image processing independently
152+
let hasImage = false
153+
try {
154+
const noteContent = await getNoteContents(note.url)
155+
const imageInfo = extractFirstImage(noteContent.content)
156+
157+
if (imageInfo?.imageUrl) {
158+
console.log('Found image in note, sending photo first')
159+
160+
// Create caption with subject and alt text
161+
let caption = markdownv2.bold(markdownv2.escape(msg.prefix))
162+
caption += markdownv2.escape(`\n\n`)
163+
caption += markdownv2.underline(markdownv2.escape(msg.title))
164+
caption += markdownv2.escape(`\n\n`)
165+
166+
// Use alt text if available, otherwise use preview text
167+
const captionText = imageInfo.altText || msg.previewText
168+
caption += markdownv2.escape(captionText)
169+
170+
if(showEditLink) {
171+
caption += markdownv2.escape(`\n\n`)
172+
caption += markdownv2.url("Edit note", msg.webUrl)
173+
}
174+
175+
// Download and send image with size limits
176+
const imageBuffer = await downloadImage(imageInfo.dataFullresSrc || imageInfo.imageUrl)
177+
await sendPhotoToTelegram(imageBuffer, caption, channelHandle)
178+
hasImage = true
179+
}
180+
} catch (imageErr) {
181+
console.warn('Image processing failed, continuing with text message:', imageErr.message)
182+
// Image processing failure doesn't affect text message flow
183+
}
184+
185+
// Send note body text
186+
let text = markdownv2.bold(markdownv2.escape(msg.prefix))
187+
text += markdownv2.escape(`\n\n`)
188+
text += markdownv2.underline(markdownv2.escape(msg.title))
189+
text += markdownv2.escape(`\n\n`)
190+
text += msg.body
191+
192+
if(showEditLink) {
193+
text += markdownv2.escape(`\n\n`)
194+
text += markdownv2.url("Edit note", msg.webUrl)
195+
}
196+
163197
await sendNoteToTelegram(text, channelHandle, { disable_web_page_preview: disablePreview });
198+
164199
resolve({
165200
status: 200,
166201
title: msg.title,
167202
body: msg.previewText,
203+
hasImage
168204
})
169205
} catch (err) {
170206
console.error('Title: ', msg.title)
171-
console.error('withTelegram', err.response.body.description)
207+
console.error('withTelegram', err.response?.body?.description || err.message)
172208
reject({
173209
status: 400,
174210
title: msg.title || 'Error',
175-
body: err.response.body.description,
211+
body: err.response?.body?.description || err.message,
176212
})
177213
}
178214
});
@@ -209,8 +245,45 @@ const sendNoteToTelegram = async(msg, channelHandle, requestArgs, escapeMsg = fa
209245
})
210246
}
211247

248+
/**
249+
* Send photo to Telegram channel using buffer
250+
* @param {Buffer} imageBuffer - Image buffer data
251+
* @param {string} caption - Photo caption in MarkdownV2 format
252+
* @param {string} channelHandle - Telegram channel ID
253+
* @returns {Promise} Telegram API response
254+
*/
255+
const sendPhotoToTelegram = async(imageBuffer, caption, channelHandle) => {
256+
const baseUrl = process.env.TELEGRAM_URL.replace(
257+
'{NotifyerBotToken}',
258+
process.env.TELEGRAM_BOT_TOKEN
259+
)
260+
const urlPath = 'sendPhoto';
261+
262+
return new Promise((resolve, reject) => {
263+
fetch
264+
.post(`${baseUrl}/${urlPath}`)
265+
.field('chat_id', channelHandle)
266+
.field('caption', caption)
267+
.field('parse_mode', 'MarkdownV2')
268+
.attach('photo', imageBuffer, 'image.jpg')
269+
.timeout({ response: 30000, deadline: 45000 })
270+
.then(response => {
271+
if (response?.ok) {
272+
console.log('Photo Pushed!')
273+
resolve(response)
274+
} else {
275+
throw new Error(response)
276+
}
277+
})
278+
.catch(err => {
279+
reject(err);
280+
})
281+
})
282+
}
283+
212284
module.exports = {
213285
withPush: push,
214286
withTelegram,
215-
sendNoteToTelegram
287+
sendNoteToTelegram,
288+
sendPhotoToTelegram
216289
}

lib/onenote.js

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,14 +313,125 @@ function getRandomNote(notes, sectionHandle) {
313313
localStorage.getItem(`recent_${sectionHandle}`, true) || []
314314

315315
if (recentNotes.includes(note.id)) {
316-
getRandomNote(notes)
316+
return getRandomNote(notes, sectionHandle)
317317
}
318318

319319
return note
320320
}
321321

322+
/**
323+
* Extract first image from OneNote HTML content
324+
* @param {string} htmlContent - OneNote page HTML content
325+
* @returns {Object|null} { imageUrl, altText, dataFullresSrc } or null if no image found
326+
*/
327+
function extractFirstImage(htmlContent) {
328+
const HTMLParser = require('node-html-parser')
329+
const wrapper = HTMLParser.parse(htmlContent)
330+
331+
const firstImg = wrapper.querySelector('img')
332+
if (!firstImg) {
333+
return null
334+
}
335+
336+
return {
337+
imageUrl: firstImg.getAttribute('src'),
338+
dataFullresSrc: firstImg.getAttribute('data-fullres-src'),
339+
altText: firstImg.getAttribute('alt') || '',
340+
width: firstImg.getAttribute('width'),
341+
height: firstImg.getAttribute('height')
342+
}
343+
}
344+
345+
/**
346+
* Check image size before downloading
347+
* @param {string} imageUrl - The Graph API image endpoint URL
348+
* @returns {Promise<number>} Image size in bytes
349+
*/
350+
async function getImageSize(imageUrl) {
351+
const { accessToken } = localStorage.getItem('onenote')
352+
353+
return new Promise((resolve, reject) => {
354+
apiRequests
355+
.head(imageUrl)
356+
.timeout({ response: 10000, deadline: 15000 })
357+
.set('Authorization', `Bearer ${accessToken}`)
358+
.then(response => {
359+
if (response?.ok) {
360+
const contentLength = response.headers['content-length']
361+
resolve(contentLength ? parseInt(contentLength) : 0)
362+
} else {
363+
throw new Error(response)
364+
}
365+
})
366+
.catch(err => {
367+
console.error('getImageSize', err)
368+
reject(err)
369+
})
370+
})
371+
}
372+
373+
/**
374+
* Download image from Microsoft Graph API endpoint with size limits
375+
* @param {string} imageUrl - The Graph API image endpoint URL
376+
* @param {number} maxSizeBytes - Maximum file size in bytes (default 3MB)
377+
* @returns {Promise<Buffer>} Image buffer
378+
*/
379+
async function downloadImage(imageUrl, maxSizeBytes = 3 * 1024 * 1024) {
380+
const { accessToken } = localStorage.getItem('onenote')
381+
382+
// Check size first
383+
try {
384+
const imageSize = await getImageSize(imageUrl)
385+
if (imageSize > maxSizeBytes) {
386+
throw new Error(`Image too large: ${imageSize} bytes (max: ${maxSizeBytes})`)
387+
}
388+
console.log(`Downloading image: ${imageSize} bytes`)
389+
} catch (err) {
390+
console.warn('Could not check image size, proceeding with download:', err.message)
391+
}
392+
393+
return new Promise((resolve, reject) => {
394+
apiRequests
395+
.get(imageUrl)
396+
.timeout({ response: 30000, deadline: 45000 })
397+
.set('Authorization', `Bearer ${accessToken}`)
398+
.buffer(true)
399+
.parse((res, callback) => {
400+
const chunks = []
401+
let downloadedBytes = 0
402+
403+
res.on('data', chunk => {
404+
downloadedBytes += chunk.length
405+
if (downloadedBytes > maxSizeBytes) {
406+
return callback(new Error(`Image download exceeded size limit: ${downloadedBytes} bytes`))
407+
}
408+
chunks.push(chunk)
409+
})
410+
411+
res.on('end', () => {
412+
console.log(`Downloaded ${downloadedBytes} bytes`)
413+
callback(null, Buffer.concat(chunks))
414+
})
415+
})
416+
.then(response => {
417+
if (response && response.ok) {
418+
resolve(response.body)
419+
} else {
420+
throw new Error(response)
421+
}
422+
})
423+
.catch(err => {
424+
console.error('downloadImage', err)
425+
reject(err)
426+
})
427+
})
428+
}
429+
322430
module.exports = {
323431
getNote,
324432
getNoteContents,
325-
setNoteSection
433+
setNoteSection,
434+
extractFirstImage,
435+
downloadImage,
436+
getImageSize
326437
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"app": "sls invoke local -f app",
88
"deploy": "sls deploy --stage prod",
99
"predeploy": "npm run remove",
10+
"remove-logs": "aws logs delete-log-group --log-group-name /aws/lambda/notifyer-cron-prod-app --region ap-southeast-2",
1011
"dev": "sls invoke local -f app --watch --path ./eventData.json",
1112
"agentdev": "sls invoke local -f app --path ./eventData.json",
1213
"offline": "sls offline start --port 4500",

0 commit comments

Comments
 (0)