From 7d28c27e6ec7cba261551f683d5048d79babc49d Mon Sep 17 00:00:00 2001 From: Salvo 'LtWorf' Tomaselli Date: Mon, 20 Nov 2023 18:59:53 +0100 Subject: [PATCH 01/11] Pass around timestamp of messages --- irc.py | 3 ++- slack.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/irc.py b/irc.py index 4099882..b455cdc 100644 --- a/irc.py +++ b/irc.py @@ -683,7 +683,8 @@ async def _messageedit(self, sl_ev: slack.MessageEdit) -> None: text=seddiff(sl_ev.previous.text, sl_ev.current.text), channel=sl_ev.channel, user=sl_ev.previous.user, - thread_ts=sl_ev.previous.thread_ts + thread_ts=sl_ev.previous.thread_ts, + ts=sl_ev.previous.ts, ) await self._message(diffmsg) diff --git a/slack.py b/slack.py index 444e5fd..5e4d895 100644 --- a/slack.py +++ b/slack.py @@ -143,6 +143,7 @@ class Message: channel: str # The channel id user: str # The user id text: str + ts: float thread_ts: Optional[str] = None files: list[File] = field(default_factory=list) @@ -150,6 +151,7 @@ class Message: class NoChanMessage(NamedTuple): user: str text: str + ts: float thread_ts: Optional[str] = None @@ -484,6 +486,7 @@ async def _history(self) -> None: user=msg.user, thread_ts=msg.thread_ts, files=msg.files, + ts=msg.ts, )) elif isinstance(msg, HistoryBotMessage): self._internalevents.append(MessageBot( From c53021d6e52a077b31ccceb1a45b0359d181cb8d Mon Sep 17 00:00:00 2001 From: Salvo 'LtWorf' Tomaselli Date: Mon, 20 Nov 2023 19:01:08 +0100 Subject: [PATCH 02/11] Function to send reactions --- slack.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/slack.py b/slack.py index 5e4d895..8f08ffa 100644 --- a/slack.py +++ b/slack.py @@ -535,6 +535,17 @@ async def typing(self, channel: Channel|str) -> None: ch_id = channel await self.client.wspacket(type='typing', channel=ch_id) + async def add_reaction(self, msg: Message, reaction: str) -> None: + r = await self.client.api_call( + 'reactions.add', + channel=msg.channel, + timestamp=msg.ts, + name=reaction, + ) + response = self.tload(r, Response) + if not response.ok: + raise ResponseException(response.error) + async def topic(self, channel: Channel, topic: str) -> None: r = await self.client.api_call('conversations.setTopic', channel=channel.id, topic=topic) response: Response = self.tload(r, Response) From b69052b65c0c5b7f1b089ba261b370890ed27be2 Mon Sep 17 00:00:00 2001 From: Salvo 'LtWorf' Tomaselli Date: Mon, 20 Nov 2023 19:04:07 +0100 Subject: [PATCH 03/11] Add a way to program the slack client to automatically react to messages --- slack.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/slack.py b/slack.py index 8f08ffa..8bf7470 100644 --- a/slack.py +++ b/slack.py @@ -340,6 +340,22 @@ class SlackStatus: """ last_timestamp: float = 0.0 + +class Autoreaction(NamedTuple): + user_id: str + reaction: str + probability: float + expiration: float + + @property + def expired(self) -> bool: + return time() > self.expiration + + def random_reaction(self) -> bool: + import random + return random.random() < self.probability + + class Slack: def __init__(self, token: str, cookie: Optional[str], previous_status: Optional[bytes]) -> None: """ @@ -364,6 +380,7 @@ def __init__(self, token: str, cookie: Optional[str], previous_status: Optional[ self._wsblock: int = 0 # Semaphore to block the socket and avoid events being received before their API call ended. self.login_info: Optional[LoginInfo] = None self.loader = dataloader.Loader() + self._autoreactions: list[Autoreaction] = [] if previous_status is None: self._status = SlackStatus() @@ -546,6 +563,37 @@ async def add_reaction(self, msg: Message, reaction: str) -> None: if not response.ok: raise ResponseException(response.error) + async def add_autoreact(self, username: str, reaction: str, probability: float, expiration: float) -> None: + + if probability > 1 or probability < 0: + raise ValueError(f'Probability must be comprised between 0 and 1') + user_id = (await self.get_user_by_name(username)).id + + a = Autoreaction( + user_id=user_id, + reaction=reaction, + probability=probability, + expiration=expiration, + ) + + if a.expired: + raise ValueError('Expired') + + self._autoreactions.append(a) + + async def _autoreact(self, msg: Message) -> None: + for i in self._autoreactions: + # Clean up + if i.expired: + self._autoreactions.remove(i) + return + + if i.user_id != msg.user: + continue + + if i.random_reaction(): + await self.add_reaction(msg, i.reaction) + async def topic(self, channel: Channel, topic: str) -> None: r = await self.client.api_call('conversations.setTopic', channel=channel.id, topic=topic) response: Response = self.tload(r, Response) @@ -928,10 +976,6 @@ async def event(self) -> Optional[SlackEvent]: if ts > self._status.last_timestamp: self._status.last_timestamp = ts - if ts in self._sent_by_self: - self._sent_by_self.remove(ts) - continue - if t in USELESS_EVENTS: continue @@ -965,7 +1009,14 @@ async def event(self) -> Optional[SlackEvent]: # the other user, and prepend them with "I say: " im = await self.get_im(msg.channel) if im and im.user != msg.user: - msg = Message(user=im.user, text='I say: ' + msg.text, channel=im.id, thread_ts=msg.thread_ts) + msg = Message(user=im.user, text='I say: ' + msg.text, channel=im.id, thread_ts=msg.thread_ts, ts=msg.ts) + + await self._autoreact(msg) + + if ts in self._sent_by_self: + self._sent_by_self.remove(ts) + continue + if subt == 'me_message': return ActionMessage(*msg) # type: ignore else: From f2a2f12e8d1f8192bbd82b1897a53e0d6bdcc240 Mon Sep 17 00:00:00 2001 From: Salvo 'LtWorf' Tomaselli Date: Mon, 20 Nov 2023 19:05:16 +0100 Subject: [PATCH 04/11] Add /autoreact command --- irc.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/irc.py b/irc.py index b455cdc..bd380cc 100644 --- a/irc.py +++ b/irc.py @@ -326,6 +326,32 @@ async def _modehandler(self, cmd: bytes) -> None: params = cmd.split(b' ', 2) await self._sendreply(Replies.RPL_CHANNELMODEIS, '', [params[1], '+']) + async def _autoreacthandler(self, cmd: bytes) -> None: + params = cmd.split(b' ') + params.pop(0) + + try: + username = params.pop(0).decode('utf8') + probability = float(params.pop(0)) + + if params: + reaction = params.pop(0).decode('utf8') + else: + reaction = 'thumbsup' + + if params: + duration = int(params.pop(0)) + else: + duration = 10 + + # async def add_autoreact(self, username: str, reaction: str, probability: float, expiration: int) -> None: + await self.sl_client.add_autoreact(username, reaction, probability, time.time() + duration * 60 ) + except Exception as e: + await self._sendreply(Replies.ERR_UNKNOWNCOMMAND, 'Syntax: /autoreact user probability [reaction] [duration]') + await self._sendreply(Replies.ERR_UNKNOWNCOMMAND, f'error: {e}') + await self._sendreply(0, f'Will react to {username} for {duration} minutes') + + async def _annoyhandler(self, cmd: bytes) -> None: params = cmd.split(b' ') params.pop(0) @@ -842,6 +868,7 @@ async def command(self, cmd: bytes) -> None: b'INVITE': self._invitehandler, b'SENDFILE': self._sendfilehandler, b'ANNOY': self._annoyhandler, + b'AUTOREACT': self._autoreacthandler, b'QUIT': self._quithandler, #CAP LS b'USERHOST': self._userhosthandler, From bba6d733fb8f6a3f3b0f3739f2f2236af4ca159c Mon Sep 17 00:00:00 2001 From: Salvo 'LtWorf' Tomaselli Date: Mon, 20 Nov 2023 19:05:55 +0100 Subject: [PATCH 05/11] CHANGELOG --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 00021d4..7011b5e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,6 @@ 1.23 +* Add /autoreact command to automatically put reactions + to messages. 1.22 * Fetch only the list of joined channels if the full list From 4286987d4896128bdd1d9a6a832e0a0e73ba9492 Mon Sep 17 00:00:00 2001 From: Salvo 'LtWorf' Tomaselli Date: Mon, 20 Nov 2023 19:13:55 +0100 Subject: [PATCH 06/11] Remove non functioning reactions --- slack.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/slack.py b/slack.py index 8bf7470..d1026b5 100644 --- a/slack.py +++ b/slack.py @@ -592,7 +592,12 @@ async def _autoreact(self, msg: Message) -> None: continue if i.random_reaction(): - await self.add_reaction(msg, i.reaction) + try: + await self.add_reaction(msg, i.reaction) + except: + # Remove reactions that fail + self._autoreactions.remove(i) + return async def topic(self, channel: Channel, topic: str) -> None: r = await self.client.api_call('conversations.setTopic', channel=channel.id, topic=topic) From 9bed55be12a4a09f3f2540ecc1c59262234c901f Mon Sep 17 00:00:00 2001 From: Salvo 'LtWorf' Tomaselli Date: Mon, 20 Nov 2023 19:14:05 +0100 Subject: [PATCH 07/11] Update manpage --- man/localslackirc.1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/man/localslackirc.1 b/man/localslackirc.1 index 56b290b..b5fd1f2 100644 --- a/man/localslackirc.1 +++ b/man/localslackirc.1 @@ -191,6 +191,18 @@ This means that whenever a typing event is received from that user, on any chann .br duration is the duration of the annoyance in minutes. It defaults to 10. .SS +.TP +.B /autoreact user probability [reaction] [duration] +To automate reacting to messages, this nice feature is available. +.br +user is the username of the user that we want to react to. +.br +probability is a number between 0 and 1, to decide how much to react. +.br +reaction is the reaction to use. The default is "thumbsup". +.br +duration indicates when to stop doing it, in minutes. Defaults to 10. +.SS .SH "SEE ALSO" .BR lsi-send (1), lsi-write (1) From 8c05d39dbbd6844c66fe69827abec0328b88dcc5 Mon Sep 17 00:00:00 2001 From: Salvo 'LtWorf' Tomaselli Date: Mon, 20 Nov 2023 19:14:14 +0100 Subject: [PATCH 08/11] README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2cdf613..62710b7 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,9 @@ They are mapped as irc channels that get automatically joined when a message is Until a thread has some activity you can't write to it. They are only tested for channels, not private groups or chats. +## Reacting to messages +Since I don't feel like manually wasting time to do it, a very nice `/autoreact` command is available to automate reacting. + ## Instructions for irssi If you need to refresh your memory about connecting in general, this is a good guide: https://pthree.org/2010/02/02/irssis-channel-network-server-and-connect-what-it-means/ From 1104e6732929a09294f37aac52a8c39e860498af Mon Sep 17 00:00:00 2001 From: Salvo 'LtWorf' Tomaselli Date: Mon, 20 Nov 2023 19:18:43 +0100 Subject: [PATCH 09/11] Inform users of the extra commands --- irc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/irc.py b/irc.py index bd380cc..ca26cd2 100644 --- a/irc.py +++ b/irc.py @@ -172,6 +172,10 @@ async def _userhandler(self, cmd: bytes) -> None: await self._sendreply(2, 'Your nickname must be: %s' % self.sl_client.login_info.self.name) await self._sendreply(2, f'Version: {VERSION}') await self._sendreply(Replies.RPL_LUSERCLIENT, 'There are 1 users and 0 services on 1 server') + await self._sendreply(2, '============= Extra IRC commands supported =============') + await self._sendreply(2, '/annoy') + await self._sendreply(2, '/autoreact') + await self._sendreply(2, '/sendfile') if self.settings.autojoin and not self.settings.nouserlist: # We're about to load many users for each chan; instead of requesting each From 87eb033b02202676c0927b23cf374f6a049f3202 Mon Sep 17 00:00:00 2001 From: Salvo 'LtWorf' Tomaselli Date: Mon, 20 Nov 2023 19:52:21 +0100 Subject: [PATCH 10/11] Make it easier to find reactions --- irc.py | 2 +- msgparsing.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/irc.py b/irc.py index ca26cd2..6b836a9 100644 --- a/irc.py +++ b/irc.py @@ -339,7 +339,7 @@ async def _autoreacthandler(self, cmd: bytes) -> None: probability = float(params.pop(0)) if params: - reaction = params.pop(0).decode('utf8') + reaction = msgparsing.get_emoji_code(params.pop(0).decode('utf8')) else: reaction = 'thumbsup' diff --git a/msgparsing.py b/msgparsing.py index a1e0234..aeb4297 100644 --- a/msgparsing.py +++ b/msgparsing.py @@ -20,10 +20,12 @@ from typing import Iterable, NamedTuple, Optional try: - from emoji import emojize # type: ignore + from emoji import emojize, demojize # type: ignore except ModuleNotFoundError: def emojize(string:str, *args, **kwargs) -> str: # type: ignore return string + def demojize(string, str, delimiters: tuple[str, str]) -> str: # type: ignore + return string SLACK_SUBSTITUTIONS = [ @@ -38,9 +40,17 @@ def emojize(string:str, *args, **kwargs) -> str: # type: ignore 'Itemkind', 'PreBlock', 'SpecialItem', + 'get_emoji_code', ] +def get_emoji_code(msg: str) -> str: + ''' + Pass a single emoji and get the code without delimiters. + ''' + return demojize(msg, ('', '')) + + def preblocks(msg: str) -> Iterable[tuple[str, bool]]: """ Iterates the preformatted and normal text blocks From c296065aaf3f421ab61d417f0649c6d2c9d883c3 Mon Sep 17 00:00:00 2001 From: Salvo 'LtWorf' Tomaselli Date: Mon, 20 Nov 2023 19:52:34 +0100 Subject: [PATCH 11/11] Do not crash on error --- irc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/irc.py b/irc.py index 6b836a9..7bc0f19 100644 --- a/irc.py +++ b/irc.py @@ -353,6 +353,7 @@ async def _autoreacthandler(self, cmd: bytes) -> None: except Exception as e: await self._sendreply(Replies.ERR_UNKNOWNCOMMAND, 'Syntax: /autoreact user probability [reaction] [duration]') await self._sendreply(Replies.ERR_UNKNOWNCOMMAND, f'error: {e}') + return await self._sendreply(0, f'Will react to {username} for {duration} minutes')