master a0027f0a3b4d cached
20 files
30.5 KB
8.2k tokens
33 symbols
1 requests
Download .txt
Repository: bryanberger/figma-discord-presence
Branch: master
Commit: a0027f0a3b4d
Files: 20
Total size: 30.5 KB

Directory structure:
gitextract_wfp9rim1/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   └── workflows/
│       └── deploy.yml
├── .gitignore
├── .node-version
├── LICENSE
├── README.md
├── build/
│   ├── entitlements.mac.plist
│   └── icon.icns
├── dev-app-update.yml
├── package.json
└── src/
    ├── afterSignHook.js
    ├── lib/
    │   ├── activity.js
    │   ├── config.js
    │   ├── events.js
    │   ├── figma.js
    │   ├── logger.js
    │   ├── tray.js
    │   ├── updater.js
    │   └── util.js
    └── main.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: bryanberger

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/workflows/deploy.yml
================================================
name: Build/release

on:
  push:
    branches:
      - master
  workflow_dispatch: {}

jobs:
  release:
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        # use macos-11 for now until we can fix python versions
        os: [macos-11, windows-latest]

    steps:
      - name: Check out Git repository
        uses: actions/checkout@v2

      - name: Install Node.js, NPM and Yarn
        uses: actions/setup-node@v1
        with:
          node-version: "14.15.0"

      - name: Build/release Electron app
        uses: samuelmeuli/action-electron-builder@v1
        with:
          # Apple code signing certs
          mac_certs: ${{ secrets.mac_certs }}
          mac_certs_password: ${{ secrets.mac_certs_password }}

          # GitHub token, automatically provided to the action
          # (No need to define this secret in the repo settings)
          github_token: ${{ secrets.github_token }}

          # If the commit is tagged with a version (e.g. "v1.0.0"),
          # release the app after building
          release: ${{ startsWith(github.ref, 'refs/tags/v') }}
          
          # Always publish to S3
          args: "-p always"
        env:
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

================================================
FILE: .gitignore
================================================
# Packages
node_modules
**/node_modules

# Build artifacts
dist/

# Log files
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Miscellaneous
.tmp/
!.vscode/launch.json
.env
notes.md

================================================
FILE: .node-version
================================================
14.15.0


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 Bryan Berger

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: README.md
================================================
# Figma Discord Presence

[![Build/release](https://github.com/bryanberger/figma-discord-presence/actions/workflows/deploy.yml/badge.svg)](https://github.com/bryanberger/figma-discord-presence/actions/workflows/deploy.yml)

<a href="https://www.producthunt.com/posts/figma-discord-presence?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-figma-discord-presence" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=305303&theme=dark" alt="Figma Discord Presence - Adds rich presence activity to Discord for Figma | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>

> Update your discord activity status with a rich presence from Figma.
> Supports Windows and MacOS

![demo](.github/demo.png?raw=true)

## Features

- Shows what you're working on in Figma
- Menubar application for convenient control and configuration
- Privacy configuration options for hiding filenames, activity status, and Figma view buttons
- Idle and active indication if you have tabbed out or are actively using Figma
- Respects Discords 15s status update limit, but, privacy options set immediately
- Support for manually reconnecting to the Discord Gateway
- Support for enabling or disabling presence reporting at will

## How does it work?

Figma does not support a native way to monitor the application state in the background (yet?), but, it does drop some state files on your machine.

This application periodically reads those files checking for updates and combines some information to determine whether Figma is in the foreground, what the current active file is, and a share link to that file.

Every ~15s your Figma activity is reported to Discord via the Discord RPC protocol.

## Troubleshooting

> Linux is currently not supported.
> This application requires Figma Desktop.
> Ensure that you have your activity status enabled in Discord, or your activity won't be visible to anyone.

**MacOS:**

- MacOS may ask for permission to control other apps. It is required to enable and communicate with Figma and Discord.
- This application assumes you install Figma Desktop normally, and have not changed or modified it in any way
- `~/Library/Saved\ Application\ State/com.figma.Desktop.savedState/windows.plist` must exist
- `~/Library/Application\ Support/Figma/settings.json` must exist
- It may take a few seconds for your activity to update to show the latest active/idle status and filename in Discord. Figma's `savedState` does not update in realtime. We could watch this file for changes and update your Discord activity when it does but since we try to honor Discord's 15s activity update limit, we currently just wait for the next tick to update your activity.

## Development

```bash
# Clone this repository
git clone https://github.com/bryanberger/figma-discord-presence

# Change directory
cd figma-discord-presence

# Copy and edit env vars
cp .env.example .env

# Install dependencies
npm install

# Run the app
npm start

# Build the electron binaries
npm run dist

# Publish (using the S3 Provider, make sure you're authenticated and have a bucket setup)
npm run publish
```

## Release

This project uses Github Actions to build for Windows and Mac. Upon successful build, if a git tag exists it will publish to S3 (given you've provided the proper access tokens).

When you want to create a new release, follow these steps:

- Update the version in your project's package.json file (e.g. 1.2.3)
- Commit that change (git commit -am v1.2.3)
- Tag your commit (git tag v1.2.3). Make sure your tag name's format is v*.*.*. Your workflow will use this tag to detect when to create a release
- Push your changes to GitHub (git push && git push --tags)

After building successfully, the action will publish your release artifacts.

## Contributing
To contribute to this repository, feel free to create a new fork of the repository and submit a pull request.

1. Fork / Clone and select the `master` branch.
2. Create a new branch in your fork.
3. Make your changes.
4. Commit your changes, and push them.
5. Submit a Pull Request [here](https://github.com/bryanberger/figma-discord-presence/pulls)!

## Notice

While I am a Discord employee, this is by no way endorsed as an "official" integration with Figma. This is a personal project and is actually kind of a hacky solution to bring Rich Presence for Figma to Discord.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.


================================================
FILE: build/entitlements.mac.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">  
<plist version="1.0">  
  <dict>  
    <key>com.apple.security.cs.allow-jit</key>  
    <true/>  
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>  
    <true/>  
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>  
    <true/>  
  </dict>  
</plist>

================================================
FILE: dev-app-update.yml
================================================
provider: s3
bucket: figma-discord-presence


================================================
FILE: package.json
================================================
{
  "name": "figma-discord-presence",
  "productName": "Figma Discord Presence",
  "version": "1.2.6",
  "description": "Discord Rich Presence for Figma",
  "main": "src/main.js",
  "scripts": {
    "postinstall": "electron-builder install-app-deps",
    "start": "electron .",
    "pack": "electron-builder --dir",
    "dist": "electron-builder --mac --windows",
    "dist:mac": "electron-builder --mac",
    "dist:win": "electron-builder --windows",
    "publish": "electron-builder --mac --windows --publish always",
    "publish:mac": "electron-builder --mac --publish always",
    "publish:win": "electron-builder --windows --publish always"
  },
  "repository": "",
  "keywords": [
    "discord",
    "figma",
    "discord-presence",
    "discord-status",
    "discord-rpc",
    "electron"
  ],
  "author": "Bryan Berger",
  "license": "MIT",
  "dependencies": {
    "@bberger/win-info-fork": "^0.2.14",
    "@sentry/electron": "^2.5.1",
    "@sentry/integrations": "^6.10.0",
    "bplist-parser": "^0.3.0",
    "convict": "^6.2.0",
    "discord-rpc": "^4.0.1",
    "dotenv": "^10.0.0",
    "electron-log": "^4.3.5",
    "electron-notarize": "^1.0.0",
    "electron-updater": "^4.4.1",
    "fs-extra": "^10.0.0",
    "lodash": "^4.17.21",
    "ps-list": "^7.2.0"
  },
  "devDependencies": {
    "electron": "^13.1.7",
    "electron-builder": "^22.11.8"
  },
  "build": {
    "asar": true,
    "appId": "com.bryanberger.figma-discord-presence",
    "productName": "Figma Discord Presence",
    "afterSign": "./src/afterSignHook.js",
    "win": {
      "target": "nsis",
      "icon": "./build/icon.ico"
    },
    "mac": {
      "hardenedRuntime": true,
      "entitlements": "./build/entitlements.mac.plist",
      "entitlementsInherit": "./build/entitlements.mac.plist",
      "icon": "./build/icon.icns",
      "target": [
        {
          "target": "dmg"
        },
        {
          "target": "zip"
        }
      ]
    },
    "publish": {
      "provider": "s3",
      "bucket": "figma-discord-presence"
    }
  }
}

================================================
FILE: src/afterSignHook.js
================================================
const fs = require("fs");
const path = require("path");
const electron_notarize = require("electron-notarize");

require("dotenv").config();
module.exports = async function (params) {
  // Only notarize the app on Mac OS only and on CI.
  if (process.platform !== "darwin" || process.env.NODE_ENV === "development") {
    return;
  }

  console.log("afterSign hook triggered", params);

  // Same appId in electron-builder.
  let appId = "com.bryanberger.figma-discord-presence";
  let appPath = path.join(
    params.appOutDir,
    `${params.packager.appInfo.productFilename}.app`
  );

  if (!fs.existsSync(appPath)) {
    throw new Error(`Cannot find application at: ${appPath}`);
  }

  console.log(`Notarizing ${appId} found at ${appPath}`);
  try {
    await electron_notarize.notarize({
      ascProvider: process.env.APPLE_TEAM_ID, // adding this because I belong to multiple teams
      appBundleId: appId,
      appPath: appPath,
      appleId: process.env.APPLE_ID, // this is your apple ID it should be stored in an .env file
      appleIdPassword: process.env.APPLE_ID_PASSWORD, // this is NOT your apple ID password. You need to create an application specific password from https://appleid.apple.com under "security" you can generate such a password
    });
  } catch (error) {
    console.error(error);
  }
  console.log(`Done notarizing ${appId}`);
};


================================================
FILE: src/lib/activity.js
================================================
const EventEmitter = require("events");
const RPC = require("discord-rpc");

const {
  getIsFigmaRunning,
  getFigmaMetaData,
  getIsFigmaActive,
} = require("./figma");
const logger = require("./logger");
const config = require("./config");
const events = require("./events");

const CLIENT_ID = "866719067092418580";

class Activity extends EventEmitter {
  constructor() {
    super();

    this.client = null;
    this.setActivityInterval = null;
    this.startTime = null;

    // if (config.get("connectOnStartup")) {
    //   this.login();
    // }
  }

  async login() {
    this.emit(events.DISCORD_CONNECTING);
    this.client = new RPC.Client({ transport: "ipc" });

    this.client.on("ready", () => {
      this.emit(events.DISCORD_READY);
      this.setActivity();
      this.startInterval();
    });

    this.client.on("disconnected", () => {
      this.emit(events.DISCORD_DISCONNECTED);
      this.destroy();
    });

    try {
      await this.client.login({ clientId: CLIENT_ID });
    } catch (err) {
      logger.error("activity", err.message);
      this.emit(events.DISCORD_LOGIN_ERROR);
      this.client = null;
    }
  }

  async setActivity() {
    if (this.client === null) return;

    try {
      const isFigmaRunning = await getIsFigmaRunning();

      if (isFigmaRunning) {
        if (!this.startTime) {
          this.startTime = new Date();
        }
      } else {
        await this.client.clearActivity();
        this.startTime = null;
        return;
      }

      const { currentFigmaFilename, shareLink } = await getFigmaMetaData();

      if (currentFigmaFilename === null) {
        return;
      }

      const isFigmaActive = await getIsFigmaActive();

      // Gather Config Options
      const isHideFilenames = config.get("hideFilenames");
      const isHideStatus = config.get("hideStatus");
      const isHideViewButton = config.get("hideViewButton");

      // Build detail string
      const details = [
        !isHideStatus ? (isFigmaActive ? "Active" : "Idle") : "",
        `${!isHideStatus && !isHideFilenames ? " " : ""}`,
        !isHideFilenames ? `in: "${currentFigmaFilename}"` : undefined,
      ];

      // You'll need to have the logo asset uploaded to
      // https://discord.com/developers/applications/<application_id>/rich-presence/assets
      this.client.setActivity({
        details: details.join("") || undefined,
        startTimestamp: this.startTime,
        largeImageKey: "logo",
        largeImageText: "Designing in Figma",
        buttons:
          !isHideViewButton && shareLink
            ? [{ label: "View in Figma", url: shareLink }]
            : undefined,
        instance: false,
      });
    } catch (err) {
      logger.error("activity", `Failed to setActivity: ${err}`);
    }
  }

  startInterval() {
    this.setActivityInterval = setInterval(() => {
      this.setActivity();
    }, 15e3);
  }

  async stopInterval() {
    clearInterval(this.setActivityInterval);
    this.setActivityInterval = null;
    this.startTime = null;
  }

  async updateOptions() {
    await this.setActivity();
  }

  async connect() {
    await this.login();
  }

  async disconnect() {
    await this.destroy();
  }

  async destroy() {
    try {
      await this.client.clearActivity();
      await this.client.destroy();
    } catch {}

    this.client = null;
    this.stopInterval();
  }
}

module.exports = Activity;


================================================
FILE: src/lib/config.js
================================================
const convict = require("convict");
const fs = require("fs-extra");

const logger = require("./logger");
const util = require("./util");

const retries = 10;

const conf = convict({
  hideFilenames: {
    doc: "Show or hide filenames",
    default: false,
    format: "Boolean",
  },
  hideStatus: {
    doc: "Show or hide active/idle status",
    default: false,
    format: "Boolean",
  },
  hideViewButton: {
    doc: "Show or hide the view in figma button",
    default: true,
    format: "Boolean",
  },
  // connectOnStartup: {
  //   doc: "Connect to Discord on application startup",
  //   default: true,
  //   format: "Boolean",
  // },
});

function load() {
  return new Promise(async (resolve, reject) => {
    try {
      const json = util.getAppData("/config.json");
      conf.loadFile(json);

      logger.debug("config", "loaded!");
      return resolve(conf.validate());
    } catch (err) {
      reject(err);
    }
  });
}

function save(times, init = false) {
  times = times || 0;
  const options = { spaces: 2 };

  if (init) {
    options.flag = "wx";
  }

  try {
    fs.writeJsonSync(
      util.getAppData("/config.json"),
      conf.getProperties(),
      options
    );
  } catch (err) {
    // if any other error than 'File already exists' then retry.
    if (err.code !== "EEXIST") {
      logger.error("config", err.message);
      if (times < retries) {
        setTimeout(() => {
          save(times + 1);
        }, 1000);
      }
    }
  }
  logger.debug("config", "saved!");
}

function getAll() {
  if (conf) {
    return conf.getProperties();
  }
}

module.exports = conf;
module.exports.load = load;
module.exports.save = save;
module.exports.getAll = getAll;


================================================
FILE: src/lib/events.js
================================================
module.exports = {
  QUIT: "QUIT",
  CONNECT: "CONNECT",
  DISCONNECT: "DISCONNECT",
  UPDATE_OPTIONS: "UPDATE_OPTIONS",
  CHECK_FOR_UPDATES: "CHECK_FOR_UPDATES",
  DISCORD_CONNECTING: "DISCORD_CONNECTING",
  DISCORD_READY: "DISCORD_READY",
  DISCORD_DISCONNECTED: "DISCORD_DISCONNECTED",
  DISCORD_LOGIN_ERROR: "DISCORD_LOGIN_ERROR",
};


================================================
FILE: src/lib/figma.js
================================================
const _ = require("lodash");
const bplist = require("bplist-parser");
const psList = require("ps-list");
const winInfo = require("@bberger/win-info-fork");
const fs = require("fs");

const logger = require("./logger");
const util = require("./util");

async function getFigmaMetaData() {
  let currentFigmaFilename = null;
  let shareLink = null;

  try {
    if (process.platform === "darwin") {
      const parsed = await bplist.parseFile(
        `${util.getHomePath()}/Library/Saved Application State/com.figma.Desktop.savedState/windows.plist`
      );

      currentFigmaFilename =
        _.flattenDeep(parsed).find((o) => o.hasOwnProperty("NSTitle"))[
          "NSTitle"
        ] || null;
    } else if (process.platform === "win32") {
      // Find the main Figma process first
      const processList = await psList();
      const figmaProcesses =
        processList.filter((p) => p.name.includes("Figma.exe")) || [];

      // The main Figma process is the one that matches as a parent pid (ppid) from the others
      // Should only be 1
      const mainFigmaProcess = _.intersectionWith(
        figmaProcesses,
        (a, b) => a.ppid === b.pid
      ).shift();

      // Lookup its window title
      if (mainFigmaProcess) {
        try {
          const figmaWindow = winInfo.getByPidSync(mainFigmaProcess.pid);
          if (figmaWindow && figmaWindow.title.includes(" - Figma")) {
            currentFigmaFilename = figmaWindow.title.split(" - Figma")[0];
          }
        } catch (err) {}
      }
    }

    if (currentFigmaFilename === null) {
      return { currentFigmaFilename, shareLink };
    }

    const figmaDataFile = fs.readFileSync(
      `${util.getPath("appData")}/Figma/settings.json`,
      "utf-8"
    );
    const figmaData = JSON.parse(figmaDataFile);
    const flatWindows = _.flattenDeep(figmaData.windows);

    flatWindows.map((window) => {
      window.tabs.map((tab) => {
        const { path, title, params } = tab;
        if (title === currentFigmaFilename) {
          // based on the current file name, we can lookup the id and generate a "view link"
          shareLink = encodeURI(
            `https://www.figma.com${path}/${params ? params : ""}`
          );
        }
      });
    });
  } catch (err) {
    logger.error("figma", err.message);
  }

  return { currentFigmaFilename, shareLink };
}

async function getIsFigmaRunning() {
  let isRunning = false;
  const processList = await psList();

  if (process.platform === "darwin") {
    isRunning =
      processList.filter((p) =>
        p.cmd.includes("Figma.app/Contents/MacOS/Figma")
      ).length > 0;
  } else if (process.platform === "win32") {
    isRunning =
      processList.filter((p) => p.name.includes("Figma.exe")).length > 0;
  }

  return isRunning;
}

async function getIsFigmaActive() {
  let isActive = false;

  try {
    const activeWin = await winInfo.getActive();
    isActive = activeWin?.owner?.name.includes("Figma") || false;
  } catch (err) {}

  return isActive;
}

module.exports = {
  getFigmaMetaData,
  getIsFigmaRunning,
  getIsFigmaActive,
};


================================================
FILE: src/lib/logger.js
================================================
const Sentry = require("@sentry/electron");
const { CaptureConsole } = require("@sentry/integrations");
const log = require("electron-log");

log.transports.console.format = "[{h}:{i}:{s}.{ms}] {text}";
log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}.{ms}] {text}";
log.transports.file.level = "debug";

Sentry.init({
  dsn: "https://b32c5d25554f4e6ebed361104462766a@o940691.ingest.sentry.io/5890015",
  integrations: [
    new CaptureConsole({
      levels: ["error"],
    }),
  ],
  ignoreErrors: ["in JSON at position"],
});

module.exports = {
  info: function (tag, msg) {
    log.info(`[${tag}] [INFO] ${msg}`);
  },
  debug: function (tag, msg) {
    log.debug(`[${tag}] [DEBUG] ${msg}`);
  },
  error: function (tag, msg) {
    log.error(`[${tag}] [ERROR] ${msg}`);
  },
  log,
};


================================================
FILE: src/lib/tray.js
================================================
const EventEmitter = require("events");
const path = require("path");
const { shell, nativeTheme, Menu, Tray } = require("electron");

const config = require("./config");
const logger = require("./logger");
const util = require("./util");
const events = require("./events");

const iconOn = path.join(__dirname, `/../../assets/on.png`);
const iconOff = path.join(__dirname, `/../../assets/off.png`);
// const iconOff = nativeImage.createFromDataURL(offUrl);
// todo use nativeImage and png loader

class CustomTray extends EventEmitter {
  tray = null;
  contextMenu = null;
  state = null;

  baseMenuTemplate = [
    { type: "separator" },
    {
      label: "Options",
      submenu: [
        {
          label: "Hide filenames",
          type: "checkbox",
          checked: config.get("hideFilenames"),
          click: (menuItem) =>
            this.saveConfigAndUpdate("hideFilenames", menuItem.checked),
        },
        {
          label: "Hide active/idle status",
          type: "checkbox",
          checked: config.get("hideStatus"),
          click: (menuItem) =>
            this.saveConfigAndUpdate("hideStatus", menuItem.checked),
        },
        {
          label: 'Hide "View in Figma" button',
          type: "checkbox",
          checked: config.get("hideViewButton"),
          click: (menuItem) =>
            this.saveConfigAndUpdate("hideViewButton", menuItem.checked),
        },
        // {
        //   label: "Connect to Discord when this app starts",
        //   type: "checkbox",
        //   checked: config.get("connectOnStartup"),
        //   click: (menuItem) =>
        //     this.saveConfigAndUpdate("connectOnStartup", menuItem.checked),
        // },
      ],
    },

    { type: "separator" },
    {
      label: "Check for Updates...",
      click: () => this.emit(events.CHECK_FOR_UPDATES),
    },
    {
      label: "Show Config",
      click: () => shell.openPath(util.getAppDataPath()),
    },
    { type: "separator" },
    {
      label: "Exit",
      click: () => this.emit(events.QUIT),
    },
  ];

  constructor(trayState) {
    super();

    logger.debug("tray", "initalized");

    this.state = trayState;

    this.tray = new Tray(this.getIconPath());
    this.update();

    nativeTheme.on("updated", () => this.update());
  }

  getIconPath() {
    const iconState = this.state.isDiscordReady ? "On" : "Off";

    if (process.platform === "darwin") {
      return path.join(__dirname, `/../../assets/Icon${iconState}Template.png`);
    } else if (process.platform === "win32") {
      // always use the darkmode icon on windows, taskbar seems to be dark regardless of theme
      return path.join(__dirname, `/../../assets/Icon${iconState}Windows.png`);
    }
  }

  update() {
    let menuTemplate;

    if (this.state.isDiscordReady) {
      menuTemplate = [
        {
          label: `Connected to Discord`,
          enabled: false,
          icon: iconOn,
        },
        { type: "separator" },
        {
          label: "Disconnect from Discord",
          click: () => this.emit(events.DISCONNECT),
        },
      ].concat(this.baseMenuTemplate);
    } else {
      if (this.state.isDiscordConnecting) {
        menuTemplate = [
          {
            label: "Connecting to Discord",
            enabled: false,
            icon: iconOff,
          },
          { type: "separator" },
          {
            label: "Stop connecting to Discord",
            click: () => this.emit(events.DISCONNECT),
          },
        ].concat(this.baseMenuTemplate);
      } else {
        menuTemplate = [
          {
            label: "Not connected to Discord",
            enabled: false,
            icon: iconOff,
          },
          { type: "separator" },
          {
            label: "Connect to Discord",
            click: () => this.emit(events.CONNECT),
          },
        ].concat(this.baseMenuTemplate);
      }
    }

    this.tray.setContextMenu(Menu.buildFromTemplate(menuTemplate));
    this.tray.setImage(this.getIconPath());
  }

  saveConfigAndUpdate(configKey, value) {
    config.set(configKey, value);
    config.save();

    // immeditely try a discord activity update
    this.emit(events.UPDATE_OPTIONS);
  }

  setState(state) {
    this.state = state;
    this.update();
  }
}

module.exports = CustomTray;


================================================
FILE: src/lib/updater.js
================================================
const { autoUpdater } = require("electron-updater");
const { app, dialog } = require("electron");
const logger = require("./logger");

autoUpdater.autoDownload = false;

autoUpdater.on("error", (error) => {
  dialog.showErrorBox(
    "Error: ",
    error == null ? "unknown" : (error.stack || error).toString()
  );
});

autoUpdater.on("update-available", async () => {
  const { response } = await dialog.showMessageBox({
    type: "question",
    title: "Found Updates",
    message: "Found a new version, do you want update now?",
    defaultId: 0,
    cancelId: 1,
    buttons: ["Yes", "No"],
  });

  if (response === 0) {
    logger.debug("updater", "update available");
    await autoUpdater.downloadUpdate();
  }
});

autoUpdater.on("update-downloaded", async () => {
  const { response } = await dialog.showMessageBox({
    type: "question",
    title: "Update Download",
    buttons: ["Install and Relaunch", "Later"],
    defaultId: 0,
    cancelId: 1,
    message: `A new version of ${app.getName()} has been downloaded!`,
  });

  if (response === 0) {
    setImmediate(() => autoUpdater.quitAndInstall());
  }
});

autoUpdater.on("download-progress", (progressObj) =>
  logger.debug(
    "updater",
    `Update Download progress: ${JSON.stringify(progressObj)}`
  )
);

async function _simpleCheck() {
  const { updateInfo } = await _update();
  const currentVersion = app.getVersion();

  logger.debug(
    "updater",
    `Current Version: ${currentVersion} | Server Version: ${updateInfo.version}`
  );

  if (updateInfo && updateInfo.version === currentVersion) {
    await dialog.showMessageBox({
      type: "info",
      message: "You're up-to-date!",
      detail: `${app.getName()} ${
        updateInfo.version
      } is currently the newest version available.`,
    });
  }
}

async function _update() {
  return new Promise(async (resolve, reject) => {
    try {
      autoUpdater.logger = logger.log;
      // return resolve(autoUpdater.checkForUpdatesAndNotify());
      return resolve(autoUpdater.checkForUpdates());
    } catch (err) {
      reject(err);
    }
  });
}

exports.update = _update;
exports.simpleCheck = _simpleCheck;


================================================
FILE: src/lib/util.js
================================================
const { app } = require("electron");
const path = require("path");

function _getAppData(subpath) {
  return path.join(_getAppDataPath(), subpath);
}

function _getAppDataPath() {
  return app.getPath("userData");
}

function _getHomePath() {
  return app.getPath("home");
}

function _getPath(path) {
  return app.getPath(path)
}

function _timeout(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

exports.getAppData = _getAppData;
exports.getAppDataPath = _getAppDataPath;
exports.getHomePath = _getHomePath;
exports.getPath = _getPath;
exports.timeout = _timeout;


================================================
FILE: src/main.js
================================================
const electron = require("electron");
const { dialog } = electron;
const psList = require("ps-list");

const events = require("./lib/events");
const updater = require("./lib/updater");
const config = require("./lib/config");
const logger = require("./lib/logger");
const CustomTray = require("./lib/tray");
const Activity = require("./lib/activity");

const { app } = electron;
let tray, activity;

const state = {
  isDiscordConnecting: false,
  isDiscordReady: false,
  isFigmaReady: false,
};

async function quit() {
  logger.debug("main", "quitting...");
  if (activity) await activity.destroy();
  app.quit();
}

if (!app.requestSingleInstanceLock()) {
  logger.debug("main", "second instance detected, quitting this one...");
  app.quit();
}

if (process.platform === "darwin") app.dock.hide();

app
  .whenReady()
  .then(() => updater.update())
  .then(() => config.save(0, true))
  .then(() => config.load())
  .then(() => (tray = new CustomTray(state)))
  .then(() => (activity = new Activity()))
  .then(() => registerEvents())
  .then(() => logger.debug("main", "initalized!"))
  .catch((err) => logger.error("main", err.message));

function registerEvents() {
  tray.on(events.QUIT, async () => await quit());

  tray.on(events.UPDATE_OPTIONS, async () => {
    await activity.updateOptions();
  });

  tray.on(events.CONNECT, async () => {
    await activity.connect();
  });

  tray.on(events.DISCONNECT, async () => {
    await activity.disconnect();
  });

  tray.on(events.CHECK_FOR_UPDATES, async () => {
    await updater.simpleCheck();
  });

  activity.on(events.DISCORD_CONNECTING, () => {
    logger.debug("main", "discord connecting...");

    state.isDiscordReady = false;
    state.isDiscordConnecting = true;
    tray.setState(state);
  });

  activity.on(events.DISCORD_READY, () => {
    logger.debug("main", "discord ready!");

    state.isDiscordReady = true;
    state.isDiscordConnecting = false;
    tray.setState(state);
  });

  activity.on(events.DISCORD_DISCONNECTED, () => {
    logger.debug("main", "discord disconnected");

    state.isDiscordReady = false;
    state.isDiscordConnecting = false;
    tray.setState(state);
  });

  activity.on(events.DISCORD_LOGIN_ERROR, async () => {
    // Is Discord open?
    let isRunning = false;
    const processList = await psList();

    if (process.platform === "darwin") {
      isRunning =
        processList.filter((p) => p.cmd.includes("MacOS/Discord")).length > 0;
    } else if (process.platform === "win32") {
      isRunning =
        processList.filter((p) => p.name.includes("Discord.exe")).length > 0;
    }

    if (!isRunning) {
      dialog.showErrorBox(
        "Figma Discord Presence",
        "Unfortunately it doesn't look like Discord is running. It must be running in order to connect and update your presence status."
      );
    }

    state.isDiscordReady = false;
    state.isDiscordConnecting = false;
    tray.setState(state);
  });
}

app.on("window-all-closed", () => {
  // should not quit
});

process.on("unhandledRejection", (err) =>
  logger.error("unhandledRejection", err.message)
);
process.on("uncaughtException", (err) =>
  logger.error("uncaughtException", err.message)
);
Download .txt
gitextract_wfp9rim1/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   └── workflows/
│       └── deploy.yml
├── .gitignore
├── .node-version
├── LICENSE
├── README.md
├── build/
│   ├── entitlements.mac.plist
│   └── icon.icns
├── dev-app-update.yml
├── package.json
└── src/
    ├── afterSignHook.js
    ├── lib/
    │   ├── activity.js
    │   ├── config.js
    │   ├── events.js
    │   ├── figma.js
    │   ├── logger.js
    │   ├── tray.js
    │   ├── updater.js
    │   └── util.js
    └── main.js
Download .txt
SYMBOL INDEX (33 symbols across 7 files)

FILE: src/lib/activity.js
  constant RPC (line 2) | const RPC = require("discord-rpc");
  constant CLIENT_ID (line 13) | const CLIENT_ID = "866719067092418580";
  class Activity (line 15) | class Activity extends EventEmitter {
    method constructor (line 16) | constructor() {
    method login (line 28) | async login() {
    method setActivity (line 52) | async setActivity() {
    method startInterval (line 106) | startInterval() {
    method stopInterval (line 112) | async stopInterval() {
    method updateOptions (line 118) | async updateOptions() {
    method connect (line 122) | async connect() {
    method disconnect (line 126) | async disconnect() {
    method destroy (line 130) | async destroy() {

FILE: src/lib/config.js
  function load (line 32) | function load() {
  function save (line 46) | function save(times, init = false) {
  function getAll (line 74) | function getAll() {

FILE: src/lib/figma.js
  function getFigmaMetaData (line 10) | async function getFigmaMetaData() {
  function getIsFigmaRunning (line 77) | async function getIsFigmaRunning() {
  function getIsFigmaActive (line 94) | async function getIsFigmaActive() {

FILE: src/lib/tray.js
  class CustomTray (line 15) | class CustomTray extends EventEmitter {
    method constructor (line 72) | constructor(trayState) {
    method getIconPath (line 85) | getIconPath() {
    method update (line 96) | update() {
    method saveConfigAndUpdate (line 146) | saveConfigAndUpdate(configKey, value) {
    method setState (line 154) | setState(state) {

FILE: src/lib/updater.js
  function _simpleCheck (line 52) | async function _simpleCheck() {
  function _update (line 72) | async function _update() {

FILE: src/lib/util.js
  function _getAppData (line 4) | function _getAppData(subpath) {
  function _getAppDataPath (line 8) | function _getAppDataPath() {
  function _getHomePath (line 12) | function _getHomePath() {
  function _getPath (line 16) | function _getPath(path) {
  function _timeout (line 20) | function _timeout(ms) {

FILE: src/main.js
  function quit (line 21) | async function quit() {
  function registerEvents (line 45) | function registerEvents() {
Condensed preview — 20 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (34K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 588,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: bryanberger\n\n---\n\n**Desc"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "chars": 1415,
    "preview": "name: Build/release\n\non:\n  push:\n    branches:\n      - master\n  workflow_dispatch: {}\n\njobs:\n  release:\n    runs-on: ${{"
  },
  {
    "path": ".gitignore",
    "chars": 195,
    "preview": "# Packages\nnode_modules\n**/node_modules\n\n# Build artifacts\ndist/\n\n# Log files\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\n"
  },
  {
    "path": ".node-version",
    "chars": 8,
    "preview": "14.15.0\n"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2021 Bryan Berger\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 4517,
    "preview": "# Figma Discord Presence\n\n[![Build/release](https://github.com/bryanberger/figma-discord-presence/actions/workflows/depl"
  },
  {
    "path": "build/entitlements.mac.plist",
    "chars": 436,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>  \n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs"
  },
  {
    "path": "dev-app-update.yml",
    "chars": 44,
    "preview": "provider: s3\nbucket: figma-discord-presence\n"
  },
  {
    "path": "package.json",
    "chars": 2032,
    "preview": "{\n  \"name\": \"figma-discord-presence\",\n  \"productName\": \"Figma Discord Presence\",\n  \"version\": \"1.2.6\",\n  \"description\": "
  },
  {
    "path": "src/afterSignHook.js",
    "chars": 1368,
    "preview": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst electron_notarize = require(\"electron-notarize\");\n\nrequire"
  },
  {
    "path": "src/lib/activity.js",
    "chars": 3407,
    "preview": "const EventEmitter = require(\"events\");\nconst RPC = require(\"discord-rpc\");\n\nconst {\n  getIsFigmaRunning,\n  getFigmaMeta"
  },
  {
    "path": "src/lib/config.js",
    "chars": 1701,
    "preview": "const convict = require(\"convict\");\nconst fs = require(\"fs-extra\");\n\nconst logger = require(\"./logger\");\nconst util = re"
  },
  {
    "path": "src/lib/events.js",
    "chars": 338,
    "preview": "module.exports = {\n  QUIT: \"QUIT\",\n  CONNECT: \"CONNECT\",\n  DISCONNECT: \"DISCONNECT\",\n  UPDATE_OPTIONS: \"UPDATE_OPTIONS\","
  },
  {
    "path": "src/lib/figma.js",
    "chars": 3097,
    "preview": "const _ = require(\"lodash\");\nconst bplist = require(\"bplist-parser\");\nconst psList = require(\"ps-list\");\nconst winInfo ="
  },
  {
    "path": "src/lib/logger.js",
    "chars": 796,
    "preview": "const Sentry = require(\"@sentry/electron\");\nconst { CaptureConsole } = require(\"@sentry/integrations\");\nconst log = requ"
  },
  {
    "path": "src/lib/tray.js",
    "chars": 4316,
    "preview": "const EventEmitter = require(\"events\");\nconst path = require(\"path\");\nconst { shell, nativeTheme, Menu, Tray } = require"
  },
  {
    "path": "src/lib/updater.js",
    "chars": 2162,
    "preview": "const { autoUpdater } = require(\"electron-updater\");\nconst { app, dialog } = require(\"electron\");\nconst logger = require"
  },
  {
    "path": "src/lib/util.js",
    "chars": 587,
    "preview": "const { app } = require(\"electron\");\nconst path = require(\"path\");\n\nfunction _getAppData(subpath) {\n  return path.join(_"
  },
  {
    "path": "src/main.js",
    "chars": 3203,
    "preview": "const electron = require(\"electron\");\nconst { dialog } = electron;\nconst psList = require(\"ps-list\");\n\nconst events = re"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the bryanberger/figma-discord-presence GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 20 files (30.5 KB), approximately 8.2k tokens, and a symbol index with 33 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!