Git Product home page Git Product logo

plugins's Introduction

Sapphire Logo

Sapphire Dev

GitHub app for Sapphire

GitHub

Description

The GitHub app that we use in Sapphire for automating various tasks.

Usage

Setup

# Install dependencies
yarn install

You will need to configure the Wrangler secrets for Cloudflare Workers environment. You will need the following secrets:

  • APP_ID

  • WEBHOOK_SECRET

  • PRIVATE_KEY

The private-key.pem file from GitHub needs to be transformed from the PKCS#1 format to PKCS#8, as the crypto APIs do not support PKCS#1:

openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private-key.pem -out private-key-pkcs8.pem

Then set the private key

cat private-key-pkcs8.pem | wrangler secret put PRIVATE_KEY

For information on what these values are and how to get them see this guide

Buy us some doughnuts

Sapphire Community is and always will be open source, even if we don't get donations. That being said, we know there are amazing people who may still want to donate just to show their appreciation. Thank you very much in advance!

We accept donations through Open Collective, Ko-fi, Paypal, Patreon and GitHub Sponsorships. You can use the buttons below to donate through your method of choice.

Donate With Address
Open Collective Click Here
Ko-fi Click Here
Patreon Click Here
PayPal Click Here

Contributors

Please make sure to read the Contributing Guide before making a pull request.

Thank you to all the people who already contributed to Sapphire!

plugins's People

Contributors

allcontributors[bot] avatar bensegal855 avatar c43721 avatar dependabot[bot] avatar depfu[bot] avatar despenser08 avatar favna avatar fc5570 avatar feralheart avatar ikrishagarwal avatar imranbarbhuiya avatar killbasa avatar kyranet avatar lioness100 avatar megatank58 avatar mzato0001 avatar noftaly avatar nytelife26 avatar orangebae avatar quantumlyy avatar r-priyam avatar realshadownova avatar renovate-bot avatar renovate[bot] avatar ricardooow avatar sawa-ko avatar stitch07 avatar undiedgamer avatar vladfrangu avatar yuansheng1549 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

plugins's Issues

request: make `plugin-logger`'s core independent from framework

Is your feature request related to a problem? Please describe.

Right now, if we want to use plugin-logger outside a Discord bot, we need to install @sapphire/framework, and with it, also discord.js and @types/ws.

Describe the solution you'd like

Make the logger framework agnostic.

Describe alternatives you've considered

Overhauling framework and separate it in different parts, @sapphire/core (or @sapphire/framework as-is), which contains the framework's core code (also no dependencies but Lexure for argument parsing), and @sapphire/framework-discord.js, which depends on discord.js.

Additional context

N/a.

request: port `applyLocalizedBuilder` and `createSelectMenuChoiceName` from `@skyra/http-framework-i18n` to `@sapphire/plugin-i18next`

Is there an existing issue or pull request for this?

  • I have searched the existing issues and pull requests

Feature description

@kyranet and I created applyLocalizedBuilder and createSelectMenuChoiceName for @skyra/http-framework-i18n to help with registering Application Commands with localized names and descriptions. It would be good for these functions to also be in @sapphire/plugin-i18next.

Desired solution

The code should be ported and exported in the plugin

The relevant code is: https://github.com/skyra-project/archid-components/blob/16b1f73a29494f9c74a47fdf10a082e97fb16541/packages/http-framework-i18n/src/lib/utils.ts#L73-L169

Alternatives considered

N.A.

Additional context

No response

bug: requiredClientPermissions set for an individual subcommand doesn't trigger *commandDenied

Is there an existing issue for this?

  • I have searched the existing issues

Description of the bug

When setting requiredClientPermissions for an individual subcommand, if it fails it will not trigger *commandDenied listeners, and will instead fail silently resulting in a "The application did not respond" response.

This will trigger *commandDenied listeners if the client doesn't have that permission.

public constructor(context: Subcommand.Context, options: Subcommand.Options) {
  super(context, {
    ...options,
    name: "example",
    description: "An example subcommand",
    requiredClientPermissions: [PermissionsBitField.Flags.ManageMessages],
    subcommands: [
      {
        name: "mysubcommand",
        chatInputRun: "chatInputRun"
      }
    ]
  });
}

This won't trigger *commandDenied listeners.

public constructor(context: Subcommand.Context, options: Subcommand.Options) {
  super(context, {
    ...options,
    name: "example",
    description: "An example subcommand",
    subcommands: [
      {
        name: "mysubcommand",
        chatInputRun: "chatInputRun",
        requiredClientPermissions: [PermissionsBitField.Flags.ManageMessages]
      }
    ]
  });
}

Steps To Reproduce

  1. Create a set of subcommands and give them individual requiredClientPermissions values
  2. Try to use them without the client having that permission

Expected behavior

It should trigger *commandDenied listeners

Screenshots

Top result when setting overall subcommand permissions, bottom result when setting per-subcommand permissions

image

Additional context

"@sapphire/framework": "^4.7.2",
"@sapphire/plugin-subcommands": "^5.0.0",
"@sapphire/utilities": "^3.13.0",
"discord.js": "^14.13.0",
"typescript": "^5.2.2"

request: generate dashless subcommands when options.generateDashlessAliases is true

Describe the solution you'd like

If CommandOptions.generateDashlessAliases is true, then subcommands should also work without aliases.
The same way that they work case-insensitively if ClientOptions.caseInsensitiveCommands is true.

Describe alternatives you've considered

Creating them myself which is janky

Additional context

I've tried to do it, in SubCommandPluginCommand's constructor:

const subCommands = [];
if (options.subCommands && options.generateDashLessAliases) {
    for (const subCommand of options.subCommands) {
        if (typeof subCommand === 'string') subCommands.push(subCommand.replace(/-/g, ''));
        // Doesn't work because input can be a function
        else subCommands.push(subCommand.input.replace(/-/g, ''));
    }
    subCommands.push(...options.subCommands);
}

this.subCommands = options.subCommands ? new SubCommandManager(subCommands) : null;

which, as written in the comments, doesn't work because input can be an (async) function, which raises several problems:

  • We don't have access to the context it needs,
  • We (obviously) can't await in a constructor
  • We don't wan't to run it twice (here and then in .match, where it is originally used)

So I'm pretty sure it can't be done this way.
I don't know where else I can put this, so I leave this feature for other people :) glhf

request: update `Target` type in `i18next`

Is there an existing issue or pull request for this?

  • I have searched the existing issues and pull requests

Feature description

When I tried to pass a ModalSubmitInteraction type in resolveKey() to Target, The type prevented me from doing this. I believe that the Target type needs to be expanded to accept interactions of the ModalSubmit.

Desired solution

Just update this line:

export type Target = CommandInteraction | ChannelTarget | Guild | MessageComponentInteraction;

To this:
export type Target = CommandInteraction | ChannelTarget | Guild | MessageComponentInteraction | ModalSubmitInteraction;
And ofc add import of type.

Alternatives considered

don't know any alternatives

Additional context

No response

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Awaiting Schedule

These updates are awaiting their schedule. Click on a checkbox to get an update now.

  • chore(deps): update all non-major dependencies (@vitest/coverage-v8, tsup, undici, vitest)

Ignored or Blocked

These are blocked by an existing closed PR and will not be recreated unless you click a checkbox below.

Detected dependencies

github-actions
.github/workflows/auto-deprecate.yml
  • actions/checkout v4
  • actions/setup-node v4
.github/workflows/codeql-analysis.yml
  • actions/checkout v4
  • github/codeql-action v3
  • github/codeql-action v3
  • github/codeql-action v3
.github/workflows/continuous-delivery.yml
  • actions/checkout v4
  • actions/setup-node v4
.github/workflows/continuous-integration.yml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/checkout v4
  • actions/setup-node v4
  • actions/checkout v4
  • actions/setup-node v4
  • codecov/codecov-action v4
.github/workflows/deprecate-on-merge.yml
  • actions/checkout v4
  • actions/setup-node v4
.github/workflows/documentation.yml
  • actions/checkout v4
  • actions/setup-node v4
  • actions/upload-artifact v4
  • actions/checkout v4
  • actions/setup-node v4
  • actions/download-artifact v4
  • actions/checkout v4
  • nick-fields/retry v3
.github/workflows/labelsync.yml
  • actions/checkout v4
  • actions/checkout v4
  • crazy-max/ghaction-github-labeler v5
.github/workflows/publish.yml
  • actions/checkout v4
  • actions/setup-node v4
.github/workflows/release-crosspost.yml
  • kludge-cs/gitcord-release-changelogger v3.0.0@5592170408dc081d7cb6a74ce025911bd1fcb7c3
npm
package.json
  • @actions/core ^1.10.1
  • @commitlint/cli ^19.3.0
  • @commitlint/config-conventional ^19.2.2
  • @favware/cliff-jumper ^4.0.2
  • @favware/npm-deprecate ^1.0.7
  • @favware/rollup-type-bundler ^3.3.0
  • @sapphire/eslint-config ^5.0.5
  • @sapphire/framework ^5.2.1
  • @sapphire/pieces ^4.3.1
  • @sapphire/prettier-config ^2.0.0
  • @sapphire/stopwatch ^1.5.2
  • @sapphire/ts-config ^5.0.1
  • @sapphire/utilities ^3.16.2
  • @types/node ^20.14.11
  • @types/ws ^8.5.11
  • @vitest/coverage-v8 ^2.0.3
  • concurrently ^8.2.2
  • cz-conventional-changelog ^3.3.0
  • discord-api-types ^0.37.92
  • discord.js ^14.15.3
  • esbuild-plugin-file-path-extensions ^2.1.2
  • esbuild-plugin-version-injector ^1.2.1
  • eslint ^8.57.0
  • eslint-config-prettier ^9.1.0
  • eslint-plugin-prettier ^5.2.1
  • lint-staged ^15.2.7
  • prettier ^3.3.3
  • rimraf ^6.0.1
  • tsup ^8.2.1
  • tsx ^4.16.2
  • turbo ^2.0.9
  • vite ^5.3.4
  • vitest ^2.0.3
  • acorn ^8.12.1
  • ansi-regex ^5.0.1
  • minimist ^1.2.8
  • yarn 4.3.1
packages/api/package.json
  • @types/ws ^8.5.11
  • @vladfrangu/async_event_emitter 2.4.4
  • tldts ^6.1.33
  • undici ^6.19.2
  • @favware/cliff-jumper ^4.0.2
  • @favware/rollup-type-bundler ^3.3.0
  • concurrently ^8.2.2
  • tsup ^8.2.1
  • tsx ^4.16.2
packages/editable-commands/package.json
  • @skyra/editable-commands ^3.0.2
  • @favware/cliff-jumper ^4.0.2
  • @favware/rollup-type-bundler ^3.3.0
  • concurrently ^8.2.2
  • tsup ^8.2.1
  • tsx ^4.16.2
packages/hmr/package.json
  • chokidar ^3.6.0
  • @favware/cliff-jumper ^4.0.2
  • @favware/rollup-type-bundler ^3.3.0
  • concurrently ^8.2.2
  • tsup ^8.2.1
  • tsx ^4.16.2
packages/i18next/package.json
  • @sapphire/utilities ^3.16.2
  • @skyra/i18next-backend ^2.0.5
  • chokidar ^3.6.0
  • i18next ^23.12.2
  • @favware/cliff-jumper ^4.0.2
  • @favware/rollup-type-bundler ^3.3.0
  • concurrently ^8.2.2
  • tsup ^8.2.1
  • tsx ^4.16.2
packages/logger/package.json
  • @sapphire/timestamp ^1.0.3
  • colorette ^2.0.20
  • @favware/cliff-jumper ^4.0.2
  • @favware/rollup-type-bundler ^3.3.0
  • concurrently ^8.2.2
  • tsup ^8.2.1
  • tsx ^4.16.2
packages/pattern-commands/package.json
  • @favware/cliff-jumper ^4.0.2
  • @favware/rollup-type-bundler ^3.3.0
  • concurrently ^8.2.2
  • tsup ^8.2.1
  • tsx ^4.16.2
packages/scheduled-tasks/package.json
  • @sapphire/stopwatch ^1.5.2
  • @sapphire/utilities ^3.16.2
  • bullmq 5.10.3
  • @favware/cliff-jumper ^4.0.2
  • @favware/rollup-type-bundler ^3.3.0
  • concurrently ^8.2.2
  • tsup ^8.2.1
  • tsx ^4.16.2
packages/subcommands/package.json
  • @sapphire/utilities ^3.16.2
  • @favware/cliff-jumper ^4.0.2
  • @favware/rollup-type-bundler ^3.3.0
  • concurrently ^8.2.2
  • tsup ^8.2.1
  • tsx ^4.16.2
packages/utilities-store/package.json
  • @favware/cliff-jumper ^4.0.2
  • @favware/rollup-type-bundler ^3.3.0
  • concurrently ^8.2.2
  • tsup ^8.2.1
  • tsx ^4.16.2

  • Check this box to trigger a request for Renovate to run again on this repository

[SFW-80] bug: provided a subcommand group is configured with a default match and the command is ran with only the name of the subcommand the default match doesn't get executed.

Is there an existing issue for this?

  • I have searched the existing issues

Description of the bug

Provided the following subcommand array:

subcommands: [
  {
    type: 'group',
    name: 'config',
    entries: [
      { name: 'edit', messageRun: 'configEdit' },
      { name: 'show', messageRun: 'configShow', default: true },
      { name: 'remove', messageRun: 'configRemove' },
      { name: 'reset', messageRun: 'configReset' }
    ]
  }
]

The expected behaviour is that running !command config would execute show as that is the configured default.

Currently however an error is thrown due to subcommandName.isSome() returning false here:

if (mapping.type === 'group' && subcommandOrGroup.isSome() && subcommandName.isSome()) {

Steps To Reproduce

  1. Copy the following subcommand code as a new command:
import { Subcommand } from '@sapphire/plugin-subcommands';
import type { Message } from 'discord.js';

export class UserCommand extends Subcommand {
  public constructor(context: Subcommand.Context) {
    super(context, {
      aliases: ['sg'],
      description: 'A message command with some subcommand groups',
      subcommands: [
        {
          type: 'group',
          name: 'config',
          entries: [
            { name: 'edit', messageRun: 'configEdit' },
            { name: 'show', messageRun: 'configShow', default: true },
            { name: 'remove', messageRun: 'configRemove' },
            { name: 'reset', messageRun: 'configReset' }
          ]
        }
      ]
    });
  }
  public async configShow(message: Message) {
    return message.channel.send('Showing!');
  }

  public async configEdit(message: Message) {
    return message.channel.send('Editing!');
  }

  public async configRemove(message: Message) {
    return message.channel.send('Removing!');
  }

  public async configReset(message: Message) {
    return message.channel.send('Resetting!');
  }
}
  1. Run !sg config
  2. Observe that show subcommand does not get executed and instead a messageSubcommandNoMatch error is thrown.

Expected behavior

The default subcommand within the subcommand group executes as described

Screenshots

No response

Additional context

No response

SFW-80

bug: The LoggerPlugin implementation do not allow to replace the logger instance via ClientOptions

Is there an existing issue for this?

  • I have searched the existing issues

Description of the bug

Since the LoggerPlugin is always creating a new instance of logger instance:

public static [preGenericsInitialization](this: SapphireClient, options: ClientOptions): void {
	options.logger ??= {};
	options.logger.instance = new Logger(options.logger);
}

It's not possible to use the instance from ClientLoggerOptions to replace the logger on SapphireClient

Steps To Reproduce

On my SapphireClient:

export class MySapphireClient extends SapphireClient {
    public constructor() {
        super({
            logger: {
                instance: new MyLogger()
            },
            ...
        });
    }
}

Fire the container.logger.info('something') and check if that is using the default Loggerprovided by the [logger plugin](https://github.com/sapphiredev/plugins/tree/main/packages/logger) of theMyLoggerinstance provided onClientOptions`

Expected behavior

container.logger.* delegates to MyLogger instead of the Logger from the logger plugin

Screenshots

No response

Additional context

No response

[SFW-83] request: support full path based for plugin-api

Is there an existing issue or pull request for this?

  • I have searched the existing issues and pull requests

Feature description

At the moment the only path based system that the API plugin has is that, like with any other pieces, it reads the file name and uses that as the name of the route. This however does not match common solutions for other frameworks with file-based routing where folder names are also respected. Furthermore, there is currently no way to set path parameters through file names, something else that other file-based routing frameworks do have.

It should be noted that if a piece is a virtual piece it will not be supported for file-based routing.

Desired solution

Implement a solution where plugin-api leverages full file-based routing. Taking inspiration from frameworks such as Nuxt the following route tree:

image

The expected routes to register would be be:

  • /guilds
  • /guilds/:guild
  • /guilds/:guild/roles
  • /guilds/:guild/channels
  • /guilds/:guild/channels/:channel

Alternatives considered

  • Not implementing a file-based routing system -> It is very nice to have, so why not make it? We have path loading already anyway.
  • A different way of doing path parameters. For example @tanstack/react-router uses the following below. However I thin the way Nuxt does it with [square braces] is better and clearer.
    • $postId
    • $name
    • $teamId
    • about/$name
    • team/$teamId
    • blog/$postId

Additional context

No response

SFW-83

bug: unknown routes respond with an 200 status and blank response

Describe the bug

All routes not defined within a file respond with a 200 status and a blank response

To Reproduce

  1. Install framework and plugin-api
  2. register plugin-api and client in a script
  3. run
  4. go to http://localhost:4000/any-route

Expected behavior

Should respond with a 404 status code

Additional context

Found that when the ServerEvents.NoMatch event is emitted the headers are already sent to the client so the status can not be changed

bug: error when using automatic language routing with ts-node

Describe the bug
When I don't specify a manual path to where the translation files are for the i18next plugin, using ts-node gives a path error.

To Reproduce

  1. Write Client with i18next plugin
  2. Do not set the "defaultLanguageDirectory" option of the i18next plugin
  3. Run the application with ts-node (ts-node --files src/main.ts)

Expected behavior
That the application correctly loads all language files.

Screenshots
image

Additional context
If the language folder path is set manually, the error disappears.

{
// client options....
  i18n: {
    defaultLanguageDirectory: join(__dirname, '..', 'languages'),
  }
}

bug: default cooldowns are ran twice for subcommands

Is there an existing issue for this?

  • I have searched the existing issues

Description of the bug

If you set a global default cooldown and attempt to run a command using the subcommand plugin, the rate limit bucket will be consumed twice. If the limit is 1, the command can never be ran.

Reference: https://discord.com/channels/737141877803057244/737142940777971773/1171224384770416730

Steps To Reproduce

  1. Create a SapphireClient with the following default cooldown options:
defaultCooldown: {
    limit: 1,
    delay: 5000 // 5 Seconds
},
  1. Create a command that uses the subcommand plugin and has a subcommand.
  2. Attempt to run the command.

Expected behavior

The command should be able to be ran once before hitting the rate limit.

Screenshots

No response

Additional context

No response

Bug: Broken ScheduledTasks typescript augmentation

Is there an existing issue for this?

  • I have searched the existing issues

Description of the bug

When augmenting the ScheduledTasks interface for multiple tasks, it's only showing the type-hint for one task.

Steps To Reproduce

  1. Clone the repo - https://github.com/r-priyam/scheduled-tasks-ts-error
  2. Try creating the tasks using the container.tasks.create() or this.container.tasks.create(). You will only see one type-hint option for the task name and not two. However, it should show two options as there are two tasks augmentation for it, mainly here -

Expected behavior

It should show taskOne and taskTwo in the type-hint

Screenshots

image

Additional context

No response

bug (subcommands): default message subcommand consuming first argument

Is there an existing issue for this?

  • I have searched the existing issues

Description of the bug

When using the default option on a message subcommand mapping, it would be expected that it isn't necessary to provide the name of the subcommand and any arguments given in the message would be passed through to the command.

Instead, the plugin appears to be always consuming the first argument, so as an example I might have a role-info role <role> command usage where role is the subcommand and <role> is the Discord role. Setting role as a default subcommand would expect role-info <role> to be valid and pass the role on to the command but instead <role> is consumed prior to reaching the command. But running role-info <any word> <role> will work and pass just the <role> on to the command where <any word> is just consumed and unused.

Steps To Reproduce

The issue can be reproduced through the following command class:

import { Subcommand, SubcommandMappingArray } from '@sapphire/plugin-subcommands';
import type { Args } from '@sapphire/framework';
import type { Message } from 'discord.js';

export class UserCommand extends Subcommand {
  public subcommandMappings: SubcommandMappingArray = [
    {
      name: 'sub1',
      default: true,
      messageRun: async (message: Message, args: Args) => {
        const arg = await args.rest('string');
        return message.reply(`sub1, ${arg}`);
      }
    }
  ];
}

Running the expected command <whatever args> will result in an error due to missing args and produces the bug.

Expected behavior

The first argument should not be consumed since the default option means it's not expecting the name of the subcommand to be provided. Unless the user specifically specifies the subcommand name in which case that should be consumed.

Screenshots

No response

Additional context

No response

bug: default message subcommand consuming first argument

Is there an existing issue for this?

  • I have searched the existing issues

Description of the bug

When using the default option on a message subcommand mapping, it would be expected that it isn't necessary to provide the name of the subcommand and any arguments given in the message would be passed through to the command.

Instead, the plugin appears to be always consuming the first argument, so as an example I might have a role-info role <role> command usage where role is the subcommand and <role> is the Discord role. Setting role as a default subcommand would expect role-info <role> to be valid and pass the role on to the command but instead <role> is consumed prior to reaching the command. But running role-info <any word> <role> will work and pass just the <role> on to the command where <any word> is just consumed and unused.

Steps To Reproduce

The issue can be reproduced through the following command class:

import { Subcommand, SubcommandMappingArray } from '@sapphire/plugin-subcommands';
import type { Args } from '@sapphire/framework';
import type { Message } from 'discord.js';

export class UserCommand extends Subcommand {
  public subcommandMappings: SubcommandMappingArray = [
    {
      name: 'sub1',
      default: true,
      messageRun: async (message: Message, args: Args) => {
        const arg = await args.rest('string');
        return message.reply(`sub1, ${arg}`);
      }
    }
  ];
}

Running the expected command <whatever args> will result in an error due to missing args and produces the bug.

Expected behavior

The first argument should not be consumed since the default option means it's not expecting the name of the subcommand to be provided. Unless the user specifically specifies the subcommand name in which case that should be consumed.

Screenshots

No response

Additional context

No response

request: Release new version of @sapphire/plugin-editable-commands

Is your feature request related to a problem? Please describe.

Currently the plugin doesn't work, as to #109

Describe the solution you'd like

Have a new release with this patch included

Describe alternatives you've considered

Modify the current release although that's impractical

Additional context

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.