Giter Site home page Giter Site logo

nmisko / monkalot Goto Github PK

View Code? Open in Web Editor NEW
17.0 4.0 8.0 659 KB

A Twitch Bot for maximum user interaction and chat spam induction.

License: MIT License

Python 99.92% Dockerfile 0.08%
twitch-bot twitch bot chatbot games moderation-bot moderator-commands twitch-tv twitch-users game

monkalot's Introduction

Monkalot

Subjectively the best twitch bot.

Easy to get running, highly modular and fast to extend, monkalot is almost fully configurable on a channel by channel basis. Fully modifiable texts allow for full localization on each channel (if you have the patience to translate). These configurations can also be controlled via REST api or through a Web Interface.

Quick Start

Clone this project.
$ git clone https://github.com/NMisko/monkalot.git

Switch into it.
$ cd monkalot

Install all necessary packages.
$ pip install -r requirements.txt

Copy the template folder in channels, rename it to whatever channel the bot needs to run on.
$ cp -r channels/template channels/<your_channel>

Set the configuration parameters in channels/<your_channel/configs/bot_config.json (see configuration section).

Then start the bot.
$ python3 monkalot.py

Multiple bots can be started by adding more folders with different configurations to channels.

Configuration:

Make sure to modify the following values in bot_config.json:

  • channel: Twitch channel which the bot will run on
  • username: The bot's Twitch user
  • clientID: Twitch ClientID for API calls.
  • oauth_key: IRC oauth_key for the bot user (from here)
  • access_token: access token for the bot user (see here)
  • owner_list: List of Twitch users which have admin powers on bot
  • ignore_list: List of Twitch users which will be ignored by the bot

Warning: Make sure all channel and user names above are in lowercase.

Additional Configuration Parameters:

  • points: Various parameters that balance point-distribution for the different games.
  • ranking: Ranking System Point Distribution -> Rank_n = base * factor^n
  • auto_game_interval: Time between automaticly started games while AutoGames are on.
  • pleb_cooldown: Time between normal chat user commands.
  • pleb_gametimer: Time between games started by normal chat users.
  • EmoteGame: Preset of emotes used in the !estart- command.

Commands

All commands that can be called from chat via different calls. Note that some commands can only be called by Moderators, Trusted-Moderators or Bot-Admins. Chat games can also be started by regular users, if they have spam points to pay for it.

All-User commands:

Command Description Examples
!any <emote> <text> Sends out any sentence interlaced by an emote. All Twitch- and BTTV-emotes and emojis are supported. !any Jebaited Long have we waited, now we Jebaited
!bttv Returns all bttv emotes on this channel (messy) -
!calc <formula> A chat calculator that can do some pretty advanced stuff like sqrt and trigonometry. !calc (5+7)/2 ,
!calc log(5^2) + sin(pi/4)
!call <emote> <text> Sends a call interlaced by an emote. All Twitch- and BTTV-emotes and emojis are supported. !call Kappa a nice stream
!fps Returns the fps of the stream -
!hug <user> Sends a hug to another user. !hug Monkalot
!kpm Returns the amount of Kappas per minute in channel. -
!minute <emote> Returns the amount of a specific emote per minute in channel. All Twitch- and BTTV-emotes and emojis are supported. !minute BabyRage
!oralpleasure on/off Turns oralpleasure on or off. -
!penta <emote> Quintuples an emote !penta PunOko
!pjsalt Sends a pjsalt pyramid in chat. -
!quote [<number>] Returns a random quote. Optional a number can be given to call a specific quote. !quote , !quote 2
!rank [<username>] Returns the current spam-rank and -points for the chatter or optional for a specific . !rank , !rank monkalot
!slap <user> Sends a slap to another user. !slap Monkalot
!smorc Returns a random SMOrc quote. -
!tenta <emote> Gives an emote some tentacles !tenta WutFace
!tip <user> <amount> Transfers an amount of channel points to another user. !tip Keepo 500
!tkp Returns the total amount of Kappas in channel. -
!topspammers Returns the five highest ranked spammers. -
!total <emote> Returns the total amount of a specific emote in channel. All Twitch- and BTTV-emotes and emojis are supported. !total EleGiggle
!uptime Returns how long this stream has been on -
!word <emote> <text> Sends a word with an emote interlaced between letters. All Twitch- and BTTV-emotes and emojis are supported. !word monkaS dragons
<botname> <text> Talk to the bot. Questions can be asked or a conversation can be started with the native speech engine. Hey Monkalot, how are you doing?, What's 2Head + 2Head? @Monkalot
@monkalot ban me Users can ask the bot to get banned (they will get banned and unbanned immediately) @monkalot ban me please :)
[<hearthstone card>] Get some information about a hearthstone card. Allows up to two spelling mistakes. [Malganis]

Chat games:

Command Description Examples
!estart, [!estop], [!emotes] Starts the GuessEmoteGame. Guess the right emote from the list. Type emotes to start playing. While the game is active !emotes shows all possible emotes. -
!kstart, [!pstop] Starts the KappaGame. Guess the right amount of Kappas to win. Type Kappas to start playing. -
!mstart, [!mstop] Starts the GuessMinionGame. Guess the right minion card. Type minion names to play. After a short time the game will give clues to the chat. -
!pstart, [!pstop] Starts the MonkalotParty. A Minigames tournament with 7 games by default. -
<emote>-pyramids Build emote pyramids to gain spampoints. All Twitch- and BTTV-emotes and emojis are supported. Kappa
Kappa Kappa
Kappa

All games can be canceled by their respected !stop command.

Moderator commands:

Command Description Examples
!addnotification <msg> Adds a message to the notification message rotation !addnotification Please remember to drink water. :)
!delnotification <msg> Removes a message from the notification message rotation !delnotification wheeeeee
!addquote <quote> Adds a quote to the quotelist. !addquote "Priest in 2k17 LUL"
!delquote <quote> Deletes a quote from the quotelist. !delquote "Priest in 2k17 LUL"
!block on/off Turns pyramidblock on or off. If on, pyramids will be interupted by the bot. -
!games on/off Turns automatic games on or off. If on, chatgames will start automaticly after a certain amount of time. -
!notifications on/off Enables or disables notifications. Notifications are messages that get sent out in regular intervals. -

Trusted-Moderator commands:

Command Description Examples
!addcommand <command> <response> Adds a command to the simplereply-list. !addcommand !ping pong
!clearcache Clears the cache. Use this e.g. to load newly released twitch emotes -
!delcommand <command> Deletes a command from the simplereply-list. !delcommand !ping
!ignore <user> Makes the bot ignore a user. Please enter the username in lowercase. -
!unignore <user> Makes the bot no longer ignore a user. Please enter the username in lowercase. -
!replylist Returns all available commands from the simplereply-list. -
!sleep Puts the bot in sleepmode. All games will be disabled and the bot only responses to admins -
!wakeup Puts the in normal mode again. -

Admin commands:

Command Description Examples
!addmod <username> Adds a mod to the list of trusted mods. !addmod Monkalot
!delmod <username> Deletes a mod from the list of trusted mods. !delmod Monkalot
!g <username> <pronouns> Allows changing gender pronouns for a user. Three pronouns have to be given. !g monkalot she her hers

Adding a new custom command

Create a command which inherits from command.py in a new file and add it to the commands folder. Then import your new class into __init__.py and add it to one of the command arrays, depending on its priority.

REST Api

The REST Api allows to control the bot via POST requests. It must be enabled by setting the port using the -p flag. You can set a password using the -s flag. Using a password gives access to all the bots. Alternatively pass a twitch id token, which gives access to the bots of the owner of the id token.

Example: Run bot with: ./monkalot.py -p 8080 -s Kappa. Assume there is one bot called Monkalot, owned by Alice.


curl --data 'user=alice&auth=Kappa' localhost:8080/bots

Returns every bot Alice is admin of.

=> ["monkalot"]


curl --data 'user=alice&bot=monkalot&auth=Kappa' localhost:8080/files

Returns all configurable files of Monkalot.

=> \["ignored_users.json", "sreply_cmds.json", "quotes.json", "pronouns.json", "smorc.json", "notifications.json", "emote_stats.json", "monkalot_party.json", "slaphug.json", "trusted_mods.json", "bot_config.json", "responses.json"]


curl --data 'user=alice&bot=monkalot&file=ignored_users.json&auth=Kappa' localhost:8080/file

Returns the content of ignored_users.json.

=> {"content": ['bob']}


curl --data 'user=alice&bot=monkalot&file=ignored_users.json&content=["Bob","Carl"]&auth=Kappa' localhost:8080/setfile```

Sets the content of ignored_users.json to ["Bob","Carl"].


curl --data 'auth=xyz' localhost:8080/getTwitchUsername

Utility function. Takes a twitch id token and returns the username associated to it. Also ensures the token is valid.


curl --data 'user=alice&bot=monkalot&pause=True&auth=Kappa' localhost:8080/pause

Pauses the bot. Set pause = false to unpause the bot.


(Based on SimpleTwitchBot by EhsanKia.)

monkalot's People

Contributors

bellyria avatar codemonkey3451 avatar ghostduck avatar nmisko avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar

monkalot's Issues

!rank command does not work

The !rank command doesn't work on its own, typing !rank doesn't give your rank, but if you type !rank username it does work

!wakeup not working for mods

I just put Monkalot to sleep using !sleep. Now !wakeup isn't working, and she isn't responding to me.

That happened yesterday too, Zetalot was able to wake the bot this morning but mods couldn't

'TwitchBot' object has no attribute 'port' in bot/bot.py

I have that error when I try to start monkalot. I can't see anywhere to give the port parameter to TwitchBot.
I have to manually add
self.port = None
before
if self.port is not None:

to prevent the error. Any ideas? Or do I setup anything wrong?
I start without inputting a port since I don't plan to use web service in my test run, not sure if related or not.

match() - Conditions

So while looking through the code in commands.py I noticed we are using almost the exact same commands to check the conditions in match(). It starts with msg.startswith("!cmd ") and then sometimes have additional conditions like "The second word has to be an emote." / "The line can only contain 3 words.", etc. Some of these condition checks can get a little messy and we are basicly repeating code with different values.

My idea is to use a standardized 'condition - line' method. First we use a function that parses the written line and extracts all the information available (number of words, text for each word, is the word a number/emote?, etc). Then we define specific condition for each class/function ("First word has to be '!', second word has to be an emote, etc.). And finaly have a function that returns wether the line matches the condition for the class or not.

  1. Write functions that can give you information about a written line.

    • How many words are in the line? -> n
    • Information about each word:
      • Is the first word a command? (!<cmd>)
      • Is the second word an emote/emoji, NOT an emote, a number, etc.
      • Length of a word
      • Total length of the line
    • Maybe structure like:
      line = {
      "n": "n-words",
      "length": "length-line",
      "words": [["text": text1, "type": type1, "length": length1], ["text": text2, …], …]
      }
    • types could be:
      0: default (no emote, no number, etc.)
      1: a command (startswith '!' or is in commandlist)
      2: a number
      3: a username (user should be in the channel)
      4: basic twitch emote
      5: basic bttv emote
      6: bttv emoji
      7: channel bttv emote
      8: special emote the user has access to (subscriber-emotes, turbo, etc.)
      ETC.

2.) Then we can define condition for the command. E.g.:
(Should probably have a better structure)
condition = [
[0, {"text": "!call"}], -> "First word is '!call'."
[1, {"type": ["emote", "emoji"]}, <- Should be able to handle multiple conditions
[3, {"length": "< 10"}, -> "Third word is shorter than 10 letters."
[n, "< 10"]
]

3.) Then in match() we can go like:
if check_match(line, cond):
return True

Instead of making the check for each condition in the match function. It would make the transition from the wording "The second word needs to be a number." into actual conditions easier/more standardized.

401 Error twitch api

Wed Dec 27 17:00:01 CET 2017 Unhandled Error Traceback (most recent call last): File "/usr/local/lib/python3.4/dist-packages/twisted/words/protocols/irc.py", line 2631, in dataReceived basic.LineReceiver.dataReceived(self, data) File "/usr/local/lib/python3.4/dist-packages/twisted/protocols/basic.py", line 571, in dataReceived why = self.lineReceived(line) File "/home/monkalot/monkalot/bot/multibot_irc_client.py", line 169, in lineReceived super().lineReceived(line) File "/usr/local/lib/python3.4/dist-packages/twisted/words/protocols/irc.py", line 2644, in lineReceived self.handleCommand(command, prefix, params) --- <exception caught here> --- File "/usr/local/lib/python3.4/dist-packages/twisted/words/protocols/irc.py", line 2699, in handleCommand method(prefix, params) File "/usr/local/lib/python3.4/dist-packages/twisted/words/protocols/irc.py", line 2056, in irc_PRIVMSG self.privmsg(user, channel, message) File "/home/monkalot/monkalot/bot/multibot_irc_client.py", line 67, in privmsg b.process_command(name, msg) File "/home/monkalot/monkalot/bot/bot.py", line 287, in process_command cmd.run(self, user, msg) File "/home/monkalot/monkalot/bot/commands/pyramid.py", line 48, in run if msg == self.pyramidLevel(self.currentEmote, self.count) and (self.currentEmote in self.emotes or self.currentEmote in self.emojis or bot.accessToEmote(user, self.currentEmote)): File "/home/monkalot/monkalot/bot/bot.py", line 501, in accessToEmote emotelist = self.getuserEmotes(userID) File "/home/monkalot/monkalot/bot/bot.py", line 486, in getuserEmotes data = self.getJSONObjectFromTwitchAPI(url) File "/home/monkalot/monkalot/bot/bot.py", line 432, in getJSONObjectFromTwitchAPI raise e File "/home/monkalot/monkalot/bot/bot.py", line 424, in getJSONObjectFromTwitchAPI r.raise_for_status() File "/usr/lib/python3/dist-packages/requests/models.py", line 825, in raise_for_status raise HTTPError(http_error_msg, response=self) requests.exceptions.HTTPError: 401 Client Error: Unauthorized

Counted emotes don't get updated

When a new (bttv) emote gets added, it doesn't get counted, meaning its counter is always 0.
Expected behaviour: In regular intervals, new emotes should be added to the count list.

Some issues i faced today (first testing it in my broadcaster channel)

First:
i'm testing it right now since my streamer is live these hours and i triggered !pstart
The party started and there was a riddle which answer was OSkomodo but:
X7MDE1B 1
i mean isn't it updating for actual twitch emotes everytime i start it up?
Also would it be possible to rename all "Monkalot" word and put our bot name in batch? (this has low priority)
About quotes: if i type !quote all the quotes are from Zetalot as this bot was born for him. So is there a way to overwrite this quotes or just add more?for ex cmd like !addquote or !quoteadd would be good as well as !editquote or !quoteedit...
Second:
also !active command doesn't output anything:
UPhS09l 1
Third:
Is there a chance to have a !hint command for every !*start ? like for ex !pstart it output like a capital of some country and almost nobody as a clue so a !hint might be very useful like outputting the number of letters in _____ and like a letter in it like i_ . Might be more letters if another !hint command is triggered for like 2 or more suggested letters for the full answer.
Forth:
just found this:
DUcctXK 1
Thanks in advance!

Refactor documentation

Emphasize usage as generic twitch bot. Add quick start at the very top, with detailed walk through. Mention Docker option.

TypeError in speech.py

Unhandled Error
Traceback (most recent call last):
File "/usr/lib/python3.4/threading.py", line 920, in _bootstrap_inner
self.run()
File "/usr/lib/python3.4/threading.py", line 868, in run
self._target(*self._args, **self._kwargs)
File "/usr/local/lib/python3.4/dist-packages/twisted/_threads/_threadworker.py", line 46, in work
task()
File "/usr/local/lib/python3.4/dist-packages/twisted/_threads/_team.py", line 190, in doWork
task()
--- ---
File "/usr/local/lib/python3.4/dist-packages/twisted/python/threadpool.py", line 250, in inContext
result = inContext.theWork()
File "/usr/local/lib/python3.4/dist-packages/twisted/python/threadpool.py", line 266, in
inContext.theWork = lambda: context.call(ctx, func, *args, **kw)
File "/usr/local/lib/python3.4/dist-packages/twisted/python/context.py", line 122, in callWithContext
return self.currentContext().callWithContext(ctx, func, *args, **kw)
File "/usr/local/lib/python3.4/dist-packages/twisted/python/context.py", line 85, in callWithContext
return func(*args,**kw)
File "/home/monkalot/monkalot/bot/commands/speech.py", line 27, in getReply
bot.write("@" + user + " " + output)
builtins.TypeError: Can't convert 'NoneType' object to str implicitly

Error on !topspammers command

Twitch gets 'unprocessable entity'. Maybe due to user id no longer existing? In that case there are three options:

  1. Delete user ids from database when they aren't recognized by twitch anymore.
  2. Skip those ids.
  3. Return (or similar) as username

Traceback (most recent call last):
File "/usr/local/lib/python3.4/dist-packages/twisted/python/log.py", line 103, in callWithLogger
return callWithContext({"system": lp}, func, *args, **kw)
File "/usr/local/lib/python3.4/dist-packages/twisted/python/log.py", line 86, in callWithContext
return context.call({ILogContext: newCtx}, func, *args, **kw)
File "/usr/local/lib/python3.4/dist-packages/twisted/python/context.py", line 122, in callWithContext
return self.currentContext().callWithContext(ctx, func, *args, **kw)
File "/usr/local/lib/python3.4/dist-packages/twisted/python/context.py", line 85, in callWithContext
return func(*args,**kw)
--- ---
File "/usr/local/lib/python3.4/dist-packages/twisted/internet/posixbase.py", line 597, in _doReadOrWrite
why = selectable.doRead()
File "/usr/local/lib/python3.4/dist-packages/twisted/internet/tcp.py", line 208, in doRead
return self._dataReceived(data)
File "/usr/local/lib/python3.4/dist-packages/twisted/internet/tcp.py", line 214, in _dataReceived
rval = self.protocol.dataReceived(data)
File "/usr/local/lib/python3.4/dist-packages/twisted/words/protocols/irc.py", line 2631, in dataReceived
basic.LineReceiver.dataReceived(self, data)
File "/usr/local/lib/python3.4/dist-packages/twisted/protocols/basic.py", line 571, in dataReceived
why = self.lineReceived(line)
File "/home/monkalot/monkalot/bot/multibot_irc_client.py", line 170, in lineReceived
self.twitch_privmsg(user, channel, message, tags)
File "/home/monkalot/monkalot/bot/multibot_irc_client.py", line 77, in twitch_privmsg
b.process_command(name, msg, tag_info)
File "/home/monkalot/monkalot/bot/bot.py", line 325, in process_command
cmd.run(self, user, msg, tag_info)
File "/home/monkalot/monkalot/bot/commands/topspammers.py", line 27, in run
result = ", ".join(["{}: Rank {}".format(bot.getDisplayNameFromID(viewer_id), bot.ranking.getHSRank(point)) for (viewer_id, point) in ranking])
File "/home/monkalot/monkalot/bot/commands/topspammers.py", line 27, in
result = ", ".join(["{}: Rank {}".format(bot.getDisplayNameFromID(viewer_id), bot.ranking.getHSRank(point)) for (viewer_id, point) in ranking])
File "/home/monkalot/monkalot/bot/bot.py", line 607, in getDisplayNameFromID
data = self.getUserDataFromID(user_id)
File "/home/monkalot/monkalot/bot/bot.py", line 509, in getUserDataFromID
data = self.getJSONObjectFromTwitchAPI(url)
File "/home/monkalot/monkalot/bot/bot.py", line 498, in getJSONObjectFromTwitchAPI
raise e
File "/home/monkalot/monkalot/bot/bot.py", line 490, in getJSONObjectFromTwitchAPI
r.raise_for_status()
File "/usr/lib/python3/dist-packages/requests/models.py", line 825, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 422 Client Error: Unprocessable Entity

Incorrect gender prounouns

Right now we use: he, him, his as default
And for females we have set: she, her, hers
in pronouns.json
But that's not correct, for example in: "/me holds in <u_pronoun2> arms. They are finally happy."
For male it is: "/me Aenduil holds Zetalot in his arms. They are finally happy."
For female: "/me Igetnokick holds Zetalot in hers arms. They are finally happy."

The pronouns should be:
He, him, his, his, himself
She, her, her, hers, herself

Handle chatbot returning None

Traceback (most recent call last):
  File "/usr/lib/python3.4/threading.py", line 920, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.4/threading.py", line 868, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python3.4/dist-packages/twisted/_threads/_threadworker.py", line 46, in work
    task()
  File "/usr/local/lib/python3.4/dist-packages/twisted/_threads/_team.py", line 190, in doWork
    task()
--- <exception caught here> ---
  File "/usr/local/lib/python3.4/dist-packages/twisted/python/threadpool.py", line 250, in inContext
    result = inContext.theWork()
  File "/usr/local/lib/python3.4/dist-packages/twisted/python/threadpool.py", line 266, in <lambda>
    inContext.theWork = lambda: context.call(ctx, func, *args, **kw)
  File "/usr/local/lib/python3.4/dist-packages/twisted/python/context.py", line 122, in callWithContext
    return self.currentContext().callWithContext(ctx, func, *args, **kw)
  File "/usr/local/lib/python3.4/dist-packages/twisted/python/context.py", line 85, in callWithContext
    return func(*args,**kw)
  File "/home/monkalot/monkalot/bot/commands/speech.py", line 51, in answer
    output = output + " monkaS"
builtins.TypeError: unsupported operand type(s) for +: 'NoneType' and 'str'

Use different string formatting that allows concatenation of None and str.
And maybe look for reason why None was returned.

Sometimes reactor thread cannot start

Today we have a small issue of emote per minute only returns 0

After a brief investigation, it seems that swapDummy stop calling itself ... for unknown reason. (bot/emotecounter.py, in line 78, self.callID = reactor.callLater(1, self.swapDummy, ) )

After restarting, the problem is fixed. However, we should take a note on this.

Party game - Encoding not displaying correctly?

Storytime: FeelsBirthdayMan and his brother PogChamp are going to Reykjav�������­k by bus.
ReykjavÃ�Â�Ã�Â�Ã�Â�Ã�­k is supposed to be Reykjavík (That i is not normal ASCII 'i')

Emote counter should use database

Currently counted emotes are written to a file. Instead, they should be written to a database. Make the database connection shared between all commands, so new commands can use it as well.

I have no Cleverbot API Key + One minor issue

Since I have no Cleverbot API Key, output in command.py:1173 will be None and crash afterwards.

Should I just get one API Key to continue testing, or are there any workaround?
I just think that if we are using more API keys, the setup of testing environment will be much more difficult too...

One smaller issue: def setResponses() in bot.py:62 seems never executed (only 1 line can be found by git grep setRes), no need to comment that out?

Lastly ... this is my first coding project in github. I am not sure if I am doing it right or not. Hope that I have not been very annoying or very noobish.

Just tried to install and run monkalot and here are my thoughts ...

After installing .NET framework >4.5.1 and C++ v14 and Windows 8.1 SDK on my machine, I can finally install twisted.

After that, I tried to run monaklot.py and I found there more packages are needed.

pip install pyparsing
pip install cleverwrap
pip install colorlog

I think it can be helpful If you can add a small text file suggesting all these packages and those Microsoft things are needed (if you are on Windows).

Now I can try to figure out how to make monkalot works locally ... (Keep seeing "Lose connection, reconnecting" in red now) monkaS

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.