Compare commits
39 Commits
cam_module
...
master
Author | SHA1 | Date |
---|---|---|
Ville Ranki | 68b4fa4772 | |
Rosa Richter | f62e2164da | |
Ville Ranki | bf3e4a7c7c | |
Aciid | 3c3eae0c39 | |
Ville Ranki | 075e94ccb6 | |
Ville Ranki | fea9be54fa | |
Aciid | 3d17818737 | |
Ville Ranki | 34dfe5e878 | |
Ville Ranki | dda525a22c | |
Ville Ranki | 2e6ff51121 | |
Aciid | 3bdf703639 | |
Aciid | 9af7a4228b | |
Aciid | bd8f574572 | |
Ville Ranki | 5da7e36e94 | |
Sebastian Schmittner | 9dff166f15 | |
Ville Ranki | 3f4c0d7d9b | |
Ville Ranki | 6243ac8da6 | |
Sebastian Schmittner | 72acb78fba | |
Sebastian Schmittner | 3c3a6535a5 | |
Sebastian Schmittner | 91dfcf8328 | |
Sebastian Schmittner | 656f0748ea | |
Sebastian Schmittner | 43e7dca53b | |
Sebastian Schmittner | 0b4b02e144 | |
dependabot[bot] | 6389df651e | |
dependabot[bot] | d31cccf2fe | |
Ville Ranki | ad7124d140 | |
Ville Ranki | 3b117419f0 | |
Ville Ranki | d7263357d3 | |
Ville Ranki | 4746c7fada | |
Ville Ranki | 2bd0703df3 | |
F-Node-Karlsruhe | 2c16ca5062 | |
F-Node-Karlsruhe | a19e124cdf | |
Sebastian Schmittner | d72ea71d71 | |
Sebastian Schmittner | 56594e6e13 | |
F-Node-Karlsruhe | b5c9c3874f | |
F-Node-Karlsruhe | 9811e2328f | |
dependabot[bot] | 45106fa647 | |
dependabot[bot] | ff51fe2ffc | |
dependabot[bot] | 02468cf72e |
|
@ -37,7 +37,7 @@ jobs:
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@49ed152c8eca782a232dede0303416e8f356c37b
|
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
@ -47,7 +47,7 @@ jobs:
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@69f6fc9d46f2f8bf0d5491e4aabe0bb8c6a4678a
|
uses: docker/metadata-action@507c2f2dc502c992ad446e3d7a5dfbe311567a96
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
flavor: latest=true
|
flavor: latest=true
|
||||||
|
@ -55,7 +55,7 @@ jobs:
|
||||||
# Build and push Docker image with Buildx (don't push on PR)
|
# Build and push Docker image with Buildx (don't push on PR)
|
||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@c84f38281176d4c9cdb1626ffafcd6b3911b5d94
|
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
|
1
Pipfile
1
Pipfile
|
@ -24,6 +24,7 @@ pillow = "*"
|
||||||
giphypop = "*"
|
giphypop = "*"
|
||||||
tzlocal = "*"
|
tzlocal = "*"
|
||||||
nest_asyncio = "*"
|
nest_asyncio = "*"
|
||||||
|
d20 = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
pylint = "*"
|
pylint = "*"
|
||||||
|
|
99
README.md
99
README.md
|
@ -279,14 +279,14 @@ mxma requires all commands to be run as bot owner.
|
||||||
#### SpaceAPI
|
#### SpaceAPI
|
||||||
|
|
||||||
Polls the status of Hack- and Makerspaces that provide an endpoint
|
Polls the status of Hack- and Makerspaces that provide an endpoint
|
||||||
that conforms to the [SpaceAPI](https://spaceapi.io/) protocol and notifies
|
that conforms to the [SpaceAPI](https://spaceapi.io/) protocol and notifies
|
||||||
about changes of the opening status.
|
about changes of the opening status.
|
||||||
|
|
||||||
To add a new endpoint simply use
|
To add a new endpoint simply use
|
||||||
`!spaceapi add https://hackspace.example.org/status`
|
`!spaceapi add https://hackspace.example.org/status`
|
||||||
|
|
||||||
For Admins: A template and I18N can be configured via settings of
|
For Admins: A template and I18N can be configured via settings of
|
||||||
the module. Use `!bot export spacepi`, then change the
|
the module. Use `!bot export spacepi`, then change the
|
||||||
settings and import again with `!bot import spacepi SETTINGS`.
|
settings and import again with `!bot import spacepi SETTINGS`.
|
||||||
|
|
||||||
### Url
|
### Url
|
||||||
|
@ -371,6 +371,40 @@ The module uses a demo API Key which can be replaced by your own api key by sett
|
||||||
|
|
||||||
You can create one at https://api.nasa.gov/#signUp
|
You can create one at https://api.nasa.gov/#signUp
|
||||||
|
|
||||||
|
### XKCD
|
||||||
|
|
||||||
|
Fetch comic strips from [XKCD](https://xkcd.com) (A webcomic of romance,
|
||||||
|
sarcasm, math, and language) and post them to the room.
|
||||||
|
|
||||||
|
Command:
|
||||||
|
|
||||||
|
* !xkcd - Sends latest XKCD comic strip to the room
|
||||||
|
* !xkcd ID - Sends xkcd number ID to the room
|
||||||
|
* !xkcd help - show command help
|
||||||
|
|
||||||
|
|
||||||
|
### Inspirobot
|
||||||
|
|
||||||
|
Query a randomly generated inspirational poster from https://inspirobot.me/ upload and send to the room.
|
||||||
|
|
||||||
|
Command:
|
||||||
|
|
||||||
|
* !inspire - Generate inspiration and post to the room
|
||||||
|
* !inspire help - Show this help
|
||||||
|
|
||||||
|
### User Status
|
||||||
|
|
||||||
|
This is a substitute for matrix' (element's?) missing user status feature.
|
||||||
|
Save a custom (status) message for users and allows to query them.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
|
||||||
|
* !status clear - clear my status
|
||||||
|
* !status show [user] - show the status of the given user. If no user is given, show all status messages
|
||||||
|
* !status help - show this text
|
||||||
|
* !status [status] - set your status (your status must not begin with another command word.)
|
||||||
|
|
||||||
|
|
||||||
### Wolfram Alpha
|
### Wolfram Alpha
|
||||||
|
|
||||||
Make queries to Wolfram Alpha
|
Make queries to Wolfram Alpha
|
||||||
|
@ -518,8 +552,8 @@ Read the documentation to create one at https://developers.giphy.com/docs/api
|
||||||
Commands:
|
Commands:
|
||||||
|
|
||||||
* !giphy apikey [apikey] - Set api key (Must be done as bot owner)
|
* !giphy apikey [apikey] - Set api key (Must be done as bot owner)
|
||||||
* !giphy [query] - Post the first image result from giphy for the given [query]
|
* !giphy [query] - Post the first image result from giphy for the given [query]
|
||||||
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
@ -533,7 +567,7 @@ Can be used to post a picture from Gfycat given a query string.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
|
|
||||||
* !gfycat [query] - Post the first image result from gfycat for the given [query]
|
* !gfycat [query] - Post the first image result from gfycat for the given [query]
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
@ -572,7 +606,7 @@ Environ variables seen by command:
|
||||||
Docker environment:
|
Docker environment:
|
||||||
|
|
||||||
Since the module needs access to the source of the running Tautulli instance volumes on both Docker (hemppa and Tautulli) should be defined and being visible each other.
|
Since the module needs access to the source of the running Tautulli instance volumes on both Docker (hemppa and Tautulli) should be defined and being visible each other.
|
||||||
When running on Docker the env variables seen by command should be defined for the bot instance.
|
When running on Docker the env variables seen by command should be defined for the bot instance.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
@ -590,8 +624,8 @@ environment but can be extended to any purpose.
|
||||||
|
|
||||||
* Create labels to github that represent for example different machines and spaces.
|
* Create labels to github that represent for example different machines and spaces.
|
||||||
You can create any number of them.
|
You can create any number of them.
|
||||||
* Define label colors for each type of asset. These are called domains in this module.
|
* Define label colors for each type of asset. These are called domains in this module.
|
||||||
For example set all machine labels to be #B60205 and space labels to be #0E8A16. These
|
For example set all machine labels to be #B60205 and space labels to be #0E8A16. These
|
||||||
can be easily picked from color chooser.
|
can be easily picked from color chooser.
|
||||||
* Edit the repository description and add a json block describing the
|
* Edit the repository description and add a json block describing the
|
||||||
label domains and their colors (array format supports multiple colors per domain). For example:
|
label domains and their colors (array format supports multiple colors per domain). For example:
|
||||||
|
@ -674,6 +708,35 @@ including version, how many users are connected, and ping time.
|
||||||
* !mumble - Show info about the configured mumble server
|
* !mumble - Show info about the configured mumble server
|
||||||
- !mumble (set|setserver) [host] ([port]) - Set the configured mumble server
|
- !mumble (set|setserver) [host] ([port]) - Set the configured mumble server
|
||||||
|
|
||||||
|
### Nitter
|
||||||
|
|
||||||
|
Reads Twitter links from room, replaces domain with nitter, removes query parameters and posts link to room.
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
|
* !nitter enable - enable converting twitter links to nitter links in this room (must be done as room admin)
|
||||||
|
* !nitter disable - disable converting twitter links to nitter links in this room (must be done as room admin)
|
||||||
|
|
||||||
|
### Wikipedia
|
||||||
|
|
||||||
|
Searches Wikipedia for a given query and returns the first result summary and link.
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
|
* !wikipedia [query] - Search Wikipedia for query
|
||||||
|
|
||||||
|
### Dice Roll
|
||||||
|
|
||||||
|
Rolls dice in XdY format.
|
||||||
|
|
||||||
|
* !roll 1d20 - roll a single d20
|
||||||
|
* !roll 1d20+4 - a skill check or attack roll
|
||||||
|
* !roll 1d20+1 adv - a skill check or attack roll with advantage
|
||||||
|
* !roll 1d20-1 dis - a skill check or attack roll with disadvantage
|
||||||
|
* !roll help - show help info
|
||||||
|
|
||||||
|
For more syntax help, see <https://d20.readthedocs.io/en/latest/start.html#dice-syntax>.
|
||||||
|
|
||||||
## Bot setup
|
## Bot setup
|
||||||
|
|
||||||
* Create a Matrix user
|
* Create a Matrix user
|
||||||
|
@ -741,7 +804,7 @@ docker-compose up
|
||||||
## Env variables
|
## Env variables
|
||||||
|
|
||||||
`MATRIX_USER` is the full MXID (not just username) of the Matrix user. `MATRIX_ACCESS_TOKEN`
|
`MATRIX_USER` is the full MXID (not just username) of the Matrix user. `MATRIX_ACCESS_TOKEN`
|
||||||
and `MATRIX_SERVER` should be url to the user's server (non-delegated server). Set `JOIN_ON_INVITE` (default true)
|
and `MATRIX_SERVER` should be url to the user's server (non-delegated server). Set `JOIN_ON_INVITE` (default true)
|
||||||
to false if you don't want the bot automatically joining rooms.
|
to false if you don't want the bot automatically joining rooms.
|
||||||
|
|
||||||
You can get access token by logging in with Element Android and looking from Settings / Help & About.
|
You can get access token by logging in with Element Android and looking from Settings / Help & About.
|
||||||
|
@ -752,6 +815,8 @@ Typically set your own id into it.
|
||||||
|
|
||||||
`OWNERS_ONLY` is an optional variable once defined only the owners can operate the bot (this is a form of whitelisting)
|
`OWNERS_ONLY` is an optional variable once defined only the owners can operate the bot (this is a form of whitelisting)
|
||||||
|
|
||||||
|
`INVITE_WHITELIST` (default empty) is an optional comma-separated list of matrix id's to restrict the acceptance of invites into rooms of the bot to users or servers. It supports wild cards: Example value: `@user:matrix.org,@*:myserver.org`
|
||||||
|
|
||||||
`LEAVE_EMPTY_ROOMS` (default true) if this is set to false, the bot will stay in empty rooms
|
`LEAVE_EMPTY_ROOMS` (default true) if this is set to false, the bot will stay in empty rooms
|
||||||
|
|
||||||
__*ATTENTION:*__ Don't include bot itself in `BOT_OWNERS` if cron or any other module that can cause bot to send custom commands is used, as it could potentially be used to run owner commands as the bot itself.
|
__*ATTENTION:*__ Don't include bot itself in `BOT_OWNERS` if cron or any other module that can cause bot to send custom commands is used, as it could potentially be used to run owner commands as the bot itself.
|
||||||
|
@ -829,7 +894,7 @@ class Bot:
|
||||||
:param bot_ignore: Flag to mark the message to be ignored by the bot
|
:param bot_ignore: Flag to mark the message to be ignored by the bot
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def send_image(self, room, url, body, event=None, mimetype=None, width=None, height=None, size=None):
|
async def send_image(self, room, url, body, event=None, mimetype=None, width=None, height=None, size=None):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -843,17 +908,17 @@ class Bot:
|
||||||
:param size: Size in bytes of the image
|
:param size: Size in bytes of the image
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def upload_image(self, url, blob=False, blob_content_type="image/png"):
|
async def upload_image(self, url, blob=False, blob_content_type="image/png"):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
:param url: Url of binary content of the image to upload
|
:param url: Url of binary content of the image to upload
|
||||||
:param blob: Flag to indicate if the first param is an url or a binary content
|
:param blob: Flag to indicate if the first param is an url or a binary content
|
||||||
:param blob_content_type: Content type of the image in case of binary content
|
:param blob_content_type: Content type of the image in case of binary content
|
||||||
:return: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri, Content type, Width, Height, Image size in bytes
|
:return: A MXC-Uri https://matrix.org/docs/spec/client_server/r0.6.0#mxc-uri, Content type, Width, Height, Image size in bytes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
async def upload_and_send_image(self, room, url, event=None, text=None, blob=False, blob_content_type="image/png"):
|
async def upload_and_send_image(self, room, url, event=None, text=None, blob=False, blob_content_type="image/png"):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -861,7 +926,7 @@ class Bot:
|
||||||
:param url: Url of binary content of the image to upload
|
:param url: Url of binary content of the image to upload
|
||||||
:param event: The event to reply to
|
:param event: The event to reply to
|
||||||
:param text: A textual representation of the image
|
:param text: A textual representation of the image
|
||||||
:param blob: Flag to indicate if the second param is an url or a binary content
|
:param blob: Flag to indicate if the second param is an url or a binary content
|
||||||
:param blob_content_type: Content type of the image in case of binary content
|
:param blob_content_type: Content type of the image in case of binary content
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
19
bot.py
19
bot.py
|
@ -35,6 +35,7 @@ class Bot:
|
||||||
self.version = '1.5'
|
self.version = '1.5'
|
||||||
self.client = None
|
self.client = None
|
||||||
self.join_on_invite = False
|
self.join_on_invite = False
|
||||||
|
self.invite_whitelist = []
|
||||||
self.modules = dict()
|
self.modules = dict()
|
||||||
self.module_aliases = dict()
|
self.module_aliases = dict()
|
||||||
self.leave_empty_rooms = True
|
self.leave_empty_rooms = True
|
||||||
|
@ -458,11 +459,25 @@ class Bot:
|
||||||
def starts_with_command(body):
|
def starts_with_command(body):
|
||||||
"""Checks if body starts with ! and has one or more letters after it"""
|
"""Checks if body starts with ! and has one or more letters after it"""
|
||||||
return re.match(r"^!\w.*", body) is not None
|
return re.match(r"^!\w.*", body) is not None
|
||||||
|
|
||||||
|
def on_invite_whitelist(self, sender):
|
||||||
|
for entry in self.invite_whitelist:
|
||||||
|
if entry == sender:
|
||||||
|
return True
|
||||||
|
controll_value = entry.split(':')
|
||||||
|
if controll_value[0] == '@*' and controll_value[1] == sender.split(':')[1]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def invite_cb(self, room, event):
|
async def invite_cb(self, room, event):
|
||||||
room: MatrixRoom
|
room: MatrixRoom
|
||||||
event: InviteEvent
|
event: InviteEvent
|
||||||
|
|
||||||
|
if len(self.invite_whitelist) > 0 and not self.on_invite_whitelist(event.sender):
|
||||||
|
self.logger.error(f'Cannot join room {room.display_name}, as {event.sender} is not whitelisted for invites!')
|
||||||
|
return
|
||||||
|
|
||||||
if self.join_on_invite or self.is_owner(event):
|
if self.join_on_invite or self.is_owner(event):
|
||||||
for attempt in range(3):
|
for attempt in range(3):
|
||||||
self.jointime = datetime.datetime.now()
|
self.jointime = datetime.datetime.now()
|
||||||
|
@ -558,6 +573,7 @@ class Bot:
|
||||||
bot_owners = os.getenv('BOT_OWNERS')
|
bot_owners = os.getenv('BOT_OWNERS')
|
||||||
access_token = os.getenv('MATRIX_ACCESS_TOKEN')
|
access_token = os.getenv('MATRIX_ACCESS_TOKEN')
|
||||||
join_on_invite = os.getenv('JOIN_ON_INVITE')
|
join_on_invite = os.getenv('JOIN_ON_INVITE')
|
||||||
|
invite_whitelist = os.getenv('INVITE_WHITELIST')
|
||||||
owners_only = os.getenv('OWNERS_ONLY') is not None
|
owners_only = os.getenv('OWNERS_ONLY') is not None
|
||||||
leave_empty_rooms = os.getenv('LEAVE_EMPTY_ROOMS')
|
leave_empty_rooms = os.getenv('LEAVE_EMPTY_ROOMS')
|
||||||
|
|
||||||
|
@ -565,6 +581,7 @@ class Bot:
|
||||||
self.client = AsyncClient(matrix_server, self.matrix_user, ssl = matrix_server.startswith("https://"))
|
self.client = AsyncClient(matrix_server, self.matrix_user, ssl = matrix_server.startswith("https://"))
|
||||||
self.client.access_token = access_token
|
self.client.access_token = access_token
|
||||||
self.join_on_invite = (join_on_invite or '').lower() == 'true'
|
self.join_on_invite = (join_on_invite or '').lower() == 'true'
|
||||||
|
self.invite_whitelist = invite_whitelist.split(',') if invite_whitelist is not None else []
|
||||||
self.leave_empty_rooms = (leave_empty_rooms or 'true').lower() == 'true'
|
self.leave_empty_rooms = (leave_empty_rooms or 'true').lower() == 'true'
|
||||||
self.owners = bot_owners.split(',')
|
self.owners = bot_owners.split(',')
|
||||||
self.owners_only = owners_only
|
self.owners_only = owners_only
|
||||||
|
@ -616,6 +633,8 @@ class Bot:
|
||||||
|
|
||||||
if self.join_on_invite:
|
if self.join_on_invite:
|
||||||
self.logger.info('Note: Bot will join rooms if invited')
|
self.logger.info('Note: Bot will join rooms if invited')
|
||||||
|
if len(self.invite_whitelist) > 0:
|
||||||
|
self.logger.info(f'Note: Bot will only join rooms when the inviting user is contained in {self.invite_whitelist}')
|
||||||
self.logger.info('Bot running as %s, owners %s', self.client.user, self.owners)
|
self.logger.info('Bot running as %s, owners %s', self.client.user, self.owners)
|
||||||
self.bot_task = asyncio.create_task(self.client.sync_forever(timeout=30000))
|
self.bot_task = asyncio.create_task(self.client.sync_forever(timeout=30000))
|
||||||
await self.bot_task
|
await self.bot_task
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import html
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from modules.common.module import BotModule
|
||||||
|
|
||||||
|
|
||||||
|
class MatrixModule(BotModule):
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
self.url_generator_url = "https://inspirobot.me/api?generate=true"
|
||||||
|
self.matrix_uri_cache = dict()
|
||||||
|
|
||||||
|
async def matrix_message(self, bot, room, event):
|
||||||
|
self.logger.debug(f"room: {room.name} sender: {event.sender} wants to be inspired!")
|
||||||
|
|
||||||
|
args = event.body.split()
|
||||||
|
|
||||||
|
if len(args) == 1:
|
||||||
|
await self.send_inspiration(bot, room, self.url_generator_url)
|
||||||
|
return
|
||||||
|
elif len(args) == 2:
|
||||||
|
if args[1] == "help":
|
||||||
|
await self.command_help(bot, room)
|
||||||
|
return
|
||||||
|
await bot.send_text(room, f"unknown command: {args}")
|
||||||
|
await self.command_help(bot, room)
|
||||||
|
|
||||||
|
async def send_inspiration(self, bot, room, url_generator_url):
|
||||||
|
self.logger.debug(f"Asking inspirobot for pic url at {url_generator_url}")
|
||||||
|
response = requests.get(url_generator_url)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
self.logger.error("unable to request inspirobot api. response: [status: %d text: %s]", response.status_code, response.text)
|
||||||
|
return await bot.send_text(room, f"sorry. something went wrong accessing inspirobot: {response.status_code}: {response.text}")
|
||||||
|
|
||||||
|
pic_url = response.text
|
||||||
|
self.logger.debug("Sending image with src='%s'", pic_url)
|
||||||
|
await bot.upload_and_send_image(room, pic_url)
|
||||||
|
|
||||||
|
def help(self):
|
||||||
|
return """I'm InspiroBot.
|
||||||
|
I am an artificial intelligence dedicated to generating unlimited amounts of unique inspirational quotes
|
||||||
|
for endless enrichment of pointless human existence.
|
||||||
|
https://inspirobot.me/
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def command_help(self, bot, room):
|
||||||
|
msg = """usage: !inspire [command]
|
||||||
|
No command to generate an inspirational poster just for you!
|
||||||
|
- help - show this. Useful, isn't it?
|
||||||
|
"""
|
||||||
|
await bot.send_html(room, f"<b>{html.escape(self.help())}</b>", self.help())
|
||||||
|
await bot.send_text(room, msg)
|
|
@ -0,0 +1,87 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
from modules.common.module import BotModule
|
||||||
|
from nio import RoomMessageText
|
||||||
|
|
||||||
|
|
||||||
|
# This module reads matrix messages and converts twitter.com links to nitter.nl
|
||||||
|
# Module will only target messages that contain only the twitter link
|
||||||
|
# Additionally module will target only profile links or post links, query parameters are removed
|
||||||
|
class MatrixModule(BotModule):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
self.regex = re.compile(r'https://twitter.com/([^?]*)')
|
||||||
|
self.bot = None
|
||||||
|
self.enabled_rooms = []
|
||||||
|
|
||||||
|
def matrix_start(self, bot):
|
||||||
|
"""
|
||||||
|
Register callback for all RoomMessageText events on startup
|
||||||
|
"""
|
||||||
|
super().matrix_start(bot)
|
||||||
|
self.bot = bot
|
||||||
|
bot.client.add_event_callback(self.text_cb, RoomMessageText)
|
||||||
|
|
||||||
|
def matrix_stop(self, bot):
|
||||||
|
super().matrix_stop(bot)
|
||||||
|
bot.remove_callback(self.text_cb)
|
||||||
|
|
||||||
|
async def text_cb(self, room, event):
|
||||||
|
"""
|
||||||
|
Handle client callbacks for all room text events
|
||||||
|
"""
|
||||||
|
if room.room_id not in self.enabled_rooms:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.bot.should_ignore_event(event):
|
||||||
|
return
|
||||||
|
|
||||||
|
# no content at all?
|
||||||
|
if len(event.body) < 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
if event.body.startswith('!'):
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(event.body.split()) <= 1:
|
||||||
|
if event.body.startswith('https://twitter.com/'):
|
||||||
|
for link in self.regex.findall(event.body):
|
||||||
|
await self.bot.send_text(room, f'https://nitter.net/{link}')
|
||||||
|
return
|
||||||
|
|
||||||
|
async def matrix_message(self, bot, room, event):
|
||||||
|
"""
|
||||||
|
Required for initialization of the module
|
||||||
|
"""
|
||||||
|
args = event.body.split()
|
||||||
|
args.pop(0)
|
||||||
|
if len(args) == 0:
|
||||||
|
await bot.send_text(room, 'Usage: !nitter <enable|disable>')
|
||||||
|
return
|
||||||
|
if len(args) == 1:
|
||||||
|
if args[0] == 'enable':
|
||||||
|
bot.must_be_admin(room, event)
|
||||||
|
self.enabled_rooms.append(room.room_id)
|
||||||
|
self.enabled_rooms = list(dict.fromkeys(self.enabled_rooms)) # Deduplicate
|
||||||
|
await bot.send_text(room, "Ok, enabling conversion of twitter links to nitter links here")
|
||||||
|
bot.save_settings()
|
||||||
|
return
|
||||||
|
if args[0] == 'disable':
|
||||||
|
bot.must_be_admin(room, event)
|
||||||
|
self.enabled_rooms.remove(room.room_id)
|
||||||
|
await bot.send_text(room, "Ok, disabling conversion of twitter links to nitter links here")
|
||||||
|
bot.save_settings()
|
||||||
|
return
|
||||||
|
|
||||||
|
def help(self):
|
||||||
|
return 'Converts Twitter links to nitter links.'
|
||||||
|
|
||||||
|
def get_settings(self):
|
||||||
|
data = super().get_settings()
|
||||||
|
data["enabled_rooms"] = self.enabled_rooms
|
||||||
|
return data
|
||||||
|
|
||||||
|
def set_settings(self, data):
|
||||||
|
super().set_settings(data)
|
||||||
|
if data.get("enabled_rooms"):
|
||||||
|
self.enabled_rooms = data["enabled_rooms"]
|
|
@ -0,0 +1,30 @@
|
||||||
|
from modules.common.module import BotModule
|
||||||
|
import d20
|
||||||
|
|
||||||
|
class MatrixModule(BotModule):
|
||||||
|
async def matrix_message(self, bot, room, event):
|
||||||
|
args = event.body.split()
|
||||||
|
args.pop(0)
|
||||||
|
|
||||||
|
if args[0] == 'help':
|
||||||
|
await bot.send_text(room, self.long_help())
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
result = d20.roll(' '.join(args), stringifier=d20.SimpleStringifier())
|
||||||
|
await bot.send_text(room, str(result), event=event)
|
||||||
|
except:
|
||||||
|
await bot.send_text(room, 'Invalid roll syntax', event=event)
|
||||||
|
|
||||||
|
def help(self):
|
||||||
|
return 'Rolls dice in XdY format'
|
||||||
|
|
||||||
|
def long_help(self, bot=None, event=None, **kwargs):
|
||||||
|
text = self.help() + (
|
||||||
|
'\n- "!roll 1d20": roll a single d20'
|
||||||
|
'\n- "!roll 1d20+4": A skill check or attack roll'
|
||||||
|
'\n- "!roll 1d20+1 adv": A skill check or attack roll with advantage'
|
||||||
|
'\n- "!roll 1d20-1 dis": A skill check or attack roll with disadvantage'
|
||||||
|
'\n- "!roll help": show this help'
|
||||||
|
'\n'
|
||||||
|
'\nFor more syntax help, see https://d20.readthedocs.io/en/latest/start.html#dice-syntax')
|
||||||
|
return text
|
|
@ -0,0 +1,64 @@
|
||||||
|
import html
|
||||||
|
|
||||||
|
from modules.common.module import BotModule
|
||||||
|
|
||||||
|
|
||||||
|
class MatrixModule(BotModule):
|
||||||
|
"""
|
||||||
|
This is a substitute for matrix' (element's?) missing user status feature.
|
||||||
|
Save a custom (status) message for users and allows to query them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
self.status = dict()
|
||||||
|
|
||||||
|
async def matrix_message(self, bot, room, event):
|
||||||
|
args = event.body.split()
|
||||||
|
args.pop(0)
|
||||||
|
if len(args) < 1 or args[0] == "help":
|
||||||
|
await bot.send_text(room, self.help())
|
||||||
|
elif args[0] == "show":
|
||||||
|
if len(args) > 1:
|
||||||
|
await self.send_status(bot=bot, room=room, user=args[1])
|
||||||
|
else:
|
||||||
|
await self.send_status(bot=bot, room=room)
|
||||||
|
elif args[0] == "clear":
|
||||||
|
self.status.pop(event.sender)
|
||||||
|
bot.save_settings()
|
||||||
|
await bot.send_text(room, f"Cleared status of {event.sender}")
|
||||||
|
else:
|
||||||
|
self.status[event.sender] = " ".join(args)
|
||||||
|
bot.save_settings()
|
||||||
|
await self.send_status(bot=bot, room=room, user=event.sender)
|
||||||
|
|
||||||
|
async def send_status(self, bot, room, user=None):
|
||||||
|
if user:
|
||||||
|
if user in self.status:
|
||||||
|
await bot.send_text(room, f"Status message of {user}: {self.status[user]}")
|
||||||
|
else:
|
||||||
|
await bot.send_text(room, f"No status known for {user}")
|
||||||
|
else:
|
||||||
|
await bot.send_html(room, "<b>All status messages:</b><br/><ul><li>" +
|
||||||
|
"</li><li>".join([f"<b>{html.escape(key)}:</b> {html.escape(value)}" for key, value in self.status.items()]) +
|
||||||
|
"</li></ul>", f"All status messages:\n{self.status}")
|
||||||
|
|
||||||
|
def get_settings(self):
|
||||||
|
data = super().get_settings()
|
||||||
|
data["user_status_list"] = self.status
|
||||||
|
return data
|
||||||
|
|
||||||
|
def set_settings(self, data):
|
||||||
|
super().set_settings(data)
|
||||||
|
if data.get("user_status_list"):
|
||||||
|
self.status = data["user_status_list"]
|
||||||
|
|
||||||
|
def help(self):
|
||||||
|
return """
|
||||||
|
Store a status message per user and display them.
|
||||||
|
Usage:
|
||||||
|
!status clear - clear my status
|
||||||
|
!status show [user] - show the status of user. If no user is given, show all status messages
|
||||||
|
!status help - show this text
|
||||||
|
!status [status] - set your status
|
||||||
|
"""
|
|
@ -0,0 +1,79 @@
|
||||||
|
import re
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from modules.common.module import BotModule
|
||||||
|
|
||||||
|
|
||||||
|
# This module searches wikipedia for query, returns page summary and link.
|
||||||
|
class MatrixModule(BotModule):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
self.api_url = 'https://en.wikipedia.org/w/api.php'
|
||||||
|
|
||||||
|
async def matrix_message(self, bot, room, event):
|
||||||
|
args = event.body.split()
|
||||||
|
|
||||||
|
if len(args) > 1:
|
||||||
|
query = event.body[len(args[0]) + 1:]
|
||||||
|
try:
|
||||||
|
response = requests.get(self.api_url, params={
|
||||||
|
'action': 'query',
|
||||||
|
'format': 'json',
|
||||||
|
'exintro': True,
|
||||||
|
'explaintext': True,
|
||||||
|
'prop': 'extracts',
|
||||||
|
'redirects': 1,
|
||||||
|
'titles': query,
|
||||||
|
})
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Get the page id
|
||||||
|
page_id = list(data['query']['pages'].keys())[0]
|
||||||
|
|
||||||
|
if page_id == '-1':
|
||||||
|
await bot.send_text(room, 'No results found')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the page title
|
||||||
|
title = data['query']['pages'][page_id]['title']
|
||||||
|
|
||||||
|
# Get the page summary
|
||||||
|
summary = data['query']['pages'][page_id]['extract']
|
||||||
|
|
||||||
|
# Remove all html tags
|
||||||
|
extract = re.sub('<[^<]+?>', '', summary)
|
||||||
|
# Remove any multiple spaces
|
||||||
|
extract = re.sub(' +', ' ', extract)
|
||||||
|
# Remove any new lines
|
||||||
|
extract = re.sub('', '', extract)
|
||||||
|
# Remove any tabs
|
||||||
|
extract = re.sub('\t', '', extract)
|
||||||
|
|
||||||
|
# Truncate the extract, Element URL preview contains nonsense Wikipedia meta content
|
||||||
|
if len(extract) <= 256:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
extract = ' '.join(extract[:256 + 1].split(' ')[0:-1]) + '...'
|
||||||
|
|
||||||
|
# Get the page url
|
||||||
|
url = f'https://en.wikipedia.org/wiki/{title}'
|
||||||
|
|
||||||
|
# Convert all spaces to underscores in url
|
||||||
|
url = re.sub(r'\s', '_', url)
|
||||||
|
|
||||||
|
# Format the response
|
||||||
|
response = f'{title}: {extract} \n{url}'
|
||||||
|
|
||||||
|
# Send the response
|
||||||
|
await bot.send_text(room, response)
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
await bot.send_text(room, str(exc))
|
||||||
|
else:
|
||||||
|
await bot.send_text(room, 'Usage: !wikipedia <query>')
|
||||||
|
|
||||||
|
def help(self):
|
||||||
|
return ('Wikipedia bot')
|
|
@ -0,0 +1,92 @@
|
||||||
|
import re
|
||||||
|
import html
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from modules.common.module import BotModule
|
||||||
|
from modules.common.exceptions import UploadFailed
|
||||||
|
|
||||||
|
|
||||||
|
class Xkcd:
|
||||||
|
"""
|
||||||
|
Uses the XKCD (json) api https://xkcd.com/json.html to fetch web comics and metadata and display them in chats.
|
||||||
|
"""
|
||||||
|
def __init__(self, title, img, alt, num):
|
||||||
|
self.title = title
|
||||||
|
self.img = img
|
||||||
|
self.alt = alt
|
||||||
|
self.num = num
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_from_json(json):
|
||||||
|
return Xkcd(json.get("title"), json.get("img"), json.get("alt"), json.get("num"))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "title: {} || explanation: {} || date: {} || original-url: {}".format(self.title,
|
||||||
|
self.explanation,
|
||||||
|
self.date,
|
||||||
|
self.hdurl)
|
||||||
|
|
||||||
|
|
||||||
|
class MatrixModule(BotModule):
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
self.uri_get_latest = 'https://xkcd.com/info.0.json'
|
||||||
|
|
||||||
|
async def matrix_message(self, bot, room, event):
|
||||||
|
self.logger.debug(f"room: {room.name} sender: {event.sender} queried the xkcd module with body: {event.body}")
|
||||||
|
|
||||||
|
args = event.body.split()
|
||||||
|
|
||||||
|
if len(args) == 1:
|
||||||
|
await self.send_xkcd(bot, room, self.uri_get_latest)
|
||||||
|
elif len(args) == 2:
|
||||||
|
if args[1] == "help":
|
||||||
|
await self.command_help(bot, room)
|
||||||
|
else:
|
||||||
|
xkcd_id = args[1]
|
||||||
|
if re.match("\\d+", xkcd_id) is not None:
|
||||||
|
await self.send_xkcd(bot, room, self.uri_get_by_id(xkcd_id))
|
||||||
|
else:
|
||||||
|
await bot.send_text(room, "Invalid comic id. ids must be positive integers.")
|
||||||
|
|
||||||
|
async def send_xkcd(self, bot, room, uri):
|
||||||
|
self.logger.debug(f"send request using uri {uri}")
|
||||||
|
response = requests.get(uri)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
self.logger.error("unable to request api. response: [status: %d text: %s]", response.status_code, response.text)
|
||||||
|
return await bot.send_text(room, "sorry. something went wrong accessing the api")
|
||||||
|
|
||||||
|
xkcd = Xkcd.create_from_json(response.json())
|
||||||
|
self.logger.debug(xkcd)
|
||||||
|
|
||||||
|
img_url = xkcd.img
|
||||||
|
try:
|
||||||
|
matrix_uri = None
|
||||||
|
matrix_uri, mimetype, w, h, size = bot.get_uri_cache(img_url)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
self.logger.debug(f"Not found in cache: {img_url}")
|
||||||
|
try:
|
||||||
|
matrix_uri, mimetype, w, h, size = await bot.upload_image(img_url)
|
||||||
|
except (UploadFailed, TypeError, ValueError):
|
||||||
|
await bot.send_text(room, f"Something went wrong uploading {img_url}.")
|
||||||
|
|
||||||
|
await bot.send_html(room, f"<b>{html.escape(xkcd.title)} ({html.escape(str(xkcd.num))})</b>", f"{xkcd.title} ({str(xkcd.num)})")
|
||||||
|
await bot.send_image(room, matrix_uri, img_url, None, mimetype, w, h, size)
|
||||||
|
await bot.send_text(room, f"{xkcd.alt}")
|
||||||
|
|
||||||
|
def uri_get_by_id(self, id):
|
||||||
|
return 'https://xkcd.com/' + str(int(id)) + '/info.0.json'
|
||||||
|
|
||||||
|
def help(self):
|
||||||
|
return 'Sends latest/any specified XCKD web comic to the room. See https://xkcd.com/ '
|
||||||
|
|
||||||
|
async def command_help(self, bot, room):
|
||||||
|
msg = """commands:
|
||||||
|
- no arguments: fetch latest xkcd comic
|
||||||
|
- (\\d+) fetch and display the xkcd comic with the given id
|
||||||
|
- help - show command help
|
||||||
|
"""
|
||||||
|
await bot.send_text(room, msg)
|
Loading…
Reference in New Issue