Skip to content

Commit 7c0f927

Browse files
authored
feat(twitter): add users.by.username handle->id read cap (#125)
1 parent 3da2408 commit 7c0f927

3 files changed

Lines changed: 47 additions & 2 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tangle-network/agent-integrations",
3-
"version": "0.46.0",
3+
"version": "0.47.0",
44
"description": "Vendor-neutral integration contracts and runtime helpers for sandbox and agent apps.",
55
"homepage": "https://github.com/tangle-network/agent-integrations#readme",
66
"repository": {

src/connectors/adapters/twitter.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,23 @@ const TWITTER_SPEC: RestConnectorSpec = {
201201
query: { max_results: '{max_results}', pagination_token: '{pagination_token}' },
202202
},
203203
},
204+
{
205+
name: 'users.by.username',
206+
class: 'read',
207+
description:
208+
'Resolve a handle to its numeric user id (and name) — for followed_by (resolve target → list users.following) and mentioned-with-target (resolve target → filter users.mentions by author_id). Returns { id, name, username }.',
209+
parameters: {
210+
type: 'object',
211+
properties: {
212+
username: {
213+
type: 'string',
214+
description: 'Twitter/X handle WITHOUT a leading @; resolves to a numeric user id.',
215+
},
216+
},
217+
required: ['username'],
218+
},
219+
request: { method: 'GET', path: '/users/by/username/{username}' },
220+
},
204221
{
205222
name: 'users.tweets',
206223
class: 'read',

tests/twitter.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,15 @@ describe('twitter adapter manifest', () => {
7070
expect(names).toContain('dms.send')
7171
})
7272

73-
it('exposes the six quest-verification read capabilities classified as reads', () => {
73+
it('exposes the seven quest-verification read capabilities classified as reads', () => {
7474
const reads = twitterConnector.manifest.capabilities
7575
.filter((c) => c.class === 'read')
7676
.map((c) => c.name)
7777
.sort()
7878
expect(reads).toEqual([
7979
'tweets.likingUsers',
8080
'tweets.retweetedBy',
81+
'users.by.username',
8182
'users.following',
8283
'users.me',
8384
'users.mentions',
@@ -165,6 +166,33 @@ describe('twitter read capabilities', () => {
165166
expect(url.searchParams.get('pagination_token')).toBe('CURSOR')
166167
})
167168

169+
it('users.by.username GETs /users/by/username/{username} with no query params', async () => {
170+
const captured = captureGet({ data: { id: 'u-7', name: 'Drew', username: 'drew' } })
171+
const result = await adapter.executeRead!({
172+
source: source(),
173+
capabilityName: 'users.by.username',
174+
args: { username: 'drew' },
175+
idempotencyKey: 'k',
176+
})
177+
const url = new URL(captured.url)
178+
expect(captured.method).toBe('GET')
179+
expect(url.origin + url.pathname).toBe('https://api.twitter.com/2/users/by/username/drew')
180+
expect([...url.searchParams.keys()]).toEqual([])
181+
expect(result.data).toEqual({ data: { id: 'u-7', name: 'Drew', username: 'drew' } })
182+
})
183+
184+
it('users.by.username requires the username', async () => {
185+
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({})))
186+
await expect(
187+
adapter.executeRead!({
188+
source: source(),
189+
capabilityName: 'users.by.username',
190+
args: {},
191+
idempotencyKey: 'k',
192+
}),
193+
).rejects.toThrow(/missing required argument: username/)
194+
})
195+
168196
it('users.following requires the user id', async () => {
169197
vi.stubGlobal('fetch', vi.fn(async () => jsonResponse({})))
170198
await expect(

0 commit comments

Comments
 (0)