Giter VIP home page Giter VIP logo

iacecil's Introduction

ia.cecil

Why is everything in Spanish?

First of all, it's Brazilian Portuguese, not Spanish.

There is no english README (apart from the tl;dr part below). Sorry about that, either learn brazilian portuguese or wait for the translation. Millions of people in the internet have been reading in english everyday for decades even though that's not their mother language. You're smart, you can do it too. If you can't, at least you can exercise your patience ;)

TL;DR

Main script uses a mix of MVC design and influence from the python frameworks in use's "way to do it (tm)" and resides at src/iacecil directory (iacecil package).

Plugins are supposed to work as independent scripts and reside at src/plugins directory (plugins package).

Configuration files and local modules / packages are supposed to reside at instance/ directory.

The doc/ directory is supposed to be helpful and have the documentation on everything. But we all know that it is impossible. If you don't agree that 100% of github repositories can only be understood by it's creator, you're not coding for long enough. That said, I'm an easy to reach person and I can answer any questions with attention if you manage to reach me.


O quê

Esta é um chatbot que funciona em múltiplas plataformas baseado em plugins e escrito em Python.

Por quê

O projeto original era a Paloma que era uma chatbot de IRC, e posteriormente se tornou bouncer para várias plataformas. Após isto, a Paloma virou MateBot que é um bot de telegram e discord. O escopo do projeto foi ampliado e se tornou a IA Cecil (ia.cecil), uma inteligência artificial controlada (???) por programação, com uma rede neural simples e acesso a algoritmos de aprendizado de máquina.

Não me pergunte o por quê do nome. Se não for óbvio, eu não quero responder.

Pra quê

Toda vez que eu estou entediado, eu escrevo mais um pouco de código aqui. Isto acontece desde 2011 (calendário gregoriano).

Se este projeto virar um produto de uma startup, a culpa não é minha. Se este projeto virar uma obra coletiva de tecnocracia, a culpa não é minha. Se este projeto virar uma inteligência artificial que cause um grande impacto na história da humanidade, a culpa não é minha. Na altamente improvável eventualidade de a culpa ser minha, eu espero ser avisado a tempo de botar a culpa em quem eu quiser (já que é minha, eu faço o que eu quero).

Estatísticas

    1199 text files.
     532 unique files.
    1033 files ignored.

github.com/AlDanial/cloc v 1.90  T=167.99 s (1.3 files/s, 186.1 lines/s)
--------------------------------------------------------------------------------
Language                      files          blank        comment           code
--------------------------------------------------------------------------------
Python                          177           1810           7242          19152
HTML                             22             19             78           1338
Markdown                          3            343              0           1210
XML                               2              0              0             22
JavaScript                        4              4             19              5
Bourne Again Shell                1              0              0              4
CSS                               3              0             10              3
TOML                              1              0              0              3
--------------------------------------------------------------------------------
SUM:                            213           2176           7349          21737
--------------------------------------------------------------------------------

Como

Roadmap

Ver também o arquivo CHANGES.TXT

Versão 0.1

Lista de forks pre-alpha

Nível de automata: combinational logic

Requisitos e escopo
  • Bot deve responder comandos com valores pré definidos
  • Bot pode ter personalidade fixa configurada previamente e que vai perdurar durante todo o seu funcionamento
  • Novas funcionalidades podem ser acrescentadas através de plugins
  • Funcionalidades podem ser ativadas ou desativadas de acordo com personalidade ou finalidade do bot
  • Sistema de log para depuração
Funcionalidades
Funcionalidades abandonadas

Funcionalidades presentes em forks ou versão v0.0.14.0

  • Conversão de valores [coinmarketcap] (cryptoforex)
  • Integração com bancos de dados externos [velivery] (vegga)
  • Envio de SMS e realização de ligações telefônicas [totalvoice] (vegga)
  • Sistema auxiliar para produção de alimentos [cr1pt0_almoco]
  • Integração com ESP32 e monitoramento climático (climobike)
  • Controle de atividades de trabalho [workrave] ()

Funcionalidades presentes em forks ou versão v0.1.0.0

  • Geração de QR Code (matebot)
  • Geração de números aleatórios (matebot)
  • Cálculo de hash de textos (matebot)
  • Recepção de novos usuários em grupos no Telegram (matebot)
  • Salvar URLs na Wayback Machine (matebot)
  • Download de vídeos do Youtube (matebot)

Versão 0.2

Anúncio versão alpha

Nível de automata: finite-state machine

Requisitos e escopo
  • Bot deve responder comandos de acordo com regras fixas e variáveis conforme aprendizado prévio
  • Bot deve ter personalidade configurada no estado inicial que pode variar e humor que deve variar
  • Funcionalidades podem ser ativadas ou desativadas de acordo com mudança de personalidade, humor ou evento de aprendizado
  • Sistema de coleta de dados para machine learning
  • Bot deve funcionar no Telegram e no Discord
Funcionalidades
  • Faz questionários para usuários e armazena as informações em banco de dados
  • Usa dados obtidos para tomar decisões e adicionar pessoas em grupos de acordo com critérios estabelecidos
  • Cria perfil de pessoas através de análise de respostas
  • Otimiza perfil de pessoas através de análise de comportamento

TODO

  • Traduzir este README
    • Translate the README back to English, Pedro Bó
  • Usar dicionários em todos os retornos de funções
  • Melhorar o empacotamento dos plugins
  • Migrar de telepot para python-telegram-bot tag v0.1.0.0a
  • Acrescentar também código para usar com aiogram tag v0.1.3.0
  • Tratar as exceções corretamente, principalmente as informativas
    • Exceções informativas para quem está tentando instalar o bot do zero suficientemente tratadas e suficientemente informativas com commit 367613a
    • Usar Exception Handling do python-telegram-bot
  • Arquivos para usar com Heroku
  • Arquivos para usar com Docker
  • Documentar o roadmap com issues, milestones e projetos do github
    • Issues feitas durante uma Terça Sem Fim

Versão 0.3

Nível de automata: finite-state machine

Em fase de planejamento, código fechado.


Onde

Importando pacote para usar em outro software

Este software ainda não está no pypi.org, então:

pip install -e git+https://github.com/iuriguilherme/iacecil.git@stable#egg=iacecil  
python -m iacecil production

Desenvolvimento

Se vossa excelência quiserdes usar o código deste bot pra fazer o vosso próprio, vós deveis:

Entenderdes e usardes a licença GPL v3

Para mais informações, veja o arquivo LICENSE.md.

Aprenderdes a usar git

...e incidentalmente, Github, Gitlab ou Notabug - que são coisas completamente diferentes de git.

Para mexer no código agora mesmo no Linux:

user@home:~$ git clone https://github.com/iuriguilherme/iacecil.git  
user@home:~$ cd iacecil  
user@home:~/iacecil$ pyenv exec python -m pip install --user --upgrade pip pipenv
user@home:~/iacecil$ pipenv install -e .
user@home:~/iacecil$ pipenv run test

Se algum dos passos acima não der certo, usardes o vosso próprio método pessoal (virtualenv, virtualenvwrapper, poetry, etc.)

Grupo de usuária(o)s e desenvolvedora(e)s

Telegram

Eu criei um grupo novo para quem quiser conversar sobre, usar, testar, desenvolver e acompanhar o processo de desenvolvimento, teste e uso da bot: https://t.me/joinchat/CwFUFkf-dKW34FPBjEJs9Q

Grupo só para testar bots (pode gerar o caos): https://t.me/joinchat/CwFUFhbgApLHBMLoNnkiRg

Grupo para testar o plugin de logs: https://t.me/joinchat/CwFUFhy2NQRi_9Cc60v_aA

Pra testar o plugin de logs, coloque o bot neste grupo e use o chat_id -481703172 no arquivo de configuração (bot.users['special']['log'])

Discord

https://discord.gg/ZZYukr66


Dependências

Este bot foi testado com Python 3.10; Se vós não tiverdes Python, instale!

Estamos usando FastAPI, Quart, Aiogram, Flask, Python Telegram Bot, discordPy, NLTK, web3, youtube-dl, ZODB, sympy, numpy, matplotlib, mpmath, apscheduler, aiohttp, pandas, openai, furhat, colorama, amazon boto3, então é necessário instalá-los para rodar o bot.

Alguns plugins que tratam arquivos de áudio e de vídeo usam o pacote python-ffmpeg que por sua vez usa o ffmpeg do sistema. Instruções para instalar ffmpeg em cada sistema fogem do escopo deste arquivo.

Ritual de instalação:

pipenv

O jeito que eu faço é com pipenv, inclusive está incluso o Pipfile no repositório:

user@home:~/iacecil$ python3.10 -m ensurepip  
user@home:~/iacecil$ python3.10 -m pip install --user --upgrade pip pipenv  
user@home:~/iacecil$ pipenv install  

Outras formas

Quem não quiser usar pipenv pode usar virtualenvwrapper, virtualenv, poetry, ou outro método de preferência se souber o que está fazendo. Um arquivo requirements.txt é mantido atualizado no repositório.

user@home:~/iacecil$ pyenv exec python -m venv iacecilenv  
user@home:~/iacecil$ ./iacecilenv/bin/activate  
(iacecilenv) pip install -r requirements.txt  

...ou coisa parecida.

TODO: Fazer instruções para usar sem pipenv, para poetry, etc.


Configurando

AVISO: As instruções estão desatualizadas. O fluxo atual é usar um arquivo _bots.py com uma lista assim:

./instance/_bots.py

bots = [
    "testebot",
    "outrobot",
]

E o diretório instance/ fica parecido com isto:

instance/
    _bots.py
    bots/
        __init__.py
        testebot.py
        outrobot.py
    zodb/
    personalidades/
        __init__.py
        teste/
            __init__.py
        outro.py

O arquivo testbot.py deve ser parecido com isto:

instance/bots/testbot.py

from pydantic import BaseSettings

try:
    from .default import DefaultBotConfig
except Exception as e:
    logger.exception(e)
    from iacecil.config import DefaultBotConfig

default_config = DefaultBotConfig()

class BotConfig(BaseSettings):
    coinmarketcap: dict = default_config.coinmarketcap
    discord: dict = default_config.discord
    donate: dict = default_config.donate
    furhat: dict = default_config.furhat
    info: dict = default_config.info
    jobs: list = default_config.jobs
    quart: dict = default_config.quart
    serpapi: dict = default_config.serpapi
    tecido: dict = default_config.tecido
    timezone: str = default_config.timezone
    tropixel: dict = default_config.tropixel
    web3: dict = default_config.web3

    personalidade: str = "cryptoforex"
    plugins: dict = dict(
        enable = [
            "admin",
            "cryptoforex",
            "donate",
            "feedback",
            "qr",
            "web3_wrapper",
            "natural",
            "default",
        ], # enable
        disable = [
            "echo",
            "bomdia",
            "greatful",
            "totalvoice",
            "portaria",
            "archive",
            "calendar",
            "hashes",
            "mate_matica",
            "storify",
            "tts",
            "ytdl",
            "tropixel",
            "welcome",
            "garimpo",
            "tracker",
        ], # disable
    telegram: dict = dict(
        default_config.telegram.copy(),
        token = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
    ) # telegram

Este exemplo vai fazer um bot de telegram parecido com o cryptoforexbot.

Tokens de bot de telegram são obtidas com o @BotFather


Furhat / ChatGPT

Existe um programa para usar um robô furhat (ou SDK) integrado com chatGPT. Depois de configurar o software segundo o passo anterior, lembrando de incluir o dicionário de configuração furhat e openai:

from pydantic import BaseSettings

try:
    from .default import DefaultBotConfig
except Exception as e:
    logger.exception(e)
    from iacecil.config import DefaultBotConfig

default_config = DefaultBotConfig()

class BotConfig(BaseSettings):
    furhat: dict = {
        'bot': "f1",
        'address': "127.0.0.1", # Furhat RemoteAPI ou SDK
        'voice': "Camila-Neural",
        'mask': "adult",
        'character': "Kione",
        'language': "pt-BR",
    }
    openai: dict = {
        'api_keys': [
            'sk-1234567890abcdefABCDEF',
        ], # api_keys
    } # openai

O programa roda com esse controlador:

python -m iacecil fpersonas  

Flask / Quart

Para usar a versão com Flask (ou Quart), é necessário também renomear o arquivo doc/default_env para .env. Ou criar um arquivo .env com as variáveis FLASK_APP e FLASK_ENV (ou QUART_APP / QUART_ENV). Caso estes arquivos não existam, as configurações padrão serão usadas.

Na configuração padrão do Quart, a aplicação é servida com um arquivo Unix Socket uvicorn.sock. Para usar uma porta e um endereço TCP/IP, basta manter a configuração das portas e mudar config.socket para None.

TODO: Testar o método acima pra ter certeza de que é verdade


Rodando

No diretório principal da iacecil:

pipenv

Para rodar com pipenv, assumindo que a configuração já está correta:

user@home:~/iacecil$ pipenv run test  

Sem pipenv

Depois de instalado como pacote editável:

python -m iacecil  

Deploy / produção

Systemd

Exemplo de arquivo para usar com systemd:

[Unit]
Description=IACecil daemon
After=network.target nss-lookup.target

[Service]
Type=simple
ExecStart=/home/user/.local/bin/pipenv run prod
## Sem pipenv:
#ExecStart=/home/user/.local/bin/python -m iacecil production
WorkingDirectory=/home/user/iacecil/
Restart=on-failure

[Install]
WantedBy=multi-user.target

O python em ExecStart deve ser o mesmo python usado para instalar as dependências.

Em um sistema Debian, este arquivo deveria estar em ${HOME}/.config/systemd/user/iacecil.service.

Habilitando o serviço na inicialização do sistema e iniciando agora:

user@home:~$ systemctl --user daemon-reload  
user@home:~$ systemctl --user enable iacecil.service  
user@home:~$ systemctl --user -l start iacecil.service  

Para ver se está funcionando:

user@home:~$ systemctl --user -l status iacecil.service  

Parar:

user@home:~$ systemctl --user stop iacecil.service  

Remover da inicialização:

user@home:~$ systemctl --user disable iacecil.service  

Reiniciar:

user@home:~$ systemctl --user -l restart iacecil.service  

Para o caso de usar systemd como root, o arquivo de configuração deve estar em /lib/systemd/system/iacecil.service, e os comandos devem ser utilizados sem o --user, como por exemplo:

root@home:/root# systemctl -l restart iacecil.service  

Mas eu não recomendo esta abordagem.

Crontab

Também é possível usar cron para verificar se o bot está no ar periodicamente:

user@home:~$ crontab -e  

Adicione uma linha como por exemplo esta na crontab:

*/10 * * * * /usr/lib/systemctl --user is-active iacecil.service || /usr/lib/systemctl --user restart iacecil.service  

Isto vai verificar se o bot está no ar a cada 10 minutos, e reiniciar o serviço caso esteja fora do ar.

Docker

AVISO: O dockerfile foi criado para uma versão antiga do software, ainda não foi adaptado para a versão atual.

FIXME: Adaptar o dockerfile e remover este aviso

Adicione seu token em BOTFATHER_TOKEN no arquivo doc/default_config.py e depois rode os comandos abaixo na raiz do projeto

docker build -t iacecil -f Dockfile .
docker run -d --name iacecil iacecil
docker inspect iacecil | grep IPAddress

Após esses comandos você terá o IP do seu container pegue esse IP e acesse via CURL IP:5000


Licença

Copyleft 2012-2023 Iuri Guilherme https://iuri.neocities.org/

Este programa é um software livre; você pode redistribuí-lo e/ou
modificá-lo sob os termos da Licença Pública Geral GNU como publicada
pela Free Software Foundation; na versão 3 da Licença, ou
(a seu critério) qualquer versão posterior.

Este programa é distribuído na esperança de que possa ser útil,
mas SEM NENHUMA GARANTIA; sem uma garantia implícita de ADEQUAÇÃO
a qualquer MERCADO ou APLICAÇÃO EM PARTICULAR. Veja a
Licença Pública Geral GNU para mais detalhes.

Você deve ter recebido uma cópia da Licença Pública Geral GNU junto
com este programa (veja o arquivo LICENSE.md).
Se não, veja http://www.gnu.org/licenses/.

iacecil's People

Contributors

dependabot[bot] avatar iuriguilherme avatar syndikos-bot avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

iacecil's Issues

[metamemo] Criar personalidade "custom"

Criar personalidade que pode ser totalmente escrita em instance/

No futuro é interessante que tenha como importar personalidade como plugin (como era antigamente), e migrar as existentes para que sejam plugins

Referência nula

aiogram.utils.exceptions.BadRequest: Not enough rights to send text messages to the chat

During handling of the above exception, aasnother exception occurred:

Traceback (most recent call last):
  File "iacecil/src/iacecil/controllers/aiogram_bot/bot.py", line 233, in exception_handler
    await error_callback(
  File iacecil/src/iacecil/controllers/aiogram_bot/callbacks.py", line 89, in error_callback
    await debug_logger(error, message, exception, descriptions)
  File "iacecil/src/iacecil/controllers/log.py", line 116, in debug_logger
    bot: Bot = dispatcher.bot
AttributeError: 'NoneType' object has no attribute 'bot'

[feature request] Monitorar sites e emitir alerta

Ninguém me avisou que o matepad tava fora do ar, até porque isso é trabalho pra robô.

Fazer uma requisição no cabeçalho (HTTP HEAD) e se der 503 (ou qualquer coisa que não seja 200), mandar mensagem pros chats configurados.

Dessa vez talvez tentar fazer um plugin que os usuários possam manipular e criar alertas sob demanda

Exceção com tts

Traceback (most recent call last):
  File "/media/sdd1/iuri/.local/share/virtualenvs/iacecil.0.2-rGDnHZYS/src/iacecil/src/iacecil/controllers/aiogram_bot/bot.py", line 65, in exception_handler
    self.command = await super_function(*args, **kwargs)
  File "/media/sdd1/iuri/.local/share/virtualenvs/iacecil.0.2-rGDnHZYS/lib/python3.10/site-packages/aiogram/bot/bot.py", line 346, in send_message
    result = await self.request(api.Methods.SEND_MESSAGE, payload)                                                                                    File "/media/sdd1/iuri/.local/share/virtualenvs/iacecil.0.2-rGDnHZYS/lib/python3.10/site-packages/aiogram/bot/base.py", line 236, in request
    return await api.make_request(await self.get_session(), self.server, self.__token, method, data, files,
  File "/media/sdd1/iuri/.local/share/virtualenvs/iacecil.0.2-rGDnHZYS/lib/python3.10/site-packages/aiogram/bot/api.py", line 140, in make_request
    return check_result(method, response.content_type, response.status, await response.text())
  File "/media/sdd1/iuri/.local/share/virtualenvs/iacecil.0.2-rGDnHZYS/lib/python3.10/site-packages/aiogram/bot/api.py", line 111, in check_result
    raise exceptions.RetryAfter(parameters.retry_after)
aiogram.utils.exceptions.RetryAfter: Flood control exceeded. Retry in 13 seconds.
ERROR:instance.personalidades.pacume:'Voice' object has no attribute 'get'
Traceback (most recent call last):
  File "/media/sdd1/iuri/usr/lib/iacecil.0.2/instance/personalidades/pacume/__init__.py", line 100, in homofobico
    command.voice.get('file_unique_id'),                                                                                                            AttributeError: 'Voice' object has no attribute 'get'

Change File Storages to fewer files

With ZODB use fewer files, if not only one, with OOBTrees and IOBTrees instead of one file per chat and test for performance improvement (or not).

[feature request] ddg

https://t.me/tropixelcafe/17825

Desobediente Civil ♾️, [27/07/2022 17:34]
pra não dizer que eu não fiz bosta nenhuma eu vou fazer o plugin de busca no duck.com pro tropecsel, filmar o processo e botar pra chover na nuvem

Desobediente Civil ♾️, [27/07/2022 17:34]
e aí arrumar um monitor pra ligar no lenovo de prêmio

Cache chat information

Chat information in the updates/ route is requested everytime the page GET or POST, which is not a blocking call (uses aiohttp) but it slows down because it's a telegram api request subject to rate limit, also the routine is an unecessary bloat of list comprehensions with tests.

Proposed solution is having a file in the form of instance/zodb/123botid.456chatid.meta.fs with versioned information which can be manually updated via a route.

Also this is required because there are private groups where some of the bots have been already kicked and therefore none of them can request information. It would be desirable that such a route bruteforces on all available bots from information on chats for this particular reason.

All routines which request information about chats should use latest valid information stored on zodb filesystem/database.

Rewrite handlers for non telegram inputs

Either find a way to integrate furhat and other systems to the aiogram handlers currently in use, or write from scratch a similar system.

The lamest, untested, not yet improved idea:

async def iterations_startswith(bot):
    ## Should return the text which the sentence should start with to trigger, and the callback to be awaited
    return database_iterations_startswith(bot)
async def handling_startswith(bot, sentence):
    for iteration in await iterations_startswith(bot):
        if sentence.startswith(iteration.text):
            return iteration.callback
    return None

async def iterations_contains(bot):
    ## Should return the text which the sentence should start with to trigger, and the callback to be awaited
    return database_iterations_contains(bot)
async def handling_contains(bot, sentence):
    for iteration in await iterations_contains(bot):
        ## This is the Python implementation for string.contains()
        if iteration.text in sentence:
            return iteration.callback
    return None

## This argument is a aiogram.Message
async def universal_handler(message):
    for dispatcher in dispatchers:
        ## We will only check full message if it doesn't starts with something pre defined
        callback = await handling_startswith(message.text)
        if callback is None:
            callback = await handling_contains(message.text)
        if callback is not None:
            await callback()

This approach uses the same mindset of the aiogram handlers in the sense that first match determines what callback will handle the given command. To change behavior for multiple triggers, the handling_ methods should return a list of callbacks instead of just one.

[web] send message

Route /admin/send_message/

"TypeError(\"int() argument must be a string, a bytes-like object or a number, not 'NoneType'\")"

Plugin openai

O botonaro parou de funcionar com os handlers da personalidade custom até que eu desativei o plugin openai.

Bot has no rights

#exception #sendMessage #TelegramAPIError 

"BadRequest('Not enough rights to send text messages to the chat')"


"Not enough rights to send text messages to the chat"


I don't know what just happened

BadRequest('Not enough rights to send text messages to the chat')

ao invés de ficar mandando essa mensagem pro grupo de debug, mandar UMA vez a mensagem pro grupo de debug dando o id do chat, e marcar o chat como não autorizado pelo menos pro sistema de log.

Pacote só funciona em modo editável

iuri@navi:~/usr/lib/iacecil$ pipenv --python 3.9 install --verbose git+https://github.com/iuriguilherme/[email protected]#egg=iacecil
iuri@navi:~/usr/lib/iacecil$ pipenv run python -m iacecil production
Running iacecil v0.2.6 with args: ['production']
No module named 'iacecil.controllers'
Traceback (most recent call last):
  File "/home/iuri/.local/share/virtualenvs/iacecil-abcdefghi/lib/python3.9/site-packages/iacecil/__main__.py", line 37, in <module>
    from .controllers._iacecil import production
ModuleNotFoundError: No module named 'iacecil.controllers'

"Solução" pra preguiçoso é instalar como pacote editável em modo de desenvolvimento

Load plugins from config file

Plugin loading currently is harcoded in aiogram init;

When it should be done using configuration directives already implemented in the config file, attributed to the bot object, but currently unused. print(current_app.dispatchers[0].bot.plugins['epsilon']) should print a list of first bot's plugins that should be loaded for general use, if properly configured.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.