Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cloudflare environment. #229

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Legend1991
Copy link

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.

@alik0211
Copy link
Owner

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?

@Legend1991
Copy link
Author

@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.

@numairawan
Copy link

@Legend1991 is your fork working with cloudflare worker?

@Legend1991
Copy link
Author

@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.

@numairawan
Copy link

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.

@Legend1991
Copy link
Author

Legend1991 commented Sep 29, 2022

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);
}

@numairawan
Copy link

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.

@Legend1991
Copy link
Author

@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/

@numairawan
Copy link

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

image

@Legend1991
Copy link
Author

@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.

@numairawan
Copy link

numairawan commented Sep 30, 2022

@Legend1991 thanks its working now.

@mageshyt
Copy link

mageshyt commented Jul 4, 2023

image

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

@parsamrrelax
Copy link

@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?

@awwong1
Copy link

awwong1 commented Mar 10, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants