mtproto-core icon indicating copy to clipboard operation
mtproto-core copied to clipboard

Add cloudflare environment.

Open Legend1991 opened this issue 2 years ago • 14 comments

Cloudflare has its own way to work with WebSockets. Turns out to make it work you have to handle the handshaking process yourself using HTTP Upgrade mechanism. Actually it is the only difference from browser implementation.

Legend1991 avatar Dec 18 '21 11:12 Legend1991

The environment means the environment in which the code will be executed. How can cloudflare be an environment? Can you please provide the full context?

alik0211 avatar Dec 22 '21 05:12 alik0211

@alik0211 So Cloudflare provides a serverless service called Cloudflare Workers. It behaves similar to JavaScript in the browser or in Node.js and under the hood, the Workers runtime uses the V8 engine, but there are some differences (e. g. WebSockets). You can read more how Workers works here if you interested in.

I spent a few days trying to understand and get it to work with Cloudflare Workers, thought it would be helpful for anyone looking to use mtproto with Workers.

Legend1991 avatar Dec 23 '21 17:12 Legend1991

@Legend1991 is your fork working with cloudflare worker?

numairawan avatar Sep 29 '22 02:09 numairawan

@Legend1991 is your fork working with cloudflare worker?

@NumairAwan yes it is. I'm using it for my project currently. That was the main purpose.

Legend1991 avatar Sep 29 '22 13:09 Legend1991

I am trying to use in worker from last 5 days but failing. Can you please give example or your main file of mtprot-core or the worker if possible. I tried to browserify the mtproto but still its not working.

numairawan avatar Sep 29 '22 13:09 numairawan

Sure. I have telegram.js file that describes telegram api that I'm using:

const MTProto = require('@mtproto/core/envs/cloudflare');
const { sleep } = require('@mtproto/core/src/utils/common');

class TelegramKVGateway {
  async set(key, value) {
    await TELEGRAM_KV.put(key, value);
  }

  async get(key) {
    return TELEGRAM_KV.get(key);
  }
}

class API {
  constructor() {
    this.mtproto = new MTProto({
      api_id: TELEGRAM_API_ID,
      api_hash: TELEGRAM_API_HASH,

      storageOptions: {
        instance: new TelegramKVGateway(),
      },
    });
  }

  async call(method, params, options = {}) {
    try {
      const result = await this.mtproto.call(method, params, options);

      return result;
    } catch (error) {
      console.log(`${method} error:`, error);

      const { error_code, error_message } = error;

      if (error_code === 420) {
        const seconds = Number(error_message.split('FLOOD_WAIT_')[1]);
        const ms = seconds * 1000;

        await sleep(ms);

        return this.call(method, params, options);
      }

      if (error_code === 303) {
        const [type, dcIdAsString] = error_message.split('_MIGRATE_');

        const dcId = Number(dcIdAsString);

        // If auth.sendCode call on incorrect DC need change default DC, because
        // call auth.signIn on incorrect DC return PHONE_CODE_EXPIRED error
        if (type === 'PHONE') {
          await this.mtproto.setDefaultDc(dcId);
        } else {
          Object.assign(options, { dcId });
        }

        return this.call(method, params, options);
      }

      return Promise.reject(error);
    }
  }
}

const randomID = () =>
  Math.ceil(Math.random() * 0xffffff) + Math.ceil(Math.random() * 0xffffff);

export async function sendMessage(user_id, access_hash, message, entities = []) {
  try {
    const api = new API();
    await api.call('messages.sendMessage', {
      clear_draft: true,
      peer: {
        _: 'inputPeerUser',
        user_id,
        access_hash,
      },
      message,
      entities: [...entities],
      random_id: randomID(),
    });
  } catch (error) {
    console.log('error:', error.message);
  }
}

export async function importContacts(phone) {
  try {
    const api = new API();
    const result = await api.call('contacts.importContacts', {
      contacts: [
        {
          _: 'inputPhoneContact',
          client_id: randomID(),
          phone,
          first_name: `${randomID()}`,
        },
      ],
    });
    return result;
  } catch (error) {
    console.log('error:', error.message);
    return { users: [{}] };
  }
}

Here I use Cloudflare KV storage (TELEGRAM_KV) and it looks like this:

Снимок экрана 2022-09-29 в 17 00 58

So you need to sign in first to get those data. And then this is how I use telegram api:

import * as telegram from './telegram.js';

async function run() {
  const phone = '+12025550163'; // example phone number
  const otp = '123456';
  const text = 'Your confirmation code is ${otp}\nDo not share this code with anyone else.\n\nThis code is valid for 2 minutes';
  const entities = [{ _: 'messageEntityBold', offset: 34, length: 6 }];
  const { users } = await telegram.importContacts(phone);
  const { id, access_hash } = users[0];
  await telegram.sendMessage(id, access_hash, text, entities);
}

Legend1991 avatar Sep 29 '22 14:09 Legend1991

Let me know if you are available. The only reason why i asked you to install it for me is, I am not understanding how you are using mtproto-core api in cloudflare worker. You cant add files and you can also not use require in cf workers. I tried to browserify the mtproto-core but its not working because mtproto-core browser using the some browser function like windows, localstorage etc that don't work in cf worker.

numairawan avatar Sep 29 '22 15:09 numairawan

@NumairAwan to be able to use require in cf workers you need to use Cloudflare's CLI called Wrangler. You can read how to use it here in the cloudflare's doc: https://developers.cloudflare.com/workers/get-started/guide/

Legend1991 avatar Sep 29 '22 18:09 Legend1991

Thanks i tried with wrangler. Seems like it will work but i am getting some errors if you can address.

image

numairawan avatar Sep 29 '22 20:09 numairawan

@NumairAwan that's because "../builder" and "../parser" are generated by npm's prepublishOnly script. install original mtproto from npm: npm install @mtproto/core and then put 2 files from this PR localy (with fixed imports) nearby to telegram.js file but into mtproto-cloudflare folder:

mtproto-cloudflare/index.js content:

const makeMTProto = require('@mtproto/core/src');
const SHA1 = require('@mtproto/core/envs/browser/sha1');
const SHA256 = require('@mtproto/core/envs/browser/sha256');
const PBKDF2 = require('@mtproto/core/envs/browser/pbkdf2');
const Transport = require('./transport');
const getRandomBytes = require('@mtproto/core/envs/browser/get-random-bytes');
const getLocalStorage = require('@mtproto/core/envs/browser/get-local-storage');

function createTransport(dc, crypto) {
  return new Transport(dc, crypto);
}

const MTProto = makeMTProto({
  SHA1,
  SHA256,
  PBKDF2,
  getRandomBytes,
  getLocalStorage,
  createTransport,
});

module.exports = MTProto;

and mtproto-cloudflare/transport.js content:

const Obfuscated = require('@mtproto/core/src/transport/obfuscated');

const subdomainsMap = {
  1: 'pluto',
  2: 'venus',
  3: 'aurora',
  4: 'vesta',
  5: 'flora',
};

const readyState = {
  OPEN: 1,
};

class Transport extends Obfuscated {
  constructor(dc, crypto) {
    super();

    this.dc = dc;
    this.url = `https://${subdomainsMap[this.dc.id]}.web.telegram.org${
      this.dc.test ? '/apiws_test' : '/apiws'
    }`;
    this.crypto = crypto;

    this.connect();
  }

  get isAvailable() {
    return this.socket.readyState === readyState.OPEN;
  }

  async connect() {
    let resp = await fetch(this.url, {
      headers: {
        Connection: 'Upgrade',
        Upgrade: 'websocket',
      },
    });

    // If the WebSocket handshake completed successfully, then the
    // response has a `webSocket` property.
    this.socket = resp.webSocket;
    if (!this.socket) {
      throw new Error("server didn't accept WebSocket");
    }

    this.socket.binaryType = 'arraybuffer';
    this.socket.accept();
    this.socket.addEventListener('error', this.handleError.bind(this));
    this.socket.addEventListener('open', this.handleOpen.bind(this));
    this.socket.addEventListener('close', this.handleClose.bind(this));
    this.socket.addEventListener('message', this.handleMessage.bind(this));
    this.handleOpen();
  }

  async handleError() {
    this.emit('error', {
      type: 'socket',
    });
  }

  async handleOpen() {
    const initialMessage = await this.generateObfuscationKeys();

    this.socket.send(initialMessage);

    this.emit('open');
  }

  async handleClose() {
    if (this.isAvailable) {
      this.socket.close();
    }

    this.connect();
  }

  async handleMessage(event) {
    const obfuscatedBytes = new Uint8Array(event.data);
    const bytes = await this.deobfuscate(obfuscatedBytes);

    const payload = this.getIntermediatePayload(bytes);

    this.emit('message', payload.buffer);
  }

  async send(bytes) {
    const intermediateBytes = this.getIntermediateBytes(bytes);

    const { buffer } = await this.obfuscate(intermediateBytes);

    this.socket.send(buffer);
  }
}

module.exports = Transport;

Then change the first require in telegram.js file to const MTProto = require('./mtproto-cloudflare');

Now this should work.

Legend1991 avatar Sep 30 '22 09:09 Legend1991

@Legend1991 thanks its working now.

numairawan avatar Sep 30 '22 14:09 numairawan

image

how i can use TELEGRAM_KV ? is it possible to use kv storage outside outside the worker.js file

mageshyt avatar Jul 04 '23 15:07 mageshyt

@Legend1991 sorry for the mention. But does this allow me use mtproto proxy on telegram on a cloudflare worker? If yes can you explain it a bit?

parsamrrelax avatar Aug 30 '23 15:08 parsamrrelax

I ran into this PR and observed that the Cloudflare Workers runtime environment supports the implementation of MTProto within the browser environment now. Maybe this changed sometime recently? Not sure if the PR is needed anymore.

awwong1 avatar Mar 10 '24 21:03 awwong1