/ clis / zhihu / target.js
target.js
 1  import { CliError } from '@jackwener/opencli/errors';
 2  const USER_RE = /^user:([A-Za-z0-9_-]+)$/;
 3  const QUESTION_RE = /^question:(\d+)$/;
 4  const ANSWER_RE = /^answer:(\d+):(\d+)$/;
 5  const ARTICLE_RE = /^article:(\d+)$/;
 6  const USER_PATH_RE = /^\/people\/([A-Za-z0-9_-]+)\/?$/;
 7  const QUESTION_PATH_RE = /^\/question\/(\d+)\/?$/;
 8  const ANSWER_PATH_RE = /^\/question\/(\d+)\/answer\/(\d+)\/?$/;
 9  const ARTICLE_PATH_RE = /^\/p\/(\d+)\/?$/;
10  const EMPTY_AUTHORITY_RE = /^https:\/\/(?::)?@/i;
11  function isAllowedZhihuUrl(url) {
12      return url.protocol === 'https:' && url.username === '' && url.password === '' && url.port === '';
13  }
14  export function parseTarget(input) {
15      const value = String(input).trim();
16      if (EMPTY_AUTHORITY_RE.test(value)) {
17          throw new CliError('INVALID_INPUT', 'Zhihu write commands require a normal HTTPS Zhihu URL without malformed authority', 'Example: https://www.zhihu.com/question/123456');
18      }
19      if (value.startsWith('answer:') && !ANSWER_RE.test(value)) {
20          throw new CliError('INVALID_INPUT', 'Invalid answer target, expected answer:<questionId>:<answerId>', 'Example: opencli zhihu like answer:123:456 --execute');
21      }
22      let match = value.match(USER_RE);
23      if (match) {
24          return { kind: 'user', slug: match[1], url: `https://www.zhihu.com/people/${match[1]}` };
25      }
26      match = value.match(QUESTION_RE);
27      if (match) {
28          return { kind: 'question', id: match[1], url: `https://www.zhihu.com/question/${match[1]}` };
29      }
30      match = value.match(ANSWER_RE);
31      if (match) {
32          return {
33              kind: 'answer',
34              questionId: match[1],
35              id: match[2],
36              url: `https://www.zhihu.com/question/${match[1]}/answer/${match[2]}`,
37          };
38      }
39      match = value.match(ARTICLE_RE);
40      if (match) {
41          return { kind: 'article', id: match[1], url: `https://zhuanlan.zhihu.com/p/${match[1]}` };
42      }
43      try {
44          const url = new URL(value);
45          if (!isAllowedZhihuUrl(url)) {
46              throw new Error('unsupported zhihu url variant');
47          }
48          if (url.hostname === 'www.zhihu.com') {
49              const userMatch = url.pathname.match(USER_PATH_RE);
50              if (userMatch) {
51                  const slug = userMatch[1];
52                  return { kind: 'user', slug, url: `https://www.zhihu.com/people/${slug}` };
53              }
54              const questionMatch = url.pathname.match(QUESTION_PATH_RE);
55              if (questionMatch) {
56                  return { kind: 'question', id: questionMatch[1], url: `https://www.zhihu.com/question/${questionMatch[1]}` };
57              }
58              const answerMatch = url.pathname.match(ANSWER_PATH_RE);
59              if (answerMatch) {
60                  return {
61                      kind: 'answer',
62                      questionId: answerMatch[1],
63                      id: answerMatch[2],
64                      url: `https://www.zhihu.com/question/${answerMatch[1]}/answer/${answerMatch[2]}`,
65                  };
66              }
67          }
68          if (url.hostname === 'zhuanlan.zhihu.com') {
69              const articleMatch = url.pathname.match(ARTICLE_PATH_RE);
70              if (articleMatch) {
71                  return { kind: 'article', id: articleMatch[1], url: `https://zhuanlan.zhihu.com/p/${articleMatch[1]}` };
72              }
73          }
74      }
75      catch { }
76      throw new CliError('INVALID_INPUT', 'Zhihu write commands require a Zhihu URL or typed target like question:123 or answer:123:456', 'Example: opencli zhihu like answer:123:456 --execute');
77  }
78  export function assertAllowedKinds(command, target) {
79      const allowed = {
80          follow: ['user', 'question'],
81          like: ['answer', 'article'],
82          favorite: ['answer', 'article'],
83          comment: ['answer', 'article'],
84          answer: ['question'],
85      };
86      if (!allowed[command]?.includes(target.kind)) {
87          throw new CliError('UNSUPPORTED_TARGET', `zhihu ${command} does not support ${target.kind} targets`);
88      }
89      return target;
90  }
91  export const __test__ = { parseTarget, assertAllowedKinds };