diff --git a/src/claimDefinitions/index.js b/src/claimDefinitions/index.js index 49e8240..984400e 100644 --- a/src/claimDefinitions/index.js +++ b/src/claimDefinitions/index.js @@ -18,6 +18,7 @@ const list = [ 'irc', 'xmpp', 'matrix', + 'telegram', 'twitter', 'reddit', 'liberapay', @@ -39,6 +40,7 @@ const data = { irc: require('./irc'), xmpp: require('./xmpp'), matrix: require('./matrix'), + telegram: require('./telegram'), twitter: require('./twitter'), reddit: require('./reddit'), liberapay: require('./liberapay'), diff --git a/src/claimDefinitions/telegram.js b/src/claimDefinitions/telegram.js new file mode 100644 index 0000000..33a0a0f --- /dev/null +++ b/src/claimDefinitions/telegram.js @@ -0,0 +1,78 @@ +/* +Copyright 2021 Yarmo Mackenbach + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +const E = require('../enums') + +const reURI = /^tg:user=([a-zA-Z0-9_]{5,32})&chat=([a-zA-Z0-9_]{5,32})/ + +const processURI = (uri) => { + const match = uri.match(reURI) + + return { + serviceprovider: { + type: 'communication', + name: 'telegram' + }, + match: { + regularExpression: reURI, + isAmbiguous: false + }, + profile: { + display: `@${match[1]}`, + uri: `https://t.me/${match[1]}`, + qr: null + }, + proof: { + uri: null, + request: { + fetcher: E.Fetcher.TELEGRAM, + access: E.ProofAccess.GRANTED, + format: E.ProofFormat.JSON, + data: { + user: match[1], + chat: match[2] + } + } + }, + claim: { + format: E.ClaimFormat.FINGERPRINT, + relation: E.ClaimRelation.EQUALS, + path: ['text'] + } + } +} + +const tests = [ + { + uri: 'tg:user=alice&chat=foobar', + shouldMatch: true + }, + { + uri: 'tg:user=complex_user_1234&chat=complex_chat_1234', + shouldMatch: true + }, + { + uri: 'tg:user=&chat=foobar', + shouldMatch: false + }, + { + uri: 'tg:user=foobar&chat=', + shouldMatch: false + } +] + +exports.reURI = reURI +exports.processURI = processURI +exports.tests = tests diff --git a/src/enums.js b/src/enums.js index f88db5b..6e16b53 100644 --- a/src/enums.js +++ b/src/enums.js @@ -49,6 +49,8 @@ const Fetcher = { XMPP: 'xmpp', /** HTTP request to Matrix API */ MATRIX: 'matrix', + /** HTTP request to Telegram API */ + TELEGRAM: 'telegram', /** HTTP request to Twitter API */ TWITTER: 'twitter' } diff --git a/src/fetcher/index.js b/src/fetcher/index.js index 5454297..df32047 100644 --- a/src/fetcher/index.js +++ b/src/fetcher/index.js @@ -18,5 +18,6 @@ exports.dns = require('./dns') exports.http = require('./http') exports.irc = require('./irc') exports.matrix = require('./matrix') +exports.telegram = require('./telegram') exports.twitter = require('./twitter') exports.xmpp = require('./xmpp') diff --git a/src/fetcher/telegram.js b/src/fetcher/telegram.js new file mode 100644 index 0000000..890e38d --- /dev/null +++ b/src/fetcher/telegram.js @@ -0,0 +1,110 @@ +/* +Copyright 2021 Yarmo Mackenbach + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +const axios = require('axios') +const validator = require('validator') + +/** + * @module fetcher/telegram + */ + +/** + * The single request's timeout value in milliseconds + * This fetcher makes two requests in total + * @constant {number} timeout + */ +module.exports.timeout = 5000 + +/** + * Execute a fetch request + * @function + * @async + * @param {object} data - Data used in the request + * @param {string} data.chat - Telegram public chat username + * @param {string} data.user - Telegram user username + * @param {object} opts - Options used to enable the request + * @param {string} opts.claims.telegram.token - The Telegram Bot API token + * @returns {object|string} + */ +module.exports.fn = async (data, opts) => { + let timeoutHandle + const timeoutPromise = new Promise((resolve, reject) => { + timeoutHandle = setTimeout( + () => reject(new Error('Request was timed out')), + data.fetcherTimeout ? data.fetcherTimeout : module.exports.timeout + ) + }) + + const apiPromise = (method) => new Promise((resolve, reject) => { + try { + validator.isAscii(opts.claims.telegram.token) + } catch (err) { + throw new Error(`Telegram fetcher was not set up properly (${err.message})`) + } + + if (!data.chat || !data.user) { + reject(new Error('Both chat name and user name must be provided')) + return + } + + const url = `https://api.telegram.org/bot${opts.claims.telegram.token}/${method}?chat_id=@${data.chat}` + axios.get(url, { + headers: { + Accept: 'application/json', + 'User-Agent': `doipjs/${require('../../package.json').version}` + }, + validateStatus: (status) => status === 200 + }) + .then(res => resolve(res.data)) + .catch(e => reject(e)) + }) + + const fetchPromise = apiPromise('getChatAdministrators').then(admins => { + if (!admins.ok) { + throw new Error('Request to get chat administrators failed') + } + + return apiPromise('getChat').then(chat => { + if (!chat.ok) { + throw new Error('Request to get chat info failed') + } + + let creator + for (const admin of admins.result) { + if (admin.status === 'creator') { + creator = admin.user.username + } + } + + if (!chat.result.description) { + throw new Error('There is no chat description') + } + + if (creator !== data.user) { + throw new Error('User doesn\'t match') + } + + return { + user: creator, + text: chat.result.description + } + }) + }) + + return Promise.race([fetchPromise, timeoutPromise]).then((result) => { + clearTimeout(timeoutHandle) + return result + }) +} diff --git a/src/proxy/api/v2/index.js b/src/proxy/api/v2/index.js index 5cddba4..c16d6ae 100644 --- a/src/proxy/api/v2/index.js +++ b/src/proxy/api/v2/index.js @@ -28,6 +28,9 @@ const opts = { instance: process.env.MATRIX_INSTANCE || null, accessToken: process.env.MATRIX_ACCESS_TOKEN || null }, + telegram: { + token: process.env.TELEGRAM_TOKEN || null + }, xmpp: { service: process.env.XMPP_SERVICE || null, username: process.env.XMPP_USERNAME || null, @@ -172,6 +175,30 @@ router.get( } ) +// Telegram route +router.get( + '/get/telegram', query('chat').isString(), + async (req, res) => { + if (!opts.claims.telegram.token) { + return res.status(501).json({ errors: 'Telegram not enabled on server' }) + } + + const errors = validationResult(req) + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }) + } + + fetcher.telegram + .fn(req.query, opts) + .then((data) => { + return res.status(200).send(data) + }) + .catch((err) => { + return res.status(400).json({ errors: err.message ? err.message : err }) + }) + } +) + // IRC route router.get('/get/irc', query('nick').isString(), async (req, res) => { if (!opts.claims.irc.nick) {