Skip to content

Instantly share code, notes, and snippets.

@devonsams
Last active March 8, 2025 17:41
Show Gist options
  • Save devonsams/f7c95f9c23aedbf3bf4d8ae3c913616a to your computer and use it in GitHub Desktop.
Save devonsams/f7c95f9c23aedbf3bf4d8ae3c913616a to your computer and use it in GitHub Desktop.
Scripts for managing group me events from google sheets
  1. Log into GroupMe on the web, open developer tools - In the various GroupMe API requests you should be able to find your groupId, and an x-access-token header. Collect these values for later use. (If you are experimenting with these scripts it is best to create a new group for testing purposes.)
  2. In a google sheet of your choosing, go to Extensions > App Script. You should be taken to the App Scripts editor where you can create all of the files found in this gist.
  3. With all of the code in place, go back to your sheet and refresh - You should see a "GroupMe" menu item.
  4. Make a sheet with the following headers: Date (formatted as date), Start Time (formatted as time), End Time (time), Name, Location Name, Location Address, Pitch, Reminders, Event ID, Status, Hash
  5. go to Group Me Events > Configuration and enter your groupId, token, and sheet name. Click the test button to make sure it works and confirm the group name is correct. (again, consider starting with a test group). Save.
  6. go to Group Me Events > Sync Events to begin the sync.
  7. When complete, the Event ID, Status, and Hash values will be populated.
  • Event ID and Hash are used to detect changes and update events when necessary on subsequent syncs.
  • Status will display success/failure info pertaining to the last sync.
/**
* GroupMe Event Management Script - API Functions
*
* Contains all functions related to GroupMe API interactions.
*/
/**
* Creates a new GroupMe event
* @param {Object} eventData - Event data
* @returns {Object} API response
*/
function createGroupMeEvent(eventData) {
const url = `https://api.groupme.com/v3/conversations/${CONFIG.GROUP_ID}/events/create`;
const options = {
method: "post",
contentType: "application/json",
headers: {
"X-Access-Token": CONFIG.ACCESS_TOKEN
},
payload: JSON.stringify(eventData),
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(url, options);
return JSON.parse(response.getContentText());
} catch (error) {
Logger.log(`Error creating event: ${error}`);
return { error: error.toString() };
}
}
/**
* Updates an existing GroupMe event
* @param {string} eventId - ID of the event to update
* @param {Object} eventData - Event data
* @returns {Object} API response
*/
function updateGroupMeEvent(eventId, eventData) {
const url = `https://api.groupme.com/v3/conversations/${CONFIG.GROUP_ID}/events/update?event_id=${eventId}`;
const options = {
method: "post",
contentType: "application/json",
headers: {
"X-Access-Token": CONFIG.ACCESS_TOKEN
},
payload: JSON.stringify(eventData),
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(url, options);
return JSON.parse(response.getContentText());
} catch (error) {
Logger.log(`Error updating event: ${error}`);
return { error: error.toString() };
}
}
/**
* Deletes a GroupMe event
* @param {string} eventId - ID of the event to delete
* @returns {Object} API response
*/
function deleteGroupMeEvent(eventId) {
const url = `https://api.groupme.com/v3/conversations/${CONFIG.GROUP_ID}/events/delete?event_id=${eventId}`;
const options = {
method: "delete",
headers: {
"X-Access-Token": CONFIG.ACCESS_TOKEN
},
muteHttpExceptions: true
};
try {
const response = UrlFetchApp.fetch(url, options);
return JSON.parse(response.getContentText());
} catch (error) {
Logger.log(`Error deleting event: ${error}`);
return { error: error.toString() };
}
}
/**
* Test the GroupMe API connection with credentials
*/
function testConnection(groupId, accessToken) {
try {
const url = `https://api.groupme.com/v3/groups/${groupId}?token=${accessToken}`;
const options = {
method: "get",
headers: {
"X-Access-Token": CONFIG.ACCESS_TOKEN
},
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, options);
const responseCode = response.getResponseCode();
if (responseCode === 200) {
const responseData = JSON.parse(response.getContentText());
return {
success: true,
message: `Connected successfully to group: ${responseData.response.name}`
};
} else {
const errorData = JSON.parse(response.getContentText());
return {
success: false,
message: `Connection failed: ${errorData.meta.errors[0] || 'Unknown error'}`
};
}
} catch (error) {
return {
success: false,
message: `Connection error: ${error.toString()}`
};
}
}
/**
* GroupMe Event Management Script - Main File
*
* This script manages events for GroupMe using their API.
* It can create new events and update existing ones based on spreadsheet data.
*/
// Global configuration object
const CONFIG = {
GROUP_ID: PropertiesService.getScriptProperties().getProperty('GROUP_ID') || '',
ACCESS_TOKEN: PropertiesService.getScriptProperties().getProperty('ACCESS_TOKEN') || '',
SHEET_NAME: PropertiesService.getScriptProperties().getProperty('SHEET_NAME') || '',
// Default values for the API
DEFAULTS: {
is_all_day: false,
timezone: "America/Indianapolis",
scheduled_call: false
}
};
/**
* Add custom menu to the spreadsheet when it opens
*/
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('GroupMe Events')
.addItem('Configuration', 'showConfigDialog')
.addItem('Sync Events', 'syncEvents')
.addSeparator()
.addItem('Delete All Events', 'confirmDeleteAllEvents')
.addToUi();
}
/**
* Main function to sync events with GroupMe
*/
function syncEvents() {
// Create a lock to prevent concurrent executions
const lock = LockService.getScriptLock();
try {
// Try to obtain a lock with a 10-second timeout
if (!lock.tryLock(10000)) {
return;
}
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) {
return;
}
// Get all data
const range = sheet.getDataRange();
const values = range.getValues();
// Check header row and find column indexes
const headers = values[0];
const colIndexes = {
date: headers.indexOf('Date'),
startTime: headers.indexOf('Start Time'),
endTime: headers.indexOf('End Time'),
name: headers.indexOf('Name'),
pitch: headers.indexOf('Pitch'),
locationName: headers.indexOf('Location Name'),
locationAddress: headers.indexOf('Location Address'),
reminders: headers.indexOf('Reminders'),
eventId: headers.indexOf('Event ID'),
status: headers.indexOf('Status'),
hash: headers.indexOf('Hash')
};
// Verify required columns exist
const requiredColumns = ['Date', 'Start Time', 'End Time', 'Name', 'Pitch', 'Location Name', 'Location Address', 'Event ID', 'Status', 'Hash'];
const missingColumns = requiredColumns.filter(col => {
const propName = col.toLowerCase().replace(' ', '');
return colIndexes[propName] === -1;
});
if (missingColumns.length > 0) {
return;
}
// Count rows to process (skip header)
const rowsToProcess = values.filter((row, index) =>
index > 0 && row[colIndexes.name] && row[colIndexes.date]
).length;
if (rowsToProcess === 0) {
return;
}
// Process each row (skip header)
let processed = 0;
let created = 0;
let updated = 0;
let skipped = 0;
let errors = 0;
for (let i = 1; i < values.length; i++) {
const row = values[i];
// Skip empty rows
if (!row[colIndexes.name] || !row[colIndexes.date]) continue;
try {
// Get data from row
const eventData = {
name: row[colIndexes.name],
description: row[colIndexes.pitch] || '',
location: {
name: row[colIndexes.locationName] || '',
address: row[colIndexes.locationAddress] || ''
},
is_all_day: CONFIG.DEFAULTS.is_all_day,
timezone: CONFIG.DEFAULTS.timezone,
reminders: processReminders(row[colIndexes.reminders]),
scheduled_call: CONFIG.DEFAULTS.scheduled_call
};
// Format dates
const date = row[colIndexes.date];
const startTime = row[colIndexes.startTime];
const endTime = row[colIndexes.endTime];
if (date && startTime && endTime) {
// Format as ISO strings with the correct timezone offset
eventData.start_at = formatDateTimeForGroupMe(date, startTime);
eventData.end_at = formatDateTimeForGroupMe(date, endTime);
} else {
// Skip rows with incomplete date/time information
sheet.getRange(i + 1, colIndexes.status + 1).setValue("ERROR: Missing date/time");
errors++;
continue;
}
// Calculate a hash for the current event data to track changes
const currentHash = calculateEventHash(eventData);
const previousHash = row[colIndexes.hash] || '';
const eventId = row[colIndexes.eventId];
let result;
// Check if this is a new event or an existing one that has changed
if (!eventId || eventId.trim() === '') {
// New event - create it
result = createGroupMeEvent(eventData);
if (result && result.response && result.response.event && result.response.event.event_id) {
sheet.getRange(i + 1, colIndexes.eventId + 1).setValue(result.response.event.event_id);
sheet.getRange(i + 1, colIndexes.status + 1).setValue(`Created: ${new Date().toLocaleString()}`);
sheet.getRange(i + 1, colIndexes.hash + 1).setValue(currentHash);
created++;
} else {
errors++;
}
} else if (currentHash !== previousHash) {
// Existing event that has changed - update it
result = updateGroupMeEvent(eventId, eventData);
if (result && !result.error) {
sheet.getRange(i + 1, colIndexes.status + 1).setValue(`Updated: ${new Date().toLocaleString()}`);
sheet.getRange(i + 1, colIndexes.hash + 1).setValue(currentHash);
updated++;
} else {
errors++;
}
} else {
// No changes detected - skip update
sheet.getRange(i + 1, colIndexes.status + 1).setValue(`No changes: ${new Date().toLocaleString()}`);
skipped++;
}
// Log any errors
if (result && result.error) {
sheet.getRange(i + 1, colIndexes.status + 1).setValue(`ERROR: ${result.error}`);
errors++;
}
// Small delay to avoid rate limits
Utilities.sleep(500);
} catch (error) {
sheet.getRange(i + 1, colIndexes.status + 1).setValue(`ERROR: ${error.toString()}`);
errors++;
}
}
// Show summary
SpreadsheetApp.getUi().alert(
'Sync Complete',
`Created: ${created}, Updated: ${updated}, Skipped: ${skipped}, Errors: ${errors}`,
SpreadsheetApp.getUi().ButtonSet.OK
);
} catch (error) {
// Handle any unexpected errors
SpreadsheetApp.getUi().alert(
'Error during sync',
error.toString(),
SpreadsheetApp.getUi().ButtonSet.OK
);
Logger.log(`Sync error: ${error.toString()}`);
} finally {
// Always release the lock
lock.releaseLock();
}
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 15px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background-color: #4285f4;
color: white;
}
.btn-secondary {
background-color: #f1f1f1;
color: #333;
}
.btn-test {
background-color: #34a853;
color: white;
}
.status {
margin-top: 15px;
padding: 10px;
border-radius: 4px;
display: none;
}
.status-success {
background-color: #d4edda;
color: #155724;
}
.status-error {
background-color: #f8d7da;
color: #721c24;
}
.help-text {
font-size: 12px;
color: #666;
margin-top: 4px;
}
</style>
</head>
<body>
<div id="configForm">
<div class="form-group">
<label for="groupId">GroupMe Group ID:</label>
<input type="text" id="groupId" name="groupId" value="<?= config.GROUP_ID ?>" required />
<div class="help-text">The numeric ID of your GroupMe group</div>
</div>
<div class="form-group">
<label for="accessToken">API Access Token:</label>
<input type="password" id="accessToken" name="accessToken" value="<?= config.ACCESS_TOKEN ?>" required />
<div class="help-text">Your GroupMe API access token from dev.groupme.com</div>
</div>
<div class="form-group">
<label for="sheetName">Events Sheet Name:</label>
<select id="sheetName" name="sheetName" required></select>
<div class="help-text">Select the sheet containing your events data</div>
</div>
<div class="button-group">
<button class="btn btn-secondary" onclick="closeDialog()">Cancel</button>
<button class="btn btn-test" onclick="testApiConnection()">Test Connection</button>
<button class="btn btn-primary" onclick="saveConfig()">Save</button>
</div>
<div id="statusMessage" class="status"></div>
</div>
<script>
// Load sheet names when the page loads
window.onload = function() {
google.script.run
.withSuccessHandler(populateSheetDropdown)
.getSheetNames();
};
// Populate the sheet dropdown
function populateSheetDropdown(sheetNames) {
const dropdown = document.getElementById('sheetName');
const currentValue = "<?= config.SHEET_NAME ?>";
sheetNames.forEach(function(name) {
const option = document.createElement('option');
option.value = name;
option.text = name;
if (name === currentValue) {
option.selected = true;
}
dropdown.appendChild(option);
});
}
// Test the API connection
function testApiConnection() {
const groupId = document.getElementById('groupId').value;
const accessToken = document.getElementById('accessToken').value;
if (!groupId || !accessToken) {
showStatus('Please enter both Group ID and Access Token first.', false);
return;
}
document.getElementById('statusMessage').innerHTML = 'Testing connection...';
document.getElementById('statusMessage').style.display = 'block';
document.getElementById('statusMessage').className = 'status';
google.script.run
.withSuccessHandler(function(result) {
showStatus(result.message, result.success);
})
.withFailureHandler(function(error) {
showStatus('Error: ' + error, false);
})
.testConnection(groupId, accessToken);
}
// Save configuration
function saveConfig() {
const formData = {
groupId: document.getElementById('groupId').value,
accessToken: document.getElementById('accessToken').value,
sheetName: document.getElementById('sheetName').value
};
if (!formData.groupId || !formData.accessToken || !formData.sheetName) {
showStatus('Please fill in all fields.', false);
return;
}
google.script.run
.withSuccessHandler(function(result) {
if (result.success) {
showStatus(result.message, true);
// Close the dialog after a short delay
setTimeout(function() {
google.script.host.close();
}, 1500);
} else {
showStatus(result.message, false);
}
})
.withFailureHandler(function(error) {
showStatus('Error: ' + error, false);
})
.saveConfiguration(formData);
}
// Display status message
function showStatus(message, isSuccess) {
const statusDiv = document.getElementById('statusMessage');
statusDiv.innerHTML = message;
statusDiv.style.display = 'block';
if (isSuccess) {
statusDiv.className = 'status status-success';
} else {
statusDiv.className = 'status status-error';
}
}
// Close the dialog
function closeDialog() {
google.script.host.close();
}
</script>
</body>
</html>
/**
* GroupMe Event Management Script - Date Utility Functions
*
* Contains all functions related to date and time handling.
*/
/**
* Formats a date and time for GroupMe API
* @param {Date} date - The date
* @param {Date} time - The time
* @returns {string} Formatted date string
*/
function formatDateTimeForGroupMe(date, time) {
// Create the date object
const dateTime = new Date(date);
if (time instanceof Date) {
// Don't know why, but hours is always 1 behind
dateTime.setHours(time.getHours() + 1);
dateTime.setMinutes(time.getMinutes());
dateTime.setSeconds(0);
dateTime.setMilliseconds(0);
}
// Format the date in the user's timezone first
return Utilities.formatDate(dateTime, "America/Indianapolis", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
}
/**
* Process reminders value from spreadsheet
* Handles different formats (string, number, array)
*/
function processReminders(value) {
if (!value) return []; // Default to empty array
if (typeof value === 'number') {
// Single number
return [value];
}
if (typeof value === 'string') {
// Try to parse as JSON first
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed;
}
return [parseInt(value)];
} catch (e) {
// Not JSON, try as comma-separated list
return value.split(',')
.map(v => parseInt(v.trim()))
.filter(v => !isNaN(v));
}
}
if (Array.isArray(value)) {
return value;
}
return [];
}

Event Data Fields Documentation

Overview

This document provides detailed information about each field in the event data structure, including format requirements, examples, and usage notes.

Required Fields

Field Format Description Example
Date Date (MM/DD/YYYY) The date on which the event takes place 3/18/2025
Start Time Time (h:mm:ss AM/PM) The time when the event begins 6:00:00 PM
End Time Time (h:mm:ss AM/PM) The time when the event concludes 7:30:00 PM
Name Text The title or name of the event Practice
Location Name Text The name of the venue where the event takes place Richmond Premier Soccer Fields

Optional Fields

Field Format Description Example
Location Address Text The physical address of the venue 7702 S Arlington Ave, Indianapolis, IN 46237
Pitch Text Notes about the specific field or area being used Field 08
Reminders Comma-delimited integers Time in seconds before the event when reminders should be sent (Max: 2) 0,300

System Fields

Field Format Description Example
Event ID Alphanumeric Unique identifier for the event 6f2d986e6c4a4f1aa42ad011a7227926
Status Text The current status of the event, including last update timestamp Updated: 3/8/2025, 11:42:41 AM
Hash Alphanumeric Verification hash for the event data -7fb63909

Note: System fields are automatically maintained by the script. These fields should not be manually edited as they ensure data integrity and tracking.

Reminders Reference

The Reminders field accepts a comma-separated list of integers representing seconds before the event when notifications should be sent:

Seconds Time Before Event
0 At time of event
300 5 minutes before
900 15 minutes before
1800 30 minutes before
3600 1 hour before
7200 2 hours before
86400 1 day before
172800 2 days before
604800 1 week before

Example Entry

4/27/2025, 1:15:00 PM, 2:45:00 PM, Away Game vs St Francis Premier 2013B Grey, St. Francis Soccer Club, "7702 S Arlington Ave, Indianapolis, IN 46237", Field 08, "86400, 7200", 6f2d986e6c4a4f1aa42ad011a7227926, "Updated: 3/8/2025, 11:42:41 AM", -7fb63909

Usage Notes

  • All required fields must be completed for each event entry
  • Date and time formats should be consistent throughout the sheet
  • Up to 2 reminders can be selected by including their corresponding values separated by a comma
  • The Status field is automatically updated with a timestamp when changes are made
  • Event ID and Hash are system-generated fields that are maintained by the script and should not be manually edited
/**
* GroupMe Event Management Script - UI Functions
*
* Contains all functions related to user interface interactions.
*/
function showConfigDialog() {
// Get current configuration values
const scriptProperties = PropertiesService.getScriptProperties();
const currentConfig = {
GROUP_ID: scriptProperties.getProperty('GROUP_ID') || '',
ACCESS_TOKEN: scriptProperties.getProperty('ACCESS_TOKEN') || '',
SHEET_NAME: scriptProperties.getProperty('SHEET_NAME') || ''
};
// Create HTML template
const htmlTemplate = HtmlService.createTemplateFromFile('ConfigDialog');
// Pass the current configuration to the template
htmlTemplate.config = currentConfig;
// Get the rendered HTML
const htmlOutput = htmlTemplate.evaluate()
.setWidth(400)
.setHeight(350)
.setTitle('GroupMe Event Manager Configuration');
// Display the dialog
SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'GroupMe Event Manager Configuration');
}
/**
* Save configuration values from the HTML form
*/
function saveConfiguration(formData) {
try {
const scriptProperties = PropertiesService.getScriptProperties();
// Save each property
scriptProperties.setProperty('GROUP_ID', formData.groupId);
scriptProperties.setProperty('ACCESS_TOKEN', formData.accessToken);
scriptProperties.setProperty('SHEET_NAME', formData.sheetName);
// Update the global CONFIG object to use new values immediately
CONFIG.GROUP_ID = formData.groupId;
CONFIG.ACCESS_TOKEN = formData.accessToken;
CONFIG.SHEET_NAME = formData.sheetName;
return { success: true, message: 'Configuration saved successfully!' };
} catch (error) {
return { success: false, message: 'Error saving configuration: ' + error.toString() };
}
}
/**
* Get the list of all sheet names in the current spreadsheet
* Used to populate the dropdown in the config dialog
*/
function getSheetNames() {
const sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
return sheets.map(sheet => sheet.getName());
}
/**
* Confirm before deleting all events
*/
function confirmDeleteAllEvents() {
const ui = SpreadsheetApp.getUi();
const response = ui.alert(
'Delete All Events',
'Are you sure you want to delete ALL events from GroupMe and clear the Event IDs from the spreadsheet? This cannot be undone.',
ui.ButtonSet.YES_NO
);
if (response == ui.Button.YES) {
deleteAllEvents();
}
}
/**
* Deletes all events and clears event IDs from the spreadsheet
*/
function deleteAllEvents() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) {
SpreadsheetApp.getUi().alert('Error', `Sheet "${CONFIG.SHEET_NAME}" not found.`, SpreadsheetApp.getUi().ButtonSet.OK);
return;
}
const values = sheet.getDataRange().getValues();
const headers = values[0];
// Find necessary column indexes
const eventIdIndex = headers.indexOf('Event ID');
const statusIndex = headers.indexOf('Status');
const hashIndex = headers.indexOf('Hash');
if (eventIdIndex === -1) {
SpreadsheetApp.getUi().alert('Error', 'Event ID column not found.', SpreadsheetApp.getUi().ButtonSet.OK);
return;
}
let deletedCount = 0;
let errorCount = 0;
let skippedCount = 0;
// Loop through all rows (skip header)
for (let i = 1; i < values.length; i++) {
const eventId = values[i][eventIdIndex];
// Skip rows without event IDs
if (!eventId || eventId.trim() === '') {
skippedCount++;
continue;
}
try {
// Delete the event from GroupMe
const result = deleteGroupMeEvent(eventId);
if (result.error) {
// Failed to delete
errorCount++;
if (statusIndex !== -1) {
sheet.getRange(i + 1, statusIndex + 1).setValue(`Delete failed: ${result.error}`);
}
} else {
// Successfully deleted
deletedCount++;
// Clear the Event ID
sheet.getRange(i + 1, eventIdIndex + 1).setValue('');
// Clear the Hash if it exists
if (hashIndex !== -1) {
sheet.getRange(i + 1, hashIndex + 1).setValue('');
}
// Update status
if (statusIndex !== -1) {
sheet.getRange(i + 1, statusIndex + 1).setValue(`Deleted: ${new Date().toLocaleString()}`);
}
}
// Small delay to avoid rate limits
Utilities.sleep(500);
} catch (error) {
errorCount++;
if (statusIndex !== -1) {
sheet.getRange(i + 1, statusIndex + 1).setValue(`ERROR: ${error.toString()}`);
}
}
}
// Show summary
SpreadsheetApp.getUi().alert(
'Delete Complete',
`Deleted: ${deletedCount}\nErrors: ${errorCount}\nSkipped: ${skippedCount}`,
SpreadsheetApp.getUi().ButtonSet.OK
);
}
/**
* GroupMe Event Management Script - Utility Functions
*
* Contains general utility functions.
*/
/**
* Calculates a hash for event data to detect changes
* @param {Object} eventData - The event data object
* @returns {string} A string hash representing the event data
*/
function calculateEventHash(eventData) {
// Create a string representation of the event's key fields
const dataString = `${eventData.name}|${eventData.description}|${eventData.location.name}|${eventData.location.address}|${eventData.start_at}|${eventData.end_at}|${eventData.reminders}`;
// Simple hash function
let hash = 0;
for (let i = 0; i < dataString.length; i++) {
const char = dataString.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return hash.toString(16);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment