add: initial working poc

This commit is contained in:
2022-01-14 14:46:59 +03:00
commit 4372632d93
7 changed files with 290 additions and 0 deletions

70
src/bot.js Normal file
View File

@@ -0,0 +1,70 @@
// std lib
const crypto = require("crypto");
// npm imports
const { Bot, Keyboard } = require("grammy");
// module imports
const cache = require("./cache");
const request = require("./request");
// TODO: get value from confini
// TODO: webhook support
const bot = new Bot("");
// TODO: handle errors
bot.command("start", async (ctx) => {
const tgLinked = await cache.get(ctx.msg.from.id);
if (tgLinked) {
await cache.set(tgLinked, crypto.randomBytes(16).toString("hex"));
const res = await request.proxy(tgLinked);
return ctx.reply(res.text);
}
const keyboard = new Keyboard();
return await ctx.reply(
"Click on the button below to link your phone number.",
{
reply_markup: keyboard.requestContact("Link Phone"),
}
);
});
// TODO: handle errors
bot.on("message:text", async (ctx) => {
const tgLinked = await cache.get(ctx.msg.from.id);
if (tgLinked) {
const res = await request.proxy(tgLinked, ctx.msg.text);
if (res.code !== "END") {
return ctx.reply(res.text);
}
return await cache.del(tgLinked);
}
return await ctx.reply(
"Session expired or account not linked. /start the bot again to access your Sarafu account."
);
});
// TODO: handle errors
bot.on("msg:contact", async (ctx) => {
// TODO: check if msg is reply from bot to prevent contact share bypass
const contact = ctx.msg.contact;
if (contact.phone_number.slice(0, 4) !== "+254") {
return ctx.reply("Sarafu is only available in Kenya at the moment.");
}
// TODO: use contact.user_id in actual guard to prevent contact share bypass
await cache.set(ctx.msg.from.id, contact.phone_number.slice(1));
return ctx.reply(
"Phone number successfully linked. /start the bot again to access your Sarafu account."
);
});
bot.start();

6
src/cache.js Normal file
View File

@@ -0,0 +1,6 @@
const Redis = require("ioredis");
// TODO: get value from confini
const cache = new Redis();
module.exports = cache;

30
src/request.js Normal file
View File

@@ -0,0 +1,30 @@
// npm imports
const phin = require("phin");
// module imports
const cache = require("./cache");
const util = require("./utils");
// proxy requests to ussd-server
// TODO: handle errors
async function proxy(phone, input = "") {
const sessionId = await cache.get(phone);
const { body } = await phin({
// TODO: get value from confini
url: "",
method: "POST",
parse: "string",
form: {
sessionId: sessionId,
phoneNumber: phone,
// TODO: get value from confini
serviceCode: "",
text: input,
},
});
return util.parseUssdResponse(body);
}
module.exports = { proxy };

20
src/utils.js Normal file
View File

@@ -0,0 +1,20 @@
// this regex extracts ussd reply options
const regex = /(\d).\s/g;
// TODO: converts the text to a telegram keyboard
function createKeyboard(input) {
return Array.from(input.matchAll(regex), (m) => m[1]);
}
// parses ussd responses to code and text
function parseUssdResponse(input) {
return {
code: input.slice(0, 3),
text: input.slice(4),
};
}
module.exports = {
createKeyboard,
parseUssdResponse,
};