Giter Site home page Giter Site logo

frangipanni's Introduction

Frangipanni

frangipanni

Program to convert lines of text into beautiful tree structures.

./frangipanni.jpg Plumeria sanalsp

The program reads each line on the standard input in turn. It breaks each line into tokens, then adds the sequence of tokens into a tree structure. Lines with the same leading tokens are placed in the same branch of the tree. The tree is printed as indented lines or JSON format. Alternatively the tree can be passed to a user-provided Lua script which can produce any output format.

Options control where the line is broken into tokens, and how it is analysed and output.

Basic Operation

Here is a simple example. Given this command sudo find /etc -maxdepth 3 | tail -9,

We get this data:

/etc/bluetooth/rfcomm.conf.dpkg-remove
/etc/bluetooth/serial.conf.dpkg-remove
/etc/bluetooth/input.conf
/etc/bluetooth/audio.conf.dpkg-remove
/etc/bluetooth/network.conf
/etc/bluetooth/main.conf
/etc/fish
/etc/fish/completions
/etc/fish/completions/task.fish

When we pipe this into the frangipanni program :

sudo find /etc -maxdepth 3 | tail -9 | frangipanni

we see this output:

etc
    bluetooth
        rfcomm.conf.dpkg-remove
        serial.conf.dpkg-remove
        input.conf
        audio.conf.dpkg-remove
        network.conf
        main.conf
    fish/completions/task.fish

By default, it reads each line and splits them into tokens when it finds a non-alphanumeric character.

In this next example we’re processing a list of files produced by find so we only want to break on directories. So we can specify -breaks /.

The default behaviour is to fold tree branches with no sub-branches into a single line of output. e.g. =fish/completions/task.fish= We turn off folding by specifying the -no-fold option. With the refined command

frangipanni -breaks / -no-fold

We see this output

etc
    bluetooth
        rfcomm.conf.dpkg-remove
        serial.conf.dpkg-remove
        input.conf
        audio.conf.dpkg-remove
        network.conf
        main.conf
    fish
        completions
            task.fish

Having restructured the data into a tree format we can output in other formats. We can ask for JSON by adding the -format json option. We get this output:

{"etc" : 
    {"bluetooth" : 
        ["rfcomm.conf.dpkg-remove",
        "serial.conf.dpkg-remove",
        "input.conf",
        "audio.conf.dpkg-remove",
        "network.conf",
        "main.conf"],
    "fish" : 
        {"completions" : "task.fish"}}}

Usage

The command is a simple filter taking standard input, and output on stdout.

cat <input> | frangipanni [options]

Options

-breaks string
      Characters to slice lines with.
-chars
      Slice line after every character.
-counts
      Print number of matches at the end of the line.
-depth int
      Maximum tree depth to print. (default 2147483647)
-format string
      Format of output: indent|json (default "indent")
-indent int
      Number of spaces to indent per level. (default 4)
-level int
      Analyse down to this level (positive integer). (default 2147483647)
-lua string
      Lua Script to run
-no-fold
      Don't fold into one line.
-order string
      Sort order input|alpha. Sort the childs either in input order or via character ordering (default "input")
-separators
      Print leading separators.
-skip int
      Number of leading fields to skip.
-spacer string
      Characters to indent lines with. (default " ")

Examples

Log files

Given input from a log file:

May 10 03:17:06 localhost systemd: Removed slice User Slice of root.
May 10 03:17:06 localhost systemd: Stopping User Slice of root.
May 10 04:00:00 localhost systemd: Starting Docker Cleanup...
May 10 04:00:00 localhost systemd: Started Docker Cleanup.
May 10 04:00:00 localhost dockerd-current: time="2020-05-10T04:00:00.629849861+10:00" level=debug msg="Calling GET /_ping"
May 10 04:00:00 localhost dockerd-current: time="2020-05-10T04:00:00.629948000+10:00" level=debug msg="Unable to determine container for /"
May 10 04:00:00 localhost dockerd-current: time="2020-05-10T04:00:00.630103455+10:00" level=debug msg="{Action=_ping, LoginUID=12345678, PID=21075}"
May 10 04:00:00 localhost dockerd-current: time="2020-05-10T04:00:00.630684502+10:00" level=debug msg="Calling GET /v1.26/containers/json?all=1&filters=%7B%22status%22%3A%7B%22dead%22%3Atrue%7D%7D"
May 10 04:00:00 localhost dockerd-current: time="2020-05-10T04:00:00.630704513+10:00" level=debug msg="Unable to determine container for containers"
May 10 04:00:00 localhost dockerd-current: time="2020-05-10T04:00:00.630735545+10:00" level=debug msg="{Action=json, LoginUID=12345678, PID=21075}"

default output is:

May 10
 03:17:06 localhost systemd
  : Removed slice User Slice of root
  : Stopping User Slice of root
 04:00:00 localhost
   dockerd-current: time="2020-05-10T04:00:00
    .629849861+10:00" level=debug msg="Calling GET /_ping
    .629948000+10:00" level=debug msg="Unable to determine container for
    .630103455+10:00" level=debug msg="{Action=_ping, LoginUID=12345678, PID=21075
    .630684502+10:00" level=debug msg="Calling GET /v1.26/containers/json?all=1&filters=%7B%22status%22%3A%7B%22dead%22%3Atrue%7D%7D
    .630704513+10:00" level=debug msg="Unable to determine container for containers
    .630735545+10:00" level=debug msg="{Action=json, LoginUID=12345678, PID=21075
   systemd
    : Started Docker Cleanup
    : Starting Docker Cleanup

with the -skip 5 option we can ignore the date and time at the beginning of each line. The output is

localhost
    systemd
        Removed slice User Slice of root
        Stopping User Slice of root
        Starting Docker Cleanup
        Started Docker Cleanup
    dockerd-current: time="2020-05-10T04:00:00
        629849861+10:00" level=debug msg="Calling GET /_ping
        629948000+10:00" level=debug msg="Unable to determine container for
        630103455+10:00" level=debug msg="{Action=_ping, LoginUID=12345678, PID=21075
        630684502+10:00" level=debug msg="Calling GET /v1.26/containers/json?all=1&filters=%7B%22status%22%3A%7B%22dead%22%3Atrue%7D%7D
        630704513+10:00" level=debug msg="Unable to determine container for containers
        630735545+10:00" level=debug msg="{Action=json, LoginUID=12345678, PID=21075

Data from environment variables

Give this input, from ~env | egrep ‘^XDG’~ :

XDG_VTNR=2
XDG_SESSION_ID=5
XDG_SESSION_TYPE=x11
XDG_DATA_DIRS=/usr/share:/usr/share:/usr/local/share
XDG_SESSION_DESKTOP=plasma
XDG_CURRENT_DESKTOP=KDE
XDG_SEAT=seat0
XDG_RUNTIME_DIR=/run/user/1000
XDG_SESSION_COOKIE=fe37f2ef4-158904.727668-469753

And run with

$ env | egrep '^XDG' | ./frangipanni -breaks '=_' -no-fold -format json

we get

{"XDG" : 
    {"VTNR" : 2,
    "SESSION" : 
        {"ID" : 5,
        "TYPE" : "x11",
        "DESKTOP" : "plasma",
        "COOKIE" : "fe37f2ef4-158904.727668-469753"},
    "DATA" : 
        {"DIRS" : "/usr/share:/usr/share:/usr/local/share"},
    "CURRENT" : 
        {"DESKTOP" : "KDE"},
    "SEAT" : "seat0",
    "RUNTIME" : 
        {"DIR" : "/run/user/1000"}}}

Split the PATH

$ echo $PATH | tr ':' '\n' | ./frangipanni -separators
/home/alice
    /work/gopath/src/github.com/birchb1024/frangipanni
    /apps
        /textadept_10.8.x86_64
        /shellcheck-v0.7.1
        /Digital/Digital
        /gradle-4.9/bin
        /idea-IC-172.4343.14/bin
        /GoLand-173.3531.21/bin
        /arduino-1.6.7
    /yed
    /bin
/usr
    /lib/jvm/java-8-openjdk-amd64/bin
    /local
        /bin
        /games
        /go/bin
    /bin
    /games
/bin

Query a CSV triplestore -> JSON

A CSV tiplestore is a simple way of recording a database of facts about objects. Each line has a Subject, Object, Predicate structure.

john1@jupiter,rdf:type,UnixAccount
joanna,hasAccount,alice1@jupiter
jupiter,defaultAccount,alice1
alice2,hasAccount,evan1@jupiter
felicity,hasAccount,john1@jupiter
alice1@jupiter,rdf:type,UnixAccount
kalpana,hasAccount,alice1@jupiter
john1@jupiter,hasPassword,felicity-pw-8
Production,was_hostname,jupiter
alice1@jupiter,rdf:type,UnixAccount
alice1@jupiter,hasPassword,alice-pw-2

In this example we want the data about the jupiter machine. We permute the input records with awk and filter the JSON output with jq.

$ cat test/fixtures/triples.csv | \
  awk -F, '{print $2,$1,$3; print $1, $2, $3; print $3, $2, $1}' | \
  ./frangipanni  -breaks ' ' -order alpha -format json -no-fold | \
  jq '."jupiter"'
{
  "defaultAccount": "alice1",
  "hasUser": [
    "alice1",
    "birchb1",
    "john1"
  ],
  "rdf:type": [
    "UnixMachine",
    "WasDmgr"
  ],
  "was_hostname": "Production"
}

Security Analysis of sudo use in Auth Log File

The Linux /var/log/auth.log file has timed records about sudo which look like this:

May 17 00:36:15 localhost sudo:   alice : TTY=pts/2 ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/jmtpfs -o allow_other /tmp/s
May 17 00:36:15 localhost sudo: pam_unix(sudo:session): session opened for user root by (uid=0)
May 17 00:36:15 localhost sudo: pam_unix(sudo:session): session closed for user root

By skipping the date/time component of the lines, and specifying -counts we can see a breakdown of the sudo commands used and how many occurred. By placing the date/time data at the end of the input lines we alse get a breakdown of the commands by hour of day.

$ sudo cat /var/log/auth.log | grep sudo | \
    awk '{print substr($0,16),substr($0,1,15)}' | \
    ./frangipanni -breaks ' ;:'  -depth 5 -counts -separators

Produces

localhost sudo: 125
   :   alice: 42
        : TTY=pts/2: 14
            ; PWD=/home/alice ; USER=root ; COMMAND=/usr/bin/jmtpfs: 5
            ; PWD=/home/alice/workspace/gopath/src/github.com/akice/frangipanni ; USER=root ; COMMAND=/usr/bin/find /etc -maxdepth 3 May 17 13: 9
        : TTY=pts/1 ; PWD=/home/alice/workspace/gopath/src/github.com/akice/frangipanni ; USER=root ; COMMAND=/bin/cat: 28
            /var/log/messages May 17 13:53:34: 1
            /var/log/auth.log May 17: 27
   : pam_unix(sudo:session): session: 83
        opened for user root by (uid=0) May 17: 42
            00: 5
            13: 28
            14: 9
        closed for user root May 17: 41
            00: 5
            13: 28
            14: 8

We can see alice has run 42 sudo commands, 28 of whuch were =cat=ing files from /var.

Output for Spreadsheets

Inevitably you will need to output reports from frangipanni into a spreadsheet. You can use the -spacer option to specify the character(s) to use for indentation and before the counts. So with the file list example from above and this command

sudo find /etc -maxdepth 3 | tail -9 | frangipanni -no-fold -counts -indent 1 -spacer $'\t'

You will have a tab-separated output which can be imported to your spreadsheet.

etc9
bluetooth6
rfcomm.conf.dpkg-remove1
serial.conf.dpkg-remove1
input.conf1
audio.conf.dpkg-remove1
network.conf1
main.conf1
fish/completions/task.fish3

Output for Markdown

To use the output with markdown or other text-based tools, sepecify the -separator option. This can be used by tools like sed to convert the leading separator into the markup required. example to get a leading minus sign for an un-numbered Markdown list, use sed to

sudo find /etc -maxdepth 3 | tail -9 | frangipanni -separators | sed 's;/; - ;'

Which results in an indented bullet list:

  • etc
    • bluetooth
      • rfcomm.conf.dpkg-remove
      • serial.conf.dpkg-remove
      • input.conf
      • audio.conf.dpkg-remove
      • network.conf
      • main.conf
    • fish/completions/task.fish

Lua Examples

JSON (again)

First, we are going tell frangipanni to output via a Lua program called ‘json.lua’, and we will format the json with the ‘jp’ program.

$ <test/fixtures/simplechars.txt frangipanni -lua json.lua | jp @

The Lua script uses the github.com/layeh/gopher-json module which is imported in the Lua. The data is made available in the variable frangipanni which has a table for each node, with fields

  • depth - in the tree starting from 0
  • lineNumber - the token was first detected
  • numMatched - the number of times the token was seen
  • sep - separation characters preceding the token
  • text - the token itself
  • children - a table containing the child nodes
local json = require("json")

print(json.encode(frangipanni))

The output shows that all the fields of the parsed nodes are passed to Lua in a Table. The root node is empty except for it’s children. The Lua script is therefore able to use the fields intelligently.

{
  "depth": 0,
  "lineNumber": -1,
  "numMatched": 1,
  "sep": "",
  "text": "",
  "children": {
    "1.2": {
      "children": [],
      "depth": 1,
      "lineNumber": 8,
      "numMatched": 1,
      "sep": "",
      "text": "1.2"
    },
    "A": {
      "children": [],
      "depth": 1,
      "lineNumber": 1,
      "numMatched": 1,
      "sep": "",
      "text": "A"
    },

Markdown

function indent(n)
    for i=1, n do
        io.write("   ")
    end
end

function markdown(node)
    indent(node.depth)
    io.write("* ")
    print(node.text)
    for k, v in pairs(node.children) do
        markdown(v)
    end
end

markdown(frangipanni)

The output can look like this:

* 
   * A
   * C
      * 2
      * D
   * x.a
      * 2
      * 1
   * Z
   * 1.2

XML

The xml.lua script provided in the release outputs very basic XML format which might suit simple inputs.

<root count="1" sep="">
   <C count="2" sep="">
      <2 count="1" sep="."/>
      <D count="1" sep="."/>
   </C>
   <x.a count="3" sep="">
      <1 count="1" sep="."/>
      <2 count="1" sep="."/>
   </x.a>
   <Z count="1" sep=""/>
   <1.2 count="1" sep=""/>
   <A count="1" sep=""/>
</root>

frangipanni's People

Contributors

billbirchatcoles avatar birchb1024 avatar marktani avatar neighborhood999 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  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

frangipanni's Issues

Pass command-line flags to Lua

The Lua scripts do not have access to the commandline arguments. This might be useful for sort ordering or other uses.
Maybe add table 'frangipanni-args' which holds the flag names and values.

Lua Markdown formatting fixes

See below; implemented this. Basically, use 1. instead of *, yielding numbered list; also, don't print the root node.
Would have made this a PR except I don't think I have perms.

diff --git a/markdown.lua b/markdown.lua
index 5dd0ee8..5711239 100644
--- a/markdown.lua
+++ b/markdown.lua
@@ -1,30 +1,34 @@
 -- Beginning of Markdown unnumbered indented bullet list
--- Print each line preceded with indents and an '*'
+-- Print each line preceded with indents and '1.'
 -- Example:
--- * 
---    * x.a
---       * 1
---       * 2
---    * Z
---    * 1.2
---    * A
---    * C
---       * D
---       * 2
+-- 1. 
+--    1. x.a
+--       1. 1
+--       1. 2
+--    1. Z
+--    1. 1.2
+--    1. A
+--    1. C
+--       1. D
+--       1. 2
 
 function indent(n)
-    for i=1, n do
-        io.write("   ")
-    end
+   for i=1, n do
+      io.write("   ")
+   end
 end
 
 function markdown(node)
-    indent(node.depth)
-    io.write("* ")
-    print(node.text)
-    for k, v in pairs(node.children) do
-        markdown(v)
-    end
+   if node.lineNumber > 0 then  -- don't write a root note
+      indent(node.depth - 1)
+      io.write("1. ")
+      print(node.text)
+   end
+   
+   for k, v in pairs(node.children) do
+      markdown(v)
+   end
+
 end
 
 markdown(frangipanni)

Example output on the first example:

$ frangipanni -lua markdown.lua
1. etc
   1. bluetooth
      1. rfcomm.conf.dpkg-remove
      1. serial.conf.dpkg-remove
      1. input.conf
      1. audio.conf.dpkg-remove
      1. network.conf
      1. main.conf
   1. fish/completions/task.fish  

which renders in Markdown as:

  1. etc
    1. bluetooth
      1. rfcomm.conf.dpkg-remove
      2. serial.conf.dpkg-remove
      3. input.conf
      4. audio.conf.dpkg-remove
      5. network.conf
      6. main.conf
    2. fish/completions/task.fish

Add a command switch for skipping the first N fields.

When analyzing log files with lines like this:

May 10 03:17:06 localhost systemd: Removed slice User Slice of root.
May 10 03:17:06 localhost systemd: Stopping User Slice of root.
May 10 04:00:00 localhost systemd: Starting Docker Cleanup...
May 10 04:00:00 localhost systemd: Started Docker Cleanup.

I often have to use awk to remove the date, time from the front and place it at the end, or throw it away:

cat /var/log/syslog | awk '{print substr($0,16),substr($0,1,15)}' | frangipanni -counts

cat /var/log/syslog | awk '{print substr($0,16)}' | frangipanni -counts

This enhancement is to add a switch to specify the number of leading fields to ignore.

cat /var/log/syslog | frangipanni -skip 5 -counts

make available via package installers (e.g. Homebrew)

This is a useful tool. It would get orders of magnitude greater spread if made available for easy install as well as version updates using the primary package manager for OSes you want to target (e.g. Homebrew for macOS).

Root node count is incorrect when seen in -lua or -format json

Got:

$ ./frangipanni -counts -format json <test/fixtures/find.txt | jp '@' | head 
{
  "_count_": 1,
  "boot": {
    "System.map-4.9.0": {
      "11-amd64": {
        "_count_": 1
      },
      "12-amd64": {
        "_count_": 1
      },

Expected much bigger count than 1

Got:

/$ ./frangipanni  -lua json.lua <test/fixtures/find.txt | jp '@' | grep numMatched | tail
                  "numMatched": 1,
              "numMatched": 2,
                  "numMatched": 1,
                  "numMatched": 1,
              "numMatched": 2,
          "numMatched": 4,
          "numMatched": 1,
          "numMatched": 1,
      "numMatched": 7,
  "numMatched": 1,

Last "numMatched": 1, is wrong also, should be the sum of the children plus 1

README.md (and included Installation docs) missing from master branch

I had to search for a while to find documention about the installation process.

It seems the Markdown version of the README file in commit cf99772 contains this information in lines 84–117:

frangipanni/README.md

Lines 84 to 117 in cf99772

# Download and Installation
## Pre-Compiled Binaries
You can download executables of `frangipanni` from the Github repository in the [Releases area.](https://github.com/birchb1024/frangipanni/releases) You will find archive files containing the binary and Lua files:
```
frangipanni_darwin_amd64.zip
frangipanni_freebsd_amd64.zip
frangipanni_js_wasm.zip
frangipanni_linux_386.tgz
frangipanni_linux_amd64.tgz
frangipanni_linux_arm64.tgz
frangipanni_netbsd_amd64.zip
frangipanni_openbsd_amd64.zip
frangipanni_windows_386.zip
frangipanni_windows_amd64.zip
```
Download the file for your operating system and hardware, then decompress the archive in a directory. Ensure the binary file is executable, and you're ready to go. Your directory should look something like this, depending on operating system:
```
.
├── [-rwxrwxr-x] frangipanni_linux_arm64
├── [-rw-rw-r--] json.lua
├── [-rw-rw-r--] markdown.lua
└── [-rw-rw-r--] xml.lua
```
I have tested `frangipanni_linux_amd64`, the others are output from the Go cross-compiler and are provided 'as-is'. Please send a Pull Request if you find an issue.
## Building From Source Code
If there is no pre-compiled binary or your platform, you can compile from the source. First you need [the 'Go' compiler](https://golang.org/doc/install), version 1.15.2 or greater. After cloning [the frangipanni repository](https://github.com/birchb1024/frangipanni) with git, it suffices to run `GO111MODULE=on go build` and the executable will be built as `frangipanni`. You can run the regression test suite with `test/confidence.sh`, but first install `jp` from [github.com/jmespath/jp](https://github.com/jmespath/jp).
Read all about `Go` here: [golang.org](https://golang.org/)

But the default README in master (README.org?), which is the default one rendered for the GitHub project, doesn't seem to include this information.

Add -auto-order option

If the -auto-order option is given, then assess the relative 'value' of each token at each level. Re-order the input and re do so that the hierarchy makes more sense. For example one way to re-order is to place tokens with higher counts higher in the hierarchy. Thought-experiment. Given:

12:34  01-01-2022 
12:43  01-01-2022 
12:45  01-01-2022 
12:50  01-01-2022 
00:10  02-01-2022 
00:12  02-01-2022 
00:34  02-01-2022 

We want

frangipanni -no-fold -counts
2022: 7
    01: 7
        01: 4
            12: 4
                34: 1
                43: 1
                45: 1
                50: 1
        02: 3
            00: 3
                10: 1
                12: 1
                34: 1

But as you can see there is ambiguity here, how do we know what the output order is, in fact?

Please expand the log parsing example

Apart from all other usecases, this looks extremely useful to make syslog more readable. However, the log example lacks the actual command being invoked, producing the result listed.

I was able to produce similar outputs with cat /var/log/syslog | frangipanni -skip 2, but I guess the author or other experienced linux users would produce something more elegant.

Add - Build on M1 Mac and Homebrew

I wanted to try this out, but had a bunch of issues, here's how I resolved them.

Default jp package for homebrew isn't JMSEPath.

brew remove jp
brew tap jmespath/jmespath
brew install jmespath/jmespath/jp 

I had to add darwn/arm64 to build.sh

❯ git diff build.sh
diff --git a/build.sh b/build.sh
index 96b2d09..030a3e3 100755
--- a/build.sh
+++ b/build.sh
@@ -33,7 +33,7 @@ for arch in 386 arm64 amd64
 do
   linux_compile "$arch"
 done
-for dist in windows/amd64 windows/386 darwin/amd64 freebsd/amd64 js/wasm netbsd/amd64 openbsd/amd64
+for dist in windows/amd64 windows/386 darwin/arm64 darwin/amd64 freebsd/amd64 js/wasm netbsd/amd64 openbsd/amd64
 do
   cross_compile "$dist"
 done

If you want to bypass the build.sh step:

GOOS=darwin GOARCH=arm64 go build -a -ldflags="-X 'main.Version=$(git describe)'" -o frangipanni_"$GOOS"_"$GOARCH" frangipanni.go

Enjoy!

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.