untappd-mcp 2.6.1 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +19 -1
- package/dist/bundle.js +904 -82
- package/dist/tools/checkin.js +144 -17
- package/dist/version.js +1 -1
- package/mint.yaml +5 -0
- package/package.json +2 -2
- package/server.json +2 -2
- package/skills/untappd-mcp/SKILL.md +9 -6
package/dist/tools/checkin.js
CHANGED
|
@@ -6,6 +6,7 @@ import { delimiter, extname, join } from 'node:path';
|
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { foodMeta } from './hints.js';
|
|
8
8
|
import { confirmTokenParam, McpToolError, assertPathWithinRoots, createHelpfulError, fileBlob, messageOf, minifiedResult, readEnvVar, readFileHead, sniffMimeBytes, toolAnnotations, UnreachableError, } from '@chrischall/mcp-utils';
|
|
9
|
+
import { blobStoreFromEnv, inlineFileSchema, isHostedEnv, MAX_UPLOAD_FILES, mintUploadSlot, resolveUploadMime, resolveUploadSource, sanitizeUploadFilename, uploadSlotRequestSchema, } from '@chrischall/mcp-utils/export';
|
|
9
10
|
import { CONFIRM_FLOW, confirmWrite } from './confirm.js';
|
|
10
11
|
const CheckinIdSchema = z.number().int().positive().describe('Untappd check-in id');
|
|
11
12
|
// Keyed by the NORMALISED extension that photoExt() returns (jpeg → jpg), so
|
|
@@ -98,6 +99,38 @@ async function photoSha256(path) {
|
|
|
98
99
|
await pipeline(createReadStream(path), hash);
|
|
99
100
|
return hash.digest('hex');
|
|
100
101
|
}
|
|
102
|
+
/** The parameter names the photo routes are spelled with here, for resolveUploadSource's messages. */
|
|
103
|
+
const PHOTO_PARAM_NAMES = {
|
|
104
|
+
path: 'photo_path',
|
|
105
|
+
inline: 'photo_inline',
|
|
106
|
+
blobKey: 'photo_blob_key',
|
|
107
|
+
slotTool: 'untappd_upload_slots',
|
|
108
|
+
};
|
|
109
|
+
/** The image types Untappd takes for a check-in photo, as bare MIME types. */
|
|
110
|
+
const PHOTO_MIME_TYPES = Object.values(PHOTO_CONTENT_TYPES);
|
|
111
|
+
/** Untappd's `photo_file_ext` for an accepted photo MIME type. */
|
|
112
|
+
function extForMime(mime) {
|
|
113
|
+
return Object.entries(PHOTO_CONTENT_TYPES).find(([, m]) => m === mime)[0];
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A photo that arrived as bytes — inline base64 or an upload-slot blob —
|
|
117
|
+
* through {@link resolveUploadSource}: exactly one source, image types only,
|
|
118
|
+
* the same cap as a local path, the bytes sniffed against the type. Called on
|
|
119
|
+
* EVERY phase, so the confirmed call re-reads the blob and a changed one no
|
|
120
|
+
* longer matches the sha256 bound into the token. Reading never deletes.
|
|
121
|
+
*
|
|
122
|
+
* A local `photo_path` does NOT come through here (it keeps {@link checkPhoto}
|
|
123
|
+
* and its photo-directory rules) — except on a hosted deployment, where
|
|
124
|
+
* resolveUploadSource is what refuses it, and when it was combined with
|
|
125
|
+
* another source, which resolveUploadSource refuses as "exactly one".
|
|
126
|
+
*/
|
|
127
|
+
function resolvePhotoSource(input) {
|
|
128
|
+
return resolveUploadSource(input, {
|
|
129
|
+
maxBytes: MAX_PHOTO_BYTES,
|
|
130
|
+
allowedMimeTypes: PHOTO_MIME_TYPES,
|
|
131
|
+
names: PHOTO_PARAM_NAMES,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
101
134
|
// Untappd ratings are 0–5 in 0.25 increments; 0 (or omitted) means no rating.
|
|
102
135
|
const RatingSchema = z
|
|
103
136
|
.number()
|
|
@@ -319,12 +352,54 @@ export function registerCheckinTools(server, client) {
|
|
|
319
352
|
const data = await client.write('POST', `/checkin/delete/${checkin_id}`);
|
|
320
353
|
return minifiedResult({ deleted: true, checkin_id, result: data?.result });
|
|
321
354
|
});
|
|
355
|
+
server.registerTool('untappd_upload_slots', {
|
|
356
|
+
title: 'Get upload URLs for Untappd check-in photos',
|
|
357
|
+
description: 'Get signed upload URLs for check-in photos — the way to attach a photo on the HOSTED connector, which cannot ' +
|
|
358
|
+
'read files on your machine. For each photo (JPEG or PNG, max 15 MB) it returns a blobKey and a ready curl ' +
|
|
359
|
+
'command: run the curl with the path to the photo (it must send exactly the returned Content-Type), then pass ' +
|
|
360
|
+
'the blobKey to untappd_checkin as photo_blob_key. URLs expire after 30 minutes. Posts nothing to Untappd. ' +
|
|
361
|
+
'Only available where the deployment provides a blob store; on a local install use photo_path or photo_inline.',
|
|
362
|
+
annotations: toolAnnotations({ title: 'Get upload URLs for Untappd check-in photos', readOnly: true, openWorld: false }),
|
|
363
|
+
_meta: foodMeta('lookup', { aliases: ['upload photo', 'beer photo'] }),
|
|
364
|
+
inputSchema: z.object({
|
|
365
|
+
slots: z
|
|
366
|
+
.array(uploadSlotRequestSchema)
|
|
367
|
+
.min(1)
|
|
368
|
+
.max(MAX_UPLOAD_FILES)
|
|
369
|
+
.describe(`One entry per photo to upload (at most ${MAX_UPLOAD_FILES}): {filename, mimeType?}. JPEG or PNG only.`),
|
|
370
|
+
}),
|
|
371
|
+
}, async ({ slots }) => {
|
|
372
|
+
const store = blobStoreFromEnv();
|
|
373
|
+
if (!store) {
|
|
374
|
+
throw createHelpfulError('Upload slots need the blob store a hosted deployment provides, and this server has no blob store.', {
|
|
375
|
+
hint: 'On a local install attach the photo with untappd_checkin photo_path (a file in UNTAPPD_PHOTO_DIR) or photo_inline (base64).',
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
// Refuse a non-photo before minting a URL the check-in would only reject later.
|
|
379
|
+
for (const req of slots) {
|
|
380
|
+
const mime = resolveUploadMime(sanitizeUploadFilename(req.filename), req.mimeType);
|
|
381
|
+
if (!PHOTO_MIME_TYPES.includes(mime)) {
|
|
382
|
+
throw createHelpfulError(`${req.filename}: ${mime} is not a check-in photo type.`, {
|
|
383
|
+
hint: 'Untappd check-in photos must be JPEG or PNG (.jpg, .jpeg or .png).',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return minifiedResult({
|
|
388
|
+
slots: slots.map((req) => mintUploadSlot(store, req)),
|
|
389
|
+
next: 'PUT each photo to its uploadUrl (the curl command, with the placeholder replaced by the file path), then ' +
|
|
390
|
+
'call untappd_checkin with photo_blob_key set to that slot\'s blobKey. The upload is deleted once the ' +
|
|
391
|
+
'check-in and its photo succeed.',
|
|
392
|
+
});
|
|
393
|
+
});
|
|
322
394
|
server.registerTool('untappd_checkin', {
|
|
323
395
|
title: 'Check in a beer on Untappd',
|
|
324
396
|
description: 'Post a NEW beer check-in to YOUR Untappd account — this publishes to your public feed. Provide the beer id ' +
|
|
325
397
|
'(bid) from untappd_search_beer; optionally a rating (0–5 in 0.25 steps), a shout (comment), a venue via ' +
|
|
326
|
-
'foursquare_id, and
|
|
327
|
-
|
|
398
|
+
'foursquare_id, and ONE photo (JPEG/PNG, max 15 MB) via exactly one of photo_path (a local file), ' +
|
|
399
|
+
'photo_inline (base64, for a small photo) or photo_blob_key. On the hosted connector, use the slot flow: call ' +
|
|
400
|
+
'untappd_upload_slots, PUT the photo with the returned curl, then pass its blobKey as photo_blob_key (a path ' +
|
|
401
|
+
'cannot work there). The preview shows the exact fields and photo that will be posted. ' +
|
|
402
|
+
`${CONFIRM_FLOW}`,
|
|
328
403
|
// Destructive: it publishes to your public feed, where friends see it; untappd_delete_checkin cannot unpublish what they saw.
|
|
329
404
|
annotations: toolAnnotations({ title: 'Check in a beer on Untappd', readOnly: false, idempotent: false, openWorld: true, destructive: true }),
|
|
330
405
|
_meta: foodMeta('change', { aliases: ['check in', 'log a beer', 'rate a beer', 'drinking'] }),
|
|
@@ -338,7 +413,18 @@ export function registerCheckinTools(server, client) {
|
|
|
338
413
|
.optional()
|
|
339
414
|
.describe('Optional path to a local JPEG/PNG photo (max 15 MB) to attach — it is published publicly. It must be ' +
|
|
340
415
|
'inside the photo directory (UNTAPPD_PHOTO_DIR, default ~/Downloads/untappd-mcp). Only use a file the ' +
|
|
341
|
-
'user explicitly chose; the preview shows the resolved path and size for them to confirm.'
|
|
416
|
+
'user explicitly chose; the preview shows the resolved path and size for them to confirm. Local installs ' +
|
|
417
|
+
'only — refused on the hosted connector (use photo_blob_key).'),
|
|
418
|
+
photo_inline: inlineFileSchema
|
|
419
|
+
.optional()
|
|
420
|
+
.describe('Optional photo as base64: {filename, contentBase64, mimeType?}, JPEG or PNG, max 15 MB — it is published ' +
|
|
421
|
+
'publicly. Works on any deployment; for anything but a small photo prefer the upload slot flow ' +
|
|
422
|
+
'(photo_blob_key). Only use a photo the user explicitly chose.'),
|
|
423
|
+
photo_blob_key: z
|
|
424
|
+
.string()
|
|
425
|
+
.optional()
|
|
426
|
+
.describe('Optional blobKey of a photo PUT to a slot from untappd_upload_slots (hosted connector) — it is published ' +
|
|
427
|
+
'publicly. Deleted from the upload store once the check-in and its photo succeed; kept if they fail.'),
|
|
342
428
|
geolat: z.number().optional().describe('Optional latitude of the check-in'),
|
|
343
429
|
geolng: z.number().optional().describe('Optional longitude of the check-in'),
|
|
344
430
|
container_id: z
|
|
@@ -356,13 +442,37 @@ export function registerCheckinTools(server, client) {
|
|
|
356
442
|
confirmToken: confirmTokenParam,
|
|
357
443
|
}),
|
|
358
444
|
}, async (args, ctx) => {
|
|
359
|
-
const { bid, rating, shout, foursquare_id, photo_path, geolat, geolng, container_id, timezone: requestedTz, confirmToken } = args;
|
|
445
|
+
const { bid, rating, shout, foursquare_id, photo_path, photo_inline, photo_blob_key, geolat, geolng, container_id, timezone: requestedTz, confirmToken, } = args;
|
|
360
446
|
const { timezone, gmt_offset } = checkinTimezone(requestedTz);
|
|
361
|
-
// Vetted on every call, so the preview names the exact
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
447
|
+
// Vetted on every call, so the preview names the exact photo that will be
|
|
448
|
+
// published — and one whose bytes change between the preview and the
|
|
449
|
+
// confirmed call no longer matches the token. A lone local photo_path keeps
|
|
450
|
+
// its own checks; every other route (and a path on a hosted deployment,
|
|
451
|
+
// which is refused there) goes through resolveUploadSource.
|
|
452
|
+
const localPath = photo_path !== undefined && photo_inline === undefined && photo_blob_key === undefined && !isHostedEnv();
|
|
453
|
+
const anyPhoto = photo_path !== undefined || photo_inline !== undefined || photo_blob_key !== undefined;
|
|
454
|
+
const source = anyPhoto && !localPath
|
|
455
|
+
? await resolvePhotoSource({ path: photo_path, inline: photo_inline, blobKey: photo_blob_key })
|
|
456
|
+
: undefined;
|
|
457
|
+
const photo = localPath ? await checkPhoto(photo_path) : undefined;
|
|
458
|
+
const photo_sha256 = photo ? await photoSha256(photo.path) : source?.sha256;
|
|
459
|
+
const photo_ext = photo?.ext ?? (source ? extForMime(source.mimeType) : undefined);
|
|
460
|
+
const photo_content_type = photo?.content_type ?? source?.mimeType;
|
|
461
|
+
const hasPhoto = photo !== undefined || source !== undefined;
|
|
462
|
+
// What the preview (and the token) names: a local file by its resolved
|
|
463
|
+
// path; bytes by name, type, size and hash (never the signed URL).
|
|
464
|
+
const photoPreview = photo
|
|
465
|
+
? photo
|
|
466
|
+
: source
|
|
467
|
+
? {
|
|
468
|
+
origin: source.origin,
|
|
469
|
+
file_name: source.fileName,
|
|
470
|
+
content_type: source.mimeType,
|
|
471
|
+
size_bytes: source.sizeBytes,
|
|
472
|
+
sha256: source.sha256,
|
|
473
|
+
...(source.blobKey !== undefined ? { blob_key: source.blobKey } : {}),
|
|
474
|
+
}
|
|
475
|
+
: undefined;
|
|
366
476
|
const form = {
|
|
367
477
|
bid,
|
|
368
478
|
rating: rating !== undefined ? rating.toFixed(2) : undefined,
|
|
@@ -373,8 +483,8 @@ export function registerCheckinTools(server, client) {
|
|
|
373
483
|
container_id,
|
|
374
484
|
timezone,
|
|
375
485
|
gmt_offset,
|
|
376
|
-
is_photo:
|
|
377
|
-
photo_file_ext:
|
|
486
|
+
is_photo: hasPhoto ? 'true' : 'false',
|
|
487
|
+
photo_file_ext: photo_ext,
|
|
378
488
|
platform: 'ios',
|
|
379
489
|
};
|
|
380
490
|
const request = { method: 'POST', path: '/checkin/add', form };
|
|
@@ -385,11 +495,13 @@ export function registerCheckinTools(server, client) {
|
|
|
385
495
|
confirmToken,
|
|
386
496
|
args,
|
|
387
497
|
target: bid,
|
|
388
|
-
payload: { ...request, photo, photo_sha256 },
|
|
498
|
+
payload: { ...request, photo: photoPreview, photo_sha256 },
|
|
389
499
|
preview: {
|
|
390
500
|
action: 'checkin',
|
|
391
501
|
...request,
|
|
392
|
-
photo:
|
|
502
|
+
photo: photoPreview
|
|
503
|
+
? { ...photoPreview, note: 'this exact photo will be uploaded PUBLICLY after the check-in is created' }
|
|
504
|
+
: undefined,
|
|
393
505
|
note: 'POSTs this check-in to your public Untappd feed.',
|
|
394
506
|
},
|
|
395
507
|
});
|
|
@@ -402,6 +514,17 @@ export function registerCheckinTools(server, client) {
|
|
|
402
514
|
// file-backed, streamed — not heap-buffered; re-checks the size cap/roots at open.
|
|
403
515
|
blob = await fileBlob(photo.path, { maxBytes: MAX_PHOTO_BYTES, label: 'Photo', allowedRoots: photoRoots() });
|
|
404
516
|
}
|
|
517
|
+
else if (source) {
|
|
518
|
+
// Already read and hashed above; copied so the Blob owns a plain ArrayBuffer.
|
|
519
|
+
blob = new Blob([new Uint8Array(source.bytes)], { type: source.mimeType });
|
|
520
|
+
}
|
|
521
|
+
// An uploaded blob is deleted only after the check-in AND its photo
|
|
522
|
+
// succeed; on any other outcome it is kept (and said so), so the caller
|
|
523
|
+
// can delete the check-in and retry with the same photo_blob_key. The
|
|
524
|
+
// blob store's retention prune reclaims it otherwise.
|
|
525
|
+
const keptBlob = source?.origin === 'blob'
|
|
526
|
+
? { photo_blob: { deleted: false, kept: true, blob_key: source.blobKey, note: 'The uploaded photo was kept because it was not attached.' } }
|
|
527
|
+
: {};
|
|
405
528
|
const sentAt = Date.now();
|
|
406
529
|
let data;
|
|
407
530
|
try {
|
|
@@ -424,8 +547,8 @@ export function registerCheckinTools(server, client) {
|
|
|
424
547
|
checkin_id: found,
|
|
425
548
|
recovered: true,
|
|
426
549
|
photo_attached: false,
|
|
427
|
-
...(
|
|
428
|
-
? { photo_error: `Check-in ${found} was created, but its photo upload URL was lost with the timed-out response, so no photo was attached
|
|
550
|
+
...(hasPhoto
|
|
551
|
+
? { photo_error: `Check-in ${found} was created, but its photo upload URL was lost with the timed-out response, so no photo was attached.`, ...keptBlob }
|
|
429
552
|
: {}),
|
|
430
553
|
note: 'Untappd did not answer in time, but the check-in was found on your account — it was created. Do not retry.',
|
|
431
554
|
});
|
|
@@ -436,10 +559,10 @@ export function registerCheckinTools(server, client) {
|
|
|
436
559
|
// never silently dropped.
|
|
437
560
|
let photo_attached = false;
|
|
438
561
|
let photo_error;
|
|
439
|
-
if (
|
|
562
|
+
if (hasPhoto) {
|
|
440
563
|
if (data?.photo_upload?.url && data.checkin_id) {
|
|
441
564
|
try {
|
|
442
|
-
await client.putBinary(data.photo_upload.url, blob,
|
|
565
|
+
await client.putBinary(data.photo_upload.url, blob, photo_content_type);
|
|
443
566
|
await client.write('POST', '/photo/uploadComplete', {
|
|
444
567
|
form: { checkin_id: data.checkin_id, destination_url: data.photo_upload.destination_url },
|
|
445
568
|
});
|
|
@@ -453,11 +576,15 @@ export function registerCheckinTools(server, client) {
|
|
|
453
576
|
photo_error = `Check-in ${data?.checkin_id} was created, but Untappd returned no photo upload URL, so no photo was attached.`;
|
|
454
577
|
}
|
|
455
578
|
}
|
|
579
|
+
// Release (delete) an uploaded blob only now that the photo is attached; a
|
|
580
|
+
// failed delete is reported, never thrown — the check-in already exists.
|
|
581
|
+
const blobOutcome = source?.origin === 'blob' ? (photo_attached ? { photo_blob: await source.release() } : keptBlob) : {};
|
|
456
582
|
return minifiedResult({
|
|
457
583
|
checked_in: true,
|
|
458
584
|
checkin_id: data?.checkin_id,
|
|
459
585
|
photo_attached,
|
|
460
586
|
...(photo_error ? { photo_error } : {}),
|
|
587
|
+
...blobOutcome,
|
|
461
588
|
response: data,
|
|
462
589
|
});
|
|
463
590
|
});
|
package/dist/version.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
// json's `extra-files`), and `versionSyncTest` guards that it stays equal to
|
|
4
4
|
// package.json. Import VERSION wherever the version is needed rather than
|
|
5
5
|
// re-declaring it.
|
|
6
|
-
export const VERSION = '2.
|
|
6
|
+
export const VERSION = '2.7.0'; // x-release-please-version
|
package/mint.yaml
CHANGED
|
@@ -92,3 +92,8 @@ egress:
|
|
|
92
92
|
# - a signed upload URL returned by the API (often storage.googleapis.com / s3)
|
|
93
93
|
# Confirm the concrete host(s) against a live run and add them, or
|
|
94
94
|
# the isolated tier will block the call.
|
|
95
|
+
#
|
|
96
|
+
# Not listed: mcp-host's own blob store (MCP_BLOB_BASE_URL, the gateway
|
|
97
|
+
# serving this connector — its host differs per deployment). The server
|
|
98
|
+
# GETs + DELETEs check-in photos the caller uploaded through
|
|
99
|
+
# untappd_upload_slots. Signed requests only; no Untappd credential is sent.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "untappd-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/untappd-mcp",
|
|
5
5
|
"description": "Untappd MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@chrischall/mcp-utils": "^3.
|
|
48
|
+
"@chrischall/mcp-utils": "^3.7.2",
|
|
49
49
|
"@modelcontextprotocol/server": "^2.3.0",
|
|
50
50
|
"dotenv": "^18.0.1",
|
|
51
51
|
"zod": "^4.6.5"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/untappd-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.7.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "untappd-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.7.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -25,9 +25,13 @@ client credentials):
|
|
|
25
25
|
|
|
26
26
|
Two things the server enforces that you should plan around:
|
|
27
27
|
|
|
28
|
-
- **Photos:**
|
|
29
|
-
`
|
|
30
|
-
`~/
|
|
28
|
+
- **Photos:** pass exactly one of `photo_path`, `photo_inline` or
|
|
29
|
+
`photo_blob_key` (JPEG/PNG, max 15 MB). Locally, `photo_path` must be a file
|
|
30
|
+
inside `UNTAPPD_PHOTO_DIR` (default `~/Downloads/untappd-mcp`); anything else
|
|
31
|
+
(e.g. `~/Pictures/beer.jpg`) is refused — ask the user to move the photo there
|
|
32
|
+
first. On the **hosted connector** a path cannot work: call
|
|
33
|
+
`untappd_upload_slots`, PUT the photo with the returned `curl`, then pass its
|
|
34
|
+
`blobKey` as `photo_blob_key` (or, for a small photo, `photo_inline` base64).
|
|
31
35
|
- **Time zone:** on a hosted connector the server usually runs in UTC, so pass
|
|
32
36
|
`timezone` (the drinker's IANA zone, e.g. `America/New_York`) on
|
|
33
37
|
`untappd_checkin` unless `UNTAPPD_TIMEZONE` is set — otherwise the check-in is
|
|
@@ -179,9 +183,8 @@ preview). `MCP_CONFIRM_MODE` on the server can make this `auto` or `refuse`.
|
|
|
179
183
|
- `untappd_toast` — toast/like a check-in (this endpoint is a toggle).
|
|
180
184
|
- `untappd_add_comment` — comment on a check-in.
|
|
181
185
|
- `untappd_checkin` — post a new beer check-in to your feed (needs a `bid` from
|
|
182
|
-
`untappd_search_beer`; optional `rating` 0–5 in 0.25 steps, `shout`, venue
|
|
183
|
-
|
|
184
|
-
Photo attachment and wishlist add/remove are not yet supported.
|
|
186
|
+
`untappd_search_beer`; optional `rating` 0–5 in 0.25 steps, `shout`, venue,
|
|
187
|
+
one photo — see **Photos** above).
|
|
185
188
|
|
|
186
189
|
## Check-in cache
|
|
187
190
|
|