@urugus/slack-cli 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/CHANGELOG.md +22 -0
- package/dist/utils/slack-operations/channel-operations.d.ts +0 -1
- package/dist/utils/slack-operations/channel-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/channel-operations.js +37 -19
- package/dist/utils/slack-operations/channel-operations.js.map +1 -1
- package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/message-operations.js +16 -4
- package/dist/utils/slack-operations/message-operations.js.map +1 -1
- package/package.json +1 -1
- package/src/utils/slack-operations/channel-operations.ts +39 -19
- package/src/utils/slack-operations/message-operations.ts +15 -4
- package/tests/commands/unread.test.ts +41 -0
- package/tests/utils/slack-operations/channel-operations.test.ts +248 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.1] - 2025-06-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Fixed unread message detection for channels where unread_count is 0 but messages exist after last_read timestamp
|
|
9
|
+
- Always check messages after last_read timestamp for accurate unread count
|
|
10
|
+
- Improved reliability of unread message detection for channels like dev_kiban_jira
|
|
11
|
+
|
|
12
|
+
## [0.2.0] - 2025-06-23
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Major version bump for improved unread message detection
|
|
16
|
+
|
|
17
|
+
## [0.1.9] - 2025-06-22
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Improved unread message detection using last_read timestamp
|
|
21
|
+
|
|
22
|
+
## [0.1.8] - 2025-06-22
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Refactored code organization with separation of concerns
|
|
26
|
+
|
|
5
27
|
## [0.1.7] - 2025-06-22
|
|
6
28
|
|
|
7
29
|
### Fixed
|
|
@@ -8,7 +8,6 @@ interface ChannelWithUnreadInfo extends Channel {
|
|
|
8
8
|
export declare class ChannelOperations extends BaseSlackClient {
|
|
9
9
|
listChannels(options: ListChannelsOptions): Promise<Channel[]>;
|
|
10
10
|
listUnreadChannels(): Promise<Channel[]>;
|
|
11
|
-
private listUnreadChannelsFallback;
|
|
12
11
|
getChannelInfo(channelNameOrId: string): Promise<ChannelWithUnreadInfo>;
|
|
13
12
|
}
|
|
14
13
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-operations.d.ts","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,UAAU,qBAAsB,SAAQ,OAAO;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,iBAAkB,SAAQ,eAAe;IAC9C,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAuB9D,kBAAkB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"channel-operations.d.ts","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,UAAU,qBAAsB,SAAQ,OAAO;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,iBAAkB,SAAQ,eAAe;IAC9C,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAuB9D,kBAAkB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAyExC,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAe9E"}
|
|
@@ -24,19 +24,6 @@ class ChannelOperations extends base_client_1.BaseSlackClient {
|
|
|
24
24
|
return channels;
|
|
25
25
|
}
|
|
26
26
|
async listUnreadChannels() {
|
|
27
|
-
try {
|
|
28
|
-
// Use users.conversations to get unread counts in a single API call
|
|
29
|
-
// This endpoint doesn't return unread_count_display by default,
|
|
30
|
-
// so we'll use the fallback method instead
|
|
31
|
-
throw new Error('Using fallback method for unread counts');
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
// Fallback to the old method if users.conversations fails
|
|
35
|
-
console.warn('Failed to use users.conversations, falling back to conversations.list');
|
|
36
|
-
return this.listUnreadChannelsFallback();
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
async listUnreadChannelsFallback() {
|
|
40
27
|
// Get all conversations the user is a member of
|
|
41
28
|
const response = await this.client.conversations.list({
|
|
42
29
|
types: 'public_channel,private_channel,im,mpim',
|
|
@@ -53,13 +40,44 @@ class ChannelOperations extends base_client_1.BaseSlackClient {
|
|
|
53
40
|
include_num_members: false,
|
|
54
41
|
});
|
|
55
42
|
const channelInfo = info.channel;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
43
|
+
// Get the latest message in the channel
|
|
44
|
+
const history = await this.client.conversations.history({
|
|
45
|
+
channel: channel.id,
|
|
46
|
+
limit: 1,
|
|
47
|
+
});
|
|
48
|
+
// Always check for messages after last_read timestamp
|
|
49
|
+
if (channelInfo.last_read) {
|
|
50
|
+
// Fetch messages after last_read
|
|
51
|
+
const unreadHistory = await this.client.conversations.history({
|
|
52
|
+
channel: channel.id,
|
|
53
|
+
oldest: channelInfo.last_read,
|
|
54
|
+
limit: 100, // Get up to 100 unread messages
|
|
55
|
+
});
|
|
56
|
+
const unreadCount = unreadHistory.messages?.length || 0;
|
|
57
|
+
if (unreadCount > 0) {
|
|
58
|
+
channelsWithUnread.push({
|
|
59
|
+
...channel,
|
|
60
|
+
unread_count: unreadCount,
|
|
61
|
+
unread_count_display: unreadCount,
|
|
62
|
+
last_read: channelInfo.last_read,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (history.messages && history.messages.length > 0) {
|
|
67
|
+
// If no last_read, all messages are unread
|
|
68
|
+
const allHistory = await this.client.conversations.history({
|
|
69
|
+
channel: channel.id,
|
|
70
|
+
limit: 100,
|
|
62
71
|
});
|
|
72
|
+
const unreadCount = allHistory.messages?.length || 0;
|
|
73
|
+
if (unreadCount > 0) {
|
|
74
|
+
channelsWithUnread.push({
|
|
75
|
+
...channel,
|
|
76
|
+
unread_count: unreadCount,
|
|
77
|
+
unread_count_display: unreadCount,
|
|
78
|
+
last_read: channelInfo.last_read,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
63
81
|
}
|
|
64
82
|
// Add delay between API calls to avoid rate limiting
|
|
65
83
|
await this.delay(100);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":";;;AAAA,+CAAgD;AAChD,0DAAsD;AACtD,4CAAwC;AASxC,MAAa,iBAAkB,SAAQ,6BAAe;IACpD,KAAK,CAAC,YAAY,CAAC,OAA4B;QAC7C,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,IAAI,MAA0B,CAAC;QAE/B,gCAAgC;QAChC,GAAG,CAAC;YACF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;gBACpD,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;gBAC1C,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,MAAM;aACP,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACtB,QAAQ,CAAC,IAAI,CAAC,GAAI,QAAQ,CAAC,QAAsB,CAAC,CAAC;YACrD,CAAC;YAED,MAAM,GAAG,QAAQ,CAAC,iBAAiB,EAAE,WAAW,CAAC;QACnD,CAAC,QAAQ,MAAM,EAAE;QAEjB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,IAAI,CAAC
|
|
1
|
+
{"version":3,"file":"channel-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":";;;AAAA,+CAAgD;AAChD,0DAAsD;AACtD,4CAAwC;AASxC,MAAa,iBAAkB,SAAQ,6BAAe;IACpD,KAAK,CAAC,YAAY,CAAC,OAA4B;QAC7C,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,IAAI,MAA0B,CAAC;QAE/B,gCAAgC;QAChC,GAAG,CAAC;YACF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;gBACpD,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;gBAC1C,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,MAAM;aACP,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACtB,QAAQ,CAAC,IAAI,CAAC,GAAI,QAAQ,CAAC,QAAsB,CAAC,CAAC;YACrD,CAAC;YAED,MAAM,GAAG,QAAQ,CAAC,iBAAiB,EAAE,WAAW,CAAC;QACnD,CAAC,QAAQ,MAAM,EAAE;QAEjB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,gDAAgD;QAChD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;YACpD,KAAK,EAAE,wCAAwC;YAC/C,gBAAgB,EAAE,IAAI;YACtB,KAAK,EAAE,IAAI;SACZ,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAqB,CAAC;QAChD,MAAM,kBAAkB,GAAc,EAAE,CAAC;QAEzC,8DAA8D;QAC9D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;oBAChD,OAAO,EAAE,OAAO,CAAC,EAAE;oBACnB,mBAAmB,EAAE,KAAK;iBAC3B,CAAC,CAAC;gBACH,MAAM,WAAW,GAAG,IAAI,CAAC,OAAgC,CAAC;gBAE1D,wCAAwC;gBACxC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC;oBACtD,OAAO,EAAE,OAAO,CAAC,EAAE;oBACnB,KAAK,EAAE,CAAC;iBACT,CAAC,CAAC;gBAEH,sDAAsD;gBACtD,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;oBAC1B,iCAAiC;oBACjC,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC;wBAC5D,OAAO,EAAE,OAAO,CAAC,EAAE;wBACnB,MAAM,EAAE,WAAW,CAAC,SAAS;wBAC7B,KAAK,EAAE,GAAG,EAAE,gCAAgC;qBAC7C,CAAC,CAAC;oBAEH,MAAM,WAAW,GAAG,aAAa,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC,CAAC;oBACxD,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;wBACpB,kBAAkB,CAAC,IAAI,CAAC;4BACtB,GAAG,OAAO;4BACV,YAAY,EAAE,WAAW;4BACzB,oBAAoB,EAAE,WAAW;4BACjC,SAAS,EAAE,WAAW,CAAC,SAAS;yBACjC,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;qBAAM,IAAI,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC3D,2CAA2C;oBAC3C,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC;wBACzD,OAAO,EAAE,OAAO,CAAC,EAAE;wBACnB,KAAK,EAAE,GAAG;qBACX,CAAC,CAAC;oBACH,MAAM,WAAW,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAC,CAAC;oBAErD,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;wBACpB,kBAAkB,CAAC,IAAI,CAAC;4BACtB,GAAG,OAAO;4BACV,YAAY,EAAE,WAAW;4BACzB,oBAAoB,EAAE,WAAW;4BACjC,SAAS,EAAE,WAAW,CAAC,SAAS;yBACjC,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;gBAED,qDAAqD;gBACrD,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACxB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,0BAA0B;gBAC1B,MAAM,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;QAED,OAAO,kBAAkB,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,eAAuB;QAC1C,MAAM,SAAS,GAAG,MAAM,kCAAe,CAAC,gBAAgB,CAAC,eAAe,EAAE,GAAG,EAAE,CAC7E,IAAI,CAAC,YAAY,CAAC;YAChB,KAAK,EAAE,wCAAwC;YAC/C,gBAAgB,EAAE,IAAI;YACtB,KAAK,EAAE,oBAAQ,CAAC,cAAc;SAC/B,CAAC,CACH,CAAC;QAEF,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;YAChD,OAAO,EAAE,SAAS;SACnB,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC,OAAgC,CAAC;IAC/C,CAAC;CACF;AAhHD,8CAgHC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-operations.d.ts","sourceRoot":"","sources":["../../../src/utils/slack-operations/message-operations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAW,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAGlG,qBAAa,iBAAkB,SAAQ,eAAe;IACpD,OAAO,CAAC,UAAU,CAAoB;gBAE1B,KAAK,EAAE,MAAM;IAKnB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAO5E,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAyB5E,gBAAgB,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"message-operations.d.ts","sourceRoot":"","sources":["../../../src/utils/slack-operations/message-operations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAW,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAGlG,qBAAa,iBAAkB,SAAQ,eAAe;IACpD,OAAO,CAAC,UAAU,CAAoB;gBAE1B,KAAK,EAAE,MAAM;IAKnB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAO5E,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAyB5E,gBAAgB,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;YAsC/D,aAAa;CAmB5B"}
|
|
@@ -39,19 +39,31 @@ class MessageOperations extends base_client_1.BaseSlackClient {
|
|
|
39
39
|
// Get unread messages
|
|
40
40
|
let messages = [];
|
|
41
41
|
let users = new Map();
|
|
42
|
-
|
|
42
|
+
let actualUnreadCount = 0;
|
|
43
|
+
if (channel.last_read) {
|
|
44
|
+
// Always fetch messages after last_read to get accurate unread count
|
|
43
45
|
const historyResult = await this.getHistory(channel.id, {
|
|
44
|
-
limit:
|
|
46
|
+
limit: 100, // Fetch up to 100 messages after last_read
|
|
45
47
|
oldest: channel.last_read,
|
|
46
48
|
});
|
|
47
49
|
messages = historyResult.messages;
|
|
48
50
|
users = historyResult.users;
|
|
51
|
+
actualUnreadCount = messages.length;
|
|
52
|
+
}
|
|
53
|
+
else if (!channel.last_read) {
|
|
54
|
+
// If no last_read, all messages are unread
|
|
55
|
+
const historyResult = await this.getHistory(channel.id, {
|
|
56
|
+
limit: 100,
|
|
57
|
+
});
|
|
58
|
+
messages = historyResult.messages;
|
|
59
|
+
users = historyResult.users;
|
|
60
|
+
actualUnreadCount = messages.length;
|
|
49
61
|
}
|
|
50
62
|
return {
|
|
51
63
|
channel: {
|
|
52
64
|
...channel,
|
|
53
|
-
unread_count:
|
|
54
|
-
unread_count_display:
|
|
65
|
+
unread_count: actualUnreadCount,
|
|
66
|
+
unread_count_display: actualUnreadCount,
|
|
55
67
|
},
|
|
56
68
|
messages,
|
|
57
69
|
users,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/message-operations.ts"],"names":[],"mappings":";;;AACA,+CAAgD;AAChD,0DAAsD;AACtD,4CAAwC;AAExC,6DAAyD;AAEzD,MAAa,iBAAkB,SAAQ,6BAAe;IAGpD,YAAY,KAAa;QACvB,KAAK,CAAC,KAAK,CAAC,CAAC;QACb,IAAI,CAAC,UAAU,GAAG,IAAI,sCAAiB,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAe,EAAE,IAAY;QAC7C,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;YACxC,OAAO;YACP,IAAI;SACL,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,OAAe,EAAE,OAAuB;QACvD,uCAAuC;QACvC,MAAM,SAAS,GAAG,MAAM,kCAAe,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CACrE,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;YAC3B,KAAK,EAAE,wCAAwC;YAC/C,gBAAgB,EAAE,IAAI;YACtB,KAAK,EAAE,oBAAQ,CAAC,cAAc;SAC/B,CAAC,CACH,CAAC;QAEF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC;YACvD,OAAO,EAAE,SAAS;YAClB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAqB,CAAC;QAEhD,sBAAsB;QACtB,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAK,CAAC,CAAC,CAAC,CAAC;QACjF,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAEhD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,eAAuB;QAC5C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;QAEtE,sBAAsB;QACtB,IAAI,QAAQ,GAAc,EAAE,CAAC;QAC7B,IAAI,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"message-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/message-operations.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -34,19 +34,6 @@ export class ChannelOperations extends BaseSlackClient {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
async listUnreadChannels(): Promise<Channel[]> {
|
|
37
|
-
try {
|
|
38
|
-
// Use users.conversations to get unread counts in a single API call
|
|
39
|
-
// This endpoint doesn't return unread_count_display by default,
|
|
40
|
-
// so we'll use the fallback method instead
|
|
41
|
-
throw new Error('Using fallback method for unread counts');
|
|
42
|
-
} catch (error) {
|
|
43
|
-
// Fallback to the old method if users.conversations fails
|
|
44
|
-
console.warn('Failed to use users.conversations, falling back to conversations.list');
|
|
45
|
-
return this.listUnreadChannelsFallback();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private async listUnreadChannelsFallback(): Promise<Channel[]> {
|
|
50
37
|
// Get all conversations the user is a member of
|
|
51
38
|
const response = await this.client.conversations.list({
|
|
52
39
|
types: 'public_channel,private_channel,im,mpim',
|
|
@@ -66,13 +53,46 @@ export class ChannelOperations extends BaseSlackClient {
|
|
|
66
53
|
});
|
|
67
54
|
const channelInfo = info.channel as ChannelWithUnreadInfo;
|
|
68
55
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
56
|
+
// Get the latest message in the channel
|
|
57
|
+
const history = await this.client.conversations.history({
|
|
58
|
+
channel: channel.id,
|
|
59
|
+
limit: 1,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Always check for messages after last_read timestamp
|
|
63
|
+
if (channelInfo.last_read) {
|
|
64
|
+
// Fetch messages after last_read
|
|
65
|
+
const unreadHistory = await this.client.conversations.history({
|
|
66
|
+
channel: channel.id,
|
|
67
|
+
oldest: channelInfo.last_read,
|
|
68
|
+
limit: 100, // Get up to 100 unread messages
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const unreadCount = unreadHistory.messages?.length || 0;
|
|
72
|
+
if (unreadCount > 0) {
|
|
73
|
+
channelsWithUnread.push({
|
|
74
|
+
...channel,
|
|
75
|
+
unread_count: unreadCount,
|
|
76
|
+
unread_count_display: unreadCount,
|
|
77
|
+
last_read: channelInfo.last_read,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} else if (history.messages && history.messages.length > 0) {
|
|
81
|
+
// If no last_read, all messages are unread
|
|
82
|
+
const allHistory = await this.client.conversations.history({
|
|
83
|
+
channel: channel.id,
|
|
84
|
+
limit: 100,
|
|
75
85
|
});
|
|
86
|
+
const unreadCount = allHistory.messages?.length || 0;
|
|
87
|
+
|
|
88
|
+
if (unreadCount > 0) {
|
|
89
|
+
channelsWithUnread.push({
|
|
90
|
+
...channel,
|
|
91
|
+
unread_count: unreadCount,
|
|
92
|
+
unread_count_display: unreadCount,
|
|
93
|
+
last_read: channelInfo.last_read,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
76
96
|
}
|
|
77
97
|
|
|
78
98
|
// Add delay between API calls to avoid rate limiting
|
|
@@ -51,21 +51,32 @@ export class MessageOperations extends BaseSlackClient {
|
|
|
51
51
|
// Get unread messages
|
|
52
52
|
let messages: Message[] = [];
|
|
53
53
|
let users = new Map<string, string>();
|
|
54
|
+
let actualUnreadCount = 0;
|
|
54
55
|
|
|
55
|
-
if (channel.last_read
|
|
56
|
+
if (channel.last_read) {
|
|
57
|
+
// Always fetch messages after last_read to get accurate unread count
|
|
56
58
|
const historyResult = await this.getHistory(channel.id, {
|
|
57
|
-
limit:
|
|
59
|
+
limit: 100, // Fetch up to 100 messages after last_read
|
|
58
60
|
oldest: channel.last_read,
|
|
59
61
|
});
|
|
60
62
|
messages = historyResult.messages;
|
|
61
63
|
users = historyResult.users;
|
|
64
|
+
actualUnreadCount = messages.length;
|
|
65
|
+
} else if (!channel.last_read) {
|
|
66
|
+
// If no last_read, all messages are unread
|
|
67
|
+
const historyResult = await this.getHistory(channel.id, {
|
|
68
|
+
limit: 100,
|
|
69
|
+
});
|
|
70
|
+
messages = historyResult.messages;
|
|
71
|
+
users = historyResult.users;
|
|
72
|
+
actualUnreadCount = messages.length;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
return {
|
|
65
76
|
channel: {
|
|
66
77
|
...channel,
|
|
67
|
-
unread_count:
|
|
68
|
-
unread_count_display:
|
|
78
|
+
unread_count: actualUnreadCount,
|
|
79
|
+
unread_count_display: actualUnreadCount,
|
|
69
80
|
},
|
|
70
81
|
messages,
|
|
71
82
|
users,
|
|
@@ -276,4 +276,45 @@ describe('unread command', () => {
|
|
|
276
276
|
expect(mockSlackClient.listUnreadChannels).toHaveBeenCalledTimes(1);
|
|
277
277
|
});
|
|
278
278
|
});
|
|
279
|
+
|
|
280
|
+
describe('last_read timestamp handling', () => {
|
|
281
|
+
it('should fetch messages after last_read timestamp even when unread_count is 0', async () => {
|
|
282
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
283
|
+
token: 'test-token',
|
|
284
|
+
updatedAt: new Date().toISOString()
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const channelWithLastRead = {
|
|
288
|
+
id: 'C08JFKGJPPE',
|
|
289
|
+
name: 'dev_kiban_jira',
|
|
290
|
+
is_channel: true,
|
|
291
|
+
is_member: true,
|
|
292
|
+
is_archived: false,
|
|
293
|
+
unread_count: 0,
|
|
294
|
+
unread_count_display: 0,
|
|
295
|
+
last_read: '1750646034.663209',
|
|
296
|
+
is_private: false,
|
|
297
|
+
created: 1742353688
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const unreadMessage = {
|
|
301
|
+
ts: '1750646072.447069',
|
|
302
|
+
user: 'U5F87BSGP',
|
|
303
|
+
text: '@Suguru Sakashita / 阪下 駿 transitioned ES-4359 ArgumentError: \'発行者\' is not a valid field_name in Clip',
|
|
304
|
+
type: 'message',
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
vi.mocked(mockSlackClient.getChannelUnread).mockResolvedValue({
|
|
308
|
+
channel: { ...channelWithLastRead, unread_count: 1, unread_count_display: 1 },
|
|
309
|
+
messages: [unreadMessage],
|
|
310
|
+
users: new Map([['U5F87BSGP', 'jira-bot']])
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--channel', 'dev_kiban_jira']);
|
|
314
|
+
|
|
315
|
+
expect(mockSlackClient.getChannelUnread).toHaveBeenCalledWith('dev_kiban_jira');
|
|
316
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(chalk.bold('#dev_kiban_jira: 1 unread messages'));
|
|
317
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('transitioned ES-4359'));
|
|
318
|
+
});
|
|
319
|
+
});
|
|
279
320
|
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { ChannelOperations } from '../../../src/utils/slack-operations/channel-operations';
|
|
3
|
+
|
|
4
|
+
vi.mock('@slack/web-api', () => ({
|
|
5
|
+
WebClient: vi.fn().mockImplementation(() => ({
|
|
6
|
+
conversations: {
|
|
7
|
+
list: vi.fn(),
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
history: vi.fn(),
|
|
10
|
+
},
|
|
11
|
+
})),
|
|
12
|
+
LogLevel: {
|
|
13
|
+
ERROR: 'error',
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('p-limit', () => ({
|
|
18
|
+
default: () => (fn: any) => fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('ChannelOperations', () => {
|
|
22
|
+
let channelOps: ChannelOperations;
|
|
23
|
+
let mockClient: any;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
mockClient = {
|
|
28
|
+
conversations: {
|
|
29
|
+
list: vi.fn(),
|
|
30
|
+
info: vi.fn(),
|
|
31
|
+
history: vi.fn(),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
// Create instance with mocked token
|
|
35
|
+
channelOps = new ChannelOperations('test-token');
|
|
36
|
+
// Replace the client with our mock
|
|
37
|
+
(channelOps as any).client = mockClient;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('listUnreadChannels', () => {
|
|
41
|
+
it('should detect unread messages when last_read is present', async () => {
|
|
42
|
+
// Mock conversations.list response
|
|
43
|
+
mockClient.conversations.list.mockResolvedValue({
|
|
44
|
+
channels: [
|
|
45
|
+
{ id: 'C123', name: 'general' },
|
|
46
|
+
{ id: 'C456', name: 'random' },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Mock conversations.info responses
|
|
51
|
+
mockClient.conversations.info
|
|
52
|
+
.mockResolvedValueOnce({
|
|
53
|
+
channel: {
|
|
54
|
+
id: 'C123',
|
|
55
|
+
name: 'general',
|
|
56
|
+
last_read: '1234567890.000100',
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
.mockResolvedValueOnce({
|
|
60
|
+
channel: {
|
|
61
|
+
id: 'C456',
|
|
62
|
+
name: 'random',
|
|
63
|
+
last_read: '1234567890.000200',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Mock conversations.history responses
|
|
68
|
+
mockClient.conversations.history
|
|
69
|
+
// First call for C123 - check latest message
|
|
70
|
+
.mockResolvedValueOnce({
|
|
71
|
+
messages: [{ ts: '1234567890.000200' }], // Newer than last_read
|
|
72
|
+
})
|
|
73
|
+
// Second call for C123 - get unread messages
|
|
74
|
+
.mockResolvedValueOnce({
|
|
75
|
+
messages: [
|
|
76
|
+
{ ts: '1234567890.000200' },
|
|
77
|
+
{ ts: '1234567890.000150' },
|
|
78
|
+
],
|
|
79
|
+
})
|
|
80
|
+
// Third call for C456 - check latest message
|
|
81
|
+
.mockResolvedValueOnce({
|
|
82
|
+
messages: [{ ts: '1234567890.000100' }], // Older than last_read
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const result = await channelOps.listUnreadChannels();
|
|
86
|
+
|
|
87
|
+
expect(result).toHaveLength(1);
|
|
88
|
+
expect(result[0]).toMatchObject({
|
|
89
|
+
id: 'C123',
|
|
90
|
+
name: 'general',
|
|
91
|
+
unread_count: 2,
|
|
92
|
+
unread_count_display: 2,
|
|
93
|
+
last_read: '1234567890.000100',
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should count all messages as unread when last_read is not present', async () => {
|
|
98
|
+
// Mock conversations.list response
|
|
99
|
+
mockClient.conversations.list.mockResolvedValue({
|
|
100
|
+
channels: [{ id: 'C789', name: 'no-read-channel' }],
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Mock conversations.info response - no last_read
|
|
104
|
+
mockClient.conversations.info.mockResolvedValue({
|
|
105
|
+
channel: {
|
|
106
|
+
id: 'C789',
|
|
107
|
+
name: 'no-read-channel',
|
|
108
|
+
// last_read is missing
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Mock conversations.history responses
|
|
113
|
+
mockClient.conversations.history
|
|
114
|
+
// First call - check if channel has messages
|
|
115
|
+
.mockResolvedValueOnce({
|
|
116
|
+
messages: [{ ts: '1234567890.000300' }],
|
|
117
|
+
})
|
|
118
|
+
// Second call - get all messages (up to 100)
|
|
119
|
+
.mockResolvedValueOnce({
|
|
120
|
+
messages: [
|
|
121
|
+
{ ts: '1234567890.000300' },
|
|
122
|
+
{ ts: '1234567890.000200' },
|
|
123
|
+
{ ts: '1234567890.000100' },
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const result = await channelOps.listUnreadChannels();
|
|
128
|
+
|
|
129
|
+
expect(result).toHaveLength(1);
|
|
130
|
+
expect(result[0]).toMatchObject({
|
|
131
|
+
id: 'C789',
|
|
132
|
+
name: 'no-read-channel',
|
|
133
|
+
unread_count: 3,
|
|
134
|
+
unread_count_display: 3,
|
|
135
|
+
last_read: undefined,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should skip channels with no messages', async () => {
|
|
140
|
+
mockClient.conversations.list.mockResolvedValue({
|
|
141
|
+
channels: [{ id: 'C999', name: 'empty-channel' }],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
mockClient.conversations.info.mockResolvedValue({
|
|
145
|
+
channel: {
|
|
146
|
+
id: 'C999',
|
|
147
|
+
name: 'empty-channel',
|
|
148
|
+
last_read: '1234567890.000100',
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// No messages in channel
|
|
153
|
+
mockClient.conversations.history.mockResolvedValue({
|
|
154
|
+
messages: [],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const result = await channelOps.listUnreadChannels();
|
|
158
|
+
|
|
159
|
+
expect(result).toHaveLength(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should skip channels with all messages read', async () => {
|
|
163
|
+
mockClient.conversations.list.mockResolvedValue({
|
|
164
|
+
channels: [{ id: 'C111', name: 'all-read' }],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
mockClient.conversations.info.mockResolvedValue({
|
|
168
|
+
channel: {
|
|
169
|
+
id: 'C111',
|
|
170
|
+
name: 'all-read',
|
|
171
|
+
last_read: '1234567890.000200',
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// First call to get latest message (limit: 1)
|
|
176
|
+
mockClient.conversations.history.mockResolvedValueOnce({
|
|
177
|
+
messages: [{ ts: '1234567890.000100' }],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Second call to get messages after last_read (should be empty)
|
|
181
|
+
mockClient.conversations.history.mockResolvedValueOnce({
|
|
182
|
+
messages: [],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const result = await channelOps.listUnreadChannels();
|
|
186
|
+
|
|
187
|
+
expect(result).toHaveLength(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should handle API errors gracefully', async () => {
|
|
191
|
+
mockClient.conversations.list.mockResolvedValue({
|
|
192
|
+
channels: [
|
|
193
|
+
{ id: 'C222', name: 'error-channel' },
|
|
194
|
+
{ id: 'C333', name: 'good-channel' },
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// First channel throws error
|
|
199
|
+
mockClient.conversations.info
|
|
200
|
+
.mockRejectedValueOnce(new Error('API Error'))
|
|
201
|
+
.mockResolvedValueOnce({
|
|
202
|
+
channel: {
|
|
203
|
+
id: 'C333',
|
|
204
|
+
name: 'good-channel',
|
|
205
|
+
last_read: '1234567890.000100',
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
mockClient.conversations.history
|
|
210
|
+
.mockResolvedValueOnce({
|
|
211
|
+
messages: [{ ts: '1234567890.000200' }],
|
|
212
|
+
})
|
|
213
|
+
.mockResolvedValueOnce({
|
|
214
|
+
messages: [{ ts: '1234567890.000200' }],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const result = await channelOps.listUnreadChannels();
|
|
218
|
+
|
|
219
|
+
// Should skip the error channel and process the good one
|
|
220
|
+
expect(result).toHaveLength(1);
|
|
221
|
+
expect(result[0].id).toBe('C333');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should handle rate limiting with delay', async () => {
|
|
225
|
+
const delaySpy = vi.spyOn(channelOps as any, 'delay');
|
|
226
|
+
|
|
227
|
+
mockClient.conversations.list.mockResolvedValue({
|
|
228
|
+
channels: [
|
|
229
|
+
{ id: 'C444', name: 'channel1' },
|
|
230
|
+
{ id: 'C555', name: 'channel2' },
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
mockClient.conversations.info.mockResolvedValue({
|
|
235
|
+
channel: { id: 'C444', name: 'channel1' },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
mockClient.conversations.history.mockResolvedValue({
|
|
239
|
+
messages: [],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await channelOps.listUnreadChannels();
|
|
243
|
+
|
|
244
|
+
// Verify delay was called between API calls
|
|
245
|
+
expect(delaySpy).toHaveBeenCalledWith(100);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|