Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,84 @@ if you're running either Windows or MacOS i cannot really give you any help with

(if you do know a way to run this on startup on any of the mentioned systems, *please* create a pull request with an updated readme)

## Salesforce (optional)

if you want your Discord rich presence to reflect activity from a
Salesforce org (for example the most-recently-modified Account), you can
enable the optional Salesforce bridge.

the integration is **off by default** and uses no extra Python dependencies
beyond `requests`, which is already required by the rest of the script.

### Quick start

1. in your Salesforce org, create a Connected App and capture the
`Consumer Key` (`CLIENT_ID`) and `Consumer Secret` (`CLIENT_SECRET`).
2. decide which OAuth 2.0 flow you want to use:
- `client_credentials` (server-to-server) — pre-authorise the
integration user on the Connected App.
- `password` (username-password) — for sandboxes/Developer orgs only;
enable *Allow OAuth Username-Password Flows* on the Connected App
policy and supply `USERNAME`, `PASSWORD` and `SECURITY_TOKEN`.
3. add the `SALESFORCE` block to your `config.json` and set
`SALESFORCE.ENABLED` to `true`. a minimal example:

```json
"SALESFORCE": {
"ENABLED": true,
"LOGIN_URL": "https://login.salesforce.com",
"AUTH_FLOW": "client_credentials",
"CLIENT_ID": "REPLACE_ME",
"CLIENT_SECRET": "REPLACE_ME",
"SOQL": "SELECT Id, Name, Industry FROM Account ORDER BY LastModifiedDate DESC LIMIT 1",
"NAME_FIELD": "Name",
"STATE_FIELD": "Industry",
"ICON_URL": "",
"ICON_TEXT": "Salesforce"
}
```

### How it shows up in Discord

when the script is not currently showing a Steam / local / webscraped
game and `SALESFORCE.ENABLED` is true, it polls the configured SOQL
query and uses the first record:

* `NAME_FIELD` becomes the Discord presence title (the equivalent of a
game name);
* `STATE_FIELD` (optional) becomes the secondary line of the presence;
* `DETAILS_FIELD` (optional) becomes the primary line;
* `ICON_URL` / `ICON_TEXT` (optional) override the small-image slot in
the Discord rich presence, useful for your company's logo.

if the SOQL query returns no records, or the upstream auth/SOQL call
fails, the script logs a warning and falls back to no presence — it
never spams the user with a broken Salesforce state.

### Templated SOQL

if you want to vary the query at runtime (for example to filter by a
specific record id stored in an env var), set `SOQL_TEMPLATE` and
`TEMPLATE_FIELDS` instead of `SOQL`:

```json
"SOQL_TEMPLATE": "SELECT Id, Name FROM Opportunity WHERE AccountId = '{account_id}'",
"TEMPLATE_FIELDS": { "account_id": "001ABC000000XYZ" }
```

unknown placeholders surface as a startup-time `ValueError` so the
misconfiguration is obvious instead of silently returning the literal
`{name}`.

### Security notes

* the Salesforce module never logs the client secret, password, or
security token;
* OAuth access tokens are cached in-process and re-used until ~60s
before their declared expiry;
* a `401` from the SOQL endpoint automatically drops the cached token
so the next cycle re-auths.

# Installation to Automatically Start on Bootupt

## Automatic Installer
Expand Down
14 changes: 14 additions & 0 deletions exampleconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@
"TEXT": "Steam Presence on Discord"
},

"SALESFORCE": {
"ENABLED": false,
"LOGIN_URL": "https://login.salesforce.com",
"AUTH_FLOW": "client_credentials",
"CLIENT_ID": "SALESFORCE_CLIENT_ID",
"CLIENT_SECRET": "SALESFORCE_CLIENT_SECRET",
"SOQL": "SELECT Id, Name FROM Account ORDER BY LastModifiedDate DESC LIMIT 1",
"NAME_FIELD": "Name",
"STATE_FIELD": "",
"DETAILS_FIELD": "",
"ICON_URL": "",
"ICON_TEXT": "Salesforce"
},

"BLACKLIST": [
"game1",
"game2",
Expand Down
120 changes: 100 additions & 20 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,19 @@

# used to get the game's cover art
from steamgrid import SteamGridDB

# used as a backup when cover art
from bs4 import BeautifulSoup

# used to check applications that are open locally
import psutil

# used to load cookies for non-steam games
import http.cookiejar as cookielib

# optional Salesforce -> Discord presence bridge, see salesforce.py
import salesforce

except:
answer = input("looks like either requests, pypresence, steamgrid, psutil, or beautifulSoup is not installed, do you want to install them? (y/n) ")
if answer.lower() == "y":
Expand All @@ -47,7 +50,8 @@
import psutil
import requests
import http.cookiejar as cookielib

import salesforce

print("\npackages installed and imported successfully!")

# just shorthand for logs and errors - easier to write in script
Expand Down Expand Up @@ -150,7 +154,26 @@ def getConfigFile():
"URL": "https://raw.githubusercontent.com/JustTemmie/steam-presence/main/readmeimages/defaulticon.png",
"TEXT": "Steam Presence on Discord"
},


# Optional Salesforce -> Discord presence bridge. Disabled by default.
# When ENABLED is true and no Steam/local/webscraped game is detected,
# the script runs the configured SOQL query and uses the first record
# to drive the Discord presence. See salesforce.py for the full
# configuration reference.
"SALESFORCE": {
"ENABLED": False,
"LOGIN_URL": "https://login.salesforce.com",
"AUTH_FLOW": "client_credentials",
"CLIENT_ID": "SALESFORCE_CLIENT_ID",
"CLIENT_SECRET": "SALESFORCE_CLIENT_SECRET",
"SOQL": "SELECT Id, Name FROM Account ORDER BY LastModifiedDate DESC LIMIT 1",
"NAME_FIELD": "Name",
"STATE_FIELD": "",
"DETAILS_FIELD": "",
"ICON_URL": "",
"ICON_TEXT": "Salesforce"
},

"BLACKLIST" : [
"game1",
"game2",
Expand Down Expand Up @@ -411,9 +434,16 @@ def getGameReviews():
def getGameImage():
global coverImage
global coverImageText

global isPlayingSalesforceRecord

# Salesforce-driven presences already have their own icon URL supplied
# through `salesforce.fetch_presence`; there is no Steam store / SGDB
# lookup that makes sense for them.
if isPlayingSalesforceRecord:
return

coverImage = ""

log(f"fetching icon for {gameName}")

# checks if there's already an existing icon saved to disk for the game
Expand Down Expand Up @@ -829,7 +859,42 @@ def getLocalPresence():
gameName = processName.title()
startTime = processCreationTime



# pulls a Salesforce record into the same globals `getSteamPresence` and
# `getLocalPresence` populate. Returns silently when the integration is
# disabled, when the SOQL query returns no records, or on any upstream
# error (already logged by `salesforce.fetch_presence`).
def getSalesforcePresence():
global isPlayingSteamGame
global isPlayingLocalGame
global gameName
global gameRichPresence
global coverImage
global coverImageText
global isPlayingSalesforceRecord

config = getConfigFile()
salesforceConfig = config.get("SALESFORCE") or {}
if not salesforceConfig.get("ENABLED"):
return

# If the user disabled SOQL or left the client id blank, do nothing
# rather than failing every cycle.
if not salesforceConfig.get("SOQL") or not salesforceConfig.get("CLIENT_ID"):
return

presence = salesforce.fetch_presence(salesforceConfig)
if presence is None:
return

gameName = presence["name"]
gameRichPresence = presence.get("state", "")
coverImage = presence.get("icon_url") or coverImage
coverImageText = presence.get("icon_text") or coverImageText
isPlayingSteamGame = False
isPlayingLocalGame = False
isPlayingSalesforceRecord = True


def setPresenceDetails():
global activeRichPresence
Expand Down Expand Up @@ -1040,6 +1105,7 @@ def main():
global isPlaying
global isPlayingLocalGame
global isPlayingSteamGame
global isPlayingSalesforceRecord

global coverImage
global coverImageText
Expand Down Expand Up @@ -1108,6 +1174,7 @@ def main():
isPlaying = False
isPlayingLocalGame = False
isPlayingSteamGame = False
isPlayingSalesforceRecord = False
startTime = 0
coverImage = None
coverImageText = None
Expand Down Expand Up @@ -1172,22 +1239,29 @@ def main():

if gameName == "" and doWebScraping:
getWebScrapePresence()

if gameName == "" and config.get("SALESFORCE", {}).get("ENABLED"):
getSalesforcePresence()

if doSteamRichPresence and isPlayingSteamGame:
getSteamRichPresence()


# if the game has changed
if previousGameName != gameName:
# try finding the game on steam, and saving it's ID to `gameSteamID`
getGameSteamID()

# fetch the steam reviews if enabled
if fetchSteamReviews:
if gameName != "" and gameSteamID != 0:
getGameReviews()
else:
gameReviewScore = 0
# Steam-side lookups only make sense for actual Steam games; the
# Salesforce integration drives its own presence without ever
# touching the Steam store API.
if not isPlayingSalesforceRecord:
# try finding the game on steam, and saving it's ID to `gameSteamID`
getGameSteamID()

# fetch the steam reviews if enabled
if fetchSteamReviews:
if gameName != "" and gameSteamID != 0:
getGameReviews()
else:
gameReviewScore = 0

# if the game has been closed
if gameName == "":
Expand Down Expand Up @@ -1219,9 +1293,15 @@ def main():

log(f"game changed, updating to '{gameName}'")

# fetch the new app ID
getGameDiscordID()

# Salesforce-driven presences do not need a Discord game-ID
# lookup - use the default application ID so the pypresence
# client can still connect.
if isPlayingSalesforceRecord:
appID = defaultAppID
else:
# fetch the new app ID
getGameDiscordID()

# get cover image
getGameImage()

Expand Down
Loading