File size: 12,827 Bytes
db445d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
04417e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db445d8
 
 
 
 
 
04417e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db445d8
04417e3
 
 
 
 
db445d8
04417e3
 
 
db445d8
04417e3
 
 
 
 
 
db445d8
 
04417e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db445d8
4fb5da0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db445d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
import os
import json
import argparse
from typing import Dict, Any, List, Optional, Tuple
from dotenv import load_dotenv

import requests

# Set up ENVIRONMENT VARIABLE
load_dotenv()
WORKSPACE_ID = os.getenv("WORKSPACE_ID")
BITBUCKET_USERNAME = os.getenv("BITBUCKET_USERNAME")
BITBUCKET_APP_PASSWORD = os.getenv("BITBUCKET_APP_PASSWORD")
DEFAULT_BASE_URL = "https://api.bitbucket.org/2.0"
SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")
SLACK_BOT_TOKEN = os.getenv("SLACK_BOT_TOKEN")
SLACK_CHANNEL = os.getenv("SLACK_CHANNEL")


def create_session(username: Optional[str] = None, app_password: Optional[str] = None) -> requests.Session:
    """
    Create an authenticated session for Bitbucket Cloud using Basic Auth (username + App Password).
    Falls back to BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD environment variables.
    """
    resolved_username = username or os.getenv("BITBUCKET_USERNAME")
    resolved_app_password = app_password or os.getenv("BITBUCKET_APP_PASSWORD")

    if not resolved_username or not resolved_app_password:
        raise ValueError(
            "Missing credentials. Provide --username and --app-password or set env vars "
            "BITBUCKET_USERNAME and BITBUCKET_APP_PASSWORD."
        )

    session = requests.Session()
    session.auth = (resolved_username, resolved_app_password)
    session.headers.update({"Accept": "application/json"})
    return session


def _request_json(session: requests.Session, method: str, url: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
    response = session.request(method, url, params=params, timeout=30)
    response.raise_for_status()
    return response.json()


def _paginate_all(session: requests.Session, url: str, params: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
    """
    Retrieve all pages for Bitbucket v2.0 endpoints that return 'values' and 'next'.
    """
    aggregated_values: List[Dict[str, Any]] = []
    next_url: Optional[str] = url
    query_params: Dict[str, Any] = dict(params or {})
    if "pagelen" not in query_params:
        query_params["pagelen"] = 100

    while next_url:
        data = _request_json(session, "GET", next_url, params=query_params if next_url == url else None)
        values = data.get("values", [])
        aggregated_values.extend(values)
        next_url = data.get("next")
    return aggregated_values


def get_pull_request_overview(session: requests.Session, workspace: str, repo_slug: str, pr_id: int, base_url: str = DEFAULT_BASE_URL) -> Dict[str, Any]:
    url = f"{base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}"
    return _request_json(session, "GET", url)


def get_pull_request_commits(session: requests.Session, workspace: str, repo_slug: str, pr_id: int, base_url: str = DEFAULT_BASE_URL) -> List[Dict[str, Any]]:
    url = f"{base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/commits"
    return _paginate_all(session, url)


def get_pull_request_comments(session: requests.Session, workspace: str, repo_slug: str, pr_id: int, base_url: str = DEFAULT_BASE_URL) -> List[Dict[str, Any]]:
    url = f"{base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/comments"
    return _paginate_all(session, url)


def get_pull_request_activity(session: requests.Session, workspace: str, repo_slug: str, pr_id: int, base_url: str = DEFAULT_BASE_URL) -> List[Dict[str, Any]]:
    url = f"{base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/activity"
    return _paginate_all(session, url)


def get_pull_request_diff(session: requests.Session, workspace: str, repo_slug: str, pr_id: int, base_url: str = DEFAULT_BASE_URL) -> str:
    """
    Returns unified diff text for the PR. This endpoint returns text/plain.
    """
    url = f"{base_url}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/diff"
    headers = {"Accept": "text/plain"}
    response = session.get(url, headers=headers, timeout=60)
    response.raise_for_status()
    return response.text


def get_diff(session, diff_url):
    response = session.request('GET', diff_url, timeout=30)
    response.raise_for_status()
    
    return response.text


def save_md_report(filename, content):
    with open(filename, "w", encoding="utf-8") as f:
        f.write(content)
        print(f"Saved {filename} file")


def _send_via_webhook(message_text: str, blocks: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
    """Gửi tin nhắn qua Incoming Webhook."""
    if not SLACK_WEBHOOK_URL:
        raise ValueError("Missing SLACK_WEBHOOK_URL for webhook message")

    payload: Dict[str, Any] = {"text": message_text}
    if blocks is not None:
        payload["blocks"] = blocks

    response = requests.post(SLACK_WEBHOOK_URL, json=payload, timeout=15)
    if 200 <= response.status_code < 300 and response.text.strip().lower() in ("ok", ""):
        return {
            "ok": True,
            "method": "webhook",
            "status_code": response.status_code,
            "response": response.text,
        }

    raise ValueError(f"Slack webhook failed (status {response.status_code}): {response.text}")


def _send_via_api(
    message_text: str,
    *,
    channel: Optional[str] = None,
    blocks: Optional[List[Dict[str, Any]]] = None,
    thread_ts: Optional[str] = None,
) -> Dict[str, Any]:
    """Gửi tin nhắn qua Slack Web API (chat.postMessage)."""
    if not SLACK_BOT_TOKEN or not (channel or SLACK_CHANNEL):
        raise ValueError("Missing SLACK_BOT_TOKEN or channel for Web API message")

    api_url = "https://slack.com/api/chat.postMessage"
    headers = {
        "Authorization": f"Bearer {SLACK_BOT_TOKEN}",
        "Content-Type": "application/json",
    }
    payload: Dict[str, Any] = {
        "channel": channel or SLACK_CHANNEL,
        "text": message_text,
    }
    if blocks is not None:
        payload["blocks"] = blocks
    if thread_ts is not None:
        payload["thread_ts"] = thread_ts

    response = requests.post(api_url, headers=headers, json=payload, timeout=15)
    try:
        response.raise_for_status()
    except Exception as exc:
        raise ValueError(f"Slack API request failed: {exc}") from exc

    data = response.json()
    if not data.get("ok", False):
        raise ValueError(f"Slack API responded with error: {data.get('error', 'unknown_error')}")

    return {
        "ok": True,
        "method": "web_api",
        "status_code": response.status_code,
        "response": data,
    }


def send_slack_message(
    message_text: str,
    *,
    channel: Optional[str] = None,
    blocks: Optional[List[Dict[str, Any]]] = None,
    use_webhook: bool = False,
) -> Dict[str, Any]:
    """
    Gửi tin nhắn mới vào Slack (không dùng thread).
    Có thể chọn gửi qua Webhook hoặc Web API.
    """
    if use_webhook:
        return _send_via_webhook(message_text, blocks)
    return _send_via_api(message_text, channel=channel, blocks=blocks)


def reply_slack_thread(
    thread_ts: str,
    message_text: str,
    *,
    channel: Optional[str] = None,
    blocks: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
    """
    Trả lời một tin nhắn trong thread (yêu cầu thread_ts).
    Bắt buộc dùng Web API (webhook không hỗ trợ thread).
    """
    return _send_via_api(message_text, channel=channel, blocks=blocks, thread_ts=thread_ts)


def list_slack_users(*, bot_token: Optional[str] = None, include_bots: bool = True) -> List[Dict[str, Any]]:
    """
    Trả về danh sách user Slack sử dụng Web API users.list (có phân trang).
    - bot_token: ưu tiên dùng tham số, nếu không có sẽ lấy SLACK_BOT_TOKEN từ env
    - include_bots: False để lọc bỏ bot
    """
    token = bot_token or SLACK_BOT_TOKEN
    if not token:
        raise ValueError("Missing Slack bot token. Provide bot_token or set SLACK_BOT_TOKEN.")

    users: List[Dict[str, Any]] = []
    cursor: Optional[str] = None
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
    }

    while True:
        params: Dict[str, Any] = {"limit": 200}
        if cursor:
            params["cursor"] = cursor

        resp = requests.get("https://slack.com/api/users.list", headers=headers, params=params, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        if not data.get("ok", False):
            raise ValueError(f"Slack API responded with error: {data.get('error', 'unknown_error')}")

        page_members = data.get("members", [])
        if not include_bots:
            page_members = [m for m in page_members if not m.get("is_bot", False)]
        users.extend(page_members)

        cursor = (data.get("response_metadata") or {}).get("next_cursor")
        if not cursor:
            break

    return users

def get_slack_usergroups(bot_token: str):
    """
    Lấy danh sách Slack User Groups (subteams).
    
    Args:
        bot_token (str): Slack Bot Token (bắt đầu bằng xoxb-...).
        
    Returns:
        list[dict]: Danh sách user groups gồm {id, handle, name, description}.
    """
    url = "https://slack.com/api/usergroups.list"
    headers = {
        "Authorization": f"Bearer {bot_token}"
    }

    response = requests.get(url, headers=headers)

    if response.status_code == 200:
        data = response.json()
        if data.get("ok"):
            usergroups = []
            for group in data.get("usergroups", []):
                usergroups.append({
                    "id": group.get("id"),                  # subteam ID
                    "handle": group.get("handle"),          # ví dụ: dev, qa
                    "name": group.get("name"),              # tên đầy đủ
                    "description": group.get("description") # mô tả
                })
            return usergroups
        else:
            raise Exception(f"Slack API error: {data.get('error')}")
    else:
        raise Exception(f"HTTP Error: {response.status_code}")



def main() -> None:
    # parser = argparse.ArgumentParser(description="Fetch full details of a Bitbucket Cloud pull request.")
    # parser.add_argument("--username", required=True, help="Bitbucket username")
    # parser.add_argument("--password", required=True, help="Bitbucket app password")
    # parser.add_argument("--workspace", required=True, help="Bitbucket workspace ID or slug")
    # parser.add_argument("--repo", required=True, help="Repository slug")
    # parser.add_argument("--pr", required=True, type=int, help="Pull request ID")
    # parser.add_argument("--username", help="Bitbucket username (or set BITBUCKET_USERNAME)")
    # parser.add_argument("--app-password", help="Bitbucket App Password (or set BITBUCKET_APP_PASSWORD)")
    # parser.add_argument(
    #     "--include",
    #     nargs="*",
    #     choices=["overview", "commits", "comments", "activity", "diff"],
    #     help="Sections to include. Default: all",
    # )
    # parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Base API URL. Default is Bitbucket Cloud v2.0")
    # args = parser.parse_args()

    session = create_session(BITBUCKET_USERNAME, BITBUCKET_APP_PASSWORD)

    data = get_pull_request_overview(
        session = session, 
        workspace = WORKSPACE_ID, # args.workspace, 
        repo_slug = REPO_SLUG, 
        pr_id = PR_ID, 
        base_url = DEFAULT_BASE_URL
    )

    # Lấy các thông tin cần thiết
    title = data["title"]
    description = data["description"]
    source_branch = data["source"]["branch"]["name"]
    destination_branch = data["destination"]["branch"]["name"]
    author = data["author"]["display_name"]
    created_on = data["created_on"]
    reviewers = data.get("reviewers", [])
    reviewer_mentions = " ".join([f"@{r.get('nickname')}" for r in reviewers]) if reviewers else "None"
    pr_link = data["links"]["html"]["href"]
    changelog = data["summary"]["raw"]
    diff_url = data["links"]["diff"]["href"]


    pr_report = f"""
# [{args.repo}] PR #{args.pr}: {title}
*{description}*

**🌿 Branch Information:**
- **Source Branch:** {source_branch} → **Target Branch:** {destination_branch}

**👤 Người tạo:** {author}

**📅 Thời gian tạo:** 2024-08-20 14:30:00 +07:00

**👥 Reviewers:**
{reviewer_mentions}

**🔗 Link Pull Request:** [PR #{args.pr}: Implement user authentication system]({pr_link})

---

## 📝 Changelogs: 
    """

    save_md_report("PR.md", pr_report)

    data = get_diff(session, diff_url)

    save_md_report("DIFF.md", data)


if __name__ == "__main__":
    send_slack_message(
        message_text = "Hello",
        webhook_url = SLACK_WEBHOOK_URL,
        bot_token = SLACK_BOT_TOKEN,
        channel = "pull-request"
    )