Meet the colleague you didn't know you had something in common with.
SecondSpace AI is a Slack bot that introduces people in a workspace who share interests, then does the awkward part for them: it reads both calendars, finds a time you are genuinely both free, and books the coffee chat.
| 🧠 Understands interests | AI, A.I. and artificial intelligence are one thing. So are hiking and hiking trails, jazz and jazz music. |
| 🤝 Ranks matches | Scores every colleague on shared interests, and quietly deprioritises people you were introduced to recently so you keep meeting new faces. |
| 📅 Finds real times | Queries both Google Calendars for free/busy, respects working hours, timezones and daylight saving, and offers times spread across different days. |
| ☕ Books the meeting | Creates the calendar event with a Google Meet link and invites you both. |
| 🏠 Has an App Home | Interests, calendar connection status and upcoming chats, all in one tab. |
| 🧪 Runs without Google | With no Google credentials configured it runs in demo mode against a simulated calendar, so the whole flow works on day one. |
- Set your interests —
/interests hiking, AI, jazz(or run/interestsfor a form). - Find a match —
/find_matchshows the best candidate, what you have in common, and a match strength. Don't like it? Show someone else. - Invite them — they get a DM with the shared interests and Accept / Decline.
- Connect calendars — each of you authorises read-only free/busy access, once.
- Pick a time — the bot offers a few slots you are both free for; one click books it.
sequenceDiagram
actor Alice
participant Bot as SecondSpace
actor Bob
participant GCal as Google Calendar
Alice->>Bot: /interests hiking, AI, jazz
Bot-->>Alice: Saved. Find someone to meet?
Alice->>Bot: /find_match
Bot-->>Alice: Meet @Bob — you both mentioned jazz (43%)
Alice->>Bot: Invite to a coffee chat
Bot-->>Bob: @Alice would like a coffee chat. Sounds good / Not this time
Bob->>Bot: Sounds good
Bot-->>Bob: Connect your calendar (once)
Bob->>GCal: OAuth consent
GCal-->>Bot: Refresh token
Bot->>GCal: freebusy(Alice, Bob, next 7 days)
GCal-->>Bot: Busy blocks
Bot-->>Bob: Tue 10:00 · Wed 14:30 · Thu 09:30
Bob->>Bot: Wed 14:30
Bot->>GCal: Create event + Meet link, invite both
GCal-->>Bot: Event link
Bot-->>Alice: ✅ Coffee chat booked with @Bob
Bot-->>Bob: ✅ Coffee chat booked with @Alice
git clone <this repo> && cd SecondSpaceAI
python -m venv .venv && source .venv/Scripts/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # add your Slack credentials
python app.pyIn another terminal, expose the port so Slack can reach you:
ngrok http 3000Put the ngrok URL in PUBLIC_BASE_URL in .env, restart, and point your Slack
app at https://<your-ngrok-url>/slack/events.
With no GOOGLE_CLIENT_ID set, the bot runs in demo mode: "connecting a
calendar" is a one-click local page, and each user gets a plausible simulated
busy calendar. Every other part of the flow — matching, slot finding, booking,
messaging — is real.
python app.py --check prints the resolved configuration without starting the server.
The fastest route is the included manifest:
- Go to https://api.slack.com/apps → Create New App → From an app manifest.
- Paste slack-app-manifest.yaml, replacing every
REPLACE-MEwith your public hostname. - Install to Workspace, then copy into
.env:- Bot User OAuth Token (
xoxb-…) →SLACK_BOT_TOKEN - Signing Secret →
SLACK_SIGNING_SECRET
- Bot User OAuth Token (
The manifest requests these bot scopes:
| Scope | Why |
|---|---|
commands |
/interests and /find_match |
chat:write |
Post match cards, invitations and confirmations |
im:write |
Open a DM with the person being invited |
im:history |
Reply with help when someone DMs the bot |
users:read |
Display names and timezones, so times render in local time |
users:read.email |
Add both people as guests on the calendar event |
app_mentions:read |
Respond to @-mentions |
Everything arrives at a single request URL: <PUBLIC_BASE_URL>/slack/events.
- Google Cloud Console → create a project.
- APIs & Services → Library → enable Google Calendar API.
- OAuth consent screen → Internal (or add your testers as test users).
- Credentials → Create Credentials → OAuth client ID → Web application.
- Add the authorised redirect URI:
<PUBLIC_BASE_URL>/google/oauth/callback - Put the client ID and secret in
.env(or download the JSON tocredentials.json).
The bot requests only two scopes: calendar.freebusy (when you are busy — not
what you are doing) and calendar.events (to create the coffee chat).
Refresh tokens are long-lived. Set TOKEN_ENCRYPTION_KEY and they are encrypted
at rest with Fernet:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"| Command | What it does |
|---|---|
/interests hiking, AI, jazz |
Save your interests |
/interests |
Open a form to edit them |
/interests show |
See what's on file |
/interests clear |
Forget your interests (removes you from matching) |
/interests help |
Show help |
/find_match |
Find someone with overlapping interests |
/find_match cooking, cycling |
Match on a one-off topic instead of your saved interests |
Everything is set through the environment or .env — see
.env.example for the annotated list.
| Variable | Default | Notes |
|---|---|---|
SLACK_BOT_TOKEN |
— | Required |
SLACK_SIGNING_SECRET |
— | Required; proves requests came from Slack |
PUBLIC_BASE_URL |
http://localhost:$PORT |
Where Google redirects back to |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
— | Blank ⇒ demo mode |
DATABASE_PATH |
secondspace.db |
SQLite file |
TOKEN_ENCRYPTION_KEY |
— | Fernet key; blank ⇒ tokens stored as plaintext |
DEFAULT_TIMEZONE |
America/New_York |
Fallback when Slack has no timezone for a user |
WORKING_HOURS_START / _END |
9 / 17 |
Local to each meeting's organiser |
MEETING_DURATION_MINUTES |
30 |
|
SCHEDULING_HORIZON_DAYS |
7 |
How far ahead to look |
MIN_LEAD_TIME_MINUTES |
120 |
Never book something in ten minutes' time |
MAX_SLOT_OPTIONS |
3 |
How many times to offer |
INCLUDE_WEEKENDS |
false |
|
REMATCH_COOLDOWN_DAYS |
30 |
How long before the same pair is suggested again |
/interests hiking, A.I., Board Games becomes the canonical tags
hiking, artificial intelligence, board game — lowercased, depluralised,
filler words dropped, and common aliases collapsed (ML → machine learning,
biking → cycling).
Two people are then compared on two signals, each measured as the average of Jaccard similarity and the overlap coefficient:
- tag overlap — whole interests in common, where a phrase that contains
another (
jazzinsidejazz music) counts for 85% of an exact match; - token overlap — the individual words, so
trail runningandrunning clubstill find each other.
The final score is 0.6 × tags + 0.4 × tokens. Averaging Jaccard with the
overlap coefficient matters: pure Jaccard punishes the colleague who listed
twenty interests, which is exactly the person you probably want to meet.
Anyone you were shown or met recently is multiplied by a penalty (0.6 and 0.25) rather than hidden — in a five-person workspace they may be the only option.
- Build a window:
now + MIN_LEAD_TIMEthroughnow + SCHEDULING_HORIZON_DAYS. - Ask Google for both people's busy blocks in that window and merge them.
- Walk every candidate slot inside local working hours, skipping weekends and anything that collides with a busy block. Slots are generated in local wall time and converted to UTC, so daylight saving transitions land correctly.
- Offer up to
MAX_SLOT_OPTIONS, round-robined across different days — "Tue 10:00, Wed 14:30, Thu 09:30" is a better menu than three slots in a row.
app.py entry point (python app.py [--check])
secondspace/
config.py environment → typed Config
db.py SQLite: users, interests, tokens, meetings
matching.py interest normalisation + similarity scoring (pure)
scheduling.py free/busy → candidate slots (pure)
calendar_integration.py Google provider + simulated provider
services.py orchestration: match → invite → schedule → book
blocks.py Block Kit builders
slack_app.py Bolt listeners (commands, buttons, modal, home)
web.py Flask routes: /slack/events, OAuth redirect, /healthz
tests/ full test suite, no network access required
slack-app-manifest.yaml one-paste Slack app setup
The two modules that decide anything interesting — matching and scheduling —
are pure functions with no clock, database or network, which is what makes them
straightforward to test and to tune.
pip install -r requirements.txt
python -m pytestThe suite covers normalisation and scoring, slot finding across a daylight
saving transition, the SQLite layer, the simulated calendar, Block Kit output
against Slack's structural limits, and the Slack listeners themselves — real
block_actions, slash command and view_submission payloads dispatched through
a real Bolt app with the Web API stubbed out. Nothing touches the network.
- The bot stores Slack user IDs, display names, emails, timezones and the interests people give it — nothing else from your workspace.
- Calendar access is free/busy only: it can see that you are booked from 2–3pm, not what the meeting is.
/interests clearremoves someone from matching entirely, and the App Home has a Disconnect button that deletes the stored calendar token.
- Group coffee chats for three or four people around a shared interest
- Icebreaker prompts generated from the actual overlap
- A weekly opt-in "introduce me to someone" digest
- Feedback after the chat, to learn which matches actually landed
- Channel-scoped matching (
/find_match #design)
PRs and issues welcome.