Skip to content
This repository has been archived by the owner on Jun 18, 2024. It is now read-only.

Commit

Permalink
Merge pull request #445 from ltworf/autoreactions
Browse files Browse the repository at this point in the history
Automatically react to messages
  • Loading branch information
ltworf authored Nov 20, 2023
2 parents 2b9bb6f + c296065 commit 7d286c5
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
35 changes: 34 additions & 1 deletion irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -326,6 +330,33 @@ 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 = msgparsing.get_emoji_code(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}')
return
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)
Expand Down Expand Up @@ -683,7 +714,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)
Expand Down Expand Up @@ -841,6 +873,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,
Expand Down
12 changes: 12 additions & 0 deletions man/localslackirc.1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 11 additions & 1 deletion msgparsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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
Expand Down
80 changes: 75 additions & 5 deletions slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,15 @@ 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)


class NoChanMessage(NamedTuple):
user: str
text: str
ts: float
thread_ts: Optional[str] = None


Expand Down Expand Up @@ -338,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:
"""
Expand All @@ -362,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()
Expand Down Expand Up @@ -484,6 +503,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(
Expand Down Expand Up @@ -532,6 +552,53 @@ 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 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():
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)
response: Response = self.tload(r, Response)
Expand Down Expand Up @@ -914,10 +981,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

Expand Down Expand Up @@ -951,7 +1014,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:
Expand Down

0 comments on commit 7d286c5

Please sign in to comment.