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 #447 from ltworf/improve_autoreact
Browse files Browse the repository at this point in the history
Improve the autoreact feature
  • Loading branch information
ltworf authored Nov 22, 2023
2 parents 7342fba + e7f56b1 commit 12ea760
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 41 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
1.24
* Add /dropautoreact command
* Add /listautoreact command
* Add /listannoy command
* Add /dropannoy command
* Store annoy and autoreact in state, so they are preserved
* Allow unlimited timeout for annoy and autoreact

1.23
* Add /autoreact command to automatically put reactions
Expand Down
67 changes: 52 additions & 15 deletions irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ def __init__(
self._usersent = False # Used to hold all events until the IRC client sends the initial USER message
self._held_events: list[slack.SlackEvent] = []
self._mentions_regex_cache: dict[str, Optional[re.Pattern]] = {} # Cache for the regexp to perform mentions. Key is channel id
self._annoy_users: dict[str, int] = {} # Users to annoy pretending to type when they type

def get_mention_str(self) -> str:
'''
Expand Down Expand Up @@ -174,7 +173,11 @@ async def _userhandler(self, cmd: bytes) -> None:
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, '/dropannoy')
await self._sendreply(2, '/listannoy')
await self._sendreply(2, '/autoreact')
await self._sendreply(2, '/dropautoreact')
await self._sendreply(2, '/listautoreact')
await self._sendreply(2, '/sendfile')

if self.settings.autojoin and not self.settings.nouserlist:
Expand Down Expand Up @@ -348,8 +351,11 @@ async def _autoreacthandler(self, cmd: bytes) -> None:
else:
duration = 10

if duration < 1 and duration != -1:
raise ValueError('Duration must be >0 or = -1')

# 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 )
await self.sl_client.add_autoreact(username, reaction, probability, time.time() + duration * 60 if duration != -1 else -1)
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}')
Expand All @@ -364,22 +370,58 @@ async def _annoyhandler(self, cmd: bytes) -> None:
try:
user = params.pop(0).decode('utf8')
if params:
duration = abs(int(params.pop()))
duration = int(params.pop())
else:
duration = 10 # 10 minutes default

if duration < 1 and duration != -1:
raise ValueError("Duration must be positive or -1")

await self.sl_client.add_annoy(user, time.time() + (duration * 60) if duration > 0 else duration)

except KeyError:
await self._sendreply(Replies.ERR_NOSUCHCHANNEL, f'Unable to find user: {user}')
return
except Exception:
await self._sendreply(Replies.ERR_UNKNOWNCOMMAND, 'Syntax: /annoy user [duration]')
return
await self._sendreply(0, f'Will annoy {user} for {duration} minutes')

async def _dropannoyhandler(self, cmd: bytes) -> None:
try:
user_id = (await self.sl_client.get_user_by_name(user)).id
user = cmd.split(b' ', 1)[1].decode('utf8')
await self.sl_client.drop_annoy(user)

except KeyError:
await self._sendreply(Replies.ERR_NOSUCHCHANNEL, f'Unable to find user: {user}')
return
except Exception:
await self._sendreply(Replies.ERR_UNKNOWNCOMMAND, 'Syntax: /dropannoy user')
return
await self._sendreply(0, f'No longer annoying {user}')

self._annoy_users[user_id] = int(time.time()) + (duration * 60)
await self._sendreply(0, f'Will annoy {user} for {duration} minutes')
async def _dropautoreacthandler(self, cmd: bytes) -> None:
try:
user = cmd.split(b' ', 1)[1].decode('utf8')
await self.sl_client.drop_autoreact(user)

except KeyError:
await self._sendreply(Replies.ERR_NOSUCHCHANNEL, f'Unable to find user: {user}')
return
except Exception:
await self._sendreply(Replies.ERR_UNKNOWNCOMMAND, 'Syntax: /dropautoreact user')
return
await self._sendreply(0, f'No longer reacting to {user}')

async def _listannoyhandler(self, _: bytes) -> None:
for i in await self.sl_client.get_annoy():
await self._sendreply(0, f'Annoying {i}')

async def _listautoreacthandler(self, _: bytes) -> None:
for k, v in (await self.sl_client.get_autoreact()).items():
await self._sendreply(0, f'Reactions for {k}')
for i in v:
await self._sendreply(0, str(i))

async def _sendfilehandler(self, cmd: bytes) -> None:
#/sendfile #destination filename
Expand Down Expand Up @@ -838,15 +880,6 @@ async def slack_event(self, sl_ev: slack.SlackEvent) -> None:
elif isinstance(sl_ev, slack.GroupJoined):
channel_name = '#%s' % sl_ev.channel.name_normalized
await self._send_chan_info(channel_name.encode('utf-8'), sl_ev.channel)
elif isinstance(sl_ev, slack.UserTyping):
if sl_ev.user not in self._annoy_users:
return
if time.time() > self._annoy_users[sl_ev.user]:
del self._annoy_users[sl_ev.user]
await self._sendreply(0, f'No longer annoying {(await self.sl_client.get_user(sl_ev.user)).name}')
return
await self.sl_client.typing(sl_ev.channel)


async def command(self, cmd: bytes) -> None:
if b' ' in cmd:
Expand All @@ -873,7 +906,11 @@ async def command(self, cmd: bytes) -> None:
b'INVITE': self._invitehandler,
b'SENDFILE': self._sendfilehandler,
b'ANNOY': self._annoyhandler,
b'LISTANNOY': self._listannoyhandler,
b'DROPANNOY': self._dropannoyhandler,
b'AUTOREACT': self._autoreacthandler,
b'LISTAUTOREACT': self._listautoreacthandler,
b'DROPAUTOREACT': self._dropautoreacthandler,
b'QUIT': self._quithandler,
#CAP LS
b'USERHOST': self._userhosthandler,
Expand Down
25 changes: 23 additions & 2 deletions man/localslackirc.1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.TH localslackirc 1 "Nov 9, 2023" "IRC gateway for slack"
.TH localslackirc 1 "Nov 22, 2023" "IRC gateway for slack"
.SH NAME
localslackirc
\- Creates an IRC server running locally, which acts as a gateway to slack for one user.
Expand Down Expand Up @@ -201,9 +201,30 @@ 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.
duration indicates when to stop doing it, in minutes. Defaults to 10. Setting it to -1 makes it never expire.
.SS

.TP
.B /dropautoreact user
Deletes all the automatic reacts for a given user
.SS

.TP
.B /dropannoy user
Stops annoying the given user
.SS

.TP
.B /listannoy
Lists the users that are currently being annoyed
.SS

.TP
.B /listautoreact
Lists the automatic reactions
.SS


.SH "SEE ALSO"
.BR lsi-send (1), lsi-write (1)

Expand Down
85 changes: 61 additions & 24 deletions slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,30 +331,36 @@ class Conversations(NamedTuple):
)


@dataclass
class SlackStatus:
"""
Not related to the slack API.
This is a structure used internally by this module to
save the status on disk.
"""
last_timestamp: float = 0.0


class Autoreaction(NamedTuple):
user_id: str
reaction: str
probability: float
expiration: float

@property
def expired(self) -> bool:
if self.expiration == -1:
return False
return time() > self.expiration

def random_reaction(self) -> bool:
import random
return random.random() < self.probability

def __str__(self):
return f'{self.reaction} at {self.probability * 100}%'


@dataclass
class SlackStatus:
"""
Not related to the slack API.
This is a structure used internally by this module to
save the status on disk.
"""
last_timestamp: float = 0.0
autoreactions: dict[str, list[Autoreaction]] = field(default_factory=dict)
annoy: dict[str, float] = field(default_factory=dict)


class Slack:
def __init__(self, token: str, cookie: Optional[str], previous_status: Optional[bytes]) -> None:
Expand All @@ -369,7 +375,6 @@ def __init__(self, token: str, cookie: Optional[str], previous_status: Optional[
self.client = SlackClient(token, cookie)
self._usercache: dict[str, User] = {}
self._usermapcache: dict[str, User] = {}
self._usermapcache_keys: list[str]
self._imcache: dict[str, str] = {}
self._channelscache: list[Channel] = []
self._joinedchannelscache: list[Channel] = []
Expand All @@ -380,7 +385,6 @@ 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 @@ -563,14 +567,40 @@ async def add_reaction(self, msg: Message, reaction: str) -> None:
if not response.ok:
raise ResponseException(response.error)

async def add_annoy(self, username, expiration: float) -> None:
user_id = (await self.get_user_by_name(username)).id
self._status.annoy[user_id] = expiration

async def drop_annoy(self, username: str) -> None:
user_id = (await self.get_user_by_name(username)).id
del self._status.annoy[user_id]

async def drop_autoreact(self, username: str) -> None:
user_id = (await self.get_user_by_name(username)).id
del self._status.autoreactions[user_id]

async def get_annoy(self) -> list[str]:
r = []
for i in self._status.annoy.keys():
try:
u = await self.get_user(i)
r.append(u.name)
except KeyError:
# The user is gone, expire it
self._status.annoy[i] = 1
r.sort()
return r

async def get_autoreact(self) -> dict[str, list[Autoreaction]]:
return {(await self.get_user(k)).name: v for k, v in self._status.autoreactions.items()}

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,
Expand All @@ -579,24 +609,31 @@ async def add_autoreact(self, username: str, reaction: str, probability: float,
if a.expired:
raise ValueError('Expired')

self._autoreactions.append(a)
if user_id not in self._status.autoreactions:
self._status.autoreactions[user_id] = []
self._status.autoreactions[user_id].append(a)

async def _annoy(self, typing: UserTyping) -> None:
if typing.user not in self._status.annoy:
return
expiration = self._status.annoy[typing.user]
if expiration > 0 and time() > expiration:
del self._status.annoy[typing.user]
await self.typing(typing.channel)

async def _autoreact(self, msg: Message) -> None:
for i in self._autoreactions:
for i in (rlist := self._status.autoreactions.get(msg.user, [])):
# Clean up
if i.expired:
self._autoreactions.remove(i)
rlist.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)
rlist.remove(i)
return

async def topic(self, channel: Channel, topic: str) -> None:
Expand Down Expand Up @@ -828,7 +865,6 @@ async def prefetch_users(self) -> None:
for user in self.tload(r['members'], list[User]):
self._usercache[user.id] = user
self._usermapcache[user.name] = user
self._usermapcache_keys = list()

async def get_user(self, id_: str) -> User:
"""
Expand All @@ -844,8 +880,6 @@ async def get_user(self, id_: str) -> User:
if response.ok:
u = self.tload(r['user'], User)
self._usercache[id_] = u
if u.name not in self._usermapcache:
self._usermapcache_keys = list()
self._usermapcache[u.name] = u
return u
else:
Expand Down Expand Up @@ -999,6 +1033,9 @@ async def event(self) -> Optional[SlackEvent]:
self._get_members_cache[ev.channel].add(ev.user)
else:
self._get_members_cache[ev.channel].discard(ev.user)
elif isinstance(ev, UserTyping):
await self._annoy(ev)
continue

if ev:
return ev
Expand Down

0 comments on commit 12ea760

Please sign in to comment.