Uma jigajoga bacana

Jigajoga |ó| (ji-ga-jo-ga) - Artifício, ludíbrio; mecanismo ou solução resultante de improvisação. Depois que eu aprendi essa palavra eu nunca mais consegui dizer hack ou gambiarra, só jigajoga. E hoje vou contar de uma jigajoga do trampo.

O galho

Esses tempos no trampo eu precisava identificar um usuário que chega no nosso whatsapp. Em geral pra mandar alguém pro whatsapp você só manda url https://wa.me/<telefone>?text=oi. O problema aí é que eu não faço ideia de como o usuário chegou no whatsapp. Ele pode ter acessado via um link ou ter simplesmente chegado no whatsapp e falado oi. A única coisa que consigo saber com isso é o texto que o usuário me mandou e o número do telefone dele.

A primeira parte da coisa é simples, ao invés de enviar o usuário para o link do whatsapp direto, manda um link pra mim, aí eu consigo gerar um fingerprint do usuário e depois redirecionar para o whatsapp. Mas depois que mandar o usuário pro whatsapp, como eu sei que o usuário que chegou lá é o usuário x?

A ideia

Logo de cara um colega me mostrou como outra empresa tava fazendo isso: mandando uma string XYZ na mensagem, pedindo para o usuário enviar essa mensagem com a string e essa string serviria de id pra identificar o usuário. Mas isso é feio pra caralho. Então minha primeira ideia foi usar caracteres “invisíveis” (non-printable) no meio da mensagem. E bom, se eu precisava de uma identificação única de usuário o óbvio seria uuid, no caso o 4.

Então peguei uma lista de 17 caracteres “invisíveis” (16 pra a-f e 1 pra “-“) e aí fazia a tradução da representação em string de um uuid v4 pra uma string invisível usando os caracteres non-printable. Subi a coisa rapidinho, testei no meu whatsapp, funcionou. Coisa linda.

O código ficou mais ou menos assim:

# -*- coding: utf-8 -*-

NON_PRINTABLE_CHARS = [
    '\u200b',
    '\u2060',
    '\u2061',
    '\u2062',
    '\u2063',
    '\u2064',
    '\u2066',
    '\u2067',
    '\u2068',
    '\u2069',
    '\u206A',
    '\u206B',
    '\u206C',
    '\u206D',
    '\u206E',
    '\u206F',
    '\uFE06',
]

PRINTABLE_CHARS = '0123456789abcdef-'


def translate_uuid_to_invisible(u):
    inv = ''
    ustr = str(u)
    for i in range(len(ustr)):
        inv += NON_PRINTABLE_CHARS[PRINTABLE_CHARS.index(ustr[i])]

    return inv

def translate_uuid_from_invisible(inv):
    u = ''
    for i in range(len(inv)):
        u += PRINTABLE_CHARS[NON_PRINTABLE_CHARS.index(inv[i])]

    return u

def put_fingerprint(text, uuid):
    inv = translate_uuid_to_invisible(uuid)
    t = text[0] + inv + text[1:]
    return t

def get_fingerprint(t):
    if t[1] not in NON_PRINTABLE_CHARS:
        return ''

    inv = t[1:37]
    u = translate_uuid_from_invisible(inv)
    return u


if __name__ == '__main__':
    from uuid import uuid4

    txt = 'Olá, mundo!'
    u = uuid4()

    t = put_fingerprint(txt, u)
    fp = get_fingerprint(t)

    assert str(u) == fp

Claro que nunca funciona de primeira

Depois com mais testes percebi que eu tinha um problema: no whatsapp web, a depender do uuid, ficava uns espaços em branco no meio do texto, algo tipo o i. e isso por causa da combinação de caracteres. Apesar dos caracteres que eu escolhi não terem uma representação, eles tem uma função e a maioria deles eu nem sei qual a função.

Pra corrigir isso, eu primeiro tentei ir substituindo os caracteres por outros até que desse certo, mas é um trabalhão danado, sem change. Então eu precisava diminuir o número de caracteres usados pra ficar mais fácil a coisa de tirar os espaços do whatsapp web.

Menos (caracteres) é mais (espaço)

Um uuid é um número de 128 bits, então eu pensei em escrever isso em “binário”, aí eu só precisaria de dois caracteres, mas em contrapartida eu teria 128 caracteres a mais em cada mensagem. Nada é grátis, mas era mais importante o texto ficar “certo” do que o tamanho da mensage.

Escolhi os caracteres u200b (zero width space) e u2060 (zero width word joiner), alerei o código pra pegar a string com a representação do número do uuid em binário e simplesmente troquei 0 e 1 pelos caracteres zero witdh escolhidos. Altera o código, sobre rapidinho, testa, testa, testa… Bada bim! Bada bam! Bada bum! Dessa fez funcionou. Merge no master, pusha e vai! Daqui a uns minutinhos tá em prd!

O código alterado ficou mais ou menos assim:

# -*- coding: utf-8 -*-

from uuid import UUID

NON_PRINTABLE_CHARS = [
    '\u200b',
    '\u2060',
]

PRINTABLE_CHARS = '01'


def translate_uuid_to_invisible(u):
    inv = ''
    bitstr = f'{u.int:128b}'.replace(' ', '0')
    for i in range(len(bitstr)):
        inv += NON_PRINTABLE_CHARS[PRINTABLE_CHARS.index(bitstr[i])]

    return inv

def translate_uuid_from_invisible(inv):
    bitstr = ''
    for i in range(len(inv)):
        bitstr += PRINTABLE_CHARS[NON_PRINTABLE_CHARS.index(inv[i])]

    n = int(bitstr, 2)
    u = UUID(int=n)
    return u

def put_fingerprint(text, uuid):
    inv = translate_uuid_to_invisible(uuid)
    t = text[0] + inv + text[1:]
    return t

def get_fingerprint(t):
    if t[1] not in NON_PRINTABLE_CHARS:
        return ''

    inv = t[1:129]
    u = translate_uuid_from_invisible(inv)
    return u


if __name__ == '__main__':
    from uuid import uuid4

    txt = 'Olá, mundo!'
    u = uuid4()

    t = put_fingerprint(txt, u)
    fp = get_fingerprint(t)

    assert u == fp

Ainda tem problemas, 128 caracteres a mais pra um simples «oi» é feio, se eu printar isso num terminal ainda fica um espaço entre as letras, mas não é pra usar no terminal mesmo… E isso é uma jigajoga, não dá pra esperar muito, só que resolva o problema em mãos.

Moral da história: Não tem!