···79798080See [Analyzing my Twitter followers with Datasette](https://simonwillison.net/2018/Jan/28/analyzing-my-twitter-followers/) for the original inspiration for this command.
81818282+## Retrieving Twitter followers
8383+8484+The `list-members` command can be used to retrieve details of one or more Twitter lists, including all of their members.
8585+8686+ $ twitter-to-sqlite list-members members.db simonw/the-good-place
8787+8888+You can pass multiple `screen_name/list_slug` identifiers.
8989+9090+If you know the numeric IDs of the lists instead, you can use `--ids`:
9191+9292+ $ twitter-to-sqlite list-members members.db 927913322841653248
9393+8294## Design notes
83958496* Tweet IDs are stored as integers, to afford sorting by ID in a sensible way
+26
twitter_to_sqlite/cli.py
···217217 identifiers = utils.resolve_identifiers(db, identifiers, attach, sql)
218218 for batch in utils.fetch_user_batches(session, identifiers, ids):
219219 utils.save_users(db, batch)
220220+221221+222222+@cli.command(name="list-members")
223223+@click.argument(
224224+ "db_path",
225225+ type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
226226+ required=True,
227227+)
228228+@click.argument("identifiers", type=str, nargs=-1)
229229+@click.option(
230230+ "-a",
231231+ "--auth",
232232+ type=click.Path(file_okay=True, dir_okay=False, allow_dash=True, exists=True),
233233+ default="auth.json",
234234+ help="Path to auth.json token file",
235235+)
236236+@click.option(
237237+ "--ids", is_flag=True, help="Treat input as list IDs, not user/slug strings"
238238+)
239239+def list_members(db_path, identifiers, auth, ids):
240240+ "Fetch lists - accepts one or more screen_name/list_slug identifiers"
241241+ auth = json.load(open(auth))
242242+ session = utils.session_for_auth(auth)
243243+ db = sqlite_utils.Database(db_path)
244244+ for identifier in identifiers:
245245+ utils.fetch_and_save_list(db, session, identifier, ids)
+36
twitter_to_sqlite/utils.py
···273273 else:
274274 sql_identifiers = []
275275 return list(identifiers) + sql_identifiers
276276+277277+278278+def fetch_and_save_list(db, session, identifier, identifier_is_id=False):
279279+ show_url = "https://api.twitter.com/1.1/lists/show.json"
280280+ args = {}
281281+ if identifier_is_id:
282282+ args["list_id"] = identifier
283283+ else:
284284+ screen_name, slug = identifier.split("/")
285285+ args.update({"owner_screen_name": screen_name, "slug": slug})
286286+ # First fetch the list details
287287+ data = session.get(show_url, params=args).json()
288288+ list_id = data["id"]
289289+ del data["id_str"]
290290+ user = data.pop("user")
291291+ save_users(db, [user])
292292+ data["user"] = user["id"]
293293+ data["created_at"] = parser.parse(data["created_at"])
294294+ db["lists"].upsert(data, pk="id", foreign_keys=("user",))
295295+ # Now fetch the members
296296+ url = "https://api.twitter.com/1.1/lists/members.json"
297297+ cursor = -1
298298+ while cursor:
299299+ args.update({"count": 5000, "cursor": cursor})
300300+ body = session.get(url, params=args).json()
301301+ users = body["users"]
302302+ save_users(db, users)
303303+ db["list_members"].upsert_all(
304304+ ({"list": list_id, "user": user["id"]} for user in users),
305305+ pk=("list", "user"),
306306+ foreign_keys=("list", "user"),
307307+ )
308308+ cursor = body["next_cursor"]
309309+ if not cursor:
310310+ break
311311+ time.sleep(1) # Rate limit = 900 per 15 minutes