GithubHelp home page GithubHelp logo

cycoresystems / ari-proxy Goto Github PK

View Code? Open in Web Editor NEW
78.0 12.0 35.0 1.76 MB

NATS or RabbitMQ message bus Asterisk REST Interface proxy system implemented in Go

License: Other

Makefile 0.09% Go 99.71% Shell 0.09% Dockerfile 0.10%
asterisk ari golang nats rabbitmq

ari-proxy's Introduction

ari-proxy

Build Status

Proxy for the Asterisk REST interface (ARI).

The ARI proxy facilitates scaling of both applications and Asterisk, independently and with minimal coordination. Each Asterisk instance and ARI application pair runs an ari-proxy server instance, which talks to a common NATS or RabbitMQ cluster. Each client application talks to the same message bus. The clients automatically and continuously discover new Asterisk instances, so the only coordination needed is the common location of the message bus.

The ARI proxy allows for:

  • Any number of applications running the ARI client
  • Any number of ari-proxy services running on any number of Asterisk instances
  • Simple call control throughout the cluster, regardless of which Asterisk instance is controlling the call
  • Simple call distribution regardless of the number of potential application services. (New calls are automatically sent to a single recipient application.)
  • Simple call event reception by any number of application clients. (No single-app lockout)

Supported message buses:

Proxy server

Docker images are kept up to date with releases and are tagged accordingly. The ari-proxy does not expose any services, so no ports need to be opened for it. However, it does need to know how to connect to both Asterisk and the message bus.

   docker run \
     -e ARI_APPLICATION="my_test_app" \
     -e ARI_USERNAME="demo-user" \
     -e ARI_PASSWORD="supersecret" \
     -e ARI_HTTP_URL="http://asterisk:8088/ari" \
     -e ARI_WEBSOCKET_URL="ws://asterisk:8088/ari/events" \
     -e MESSAGEBUS_URL="nats://nats:4222" \
     cycoresystems/ari-proxy

Binary releases are available on the releases page.

You can also install the server manually:

   go install github.com/CyCoreSystems/ari-proxy/v5

Client library

ari-proxy uses semantic versioning and standard Go modules. To use it in your own Go package, simply reference the github.com/CyCoreSystems/ari-proxy/client/v5 package, and your dependency management tool should be able to manage it.

Usage

Connecting the client to NATS is simple:

import (
   "github.com/CyCoreSystems/ari/v5"
   "github.com/CyCoreSystems/ari-proxy/v5/client"
)

func connect(ctx context.Context, appName string) (ari.Client,error) {
   c, err := client.New(ctx,
      client.WithApplication(appName),
      client.WithURI("nats://natshost:4222"),
   )
}

Connecting the client to RabbitMQ is like:

import (
   "github.com/CyCoreSystems/ari/v5"
   "github.com/CyCoreSystems/ari-proxy/v5/client"
)

func connect(ctx context.Context, appName string) (ari.Client,error) {
   c, err := client.New(ctx,
      client.WithApplication(appName),
      client.WithURI("amqp://user:password@rabbitmqhost:5679/"),
   )
}

Configuration of the client can also be done with environment variables. ARI_APPLICATION can be used to set the ARI application name, and MESSAGEBUS_URL can be used to set the message bus URL. Doing so allows you to get a client connection simply with client.New(ctx).

Once an ari.Client is obtained, the client functions exactly as the native ari client.

More documentation:

Context

Note the use of the context.Context parameter. This can be useful to properly shutdown the client by a controlling context. This shutdown will also close any open subscriptions on the client.

Layers of clients can be used efficiently with different contexts using the New(context.Context) function of each client instance. Subtended clients will be closed with their parents, use a common internal message bus connection, and can be severally closed by their individual contexts. This makes managing many active channels easy and efficient.

Lifecycle

There are two levels of client in use. The first is a connection, which is a long-lived network connection to the message bus. In general, the end user should not close this connection. However, it is available, if necessary, as DefaultConn and offers a Close() function for itself.

The second level is the ARI client. Any number of ARI clients may make use of the same underlying connection, but each client maintains its own separate bus and subscription implementation. Thus, when a user closes its client, the connection is maintained, but all subscriptions are released. Users should always Close() their clients when done with them to avoid accumulating stale subscriptions.

Clustering

The ARI proxy works in a cluster setting by utilizing two coordinates:

  • The Asterisk ID
  • The ARI Application

Between the two of these, we can uniquely address each ARI resource, regardless of where the client is located. These pieces of information are handled transparently and internally by the ARI proxy and the ARI proxy client to route commands and events where they should be sent.

Message bus protocol details

The protocol details described below are only necessary to know if you do not use the provided client and/or server. By using both components in this repository, the protocol details below are transparently handled for you.

Subject structure

The message bus subject prefix defaults to ari., and all messages used by this proxy will be prefixed by that term.

Next is added one of four resource classifications:

  • event - Messages from Asterisk to clients
  • get - Read-only requests from clients to Asterisk
  • command - Non-creation operational requests from clients to Asterisk
  • create - Creation-related requests from clients to Asterisk

After the resource, the ARI application is appended.

Finally, the Asterisk ID will be added to the end. Thus, the subject for an event for the ARI application "test" from the Asterisk box with ID "00:01:02:03:04:05" would look like:

ari.event.test.00:01:02:03:04:05

For a channel creation command to the same app and node:

ari.create.test.00:01:02:03:04:05

The Asterisk ID component of the subject is optional for commands. If a command does not include an Asterisk ID, any ARI proxy running the provided ARI application may respond to the request. (Thus, implicitly, each ARI proxy service listens to both its Asterisk ID-specific command subject and its generic ARI application command subject. In fact, each ARI proxy listens to each of the three levels. A request to ari.command will result in all ARI proxies responding.)

This setup allows for a variable generalization in the listeners by using message bus wildcard subscriptions. For instance, if you want to receive all events for the "test" application regardless from which Asterisk machine they come, you would subscribe to:

ari.event.test.> //NATS ari.event.test.# //RabbitMQ

Dialogs

Events may be further classified by the arbitrary "dialog" ID. If any command specifies a Dialog ID in its metadata, the ARI proxy will further classify events related to that dialog. Relationships are defined by the entity type on which the Dialog-infused command operates.

Dialog-related events are published on their own message bus subject tree, dialogevent. Thus dialogs abstract ARI application and Asterisk ID. An event for dialog "testme123" would be published to:

ari.dialogevent.testme123

Keep in mind that regardless of dialog associations, all events are also published to their appropriate canonical message bus subjects. Dialogs are intended as a mechanism to:

  • reduce client message traffic load
  • transcend ARI Applications and/or Asterisk nodes while maintaining logical separation of events

Message delivery

The means of a delivery for a generically-routed message depends on the type of message it is.

  • Events are always delivered to all listeners.
  • Read-only commands are delivered to all listeners.
  • Non-creation operation commands are delivered to all listeners.
  • Creation-related commands are delivered to only one listener.

Thus, for efficiency, it is always recommended to use as precise a subject line as possible.

Node discovery

Each ARI proxy sends out a periodic ping announcing itself in the cluster. Clients may aggregate these pings to construct an expected map of machines in the cluster. Knowing this map allows the client to optimize its all-listener commands by cancelling the wait period if it receives responses from all nodes before the timeout has elapsed.

ARI proxies listen to ari.ping and send announcements on ari.announce. The structure of the announcement is thus:

{
   "asterisk": "00:10:20:30:40:50",
   "application": "test"
}

Payload structure

For most requests, payloads exactly match their ARI library values. However, treatment of handlers is slightly different.

Instead of a handler, an Entity or array of Entitys is returned. This response type contains the Metadata for the entity (ARI application, Asterisk ID, and optionally Dialog) as well as the unique ID of the entity.

ari-proxy's People

Contributors

darrensessions avatar goharahmed avatar jose-lopes avatar mtryfoss avatar sheenobu avatar ulexus avatar vitorespindola avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ari-proxy's Issues

go get: multiple-value uuid.NewV1() in single-value context

Hi, when I launch "go get" to install your proxy, I get these errors:

../../../ari/client/native/bridge.go:28: multiple-value uuid.NewV1() in single-value context
../../../ari/client/native/bridge.go:140: multiple-value uuid.NewV1() in single-value context
../../../ari/client/native/channel.go:92: multiple-value uuid.NewV1() in single-value context
../../../ari/client/native/channel.go:121: multiple-value uuid.NewV1() in single-value context
../../../ari/client/native/channel.go:343: multiple-value uuid.NewV1() in single-value context
../../../ari/client/native/client.go:89: multiple-value uuid.NewV1() in single-value context

Am I doing something wrong or is there any bug?
Is it just a notice message that I can ignore?
Thanks
Gabriele

Client requests needs to handle multiple responses

Client requests which do not have complete coordinates (Dialog or App+AsteriskNode), need to wait for responses from all proxies, since they do not know which proxy (if any) a given resource is on.

To do this, we need to:

  • maintain the list of proxies
    • when a client starts, it should ping for proxies
    • clients should have a maintenance thread which listens for, adds, and expires its list of proxies over time
  • instead of using nc.Request for such queries, use explicit nc.Publish and nc.Subscribe operations with an accumulator
  • filter responses to incomplete-coordinate requests such that failing responses are ignored

client: various handle-producing routines must be validated

The current commit a563998 made a number of changes to the request types which are not allowable.

We have to allow for the fact that keys may not be complete. For handles derived from other existing resources, we cannot use a simple commandRequest. We have to use a request which can scale out to locate the other resource.

This can be done by first performing a getRequest for the original resource to get the location details from that key, then using something like ari.NewKey(key.Kind, key.ID, ari.WithLocationOf(foundKey)) to construct a complete key.

Fix subscription model

Server subscriptions are functional, but Client subscriptions are completely broken.

Any time a XXXHandle is returned or created on the client, it needs to have metadata for that resource. This includes XXX.Get() calls, XXX.List() calls, and any creation calls. Lists should now all include metadata from the server side, and Create() calls should always have done so.

  • Get calls need to obtain the metadata for their handle, which will likely include requesting Data() for that resource from the proxy server.
  • Next, all of the client Requests need to be re-written to make use of this metadata in construction of their subjects/topics.
  • Add a proxy.MetadataOf() function which can take in a handle and return its metadata. This is necessary because the ari.XXXHandle interfaces do not supply an accessor function for ari-proxy-specific Metadata.

Add support for Asterisk > 14 ARI "channelvars" support in ws events

example of event:
< {
"type": "Dial",
"timestamp": "2018-04-21T18:48:30.969+0200",
"dialstatus": "",
"forward": "",
"dialstring": "100",
"peer": {
"id": "1",
"name": "SIP/100-00000001",
"state": "Down",
"caller": {
"name": "Joe User 100",
"number": "000000"
},
"connected": {
"name": "",
"number": "000000"
},
"accountcode": "",
"dialplan": {
"context": "local",
"exten": "",
"priority": 1
},
"creationtime": "2018-04-21T18:48:30.968+0200",
"language": "en",
"channelvars": {
"foo": "foovariabletest",
"bar": "barvariabletest"
}
},

Channelvars with variables foo ad bar are not parsed in ari-proxy.

EDIT:
more info
https://issues.asterisk.org/jira/browse/ASTERISK-26492

Thanks

Getting error Failed to construct websocket url

I am running ari-proxy 5.2.3 but getting following error

Error: failed to connect to ARI: failed to create websocket configuration: Failed to construct websocket config: parse "\"ws://localhost:8088/ari/events\"?app=my_test_app": invalid URI for request
t=2022-06-02T13:04:47+0000 lvl=info msg="starting ari-proxy server" version=5.2.3
t=2022-06-02T13:04:47+0000 lvl=eror msg="server died" error="failed to connect to ARI: failed to create websocket configuration: Failed to construct websocket config: parse \"\\\"ws://localhost:8088/ari/events\\\"?app=my_test_app\": invalid URI for request"

Passed all environment variables as follows.

     -e ARI_APPLICATION="my_test_app" \
     -e ARI_USERNAME="demo-user" \
     -e ARI_PASSWORD="supersecret" \
     -e ARI_HTTP_URL="http://localhost:8088/ari" \
     -e ARI_WEBSOCKET_URL="ws://localhost:8088/ari/events" \
     -e NATS_URL="nats://nats:4222" \

Issue with Asterisk node-id

Hello,

We've got Asterisk running on a server where two physical interfaces are bonded. The bonding driver picks one of the physical adapters MAC-address.

I did some maintenance which caused a quick down/up of the interface. For some reason the other MAC-address was then selected.
ari-proxy was running through the process.

After this all calls stopped working, and looking at NATS-traces it's pretty clear what happens:

PUB ari.announce 57.
{"node":"ac:1f:6b:76:7a:ba","application":"mobile_stage"}. <- old mac used in announce

MSG ari.event.mobile_stage.ac:1f:6b:76:7a:ba 2 615. <- old mac used for subject string
{"application":"mobile_stage","asterisk_id":"ac:1f:6b:76:7a:bb" <- new mac used in the actual event

This is a corner case, but I thought would let you know.

Maybe we could add some check to the function publishing StasisStart and see if asterisk_id has been changed and then maybe reinitialize everything?

Asterisk down, but ari-proxy is still running

Normally not an issue, but if this happens the ari-proxy will consume for example originate commands not destined for a specific Asterisk server from NATS and they will fail.

Should there be some kind of temp unsubscribe feature in ari-proxy against NATS if the connection towards Asterisk is down?

Wait for nats (and asterisk)

In k8s reconciliation model, a failing ari-proxy that triggers a container restart might indicate a problem.

Therefore, shift the reconciliation loop from the scheduler into the container run time.

The best place seems to be the init script. Just saw: its FROM scratch, so a built-in reconciliation loop would probably be the right choice?

Interestingly the binary already iterates on the asterisk availability. Hare are the complete logs that triggered a restart:

t=2020-10-27T16:43:42+0000 lvl=info msg="starting ari-proxy server" version=5.0.1
t=2020-10-27T16:43:42+0000 lvl=eror msg="failed to connect to Asterisk" error="websocket.Dial ws://localhost:8088/ari/events?app=demo: dial tcp [::1]:8088: connect: connection refused"
t=2020-10-27T16:43:43+0000 lvl=eror msg="failed to connect to Asterisk" error="websocket.Dial ws://localhost:8088/ari/events?app=demo: dial tcp [::1]:8088: connect: connection refused"
t=2020-10-27T16:43:44+0000 lvl=eror msg="failed to connect to Asterisk" error="websocket.Dial ws://localhost:8088/ari/events?app=demo: dial tcp [::1]:8088: connect: connection refused"
t=2020-10-27T16:43:45+0000 lvl=eror msg="failed to connect to Asterisk" error="websocket.Dial ws://localhost:8088/ari/events?app=demo: dial tcp [::1]:8088: connect: connection refused"
t=2020-10-27T16:43:46+0000 lvl=eror msg="failed to connect to Asterisk" error="websocket.Dial ws://localhost:8088/ari/events?app=demo: dial tcp [::1]:8088: connect: connection refused"
Error: failed to connect to NATS: nats: no servers available for connection
Usage:
  ari-proxy [flags]
Flags:
      --ari.application string     ARI Stasis Application
      --ari.http_url string        HTTP Base URL for connecting to ARI (default "http://localhost:8088/ari")
      --ari.password string        Password for connecting to ARI
      --ari.username string        Username for connecting to ARI
      --ari.websocket_url string   Websocket URL for connecting to ARI (default "ws://localhost:8088/ari/events")
      --config string              config file (default is $HOME/.ari-proxy.yaml)
  -h, --help                       help for ari-proxy
      --nats.url string            URL for connecting to the NATS cluster (default "nats://127.0.0.1:4222")
  -v, --verbose                    Enable verbose logging
  -V, --version                    Print version information and exit
t=2020-10-27T16:43:48+0000 lvl=eror msg="server died" error="failed to connect to NATS: nats: no servers available for connection"

Production Use

Is this production ready? is this getting used anywhere in production because I need it to use in production.

Best practice regarding subscriptions

Hello, and thanks for a really nice lib!

I got a question regarding subscription of channel events. It seems like a normal channelhandle.Subscribe() adds a subscription to "ari.event.." against NATS.

On a heavily used Asterisk node that will cause a lot of duplicate MSG's from NATS which then is discarded by the client (not belonging to me..).

Is it possible to make ari-proxy publish channel-messages on a subject for that channel only?

Library broken?

./github.com/CyCoreSystems/ari-proxy/client/bridge.go:25:28: cannot use b (type *bridge) as type ari.Bridge in argument to ari.NewBridgeHandle:
,*bridge does not implement ari.Bridge (missing AddChannelWithOptions method)

If you checkout commit 0b99272cec6482252f0f578329fcc197cb54cee2 or ari-lib it still works, but not any newer.

Multiple Statis Applications but using only a single proxy?

Hi,
I am not sure if I am missing something.

I am able to register 2 apps via Docker if I do this:

    environment:
        ARI_APPLICATION: app1,app2
        ARI_USERNAME: asterisk
        ARI_PASSWORD: asterisk

Asterisk will then show:

 Creating Stasis app 'app1'
 Creating Stasis app 'app2'

However, I can't seem to listen for those apps as the NATS events appear to be combined:

Received on [ari.event.app1,app2.00:d8:61:0c:d0:a5]: 

But the listeners are looking out for:

DBUG[07-15|21:29:21] listening for events                     subject=ari.event.app1.>
DBUG[07-15|21:29:21] listening for events                     subject=ari.event.app2.>

I am able to circumvent this by running 2 proxies but that doesn't seem right.

Can anybody point me in the right direction?

Kind regards,
Thomas.

Runtime error using NATS client without go ARI package

Hi!

Thank for this great project.

I'm connecting into the proxy using a python client and can manage to exchange a few messages successfully.
I'm getting this error after sending a ChannelRing to a channel:

"panic: runtime error: invalid memory address or nil pointer dereference" error

The ChannelRing is executed successfully, but this error appear right after and the proxy process crashes.

ari-proxy_1 | t=2020-03-30T18:55:14+0000 lvl=info msg="starting ari-proxy server" version=5.0.1 ari-proxy_1 | t=2020-03-30T18:55:18+0000 lvl=warn msg="failed to publish NATS message" subject= data="&{Error: Data:<nil> Key:<nil> Keys:[]}" error="nats: invalid subject" ari-proxy_1 | panic: runtime error: invalid memory address or nil pointer dereference ari-proxy_1 | [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x867b53] ari-proxy_1 | ari-proxy_1 | goroutine 45 [running]: ari-proxy_1 | github.com/CyCoreSystems/ari-proxy/v5/server.(*Server).dialogsForEvent(0xc00009c500, 0xb77140, 0xc0001be100, 0xa3cfc0, 0xc0001be100, 0xc0000266f0) ari-proxy_1 | /home/travis/gopath/src/github.com/CyCoreSystems/ari-proxy/server/events.go:7 +0x113 ari-proxy_1 | github.com/CyCoreSystems/ari-proxy/v5/server.(*Server).runEventHandler(0xc00009c500, 0xb72a40, 0xc00005fb80) ari-proxy_1 | /home/travis/gopath/src/github.com/CyCoreSystems/ari-proxy/server/server.go:276 +0x51d ari-proxy_1 | created by github.com/CyCoreSystems/ari-proxy/v5/server.(*Server).listen ari-proxy_1 | /home/travis/gopath/src/github.com/CyCoreSystems/ari-proxy/server/server.go:221 +0x138e vagrant_ari-proxy_1 exited with code 2

Requests documentation

Hi all,

Before all, thanks for this project.
I'm trying to make a nodejs nats client for ari-proxy. But it's not clear to me which is the request format. I have not experience with go so it's really hard to understand how the nats request are made.
I tried to create, for example, a bridge with subject ari.create.myapp.asteriskid. But i'm not sure how the payload have to be send i try with:

{
  "type": "mixed"
}
{
  "Kind": "BridgeCreate",
  "Data": {
    "type": "mixed"
  }
}
{
  "Kind": "BridgeCreate",
  "BridgeCreate": {
    "type": "mixed"
  }
}

And more that I don't remember right now. Is there any doc or can you provide a example of payload for create a resource, as bridge or channel, and for example a delete, to see how id is passed to nats?

Return path for Stasis-Start event triggered by a Channel Creation in ari application

Hello,

If my ari application, using Ari-proxy, creates an outgoing channel, the Stasis-Start event can be received by any of the applications that are running in my cluster.

Is there a way to tell ari-proxy to send the Stasis event back to the application that initiated the channel creation? This would be particularly useful as my application needs to maintain some state which is not readily available on the rest of the application cluster at present.

[wip] Provide Node/Dialog convenience wrappers

The proxy package should offer the WithNode(ari.Client, string) and WithDialog(ari.Client, string) functions, which ari.Client interfaces which, when executed, will filter their commands by that node or dialog, respectively.

Usage concept example:

 proxy.WithNode(c, asteriskNode).Channel().Create(...)

This might be accomplished with the client.FromClient function, passing in the appropropriate OptionFunc for setting the Node or the Dialog.

The critical thing to manage is the client's Bus. It is a reference, so making a copy of the client (as in client.FromClient) will not duplicate the Bus. This seems ideal, in the sense that we do not want to have a whole bunch of Buses running. (perhaps reconsider this; a bus is not a heavy thing; consider whether the user interface of single or multiple buses would be distinguished)

c.bindEvents may need to be executed, though, if the client bus is not already listening to events from the given dialog and/or ARI application. This may require exposing an ability to query the current bindings of a client's Bus.

Closure of the client's context, its cancel function, and closure of the client itself will all need to be handled carefully. Basically, derived clients (FromClient) need to:

  • subtend their own context and cancel function
  • never stop the Bus on closure (maybe... consider previous paragraph on Bus)
  • never close the NATS connection on closure

Cluster status notifications

Looking at the code I see that the proxy client is tracking available Asterisk instances.
This is tracked in the client/cluster/cluster.go but does not seem accessible.
For cleanup purposes we want to have a notification if an asterisk instance is longer available.
Is there any way to get an notification, and if not, could this be added ?

Multi-Instance Asterisk & ARI,ARI-proxy

Hi,
Probably my lack of understanding this that just requires some explanation. I've one physical server with multiple asterisk instances on it started as different binaries and their own set of configurations. Each one of them have their own instance of ari-proxy connected with them. When the ARI stasis App receives incoming call from such a setup and it tries to execute some ARI commands they are either sent to wrong asterisk instance or there is a mixup of events and we get 404 Errors back from the wrong asterisk which is not hosting this call.

If we turn off all asterisks except one then everything works fine.

So, I'm thinking that the each asterisk is using the same MAC address as their instances ID and the ARI application is getting confused with this. Is my understanding correct ? or maybe some code mess up!

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.