-
Notifications
You must be signed in to change notification settings - Fork 7.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(route): add malaysiakini.com (#16752)
* feat(route): add malaysiakini.com * fixing issues * Added additonal authentication method with email and password * Apply suggestions from code review Co-authored-by: Tony <TonyRL@users.noreply.github.com> * Apply suggestions from code review ---------
- Loading branch information
1 parent
034081a
commit 87723d1
Showing
3 changed files
with
200 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
import { Route } from '@/types'; | ||
import cache from '@/utils/cache'; | ||
import got from '@/utils/got'; | ||
import parser from '@/utils/rss-parser'; | ||
import { config } from '@/config'; | ||
import { FetchError } from 'ofetch'; | ||
|
||
export const route: Route = { | ||
path: '/:lang/:category?', | ||
categories: ['new-media'], | ||
example: '/malaysiakini/en', | ||
parameters: { | ||
lang: 'Language, see below', | ||
category: 'Category, see below, news by default', | ||
}, | ||
features: { | ||
requirePuppeteer: false, | ||
antiCrawler: false, | ||
supportBT: false, | ||
supportPodcast: false, | ||
supportScihub: false, | ||
requireConfig: [ | ||
{ | ||
name: 'MALAYSIAKINI_EMAIL', | ||
optional: true, | ||
description: 'Malaysiakini Email or Username', | ||
}, | ||
{ | ||
name: 'MALAYSIAKINI_PASSWORD', | ||
optional: true, | ||
description: 'Malaysiakini Password', | ||
}, | ||
{ | ||
name: 'MALAYSIAKINI_REFRESHTOKEN', | ||
optional: true, | ||
description: 'To obtain the refresh token, log into Malaysiakini and look for the cookie `nl____refreshToken` within document.cookie in the browser console. The token is the value of the cookie.', | ||
}, | ||
], | ||
}, | ||
name: 'News', | ||
maintainers: ['quiniapiezoelectricity'], | ||
handler, | ||
description: ` | ||
| Language | English | Bahasa Malaysia | 华文 | | ||
| -------- | ------ | ------- | ------ | | ||
| \`:lang\` | \`en\` | \`my\` | \`zh\` | | ||
| Category | \`:category\` | | ||
| ---------------------- | ------------- | | ||
| News | \`news\` | | ||
| Columns | \`columns\` | | ||
| From Our Readers | \`letters\` |`, | ||
radar: [ | ||
{ | ||
source: ['malaysiakini.com/'], | ||
target: '/en', | ||
}, | ||
{ | ||
source: ['malaysiakini.com/:lang'], | ||
target: '/:lang', | ||
}, | ||
{ | ||
source: ['www.malaysiakini.com/:lang/latest/:category'], | ||
target: '/:lang/:category', | ||
}, | ||
], | ||
}; | ||
|
||
async function handler(ctx) { | ||
const lang = ctx.req.param('lang'); | ||
const category = ctx.req.param('category') ?? 'news'; | ||
const apiKey = 'UFXL7F1EL53S8DZ5SGJUMG5IIFVRY4WI'; // Assuming the apiKey is static | ||
|
||
const key = { | ||
email: config.malaysiakini.email, | ||
password: config.malaysiakini.password, | ||
apiKey, | ||
}; | ||
const body = JSON.stringify(key); | ||
|
||
let cookie; | ||
|
||
const cacheIn = await cache.get('malaysiakini:cookie'); | ||
if (cacheIn) { | ||
cookie = cacheIn; | ||
} | ||
|
||
if (cookie === undefined && config.malaysiakini.email && config.malaysiakini.password) { | ||
const login = await got.post('https://membership.malaysiakini.com/api/v1/auth/local', { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
Accept: '*/*', | ||
Connection: 'keep-alive', | ||
}, | ||
body, | ||
}); | ||
if (login.data.accessToken && login.data.refreshToken) { | ||
cookie = `nl____accessToken=${login.data.accessToken}; nl____refreshToken=${login.data.refreshToken};`; | ||
// Refresh token should be sufficient for authenticating for full text, but access token is included for good measure. | ||
cache.set('malaysiakini:cookie', cookie); | ||
} | ||
} | ||
|
||
if (cookie === undefined && config.malaysiakini.refreshToken) { | ||
cookie = `nl____refreshToken=${config.malaysiakini.refreshToken};`; | ||
cache.set('malaysiakini:cookie', cookie); | ||
} | ||
|
||
const link = `https://www.malaysiakini.com/rss/${lang}/${category}.rss`; | ||
const feed = await parser.parseURL(link); | ||
|
||
if (cookie) { | ||
// Testing the cookie with the first item of the feed | ||
try { | ||
await got({ | ||
method: 'get', | ||
url: `https://www.malaysiakini.com/api/full_content/${feed.items[0].guid}`, | ||
headers: { | ||
Cookie: cookie, | ||
}, | ||
}); | ||
} catch (error) { | ||
if (error instanceof FetchError && error.statusCode === 401) { | ||
await cache.set('malaysiakini:cookie', ''); | ||
} | ||
throw error; | ||
} | ||
} | ||
|
||
const items = await Promise.all( | ||
feed.items.map((item) => | ||
cache.tryGet(item.link, async () => { | ||
const response = await got(`https://www.malaysiakini.com/api/content/${item.guid}`); | ||
if (response.data.stories.content) { | ||
item.description = response.data.stories.content; | ||
} else { | ||
item.description = response.data.stories.teaser; | ||
if (cookie) { | ||
let fullResponse; | ||
try { | ||
fullResponse = await got({ | ||
method: 'get', | ||
url: `https://www.malaysiakini.com/api/full_content/${item.guid}`, | ||
headers: { | ||
Cookie: cookie, | ||
}, | ||
}); | ||
} finally { | ||
if (fullResponse) { | ||
item.description = fullResponse.data.content; | ||
} | ||
} | ||
} | ||
} | ||
if (response.data.stories.image_feat) { | ||
const cover = response.data.stories.image_feat; | ||
if (cover.length > 0) { | ||
item.description = `<img src=${cover[0]}>` + item.description; | ||
} | ||
} | ||
if (response.data.stories.author) { | ||
item.author = response.data.stories.author; | ||
} | ||
if (response.data.stories.tags) { | ||
item.category = response.data.stories.tags; | ||
} | ||
return item; | ||
}) | ||
) | ||
); | ||
|
||
return { | ||
title: feed.title, | ||
link: feed.link, | ||
description: feed.description, | ||
language: lang, | ||
item: items, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import type { Namespace } from '@/types'; | ||
|
||
export const namespace: Namespace = { | ||
name: 'Malaysiakini 当今大马', | ||
url: 'malaysiakini.com', | ||
description: `Provides an easy-to-use RSS feed for Malaysiakini.com with teaser/full-text fetching. | ||
:::warning | ||
A subscription is required for fetching full articles. | ||
Please refer to the deployment config for more information. | ||
:::`, | ||
}; |