Skip to content

Commit 8bd2f77

Browse files
authored
Merge pull request #238 from evex-dev/codex/album-release
feat: integrate reviewed album service and session docs (#214, #212)
2 parents 78a73c2 + 3dba544 commit 8bd2f77

8 files changed

Lines changed: 792 additions & 0 deletions

File tree

docs/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default defineConfig({
4040
// { text: "Utils", link: "/docs/utils" },
4141
{ text: "Client Methods", link: "/docs/methods" },
4242
{ text: "Calls", link: "/docs/call" },
43+
{ text: "Album (Moa)", link: "/docs/moa" },
4344
],
4445
},
4546
{

docs/docs/auth.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,53 @@ const client = await loginWithAuthToken("YOUR_AUTH_TOKEN", {
5959
device: "IOSIPAD",
6060
});
6161
```
62+
63+
### Persisting authToken across sessions
64+
65+
`FileStorage` automatically persists things like `cert`, `refreshToken` and
66+
`expire`, but **the `authToken` itself is not one of them** — you need to save
67+
it yourself if you want to reuse it on the next run. LINEJS emits an
68+
`update:authtoken` events when tokens are issued or refreshed. Attach the
69+
listener **before login**, since login itself can rotate the token:
70+
71+
```ts
72+
import { Client } from "@evex/linejs";
73+
import { BaseClient } from "@evex/linejs/base";
74+
import { FileStorage } from "@evex/linejs/storage";
75+
76+
const storage = new FileStorage("./session.json");
77+
const TOKEN_KEY = "userAuthToken";
78+
const base = new BaseClient({ device: "IOSIPAD", storage });
79+
80+
// Event listeners are not awaited by the emitter. Queue writes in order and
81+
// handle failures without creating an unhandled rejection.
82+
let pendingSave = Promise.resolve();
83+
base.on("update:authtoken", (token) => {
84+
base.authToken = token;
85+
pendingSave = pendingSave.then(() => storage.set(TOKEN_KEY, token)).catch(() => {
86+
console.error("Could not persist the LINE session token.");
87+
});
88+
});
89+
base.on("pincall", (pin) => console.log("Enter this pincode:", pin));
90+
91+
const saved = await storage.get(TOKEN_KEY);
92+
93+
await base.loginProcess.login(typeof saved === "string" && saved
94+
? { authToken: saved }
95+
: {
96+
email: "you@example.com",
97+
password: "password",
98+
});
99+
await pendingSave;
100+
const client = new Client(base);
101+
// Use client here. Also await pendingSave before an explicit process exit.
102+
```
103+
104+
The first run uses email + password (and PIN); later runs attempt to reuse the
105+
saved token. Expired or revoked credentials can still require manual login.
106+
Do not automatically loop password logins on every error.
107+
108+
`session.json` contains plaintext credentials and E2EE key material. Exclude it
109+
from version control, restrict file access to your user, and do not log or share
110+
its contents. Load your password from a private configuration or environment
111+
variable rather than committing it. Token reuse does not guarantee account safety.

docs/docs/methods.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,39 @@ for await (const operation of polling.listenTalkEvents()) {
8080
for await (const event of polling.listenSquareEvents()) {
8181
}
8282
```
83+
84+
# Calling LINE REST endpoints directly
85+
86+
Some LINE subsystems (Album/Moa, Timeline REST helpers, ...) speak JSON over
87+
HTTPS through the LEGY proxy rather than Thrift, but LINEJS's default
88+
`getHeader("GET")` helper is tuned for Thrift and sets
89+
`accept: application/x-thrift` and `content-type: application/x-thrift`. If you
90+
call a JSON REST endpoint with those defaults you may get
91+
`{"code":102001,"message":"一時的なエラーが発生しました。"}` back with HTTP 200.
92+
93+
When you hand-craft a REST request, **override both headers to
94+
`application/json`**:
95+
96+
```ts
97+
const headers = {
98+
...client.base.request.getHeader("GET"),
99+
accept: "application/json",
100+
"content-type": "application/json; charset=UTF-8",
101+
"X-Line-ChannelToken": token,
102+
"X-Line-Mid": client.base.profile!.mid,
103+
};
104+
const res = await client.base.fetch(url, {
105+
method: "POST",
106+
headers,
107+
body: new Uint8Array(),
108+
signal: AbortSignal.timeout(client.base.config.timeout),
109+
});
110+
if (!res.ok) throw new Error(`REST request failed: HTTP ${res.status}`);
111+
const data = await res.json();
112+
if (data.code !== undefined && data.code !== 0) {
113+
throw new Error(`REST request failed: code ${data.code}`);
114+
}
115+
```
116+
117+
Use `client.base.moa` for [album operations](./moa.md); it handles these headers
118+
and checks HTTP and application errors for you.

docs/docs/moa.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Album (Moa)
2+
3+
`MoaService` exposes the LINE Album (Moa) REST API on `client.base.moa`. Unlike
4+
Talk / Square, Moa speaks JSON over HTTPS through the LEGY proxy, so this
5+
service uses `client.base.fetch` directly with a channel token issued for the album
6+
channel (`1375220249`).
7+
8+
## Listing albums
9+
10+
```ts
11+
import { loginWithAuthToken } from "@evex/linejs";
12+
13+
const client = await loginWithAuthToken("YOUR_AUTH_TOKEN", {
14+
device: "IOSIPAD",
15+
});
16+
17+
let cursor = "";
18+
while (true) {
19+
const resp = await client.base.moa.getAlbums({ cursor });
20+
const result = resp.result;
21+
for (const album of result?.albums ?? []) {
22+
console.log(`[${album.albumId}] ${album.title} (${album.photoCount} photos)`);
23+
}
24+
const next = result?.nextCursor ?? result?.cursor ?? "";
25+
if (!next || next === cursor || !(result?.hasMore ?? true)) break;
26+
cursor = next;
27+
}
28+
```
29+
30+
## Listing photos in an album
31+
32+
```ts
33+
let cursor = "";
34+
while (true) {
35+
const resp = await client.base.moa.getPhotos({
36+
chatId: "cxxxxxxxxxxxxxxx",
37+
albumId: "1234567890123456789",
38+
cursor,
39+
pageSize: 100,
40+
});
41+
for (const photo of resp.result?.photos ?? []) {
42+
console.log(photo.oid, "shot at", photo.shotTime);
43+
}
44+
const next = resp.result?.nextCursor ?? "";
45+
if (!next || next === cursor) break;
46+
cursor = next;
47+
}
48+
```
49+
50+
## Downloading the original bytes of a photo
51+
52+
`downloadPhoto` returns a `Uint8Array` so the caller can save it or process it
53+
further.
54+
55+
```ts
56+
import { writeFileSync } from "node:fs";
57+
58+
const bytes = await client.base.moa.downloadPhoto({
59+
chatId: "cxxxxxxxxxxxxxxx",
60+
albumId: "1234567890123456789",
61+
oid: "someOid",
62+
});
63+
writeFileSync("./out.jpg", bytes);
64+
```
65+
66+
For videos, pass `prefix: "album/v"` (the `sid` field on `obsResourceId` tells
67+
you whether the item is an image or a video):
68+
69+
```ts
70+
const isVideo = photo.obsResourceId?.sid === "v";
71+
const bytes = await client.base.moa.downloadPhoto({
72+
chatId,
73+
albumId,
74+
oid: photo.obsResourceId!.oid!,
75+
prefix: isVideo ? "album/v" : "album/a",
76+
});
77+
```
78+
79+
## Notes
80+
81+
- The channel token issued for `1375220249` is memoised inside the service and
82+
reused within the same session. Changing the access token, MID or endpoint
83+
invalidates it. If the token is rejected (expiry or revocation), call
84+
`client.base.moa.clearAlbumChannelToken()` before an explicit retry. Requests
85+
are not automatically retried. REST requests and downloads use
86+
`client.base.config.timeout`.
87+
- Moa uses `X-Line-ChannelToken`, `X-Line-Mid` (your MID) and — for photo
88+
fetches — `X-Line-Album` (the album id) and `X-Line-Mid` set to the *chat*
89+
id. All of these are set for you automatically.
90+
- The header override `accept: application/json` is important: the default
91+
`client.base.request.getHeader("GET")` returns `application/x-thrift`, which
92+
LEGY rejects for REST endpoints with
93+
`{"code":102001,"message":"一時的なエラーが発生しました。"}`.

packages/linejs/base/core/mod.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
CallService,
2121
ChannelService,
2222
LiffService,
23+
MoaService,
2324
RelationService,
2425
SquareLiveTalkService,
2526
SquareService,
@@ -130,6 +131,7 @@ export class BaseClient extends TypedEventEmitter<ClientEvents> {
130131
readonly call: CallService;
131132
readonly channel: ChannelService;
132133
readonly liff: LiffService;
134+
readonly moa: MoaService;
133135
readonly relation: RelationService;
134136
readonly livetalk: SquareLiveTalkService;
135137
readonly square: SquareService;
@@ -206,6 +208,7 @@ export class BaseClient extends TypedEventEmitter<ClientEvents> {
206208
this.channel = new ChannelService(this);
207209
this.liff = new LiffService(this);
208210
this.livetalk = new SquareLiveTalkService(this);
211+
this.moa = new MoaService(this);
209212
this.relation = new RelationService(this);
210213
this.square = new SquareService(this);
211214
this.talk = new TalkService(this);

0 commit comments

Comments
 (0)